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
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 toC-c C-o
by default),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:
to use the function org-info-open when we open
info
type links,to use the function org-info-export when we export open
info
type links,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