Elisp Posts

Link to a git commit from Org mode using Magit | THIS IS EMACS

2022-04-29
/Tony Aldon/
comment on reddit
/
org-mode revision: af6f1298b6f6

Hey Org mode lovers,

In the article Hyperlink (wikipedia) we can read that a hyperlink "is a reference to data that the user can follow by clicking".

Do you believe me if I tell you that with Org mode the data we refer to in a link can be a buffer in magit-revision-mode (from magit package) showing us a specific commit of some git repository?

For a non-Emacs user, it's not that you can or can't believe it, it's just that this sentence doesn't make sense.

But for me (a regular Emacs user like you), when I read that sentence I think:

  1. Really! Emacs, Org, Magit, Buffer, Link! Integration. Non-context switching. Maybe a bit of lisp. Extensible. Yes it makes sense.

  2. AFTER ALL THIS IS EMACS!

  3. Show me how to do it!

The key elements to doing so lie in:

  1. Org mode provides the built-in elisp type link that allows to evaluate any Elisp s-exps or to call any commands when we open this type of link with org-open-at-point (bound to C-c C-o by default),

  2. and that the command magit-show-commit used to visit commits in magit buffers when we are on a commit line can also be used non interactively as a regular function where we have to provide it the commit hash and the repository ("module").

First we look at the syntax of elisp type links.

A bracket link is a valid elisp type link if it starts with the identifier (the type) elisp, then is followed by a colon : and then followed by either an Elisp form in parentheses or an Elisp command.

For instance this link:

[[elisp:(+ 1 1)]]

when followed with org-open-at-point prints the following in the echo area:

(+ 1 1) => 2

And this link:

[[elisp:tetris]]

when followed with org-open-at-point starts the built-in Tetris.

By default, the variable org-link-elisp-confirm-function is set to the function yes-or-no-p which means that when we open an elisp link we are asked for confirmation before executing the elisp link.

We can set this variable to nil if we never want to be asked for confirmation. But it might be dangerous (see the docstring of this variable for an example of a dangerous elisp link that can remove all our user home directory).

Besides setting org-link-elisp-confirm-function to nil in order not to be asked confirmation everytime we open an elisp link we can use the variable org-link-elisp-skip-confirm-regexp.

Indeed, any elisp link that matches the regexp org-link-elisp-skip-confirm-regexp is executed without asking confirmation.

For instance, setting up the variable org-link-elisp-skip-confirm-regexp to "tetris" lets us open the link [[elisp:tetris]] without asking us confirmation.

(setq org-link-elisp-skip-confirm-regexp "tetris")

Now things are getting spicy :)

We add the Magit layer to our elisp link.

The scenario is the following. We are "working" on the Org repository that we assume is cloned under the directory /tmp/org-mode/. At some point we've looked at the commit baffebbc3 that fixes a bug in org-get-heading and we want to keep in our note a link to that commit.

To do so we can use the following link:

[[elisp:(magit-show-commit "baffebbc3" nil nil "/tmp/org-mode/")]]

Now, if we open that link with org-open-at-point, we jump to the following buffer in magit-revision-mode:

baffebbc33e600ec7abfe5e54b60ea6753d4f272
Author:     XXX <XXX@XXX.com>
AuthorDate: Fri Apr 30 14:09:05 2021 +0200
Commit:     YYY <YYY@YYY.com>
CommitDate: Mon Apr 25 19:40:05 2022 +0800

Parent:     240a14988 Fix typo: delete-duplicates → delete-dups
Contained:  main
Follows:    release_9.5.3 (433)

Fix bug in org-get-heading

Fixes #26, where fontification could make the matching and extraction of heading
components fail.

