refactor(git): URI scheme is now a git revspec, loaded via cat-file -p

This commit is contained in:
2026-04-28 13:20:38 +02:00
parent a0c87b9b7b
commit 68f2ad1b52
6 changed files with 55 additions and 56 deletions
+2 -6
View File
@@ -74,7 +74,7 @@ end
---@param path string ---@param path string
local function show_file_in_split(worktree, user_ref, path) local function show_file_in_split(worktree, user_ref, path)
local label = repo.rev_parse(worktree, user_ref, true) or user_ref local label = repo.rev_parse(worktree, user_ref, true) or user_ref
local uri = "git://" .. label .. "//" .. path local uri = "git://" .. label .. ":" .. path
local buf = vim.fn.bufadd(uri) local buf = vim.fn.bufadd(uri)
vim.b[buf].git_worktree = worktree vim.b[buf].git_worktree = worktree
vim.cmd("split " .. vim.fn.fnameescape(uri)) vim.cmd("split " .. vim.fn.fnameescape(uri))
@@ -119,11 +119,7 @@ local function run_in_split(worktree, args, conf)
vim.b[buf].git_parent_ref = vim.b[buf].git_parent_ref =
repo.rev_parse(worktree, user_ref .. "^", true) repo.rev_parse(worktree, user_ref .. "^", true)
end end
pcall( pcall(vim.api.nvim_buf_set_name, buf, "git://" .. (sha or user_ref))
vim.api.nvim_buf_set_name,
buf,
"git://" .. (sha or user_ref) .. "//"
)
vim.bo[buf].filetype = conf.ft vim.bo[buf].filetype = conf.ft
else else
vim.bo[buf].filetype = conf.ft vim.bo[buf].filetype = conf.ft
+26 -27
View File
@@ -57,32 +57,31 @@ local function attach_index_writer(buf, worktree, path)
}) })
end end
---Return a buffer holding the content at `<ref>:<path>`. `ref` is ---Return a buffer holding the content addressed by a git revspec. The
---"index", "HEAD", or a commit revspec. ---URI is `git://<revspec>` and BufReadCmd loads via `git cat-file -p`.
---@param worktree string ---@param worktree string
---@param ref string ---@param revspec string any revspec git understands (e.g. `HEAD:foo`, `:foo`, `:1:foo`, `<sha>`, `<sha>:foo`)
---@param path string
---@return integer ---@return integer
function M.git_show_buf(worktree, ref, path) function M.git_show_buf(worktree, revspec)
local name = "git://" .. ref .. "//" .. path local name = "git://" .. revspec
local buf = vim.fn.bufadd(name) local buf = vim.fn.bufadd(name)
vim.b[buf].git_worktree = worktree vim.b[buf].git_worktree = worktree
vim.fn.bufload(buf) vim.fn.bufload(buf)
return buf return buf
end end
---BufReadCmd handler for `git://<ref>//<path>` URIs. Worktree comes from ---BufReadCmd handler for `git://<revspec>` URIs. Loads content via
---`vim.b[buf].git_worktree` if set, else from cwd. Ref of "index" maps ---`git cat-file -p <revspec>`. Worktree comes from `vim.b[buf]
---to `git show :<path>`; "worktree" leaves the buffer empty (placeholder ---.git_worktree` if set, else from cwd. Index entries (revspec form
---for missing files); anything else is a revspec. ---`:<path>` for stage 0) are made writable via `attach_index_writer`,
---so `:w` updates the index. Other revspecs are read-only.
---@param buf integer ---@param buf integer
function M.read_uri(buf) function M.read_uri(buf)
local name = vim.api.nvim_buf_get_name(buf) local name = vim.api.nvim_buf_get_name(buf)
local ref, path = name:match("^git://(.-)//(.*)$") local revspec = name:match("^git://(.+)$")
if not ref or path == "" then if not revspec then
return return
end end
---@cast path -nil
local worktree = vim.b[buf].git_worktree or select(2, repo.resolve_cwd()) local worktree = vim.b[buf].git_worktree or select(2, repo.resolve_cwd())
if not worktree then if not worktree then
@@ -94,27 +93,26 @@ function M.read_uri(buf)
vim.bo[buf].swapfile = false vim.bo[buf].swapfile = false
vim.bo[buf].bufhidden = "wipe" vim.bo[buf].bufhidden = "wipe"
if ref == "worktree" then local stdout = util.exec(
vim.bo[buf].buftype = "nofile" { "git", "cat-file", "-p", revspec },
vim.bo[buf].modifiable = false { cwd = worktree }
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 stdout = util.exec({ "git", "show", revspec }, { cwd = worktree })
if stdout then if stdout then
vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout)) vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout))
end end
if ref == "index" then -- Stage-0 index entries (`:<path>` with no further `:`) are
-- editable; `:w` rewrites the index entry via `attach_index_writer`.
-- Anything else (HEAD:, <sha>:, :1:, :2:, :3:, bare object refs)
-- is read-only.
local index_path = revspec:match("^:([^:]+)$")
if index_path then
vim.bo[buf].buftype = "acwrite" vim.bo[buf].buftype = "acwrite"
-- Re-running BufReadCmd (e.g. on `:edit`) would otherwise stack -- Re-running BufReadCmd (e.g. on `:edit`) would otherwise stack
-- another BufWriteCmd on the same buffer, so each `:w` runs -- another BufWriteCmd on the same buffer, so each `:w` runs
-- hash-object + update-index N times. -- hash-object + update-index N times.
if not vim.b[buf].git_index_writer then if not vim.b[buf].git_index_writer then
attach_index_writer(buf, worktree, path) attach_index_writer(buf, worktree, index_path)
vim.b[buf].git_index_writer = true vim.b[buf].git_index_writer = true
end end
else else
@@ -221,8 +219,9 @@ function M.split(opts)
return return
end end
local label = opts.ref == "" and "index" or opts.ref -- Stage 0 (index) is `:<path>`; named refs are `<ref>:<path>`.
local uri = "git://" .. label .. "//" .. rel local revspec = opts.ref == "" and (":" .. rel) or (opts.ref .. ":" .. rel)
local uri = "git://" .. revspec
-- Stash the worktree on the buffer so the BufReadCmd handler doesn't -- Stash the worktree on the buffer so the BufReadCmd handler doesn't
-- fall back to cwd resolution (wrong when cwd != worktree). -- fall back to cwd resolution (wrong when cwd != worktree).
local buf = vim.fn.bufadd(uri) local buf = vim.fn.bufadd(uri)
+1 -1
View File
@@ -63,7 +63,7 @@ function M.setup()
end end
vim.filetype.add({ vim.filetype.add({
pattern = { pattern = {
["git://.-//(.+)"] = function(_, bufnr, inner) ["git://.*:([^:]+)$"] = function(_, bufnr, inner)
return vim.filetype.match({ filename = inner, buf = bufnr }) return vim.filetype.match({ filename = inner, buf = bufnr })
end, end,
}, },
+10 -6
View File
@@ -87,11 +87,14 @@ end
---@param ref string the commit ref the blob represents (e.g. `<sha>` or `<sha>^`) ---@param ref string the commit ref the blob represents (e.g. `<sha>` or `<sha>^`)
---@return integer ---@return integer
local function blob_buf(worktree, blob, path, ref) local function blob_buf(worktree, blob, path, ref)
local name = "git://" .. ref .. "//" .. path local revspec = ref .. ":" .. path
if is_zero(blob) then if is_zero(blob) then
return diff.empty_buf({ name = name, bufhidden = "hide" }) return diff.empty_buf({
name = "git://" .. revspec,
bufhidden = "hide",
})
end end
return diff.git_show_buf(worktree, ref, path) return diff.git_show_buf(worktree, revspec)
end end
---@param worktree string ---@param worktree string
@@ -142,7 +145,7 @@ end
function M.open_commit(worktree, ref, opts) function M.open_commit(worktree, ref, opts)
local split = opts and opts.split local split = opts and opts.split
local sha = repo.rev_parse(worktree, ref, true) or ref local sha = repo.rev_parse(worktree, ref, true) or ref
local name = "git://" .. sha .. "//" local name = "git://" .. sha
local existing = vim.fn.bufnr(name) local existing = vim.fn.bufnr(name)
if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then
if split == false then if split == false then
@@ -218,7 +221,7 @@ function M.open_object(worktree, ref, opts)
local split = opts and opts.split local split = opts and opts.split
local sha = repo.rev_parse(worktree, ref, true) or ref local sha = repo.rev_parse(worktree, ref, true) or ref
local name = "git://" .. sha .. "//" local name = "git://" .. sha
local existing = vim.fn.bufnr(name) local existing = vim.fn.bufnr(name)
if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then
if split == false then if split == false then
@@ -279,7 +282,8 @@ function M.open_under_cursor()
line:match("^%d+ (%w+) (%x+)\t(.+)$") line:match("^%d+ (%w+) (%x+)\t(.+)$")
if entry_sha then if entry_sha then
if entry_type == "blob" then if entry_type == "blob" then
local buf = diff.git_show_buf(ctx.worktree, ctx.ref, entry_name) local buf =
diff.git_show_buf(ctx.worktree, ctx.ref .. ":" .. entry_name)
vim.cmd.normal({ "m'", bang = true }) vim.cmd.normal({ "m'", bang = true })
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
else else
+1 -1
View File
@@ -16,7 +16,7 @@ local M = {}
function M.show(worktree, ref, opts) function M.show(worktree, ref, opts)
local split = opts and opts.split local split = opts and opts.split
local sha = repo.rev_parse(worktree, ref, true) or ref local sha = repo.rev_parse(worktree, ref, true) or ref
local name = "git://" .. sha .. "//" local name = "git://" .. sha
-- Commit SHAs are immutable so a previously-opened buffer is still -- Commit SHAs are immutable so a previously-opened buffer is still
-- valid. Reuse it instead of refetching. -- valid. Reuse it instead of refetching.
local existing = vim.fn.bufnr(name) local existing = vim.fn.bufnr(name)
+15 -15
View File
@@ -484,15 +484,19 @@ end
---@return ow.Git.DiffSide ---@return ow.Git.DiffSide
local function head_pane(worktree, path) local function head_pane(worktree, path)
return { return {
buf = diff.git_show_buf(worktree, "HEAD", path), buf = diff.git_show_buf(worktree, "HEAD:" .. path),
name = "git://HEAD//" .. path, name = "git://HEAD:" .. path,
} }
end end
---@param worktree string
---@param path string ---@param path string
---@return ow.Git.DiffSide ---@return ow.Git.DiffSide
local function head_empty_pane(path) local function absent_pane(worktree, path)
return { buf = diff.empty_buf(), name = "git://HEAD//" .. path } return {
buf = diff.empty_buf(),
name = "[absent] " .. vim.fs.joinpath(worktree, path),
}
end end
---@param worktree string ---@param worktree string
@@ -505,12 +509,6 @@ local function worktree_pane(worktree, path)
} }
end end
---@param path string
---@return ow.Git.DiffSide
local function worktree_empty_pane(path)
return { buf = diff.empty_buf(), name = "git://worktree//" .. path }
end
---@param s ow.Git.SidebarState ---@param s ow.Git.SidebarState
---@param entry ow.Git.FileEntry ---@param entry ow.Git.FileEntry
---@return ow.Git.DiffSide ---@return ow.Git.DiffSide
@@ -519,10 +517,12 @@ local function index_pane(s, entry)
entry.section == "Untracked" entry.section == "Untracked"
or (entry.section == "Staged" and entry.x == "D") or (entry.section == "Staged" and entry.x == "D")
) )
if not in_index then
return absent_pane(s.worktree, entry.path)
end
return { return {
buf = in_index and diff.git_show_buf(s.worktree, "index", entry.path) buf = diff.git_show_buf(s.worktree, ":" .. entry.path),
or diff.empty_buf(), name = "git://:" .. entry.path,
name = "git://index//" .. entry.path,
} }
end end
@@ -534,7 +534,7 @@ local function other_pane(s, entry)
local worktree = s.worktree local worktree = s.worktree
if entry.section == "Staged" then if entry.section == "Staged" then
if entry.x == "A" then if entry.x == "A" then
return head_empty_pane(p) return absent_pane(worktree, p)
end end
if entry.x == "D" then if entry.x == "D" then
return head_pane(worktree, p) return head_pane(worktree, p)
@@ -544,7 +544,7 @@ local function other_pane(s, entry)
end end
if entry.section == "Unstaged" then if entry.section == "Unstaged" then
if entry.y == "D" then if entry.y == "D" then
return worktree_empty_pane(p) return absent_pane(worktree, p)
end end
return worktree_pane(worktree, p) return worktree_pane(worktree, p)
end end