Fringe Matters: Finding the Right Difference

Continuing my avocation of writing to increasingly niche audiences, today we have a matter at the intersection of several small Venn bubbles:

  • the group of Emacs users who code in Emacs,
  • who use Git (or version control) everywhere,
  • who work using offshoot or feature branches,
  • while using the diff-hl package to visually track changes in their buffers.

As minutiae go, this one is quite minute, a real fringe matter. It’s also not a paen to Emacs’ extensibility or composability, because as much as I enjoy beating that drum the feature we’re looking for is actually in plain sight for once.

It’s just really neat though, so I’m going to describe it to you. You’re here now. Settle in.

Rise and shine, Dr. Diffman. Rise and shine.

If you load a file that’s tracked via version control into your editor, most modern text or code editors will indicate in the buffer the bits that have been modified since the last commit, i.e. the last set of registered changes. Here’s the Zed editor showing this via yellow/red marks in the fringe to the left of the text:

In Emacs this is provided by Dmitry Gutov’s diff-hl or Shohei Yoshida’s git-gutter packages, with the former pictured here:

This inline-indication business usually goes much further, with editors annotating lines with when they were last changed, or whose fault it was. My relatively tiny code or prose projects with 2-4 participants (at most) have little need for these fancier features. Emacs makes it easy enough to generate them when required anyway vc-region-history (C-x v h) and vc-annotate (C-x v g), respectively – no Magit required. .

But diff-hl is mighty handy. Being able to identify changed regions at a glance is good, but you can also navigate and act on them. Jump between hunks with one key, see changes inline, mark or revert hunks and more:

Play by play

The keys being pressed are indicated in the header.

  • With diff-hl-mode enabled in the buffer,
  • Run diff-hl-next-hunk (C-x v n). This moves the cursor to the next hunk,
  • and it activates a repeat-map, making subsequent diff-hl commands accessible with just n, p and so on.
  • Press * (shortened from C-x v * by repeat-mode) to show inline diffs of changed hunks.
  • After navigating some more, press SPC (shortened from C-x v SPC by repeat-mode) to mark a hunk.

The right diff in the wrong place…

There’s only one problem: when working on a branch with many commits, diff-hl tracks changes relative to the HEAD of the branch. This is almost never what I want when I’m working on a feature off in a different branch. Representing each commit as a sphere, here’s what I mean:

At this stage, the change from feature / HEAD to the worktree is at best an incomplete picture. Unless looking for something very specific, it doesn’t help to highlight worktree changes against earlier commits on feature either. The previous commits in this branch are temporary checkpoints in an experiment, and don’t necessarily represent atomic, self-contained or meaningful changes – hence the jumble of colors in each commit in the figure. What I find actually helpful is to compare the contents of the buffer to the state of the main branch:

This is, of course, readily accessible via Magit or VC via a couple of keystrokes. Either one can produce pleasingly formatted diff buffers showing the right changes for any range. But as mentioned above, being able to have the right changes indicated in the work buffer is only one of the advantages. With diff-hl and the right range of changes indicated, you can jump between them, mark them, revert them and more.

Recreating the commits in feature to encode atomic – or at least limited – changes is on the docket, but for later. This involves “unscrambling” the commits into uniformly colored spheres, often via an interactive rebase, or just selective git-resets:

Usually the true “shape” of the changes can only be limned at the end of the feature experiment, at which point a “logical” history can be forged before merging into the main branch using your preferred strategy Unless your preferred strategy is to squash all the carefully recreated, atomic commits into a giant merge commit. Then your preferred strategy is wrong. .

…can make all the difference in the world.

As it turns out diff-hl understands this problem to a limited extent. It provides a secondary minor-mode, diff-hl-amend-mode, that shows the change markers with respect to HEAD^, the parent of HEAD. This is typically what you want when your changes are intended to amend the latest commit instead of creating a new one. While it’s not what we’re looking for, it’s a good starting point.

But the mechanism is simple enough. In this pseudo Elisp block:

(show-changes-between from-commit current-buffer-state)

we’d like a way to set from-commit, but just for this buffer or project. If you use diff-hl-amend-mode, from-commit would be HEAD^. If we can set it to master or master^ (or main), we’re done.

And set it we can. from-commit is actually a single global variable conveniently exposed by diff-hl: diff-hl-reference-revision. We can deal with the globality by making it buffer local. And that’s it as far as the Elisp hacking goes – no hooks or advice needed today.

Not that I wish to imply you have been sleeping on the margins.

Only there’s still the matter of the interface. The absolute spartan way of using this option would be to run M-x eval-expression (M-:) and type in

(setq-local diff-hl-reference-revision "master")

On later uses, you can search the minibuffer history for this code via minibuffer-complete-history (M-r). After a few weeks of this, the mild frustration mounts until it crosses a threshold: I should really put this somewhere more convenient.

The logical next step is to define a command that does this. diff-hl actually includes one It’s diff-hl-set-reference-rev , but that sets it globally, across Emacs. So we sigh and grow our configuration files a little bit:

