Link to a git commit from Org mode using Magit | THIS IS EMACS
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:
Really! Emacs, Org, Magit, Buffer, Link! Integration. Non-context switching. Maybe a bit of lisp. Extensible. Yes it makes sense.
AFTER ALL THIS IS EMACS!
Show me how to do it!
The key elements to doing so lie in:
Org mode provides the built-in
elisptype 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-oby default),
and that the command
magit-show-commitused 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:
when followed with org-open-at-point starts the built-in Tetris.
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).
elisp link that matches the regexp
org-link-elisp-skip-confirm-regexp is executed without asking
For instance, setting up the variable
"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
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
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
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
(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
: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:
to use the function org-info-open when we open
to use the function org-info-export when we export open
to use the function org-info-store-link when we store
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
and what are their accepted values.
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)]]
(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
path respectively to
"(+ 1 1)" getting those values out of
(link (:type "elisp" :path "(+ 1 1)" ...)) using the function
type is not equal to any of the following types
"radio" but equal to
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))
(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.
path equal to
"(+ 1 1)" starts by a left parentheses
"(+ 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
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
WE ARE DONE!!!
If this is the first time you have seen the
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