xunhen

New branches grow beneath the brush; look back, and seek old traces.

See the code you undid.

Undo a change in Neovim, write something else, and the first version leaves the screen. With 'undofile' on, Neovim may still keep it on disk, on a branch of the undo tree that never reached a file or git. xunhen reads that history and shows every state it kept, without changing anything.

Select a node of the example history
Node 0, retained root, time unknown, 3 lines1 package sample2 3 // seed
Node 3 -> node 0: 1 hunk@@ -1,3 +1,3 @@ package sample -func chosen() int { return 1 }+// seed
Node 1, recorded 2026-09-19 00:04:48 CEST, 3 lines1 package sample2 3 // common
Node 3 -> node 1: 1 hunk@@ -1,3 +1,3 @@ package sample -func chosen() int { return 1 }+// common
Node 3, recorded 2026-09-19 00:04:48 CEST, 3 lines1 package sample2 3 func chosen() int { return 1 }
Node 3 -> node 3: identical statesThe two states are identical.
Node 2, recorded 2026-09-19 00:04:48 CEST, 3 lines1 package sample2 3 func experiment() int { return 42 }
Node 3 -> node 2: 1 hunk@@ -1,3 +1,3 @@ package sample -func chosen() int { return 1 }+func experiment() int { return 42 }
Previewing node 0 Comparing node 3 with node 0 Previewing node 1 Comparing node 3 with node 1 Previewing node 3 Comparing node 3 with node 3 Previewing node 2 Comparing node 3 with node 2
Show the selected node's text, or compare it with node 3
A history from xunhen's test corpus, shown as xunhen browse shows it. Node 2 is an experiment that was undone and never saved. Select it, then compare it with node 3, the version that was saved.

The example, step by step

  1. The last line of retry.go became // common. Node 1.
  2. That line became an experiment, func experiment() int { return 42 }. Node 2.
  3. The experiment was undone before the file was ever saved.
  4. A different fix, func chosen() int { return 1 }, was written and saved. Node 3.

Git could only ever have seen node 3. The experiment exists nowhere but the undo file, and xunhen rebuilds it from there.

xunhen show retry.go --node 2
package sample

func experiment() int { return 42 }

Commands

Every command takes the source file and finds its history in the undo directories you name, once in your shell or with --undo-dir:

export XUNHEN_UNDO_DIR="$HOME/.local/state/nvim/undo"
xunhen inspect retry.go
Lists the retained states, their branches, and when each was recorded.
xunhen show retry.go --node 2
Rebuilds one state as text. With --raw, it writes the exact text for a file.
xunhen diff retry.go --from 2 --to 3
Compares two states as a unified diff.
xunhen browse retry.go
Does all three in the terminal, as above.

:echo &undodir in Neovim prints where your undo files are. The usage guide walks through a complete recovery and lists every flag.

xunhen diff retry.go --from 2 --to 3
--- node 2
+++ node 3
@@ -1,3 +1,3 @@
 package sample
 
-func experiment() int { return 42 }
+func chosen() int { return 1 }

What survives

  • Only history Neovim wrote to disk. With 'undofile' on, Neovim writes the undo file each time you save.
  • Edits after the last save, and states older than 'undolevels' keeps, are gone.
  • Rebuilding text needs the source as it was at the last save. The undo file stores changes, not copies of the file.
  • Undo format 3, as Neovim 0.12.5 writes it on Linux x86-64, is the one tested. Other producers are unverified.
  • Text must be UTF-8 with LF line endings. Anything else is refused with the reason.

The troubleshooting guide explains what to do when a state is missing.

Turn on persistent undo

Neovim keeps undo history on disk only with 'undofile' set. Add this to your init.lua:

vim.o.undofile = true

From then on, every save writes the history xunhen reads.

Install

No release has been published yet. Until the first one, build xunhen from a clone with Go 1.27.1:

git clone https://github.com/nuggocto/xunhen.git
cd xunhen
CGO_ENABLED=0 go build -trimpath -o bin/xunhen ./cmd/xunhen

The executable is bin/xunhen. Copy it anywhere on your PATH.

With the first release

The install guide covers each of these, with upgrades and removal: