Created an Emacs Lisp function to insert the title of a GitHub Issue/PR/Discussion URL

Takashi MasudaTakashi Masuda
3 min read

This article is a translation of https://masutaka.net/2025-04-15-1/.

On GitHub, when you paste Issue/PR/Discussion URLs into comments or descriptions, GitHub automatically renders the status, title, and number.

For example, if you write the following:

* 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

It renders like this:

Rendering example

However, when writing in a plain text area or text editor, the URLs aren't automatically expanded, so you can't see the titles or statuses at a glance. This isn't a problem with just a few URLs, but it can become confusing when dealing with many.

Previously, I added comments manually like this, but it became tedious, so I created an Emacs Lisp function to automate the process.

* 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 ミーティングレポート -->

The Function I Created

🔗 ~/.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"))))))

Since there doesn't seem to be a GitHub REST API for retrieving Discussion information, I'm using the GraphQL API.

I try not to use many third-party packages, but I do like using request.el.

my-lisp-load is a function I introduced in 2016-05-06-2. If you create a PAT at https://github.com/settings/tokens and save it to ~/.emacs.d/spec/github-expand-link-token, you can load it with (my-lisp-load "github-expand-link-token"). If you want to reference Issues/PRs/Discussions in private repositories, set the PAT scope to repo.

I've assigned the keybinding ⌘-i to this function.

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

Conclusion

Being able to automatically insert titles, reminiscent of shell TAB completion, has proven more useful than I expected.

While writing this article, I decided to add Issue status as well, which required substantial changes.

I don't write Emacs Lisp frequently, so in the past, it was tedious to look things up via Google or check the official documentation. However, recently it's been convenient to learn about useful functions through ChatGPT and similar tools. This time, I learned about the alist-get function that way.

0
Subscribe to my newsletter

Read articles from Takashi Masuda directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Takashi Masuda
Takashi Masuda