GitHub では Issue/PR/Discussion URL をリストで書くと、ステータスやタイトル、No. を描画してくれる。

例えばこのように書くと、

* https://github.com/masutaka/sandbox/issues/93
* https://github.com/masutaka/sandbox/issues/70
* https://github.com/masutaka/sandbox/pull/90
* https://github.com/masutaka/sandbox/discussions/91

このように描画される。

描画例

ただ、テキストエリアやテキストエディタで書いている時は描画されないので、タイトルやステータスが分からない。この数なら問題ないけど、数が多くなると取り違えることがある。

以前は手でこんなコメントを書いていたが、流石に面倒になったので、そんな Emacs Lisp 関数を作った。

* https://github.com/masutaka/sandbox/issues/93 <!-- Create test.rb --> (In Progress)
* https://github.com/masutaka/sandbox/issues/70 <!-- 2024/09 Sample Issue --> (Done)
* https://github.com/masutaka/sandbox/pull/90 <!-- Give CodeQL no running part2 -->
* https://github.com/masutaka/sandbox/discussions/91 <!-- 2025-02-05 ミーティングレポート -->

作成した関数

🔗 ~/.emacs.d/init.el#L109-L152

(require 'cl-lib)
(require 'request)

(defun github-expand-link ()
  "Use the GitHub API to get the information and
insert a comment at the end of the current line in the following format:

- Issue URL <!-- Title --> (Done or In Progress)
- PR/Discussion URL <!-- Title -->"
  (interactive)
  (let ((url (thing-at-point 'url 'no-properties)))
    (if (not url)
        (message "[github-expand-link] No URL at point")
      (let* ((parsed-url (url-generic-parse-url url))
             (host (url-host parsed-url))
             (parts (split-string (url-filename parsed-url) "/" t)))
        (if (and (string-match-p "github\\.com$" host) (>= (length parts) 4))
            (let ((access-token (my-lisp-load "github-expand-link-token"))
                  (org (nth 0 parts))
                  (repo (nth 1 parts))
                  (type (nth 2 parts))
                  (number (nth 3 parts))
                  (type-alist '(("issues" . "issue") ("pull" . "pullRequest") ("discussions" . "discussion"))))
              (request
                "https://api.github.com/graphql"
                :type "POST"
                :headers `(("Authorization" . ,(concat "Bearer " access-token)))
                :data (json-encode `(("query" . ,(format "query { repository(owner: \"%s\", name: \"%s\") { %s(number: %d) { %s } } }"
                                                         (url-hexify-string org) (url-hexify-string repo)
                                                         (cdr (assoc type type-alist)) (string-to-number number)
                                                         (if (equal type "issues") "title state" "title")))))
                :parser 'json-read
                :sync t
                :success (cl-function
                          (lambda (&key data response &allow-other-keys)
                            (let* ((body (car (alist-get 'repository (alist-get 'data data))))
                                   (title (alist-get 'title body))
                                   (state (alist-get 'state body)))
                              (end-of-line)
                              (insert (if state
                                          (format " <!-- %s --> (%s)" title (if (equal "CLOSED" state) "Done" "In Progress"))
                                        (format " <!-- %s -->" title))))))
                :error (cl-function
                        (lambda (&key error-thrown response &allow-other-keys)
                          (message "[github-expand-link] Fail %S to POST %s"
                                   error-thrown (request-response-url response))))))
          (message "[github-expand-link] Not a valid GitHub Issue/PR/Discussion URL"))))))

Discussion の情報を取得する GitHub の REST API はないようなので、GraphQL API を使っている。

サードパーティのパッケージはあまり使わないようにしているけど、request.el は好んで使っている。

my-lisp-load は [2016-05-06-2] で紹介した関数。 https://github.com/settings/tokens で作った PAT を ~/.emacs.d/spec/github-expand-link-token に保存すると、(my-lisp-load "github-expand-link-token") で読み込める。プライベートリポジトリの Issue/PR/Discussion を参照したい時は PAT の scope を repo にする。

キーバインドは ⌘-i に割り当ててる。

(define-key global-map (kbd "s-i") 'github-expand-link)

まとめ

シェルの TAB 補完の感覚でタイトルを補完できるので、予想以上に便利に使っている。

今回の記事を書いていたら、Issue のステータスも追加したくなったので、割とガッツリ変更した。

Emacs Lisp は頻繁に書かないので、昔はググったり公式ドキュメント確認したり面倒だったけど、最近は ChatGPT とかで便利関数を知ることができるのは便利。今回はそれで alist-get 関数を知った。