Skip to content

sh1bot/git-rev.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 

Repository files navigation

gitrev.nvim

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 abc123

How a name is interpreted

The 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):

  1. <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.

  2. <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:.

  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}, and deadbeef are picked up, while HEAD, master, README, v1.2.3, and my-notes are 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.

Paths are loose about your working directory

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 it

It 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.

Deducing the filename

For forms 2 and 3 there is no path, so one is deduced from the surrounding context, in this order:

  1. the alternate file (#),
  2. another window in the current tab page,
  3. the argument list (this is what makes nvim -d file.txt HEAD^1 work),
  4. 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).

Diff companion

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 :q on the real file quits as if the companion weren't there: it exits when clean, and aborts with E37 when 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.

Safety

  • 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 than max_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 + nomodifiable and buftype=nofile, so the historical content can never be accidentally written back to a file literally named HEAD^1.

Performance / robustness

  • No git is run for names that are not revision-shaped.
  • A successful in-fill costs two git calls: one git cat-file --batch-check metadata 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 explicit rev:path that is loosely retried both cwd- and root-relative, which costs at most two probes.
  • Every git call goes through vim.system with a timeout (default 2000 ms), so a slow or hung git can never freeze the editor. vim.system captures 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.

Configuration

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.

Requirements

  • Neovim 0.10+ (for vim.system)
  • git on PATH

Installation

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'

Native packages (no plugin manager)

git clone https://github.com/sh1bot/git-rev.nvim \
  ~/.local/share/nvim/site/pack/plugins/start/git-rev.nvim

Local checkout

To 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.

Layout

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

Tests

test/run.sh   # parser unit checks and end-to-end scenarios (needs nvim + git)

About

neovim plugin to handle git revisions like filenames

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors