Open a git revision as if it were a file.
When you ask Neovim to edit a path that does not exist, this plugin checks whether the name looks like a git revision. If it does, and it resolves to a blob in the repository, the buffer is filled in with that blob's contents, marked read-only, and given the filetype (and therefore syntax highlighting) of the real file it stands in for.
nvim -d file.txt HEAD^1 # diff the working tree against file.txt @ HEAD^1:diffsplit HEAD^1 " diff the current file against its parent revision
:e HEAD:src/main.c " open src/main.c as it is at HEAD, read-only
:e abc123: " open the current file at commit abc123The hard part is telling a revision apart from a filename you are about to create, without being annoying. The rules, cheapest first (no git is run until a name passes the syntactic gate):
-
<rev>:<path>— the standard git blob syntax. Unambiguous. e.g.HEAD:Makefile,HEAD^1:src/main.c,abc123:a/b.txt. An empty<rev>means the index/staging area::staged.txt. -
<rev>:(trailing colon) — the explicit "this is a revision, work out the filename for me" marker. This is the baseline way to force revision interpretation for a name that would otherwise look like a file. e.g.HEAD:,HEAD^1:,v1.2.3:. -
Bare
<rev>(no colon) — treated as a revision only when it is clearly not an ordinary filename, i.e. it is either:- a hex object id (7–64 hex digits, like git's own abbreviations), or
- contains a character git uses in revisions but filenames rarely do:
^ ~ @ { }.
So
HEAD^1,HEAD~3,@{u},main@{yesterday}, anddeadbeefare picked up, whileHEAD,master,README,v1.2.3, andmy-notesare left alone. To open one of those as a revision, add the trailing colon (HEAD:).
In every case the revision must actually resolve to a blob in the git repo. If it does not — no such object, or you are not inside a repository — the plugin does nothing and Neovim goes on to create a normal new file with that name.
Git's native rev:path resolves path relative to the repo root of git's
working directory, which is surprising: from a subdirectory the path is wrong,
and if your cwd is not in a repo at all it fails outright — even when the file
plainly lives in a repo somewhere else.
This plugin treats the path the way an ordinary filename works instead. It
resolves path to a real location relative to your current directory, then lets
git discover the repository that contains that file (not the repo, if any,
at your cwd). So all of these work:
" cwd = repo/src
:e HEAD:main.c " finds src/main.c, no need to type src/main.c
" cwd = repo root
:e HEAD:src/main.c " finds src/main.c
" cwd = $HOME (not a repo at all), file lives under $HOME/project (a repo)
:e HEAD:project/dir/file.txt " discovers the repo at project/ and resolves itIt then falls back to git's repo-root-relative reading from cwd, so a path typed
relative to the repo root from a subdirectory keeps working too. Anchor a path
yourself with ./, ../, or a leading / to pin the meaning.
The same rule powers the deduced forms: :diffsplit HEAD: while editing a file
in a repo resolves against that file's repo, regardless of where your cwd
is.
For forms 2 and 3 there is no path, so one is deduced from the surrounding context, in this order:
- the alternate file (
#), - another window in the current tab page,
- the argument list (this is what makes
nvim -d file.txt HEAD^1work), - any other loaded buffer.
The first real, existing file found lends its name (addressed relative to its
own directory via git's rev:./name syntax, so no repo-root computation is
needed).
When a revision is in-filled as one side of a diff — :diffsplit HEAD^1 or
nvim -d file.txt HEAD^1 — it behaves as a companion to the real file:
- focus returns to the real, editable file, not the read-only revision, and
- a single
:qon the real file quits as if the companion weren't there: it exits when clean, and aborts withE37when the file has unsaved changes — even with'hidden'set — instead of hiding the file and stranding you in the revision.
This works by flipping the companion to an auxiliary buftype=help for the
instant Neovim decides the quit (on QuitPre), then back to nofile; an
auxiliary window isn't counted, so the real window is judged the last one. :q!
and :wq pass straight through.
With other windows open, :q on the real file is a window close rather than an
exit (under 'hidden' the file is hidden, not lost); the companion is closed
along with the real window so it isn't left as an orphaned diff view. Closing the
companion's own window discards its buffer (bufhidden=wipe), so re-issuing
:diffsplit HEAD^1 builds a fresh one.
Only a diff context is affected; a plain :e HEAD:path opens the revision in the
current window as an ordinary read-only buffer. Disable with
diff_companion = false.
- Large files: blobs larger than
max_size(default 10 MiB) are skipped with a warning. The size is checked before the content is read, so a huge blob is never pulled into memory. - Huge line counts: even within
max_size, a blob with more thanmax_lines(default 500 000) lines is skipped. Newlines are counted with an early bail, so an over-cap blob is rejected before the line list is ever built. - Binary files: a blob containing a NUL byte is treated as binary and skipped (git's own signal; also required because a buffer line cannot contain a NUL/newline).
- Read-only: in-filled buffers are
readonly+nomodifiableandbuftype=nofile, so the historical content can never be accidentally written back to a file literally namedHEAD^1.
- No git is run for names that are not revision-shaped.
- A successful in-fill costs two git calls: one
git cat-file --batch-checkmetadata probe, then one blob read (done only after the size guard passes, so a huge blob is never read into memory). A miss costs a single probe — except an explicitrev:paththat is loosely retried both cwd- and root-relative, which costs at most two probes. - Every git call goes through
vim.systemwith atimeout(default 2000 ms), so a slow or hung git can never freeze the editor.vim.systemcaptures raw bytes, so binary content is detected directly (a NUL byte) with no encoding games, and the blob read only ever pulls a size-guarded blob into memory.
The plugin works with no configuration. To change defaults:
require("gitrev").setup({
enabled = true,
max_size = 10 * 1024 * 1024, -- bytes; larger blobs are skipped
max_lines = 500000, -- lines; blobs with more are skipped
timeout = 2000, -- ms; hard ceiling on any git call
min_hex = 7, -- min length for a bare hex token to be an id
notify = true, -- warn when a guard skips a blob
diff_companion = true, -- focus/quit behaviour for diff companions
})An in-filled buffer exposes b:gitrev_object (the resolved rev:path) for use
in a statusline or other tooling.
- Neovim 0.10+ (for
vim.system) gitonPATH
The plugin works out of the box — no setup() call is required. Calling
setup() is only needed to override a default (see
Configuration).
{ "sh1bot/git-rev.nvim" }Or, to override a default:
{
"sh1bot/git-rev.nvim",
opts = {
max_size = 20 * 1024 * 1024,
},
}use("sh1bot/git-rev.nvim")Plug 'sh1bot/git-rev.nvim'git clone https://github.com/sh1bot/git-rev.nvim \
~/.local/share/nvim/site/pack/plugins/start/git-rev.nvimTo develop against a local clone, point your manager at the directory. For example with lazy.nvim:
{ dir = "/path/to/git-rev.nvim" }Or drop the directory into your runtimepath (packpath) — it is a standard
plugin/ + lua/ layout with no build step.
Whichever method you use, make sure the requirements are met:
Neovim 0.10+ and git on your PATH.
lua/gitrev.lua the whole plugin (parser + git layer + in-fill logic)
plugin/gitrev.lua autoload shim: registers the BufNewFile autocmd
test/run.sh parser unit checks + end-to-end scenarios
test/run.sh # parser unit checks and end-to-end scenarios (needs nvim + git)