refactor(git): load git:// URI buffers via BufReadCmd

This commit is contained in:
2026-04-28 07:34:07 +02:00
parent 4f7edfa184
commit f8cf18a2c0
5 changed files with 127 additions and 95 deletions
+79 -63
View File
@@ -59,33 +59,58 @@ local function attach_index_writer(buf, worktree, path)
})
end
---Internal builder: create a scratch buffer and fill it with the output of
---`git show <revspec>`. Synchronous so the buffer is ready by the time the
---caller wires up windows / `:diffthis`. An empty buffer briefly visible
---to the diff engine produces a spurious whole-file diff.
---Return a buffer holding the content at `<ref>:<path>`. `ref` is
---"index", "HEAD", or a commit revspec.
---@param worktree string
---@param revspec string
---@param is_index boolean
---@param index_path string? required when is_index is true
---@param ref string
---@param path string
---@return integer
local function build_show_buf(worktree, revspec, is_index, index_path)
local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].buftype = "nofile"
vim.bo[buf].bufhidden = "wipe"
vim.bo[buf].swapfile = false
function M.git_show_buf(worktree, ref, path)
local name = "git://" .. ref .. "//" .. path
local buf = vim.fn.bufadd(name)
vim.b[buf].git_worktree = worktree
vim.fn.bufload(buf)
return buf
end
---BufReadCmd handler for `git://<ref>//<path>` URIs. Worktree comes from
---`vim.b[buf].git_worktree` if set, else from cwd. Ref of "index" maps
---to `git show :<path>`; "worktree" leaves the buffer empty (placeholder
---for missing files); anything else is a revspec.
---@param buf integer
function M.read_uri(buf)
local name = vim.api.nvim_buf_get_name(buf)
local ref, path = name:match("^git://(.-)//(.*)$")
if not ref or path == "" then
return
end
---@cast path -nil
local worktree = vim.b[buf].git_worktree or select(2, repo.resolve_cwd())
if not worktree then
log.error("git BufReadCmd %s: cannot resolve worktree", name)
return
end
vim.b[buf].git_worktree = worktree
vim.bo[buf].swapfile = false
vim.bo[buf].bufhidden = "wipe"
if ref == "worktree" then
vim.bo[buf].buftype = "nofile"
vim.bo[buf].modifiable = false
vim.bo[buf].modified = false
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf })
return
end
local revspec = ref == "index" and (":" .. path) or (ref .. ":" .. path)
local result = vim.system(
{ "git", "show", revspec },
{ cwd = worktree, text = true }
)
:wait()
if result.code ~= 0 then
log.error(
"git show %s failed: %s",
revspec,
vim.trim(result.stderr or "")
)
else
if result.code == 0 then
vim.api.nvim_buf_set_lines(
buf,
0,
@@ -93,45 +118,51 @@ local function build_show_buf(worktree, revspec, is_index, index_path)
false,
util.split_lines(result.stdout or "")
)
else
log.error(
"git show %s failed: %s",
revspec,
vim.trim(result.stderr or "")
)
end
if is_index then
if ref == "index" then
vim.bo[buf].buftype = "acwrite"
attach_index_writer(buf, worktree, assert(index_path))
-- Re-running BufReadCmd (e.g. on `:edit`) would otherwise stack
-- another BufWriteCmd on the same buffer, so each `:w` runs
-- hash-object + update-index N times.
if not vim.b[buf].git_index_writer then
attach_index_writer(buf, worktree, path)
vim.b[buf].git_index_writer = true
end
else
vim.bo[buf].buftype = "nofile"
vim.bo[buf].modifiable = false
end
vim.bo[buf].modified = false
return buf
end
---@param worktree string
---@param ref string '' for index, 'HEAD' or a sha for committed refs
---@param path string
---@param is_index boolean? true to hook :w to update the git index
---@return integer
function M.git_show_buf(worktree, ref, path, is_index)
return build_show_buf(worktree, ref .. ":" .. path, is_index or false, path)
end
---@param worktree string
---@param blob string the blob SHA (full or abbreviated)
---@return integer
function M.git_show_blob(worktree, blob)
return build_show_buf(worktree, blob, false, nil)
-- BufReadCmd suppresses the normal BufReadPost dispatch, so filetype
-- detection and modeline parsing don't run unless we fire it ourselves.
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf })
end
---@class ow.Git.EmptyBufOpts
---@field name string?
---@field bufhidden ("hide"|"wipe")? defaults to "wipe"
---Build a read-only scratch buffer, optionally naming it via
---`nvim_buf_set_name` (silently no-op if a buffer with that name
---already exists).
---Build a read-only scratch buffer, optionally naming it. When `opts.name`
---is set and a loaded buffer with that name already exists, returns it
---instead of creating a duplicate.
---@param opts ow.Git.EmptyBufOpts?
---@return integer
function M.empty_buf(opts)
opts = opts or {}
if opts.name then
local existing = vim.fn.bufnr(opts.name)
if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then
return existing
end
end
local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].buftype = "nofile"
vim.bo[buf].bufhidden = opts.bufhidden or "wipe"
@@ -215,30 +246,15 @@ function M.split(opts)
return
end
local is_index = opts.ref == ""
local other = M.git_show_buf(worktree, opts.ref, rel, is_index)
local label = is_index and "index" or opts.ref
M.set_buf_name_and_filetype(other, "git://" .. label .. "/" .. rel)
local label = opts.ref == "" and "index" or opts.ref
local uri = "git://" .. label .. "//" .. rel
-- Stash the worktree on the buffer so the BufReadCmd handler doesn't
-- fall back to cwd resolution (wrong when cwd != worktree).
local buf = vim.fn.bufadd(uri)
vim.b[buf].git_worktree = worktree
local cur_win = vim.api.nvim_get_current_win()
local other_win = vim.api.nvim_open_win(other, true, {
split = opts.vertical and "left" or "above",
win = cur_win,
})
-- The synthetic index/HEAD buffer can't run BufRead, so its filetype
-- detection in `set_buf_name_and_filetype` only catches
-- filename-pattern matches. Mirror cur_buf's filetype, since this is
-- the same logical file at a different version. Done after the
-- window opens so any BufWinEnter / BufEnter autocmds that fire on
-- nvim_open_win can't undo it.
local cur_ft = vim.bo[cur_buf].filetype
if cur_ft ~= "" then
vim.bo[other].filetype = cur_ft
end
M.set_diff(cur_win, true)
M.set_diff(other_win, true)
local prefix = opts.vertical and "leftabove vert " or "leftabove "
vim.cmd(prefix .. "diffsplit " .. vim.fn.fnameescape(uri))
end
return M