modified   lisp/org.el
@@ -6167,8 +6167,9 @@ Return nil before first heading."
       (let ((case-fold-search nil))
  (looking-at org-complex-heading-regexp)
         ;; When using `org-fold-core--optimise-for-huge-buffers',
-        ;; returned text may be invisible.  Clear it up.
-        (org-fold-core-remove-optimisation (match-beginning 0) (match-end 0))
+        ;; returned text will be invisible.  Clear it up.
+        (save-match-data
+          (org-fold-core-remove-optimisation (match-beginning 0) (match-end 0)))
         (let ((todo (and (not no-todo) (match-string 2)))
        (priority (and (not no-priority) (match-string 3)))
        (headline (pcase (match-string 4)

TAKING NOTE WITH EMACS/ORG-MODE IS F***ING CRAZY.

DO YOU AGREE???

Now that we've seen how to use elisp type links with practical examples, let's see how they are implemented.

Except for the reserved link types coderef, custom-id, fuzzy and radio we can defined new link types or modify an existing link types using the function org-link-set-parameters.

For instance the link type elisp is defined like this in the file lisp/ol.el

(org-link-set-parameters "elisp" :follow #'org-link--open-elisp)

Fine, but what does it mean to define a link type?

It means that we add an entry to the alist org-link-parameters where we specify for a link type how we want the Org mode features related to links to behave regarding that type.

With the default configuration the variable org-link-parameters looks like this (with some link types skipped):

(("eww" :follow org-eww-open :store org-eww-store-link)
 ...
 ("info" :follow org-info-open :export org-info-export :store org-info-store-link)
 ...
 ("id" :follow org-id-open)
 ...
 ("shell" :follow org-link--open-shell)
 ...
 ("help" :follow org-link--open-help :store org-link--store-help)
 ("file" :complete org-link-complete-file)
 ("elisp" :follow org-link--open-elisp))

For instance, the variable org-link-parameters tells org-mode that when we open an elisp type link with org-open-at-point (bound to C-c C-o by default) the function that finally opens the link is org-link--open-elisp (the one after the keyword :follow in the variable org-link-parameters).

And for the info type links (links to info node) the variable org-link-parameters tells org-mode:

  1. to use the function org-info-open when we open info type links,

  2. to use the function org-info-export when we export open info type links,

  3. to use the function org-info-store-link when we store info type links.

If we want to write our own type link we can look at the docstring of org-link-parameters to know what are the supported keys (:follow, :export, :store, :activate-func, :complete, :display, :face, :help-echo, :htmlize-link, :keymap, :mouse-face) and what are their accepted values.

Back to elisp type links.

When we call org-open-at-point (bound to C-c C-o by default) without universal argument on the following elisp type link:

[[elisp:(+ 1 1)]]

in its body, org-open-at-point parses the link at point and "delegates" the work to the function org-link-open passing it as first argument the parsed link like this:

(org-link-open
 '(link
   (:type "elisp"
    :path "(+ 1 1)"
    :format bracket
    :raw-link "elisp:(+ 1 1)"
    ...
    :parent (paragraph ... :parent (section ... :parent (org-data ...)))))
 nil ;; called without universal argument
 )

Then the function org-link-open locally binds the variables type and path respectively to "elisp" and "(+ 1 1)" getting those values out of the link (link (:type "elisp" :path "(+ 1 1)" ...)) using the function org-element-property.

Then as type is not equal to any of the following types "file", "coderef", "custom-id", "fuzzy" and "radio" but equal to "elisp" the function org-link-open decides to use the dedicated function org-link--open-elisp to open elisp type links.

This is done by locally binding f to org-link--open-elisp retrieving this value using the function org-link-get-parameter which is a thin wrapper around the variable org-link-parameters as we can see in the following code snippet

(defun org-link-get-parameter (type key)
  "..."
  (plist-get (cdr (assoc type org-link-parameters)) key))

and as org-link--open-elisp is a function that accepts two arguments, org-link-open finally returns with the following call:

(funcall 'org-link--open-elisp "(+ 1 1)" nil)

Here are the parts of org-link-open we've just discussed:

(defun org-link-open (link &optional arg)
  "..."
  (let ((type (org-element-property :type link))
        (path (org-element-property :path link)))
    (pcase type
      ("file"
       ...)
      ((or "coderef" "custom-id" "fuzzy" "radio")
       ...)
      (_
       ;; Look for a dedicated "follow" function in custom links.
       (let ((f (org-link-get-parameter type :follow)))
         (when (functionp f)
           ;; Function defined in `:follow' parameter may use a single
           ;; argument, as it was mandatory before Org 9.4.  This is
           ;; deprecated, but support it for now.
           (condition-case nil
               (funcall (org-link-get-parameter type :follow) path arg)
             (wrong-number-of-arguments
              (funcall (org-link-get-parameter type :follow) path)))))))))

Now we are left with the function org-link--open-elisp.

Assuming we answer yes to the question raised by calling org-link-elisp-confirm-function in the condition of the if form, the THEN part of the if is evaluated.

As the path equal to "(+ 1 1)" starts by a left parentheses (, the string "(+ 1 1)" is "transformed" into a lisp object by the function read, then is evaluted by the function eval, and its result is substituted in the second placeholder %s in the string of the message function.

Here are the parts of org-link--open-elisp we've just discussed:

(defun org-link--open-elisp (path _)
  "..."
  (if (...)
      (message "%s => %s" path
               (if (eq ?\( (string-to-char path))
                   (eval (read path))
                 (call-interactively (read path))))
    (user-error "Abort")))

And this is how the message (+ 1 1) => 2 is printed in the echo area when we call org-open-at-point on top of the elisp type link [[elisp:(+ 1 1)]].

WE ARE DONE!!!

If this is the first time you have seen the read and eval functions, you may be interested in these examples:

(read "(+ 1 2)") ; (+ 1 2)
(type-of (read "(+ 1 2)")) ; cons
(functionp (read "(+ 1 2)")) ; nil
(type-of (eval (read "(+ 1 2)"))) ; integer

(read "(lambda () (+ 1 2))") ; (lambda nil (+ 1 2))
(type-of (read "(lambda () (+ 1 2))")) ; cons
(functionp (read "(lambda () (+ 1 2))")) ; t
(type-of (eval (read "(lambda () (+ 1 2))"))) ; cons

(read "foo") ; foo
(type-of (read "foo")) ; symbol
(type-of (eval (read "foo"))) ; error: (void-variable foo)

(read "\"bar\"") ; "bar"
(type-of (read "\"bar\"")) ; string
(type-of (eval (read "\"bar\""))) ; string