Obscure Emacs package appreciation: backup-walker

| emacs

The Emacs Carnival theme for September is obscure packages, which made me think of how the backup-walker package saved me from having to write some code all over again. Something went wrong when I was editing my config in Org Mode. I probably accidentally deleted a subtree due to over-enthusiastic speed commands. (… Maybe I should make my k shortcut for my-org-cut-subtree-or-list-item only work in my Inbox.org and news.org files.) Chunks of my literate Emacs configuration were gone, including the code that defined my-org-insert-link-dwim. Before I noticed, I'd already exported my (now slightly shorter) Emacs configuration file with org-babel-tangle and restarted Emacs. I couldn't recover the definition from memory using symbol-function. I couldn't use vundo to browse the Emacs undo tree. As usual, I'd been neglecting to commit my config changes to Git, so I couldn't restore a previous version. Oops.

Well, not the first time I've needed to rewrite code from scratch because of a brain hiccup. I started to reimplement the function. Then I remembered that I had other backups. I have a 2 TB SSD in my laptop, and I had configured Emacs to neatly save numbered backups in a separate directory, keeping all the versions without deleting any of the old ones.

(setq backup-directory-alist '(("\\.env$" . nil)
                               ("." . "~/.config/emacs/backups")))
(with-eval-after-load 'tramp
  (setq tramp-backup-directory-alist nil))
(setq delete-old-versions -1)
(setq version-control t)
(setq auto-save-file-name-transforms '((".*" "~/.config/emacs/auto-save-list/" t)))

At the moment, there are about 12,633 files adding up to 3 GB. Totally worth it for peace of mind. I could probably use grep to search for the function, but it wasn't easy to see what changed between versions.

I had learned about backup-walker in the process of writing about Thinking about time travel with the Emacs text editor, Org Mode, and backups. So I used backup-walker to flip through my file's numbered backups in much the same way that git-timemachine lets you flip through Git versions of a file. After M-x backup-walker-start, I tapped p to go through the previous backups. The diff it showed me made it easy to check with C-s (isearch-forward) if this was the version I was looking for. When I found the change, I pressed RET to load the version with the function in it. Once I found it, it was easy to restore that section. I also restored a couple of other sections that I'd accidentally deleted too, like the custom plain text publishing backend I use to export Emacs News with less punctuation. It took maybe 5 minutes to figure this out. Hooray for backup-walker!

Note that the backup-walker diff was the other way around from what I expected. It goes "diff new old" instead of "diff old new", so the green regions marked with + indicate stuff that was removed by the newer version (compared to the one a little older than it) and the red regions marked with - indicate stuff that was added. This could be useful if you think backwards in time, kind of like the Emacs Antinews file, but my mind doesn't quite work that way. I wanted it to look like a regular diff, with the additions in newer versions marked with +. Emacs being Emacs, I changed it. Here's an example showing what it looks like now:

2025-09-17_13-46-12.png
Figure 1: backup-walker diffs going the direction I want them to: additions (+) marked in green, deletions (-) in red

The following code makes it behave the way I expect:

(defun my-backup-walker-refresh ()
  (let* ((index (cdr (assq :index backup-walker-data-alist)))
         (suffixes (cdr (assq :backup-suffix-list backup-walker-data-alist)))
         (prefix (cdr (assq :backup-prefix backup-walker-data-alist)))
         (right-file (concat prefix (nth index suffixes)))
         (right-version (format "%i" (backup-walker-get-version right-file)))
         diff-buff left-file left-version)
    (if (eq index 0)
        (setq left-file (cdr (assq :original-file backup-walker-data-alist))
              left-version "orig")
      (setq left-file (concat prefix (nth (1- index) suffixes))
            left-version (format "%i" (backup-walker-get-version left-file))))
    ;; we change this to go the other way here
    (setq diff-buf (diff-no-select right-file left-file nil 'noasync))
    (setq buffer-read-only nil)
    (delete-region (point-min) (point-max))
    (insert-buffer diff-buf)
    (set-buffer-modified-p nil)
    (setq buffer-read-only t)
    (force-mode-line-update)
    (setq header-line-format
          (concat (format "{{ ~%s~ → ~%s~ }} "
                          (propertize left-version 'face 'font-lock-variable-name-face)
                          (propertize right-version 'face 'font-lock-variable-name-face))
                  (if (nth (1+ index) suffixes)
                      (concat (propertize "<p>" 'face 'italic)
                              " ~"
                              (propertize (int-to-string
                                           (backup-walker-get-version (nth (1+ index) suffixes)))
                                          'face 'font-lock-keyword-face)
                              "~ ")
                    "")
                  (if (eq index 0)
                      ""
                    (concat (propertize "<n>" 'face 'italic)
                            " ~"
                            (propertize (int-to-string (backup-walker-get-version (nth (1- index) suffixes)))
                                        'face 'font-lock-keyword-face)
                            "~ "))
                  (propertize "<return>" 'face 'italic)
                  " open ~"
                  (propertize (propertize (int-to-string (backup-walker-get-version right-file))
                                          'face 'font-lock-keyword-face))
                  "~"))
    (kill-buffer diff-buf)))
(with-eval-after-load 'backup-walker
  (advice-add 'backup-walker-refresh :override #'my-backup-walker-refresh))

backup-walker is not actually a real package in the sense of M-x package-install, but fortunately, recent Emacs makes it easier to install from a repository. I needed to install it from https://github.com/lewang/backup-walker. It was written so long ago that I needed to defalias some functions that were removed in Emacs 26.1. Here's the use-package snippet from my configuration:

(use-package backup-walker
  :vc (:url "https://github.com/lewang/backup-walker")
  :commands backup-walker-start
  :init
  (defalias 'string-to-int 'string-to-number)  ; removed in 26.1
  (defalias 'display-buffer-other-window 'display-buffer))

So there's an obscure package recommendation: backup-walker. It hasn't been updated for more than a decade, and it's not even installable the regular way, but it's still handy.

I can imagine all sorts of ways this workflow could be even better. It might be nice to dust off backup-walker off, switch out the obsolete functions, add an option for the diff direction, and maybe sort things out so that you can reverse the diff, split hunks, and apply hunks to your original file. And maybe a way to walk the backup history for changes in a specific region? I suppose someone could make a spiffy Transient-based user interface to modernize it. But it's fine, it works. Maybe there's a more modern equivalent, but I didn't see anything in a quick search of M-x list-packages / N (package-menu-filter-by-name-or-description) for "backup~, except maybe vc-backup. (The original repo is missing, but you can read it via ELPA's copy.) Is there a general-purpose VC equivalent to git-timemachine? That might be useful.

I should really be saving things in proper version control, but this was a good backup. That reminds me: I should backup my backup backups. I had initially excluded my ~/.config directory from borgbackup because of the extra bits and bobs that I wouldn't need when restoring from backup (like all the Emacs packages I'd just re-download). But my file backups… Yeah, that's worth it. I changed my --exclude-from to --patterns-from and changing my borg-patterns file to look like this:

+ /home/sacha/.config/emacs/backups
- /home/sacha/.config/*
# ... other rules

May backup-walker save you from a future oops!

This is part of my Emacs configuration.
View org source for this post