(defun my/diff-hl-set-reference ()
  "Set the reference revision for showing diff-hl changes.

Do so buffer-locally."
  (interactive)
  (setq-local
   diff-hl-reference-revision
   (read-string
    (format "Set reference revision (buffer %s): "
            (buffer-name))))
  (diff-hl-update))

Dust your hands, the problem’s solved. M-x my/diff-hl-set-reference works pleasingly for a few weeks, especially since the minibuffer completion system floats this up with frequent usage You do use savehist-mode or prescient, right? for easy access.

But eventually two further needs appear:

  1. I use this so often that I need it on a key, dammit.
  2. Can’t I just set this across my project instead of in every buffer?

Needs 1 and 2 obviate each other somewhat, in that either one makes it less necessary to call the -set-reference command via M-x. But let’s address them separately anyway.

All the effort in the world would have gone to waste until…

Setting up a keybind is easy if we can find room for it. And then if we can remember it. Emacs keymaps are crowded spaces. On more than one occasion I’ve opened up my init file intending to add a handy keybinding for a useful command, only to discover that I already did months ago… and then forgot it was available.

To some extent you can mitigate this effect by placing it logically with the other diff-hl commands, which are conveniently grouped together into diff-hl-command-map. So assuming you use any diff-hl commands, this one should show up in which-key or your keymap prompter of choice.

But in my case even this is a lost cause. I already combine vc-prefix-map, used by Emacs for version control actions, with diff-hl-command-map under the C-x v prefix because their functions are related. So C-x v is quite a busy junction. Here’s which-key doing its best to show me what’s available:

The solution is to place it where I turn on/off diff-hl-mode in the first place: in a catchall transient menu I use to access all minor-modes I need in Emacs buffers A good chunk of this menu deals with settings that make no sense for most Emacs users – for example, a switch to toggle requiring sentence ends to be single/double spaced, adjust line-spacing on the fly, or switch between different sets of CAPFs.

Calling the diff-hl option from this menu with a prefix-arg now (essentially) runs the above function, allowing me to set the reference revision before turning on the mode (if required).

Here’s a demo combining this with a few other nifty diff-hl commands and repeat-mode:

Play by play

The keys being pressed are indicated at the top.

  • Begin in a buffer in some feature branch that is some “distance” away from master.
  • Use diff-hl commands to navigate to hunks. diff-hl is currently showing buffer changes in the working tree against HEAD.
  • Use diff-hl-show-hunk (C-x v *) to see the diffs for individual hunks inline.
  • Call the toggle-modes transient mentioned above.
  • Choose the g option with a prefix-arg (C-u). This causes the reference revision (the from-commit we want) to be read from the minibuffer. Ask to see changes from master.
  • diff-hl updates to mark changes from master. As you might expect, there are more changes – everything that’s changed since master.
  • Navigate through the changes again with diff-hl commands.
  • Pick the toggle-modes transient menu option again, this time setting the reference revision to HEAD’s grandparent, HEAD^^.
  • diff-hl updates the changes again. As you might expect, there are fewer changes.

Here’s the actual function that g in the toggle-modes menu is bound to:

(lambda (&optional arg)
  "Toggle `diff-hl-mode'.

With prefix ARG ensure `diff-hl-mode' and set revision to compare
against."
  (interactive "P")
  (if (null arg)
      (diff-hl-mode 'toggle)
    (let ((ref (read-string "Reference revision for diff-hl: ")))
      (setq-local diff-hl-reference-revision ref)
      (diff-hl-mode) (diff-hl-update)
      (message "Showing changes against %s" ref))))

…well, let’s just say your hour has come again.

As you might expect, the way to apply diff-hl’s reference revision setting to all buffers in a project is to use a directory-local variable. In most cases, setting it once per project (or git-worktree, in my case) is all you need.

This approach requires a little forethought but no moment-to-moment busywork: Run M-x add-dir-local-variable

  • with a major-mode choice of nil,
  • and setting the variable diff-hl-reference-revision
  • to "master" .

This produces the following .dir-locals.el file:

;;; Directory Local Variables            -*- no-byte-compile: t -*-
;;; For more information see (info "(emacs) Directory Variables")

((nil . ((diff-hl-reference-revision . "master"))))

Typically you would want to chuck this file into version control as well – but in this case that may be something you want to consider carefully!

So, wake up, Dr. Diffman. Wake up and mark the fringes.

Before I thought to adapt diff-hl to how I worked, I was coding in feature branches in a strange way. I would make a bunch of work-in-progress checkpoint commits and store them on a remote, then soft-reset the branch to the original branch point so I could see indicators for all changed regions in buffers, jump between them with diff-hl-next-hunk and so on.

It took a while a “while” is code for years to consider that it didn’t have to be this way. This isn’t an adjustment that requires Emacs’ much vaunted flexibility – you can probably do this in any editor that provides the diff-hl feature. I just never thought to do it, and worked around the tool instead of making the tool work for me. Well that’s sorted.

There is no other upshot. This yak has been shaved. You’re free now, go mark some fringes.