refactor(git): load git:// URI buffers via BufReadCmd
This commit is contained in:
+30
-1
@@ -68,10 +68,39 @@ local function first_positional(args, start)
|
||||
end
|
||||
end
|
||||
|
||||
---Open `<ref>:<path>` in a split via the `git://` BufReadCmd loader.
|
||||
---Resolves to a sha first so the URI stays stable if the ref moves.
|
||||
---@param worktree string
|
||||
---@param user_ref string
|
||||
---@param path string
|
||||
local function show_file_in_split(worktree, user_ref, path)
|
||||
repo.rev_parse(worktree, user_ref, true, function(sha)
|
||||
local label = sha or user_ref
|
||||
local uri = "git://" .. label .. "//" .. path
|
||||
local buf = vim.fn.bufadd(uri)
|
||||
vim.b[buf].git_worktree = worktree
|
||||
vim.cmd("split " .. vim.fn.fnameescape(uri))
|
||||
end)
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param args string[]
|
||||
---@param conf ow.Git.SplitHandler
|
||||
local function run_in_split(worktree, args, conf)
|
||||
-- `<ref>:<path>` is a file lookup; the URI must carry the path so
|
||||
-- filetype detection has something to match against.
|
||||
if args[1] == "show" then
|
||||
local arg = first_positional(args, 2)
|
||||
if arg then
|
||||
local ref, path = arg:match("^(.-):(.+)$")
|
||||
if ref then
|
||||
---@cast path -nil
|
||||
show_file_in_split(worktree, ref, path)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local buf = git.new_scratch()
|
||||
vim.b[buf].git_worktree = worktree
|
||||
if conf.needs_ref then
|
||||
@@ -80,7 +109,7 @@ local function run_in_split(worktree, args, conf)
|
||||
if not vim.api.nvim_buf_is_valid(buf) then
|
||||
return
|
||||
end
|
||||
pcall(vim.api.nvim_buf_set_name, buf, "git://" .. label .. "/")
|
||||
pcall(vim.api.nvim_buf_set_name, buf, "git://" .. label .. "//")
|
||||
vim.bo[buf].filetype = conf.ft
|
||||
end
|
||||
repo.rev_parse(worktree, user_ref, true, function(sha)
|
||||
|
||||
+79
-63
@@ -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
|
||||
|
||||
+8
-1
@@ -58,12 +58,19 @@ function M.setup()
|
||||
end
|
||||
vim.filetype.add({
|
||||
pattern = {
|
||||
["git://[^/]+/(.+)"] = function(_, bufnr, inner)
|
||||
["git://.-//(.+)"] = function(_, bufnr, inner)
|
||||
return vim.filetype.match({ filename = inner, buf = bufnr })
|
||||
end,
|
||||
},
|
||||
})
|
||||
local group = vim.api.nvim_create_augroup("ow.git", { clear = true })
|
||||
vim.api.nvim_create_autocmd("BufReadCmd", {
|
||||
pattern = "git://*",
|
||||
group = group,
|
||||
callback = function(args)
|
||||
require("git.diff").read_uri(args.buf)
|
||||
end,
|
||||
})
|
||||
vim.api.nvim_create_autocmd(
|
||||
{ "BufReadPost", "BufNewFile", "BufWritePost", "FileChangedShellPost" },
|
||||
{
|
||||
|
||||
+5
-16
@@ -80,30 +80,19 @@ local function is_zero(sha)
|
||||
return sha == nil or sha:match("^0+$") ~= nil
|
||||
end
|
||||
|
||||
---Build a buffer holding the file's content at a given blob, named after the
|
||||
---commit ref it corresponds to (so the name lines up with `git log` output
|
||||
---instead of an opaque blob hash).
|
||||
---Buffer for the file at `<ref>:<path>`. A zero/nil blob (file absent on
|
||||
---this side of the diff) yields an empty placeholder.
|
||||
---@param worktree string
|
||||
---@param blob string?
|
||||
---@param path string
|
||||
---@param ref string the commit ref the blob represents (e.g. `<sha>` or `<sha>^`)
|
||||
---@return integer
|
||||
local function blob_buf(worktree, blob, path, ref)
|
||||
local name = "git://" .. ref .. "/" .. path
|
||||
-- Reuse an existing buffer with this name (commit refs and blobs are
|
||||
-- immutable, so the content is stable); avoids E95 collisions and
|
||||
-- prevents accumulating duplicate buffers as the user navigates.
|
||||
local existing = vim.fn.bufnr(name)
|
||||
if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then
|
||||
return existing
|
||||
end
|
||||
local name = "git://" .. ref .. "//" .. path
|
||||
if is_zero(blob) then
|
||||
return diff.empty_buf({ name = name, bufhidden = "hide" })
|
||||
end
|
||||
---@cast blob string
|
||||
local buf = diff.git_show_blob(worktree, blob)
|
||||
diff.set_buf_name_and_filetype(buf, name)
|
||||
return buf
|
||||
return diff.git_show_buf(worktree, ref, path)
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
@@ -143,7 +132,7 @@ end
|
||||
function M.open_commit(worktree, ref)
|
||||
repo.rev_parse(worktree, ref, true, function(resolved)
|
||||
local sha = resolved or ref
|
||||
local name = "git://" .. sha .. "/"
|
||||
local name = "git://" .. sha .. "//"
|
||||
-- Reuse a previously-opened buffer for the same commit; commit SHAs
|
||||
-- are immutable so the content is stable.
|
||||
local existing = vim.fn.bufnr(name)
|
||||
|
||||
+5
-14
@@ -485,14 +485,14 @@ end
|
||||
local function head_pane(worktree, path)
|
||||
return {
|
||||
buf = diff.git_show_buf(worktree, "HEAD", path),
|
||||
name = "git://HEAD/" .. path,
|
||||
name = "git://HEAD//" .. path,
|
||||
}
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return ow.Git.DiffSide
|
||||
local function head_empty_pane(path)
|
||||
return { buf = diff.empty_buf(), name = "git://HEAD/" .. path }
|
||||
return { buf = diff.empty_buf(), name = "git://HEAD//" .. path }
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
@@ -508,7 +508,7 @@ end
|
||||
---@param path string
|
||||
---@return ow.Git.DiffSide
|
||||
local function worktree_empty_pane(path)
|
||||
return { buf = diff.empty_buf(), name = "git://worktree/" .. path }
|
||||
return { buf = diff.empty_buf(), name = "git://worktree//" .. path }
|
||||
end
|
||||
|
||||
---@param s ow.Git.SidebarState
|
||||
@@ -520,9 +520,9 @@ local function index_pane(s, entry)
|
||||
or (entry.section == "Staged" and entry.x == "D")
|
||||
)
|
||||
return {
|
||||
buf = in_index and diff.git_show_buf(s.worktree, "", entry.path, true)
|
||||
buf = in_index and diff.git_show_buf(s.worktree, "index", entry.path)
|
||||
or diff.empty_buf(),
|
||||
name = "git://index/" .. entry.path,
|
||||
name = "git://index//" .. entry.path,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -727,15 +727,6 @@ local function show_diff(s, entry, focus_left)
|
||||
diff.set_buf_name_and_filetype(side.buf, side.name)
|
||||
end
|
||||
end
|
||||
-- Synthetic index/HEAD buffers never run BufRead, so modeline-only
|
||||
-- filetypes aren't detected. Copy from the side that did resolve.
|
||||
local left_ft = vim.bo[pair.left.buf].filetype
|
||||
local right_ft = vim.bo[pair.right.buf].filetype
|
||||
if left_ft == "" and right_ft ~= "" then
|
||||
vim.bo[pair.left.buf].filetype = right_ft
|
||||
elseif right_ft == "" and left_ft ~= "" then
|
||||
vim.bo[pair.right.buf].filetype = left_ft
|
||||
end
|
||||
diff.set_diff(left_win, true)
|
||||
diff.set_diff(right_win, true)
|
||||
s.last_shown_key = key
|
||||
|
||||
Reference in New Issue
Block a user