Open a file in Neovim and step backward/forward through its git history with a key combination, staying positioned on the same function across versions so the change is visible as a diff. This is like vimdiff on steroids for git history viewing.
Both screenshots are real vhist sessions, running on files from this repo's
own history.
- Go 1.21+
gitnvim(Neovim; drives the editor)ctags— must be Universal Ctags, not the BSD ctags shipped with macOS. On macOS:brew install universal-ctags neovim. Without it, vhist still works but falls back to plain line-number navigation instead of tracking the enclosing function.
go build -o vhist ./cmd/vhist./vhist --line 42 path/to/file.goInside Neovim:
Alt-Left(<M-Left>) — jump to the previous revision of the fileAlt-Right(<M-Right>) — jump to the next revisionAlt-Up(<M-Up>) — toggle what each revision is diffed against: its parent commit (default) or the current file on diskAlt-Down(<M-Down>) — toggle line numbers on both panes (or run:VhistNumbers, handy where Alt/Option keys don't reach nvim, e.g. macOS Terminal, whereOption+letter is a dead key):q— quit vhist. The diff is two panes in one Neovim; a lone:q/:q!/:wqis expanded to quit both at once, so a single quit leaves the editor.
(Plain-letter combos like <leader>[ and bare function keys like <F7> were
tried first: <leader>-based ones get silently re-bound by language-server
on_attach hooks or treesitter-textobjects FileType autocmds — a
buffer-local mapping always wins over a global one, so on e.g. Go files
gopls's own binding for the same keys would shadow vhist's; bare function
keys are often intercepted as OS media keys or mangled by the terminal.
Alt+arrow avoids both. vhist also reasserts its binding buffer-locally on
every LspAttach/FileType event, so it can't be shadowed later either. To
check what's bound to a key in a given buffer: :verbose nmap <M-Left>.)
Each jump shows one commit's own change to the file as a vertical diffthis
split: the revision on the right, its immediate predecessor in the file's
history on the left (the oldest revision reads as the file's creation, with an
empty left pane). So stepping back through history shows a different,
self-contained diff at each commit rather than an accumulating diff against
your current file. The cursor is repositioned to the same enclosing
function/symbol when it can be found in ctags output for that revision
(falling back to the same line number otherwise).
Press Alt-Up to switch the left pane to the current file on disk instead of
the parent commit — i.e. "how does this old version differ from what I have
now" — and again to switch back. The toggle redraws the revision already
onscreen, so you keep your place.
For a GitHub-style diff in a browser instead of Neovim:
./vhist --view web path/to/file.goThis starts a small HTTP server bound to 127.0.0.1 on a random port and
opens the page in your default browser. Each commit that touched the file is
a step on a timeline you walk with the ←/→ arrow keys (or the sidebar):
each step shows what that commit changed to the file, the oldest revision
reads as the file's creation, and a final "working tree" step shows any
uncommitted changes against HEAD. Toggle between side-by-side and unified
layouts in the header. Press Ctrl-C in the terminal to stop the server.
The web view is a second frontend over the same history.Provider the editor
uses; it needs no editor and no ctags. Requests carry a per-session token so
another local process can't read your file's history off the port. git is
the diff engine (git diff --no-index), and the renderer
(diff2html) is embedded in the
binary via go:embed, so there is no Node.js, network access, or separate
asset directory at runtime.
v1: git only. Two frontends — Neovim (--view editor, the default) and a
browser diff view (--view web). See internal/history, internal/editor,
internal/symbol, internal/app, and internal/webview for the component
breakdown.

