refactor(git): unify diff/object dispatch, codify naming, add :Gdiffsplit

This commit is contained in:
2026-04-29 09:47:51 +02:00
parent 44f9503960
commit 5c5da7a854
10 changed files with 587 additions and 443 deletions
+79 -88
View File
@@ -1,4 +1,5 @@
local git = require("git")
local commit = require("git.commit")
local object = require("git.object")
local repo = require("git.repo")
local util = require("git.util")
@@ -8,11 +9,12 @@ local M = {}
---@field ft string
---@field needs_ref boolean?
---Subcommands whose output goes to a buffer. Subcommands with their
---own dispatch (`commit`, `show`, `cat-file`) call `run_in_split`
---directly with a one-off conf.
---@type table<string, ow.Git.SplitHandler>
local SPLIT_HANDLERS = {
log = { ft = "git" },
show = { ft = "git", needs_ref = true },
["cat-file"] = { ft = "git", needs_ref = true },
diff = { ft = "diff" },
}
@@ -67,109 +69,70 @@ 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)
local label = repo.rev_parse(worktree, user_ref, true) 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))
---Find or create the named scratch buffer and place it in a window.
---@param name string
---@return integer buf
local function place_split(name)
local buf = vim.fn.bufnr("\\V" .. name)
if buf == -1 or not vim.api.nvim_buf_is_loaded(buf) then
buf = util.new_scratch()
pcall(vim.api.nvim_buf_set_name, buf, name)
return buf
end
local win_id = vim.fn.bufwinid(buf)
if win_id ~= -1 then
vim.api.nvim_set_current_win(win_id)
else
vim.api.nvim_open_win(buf, true, {
split = vim.o.splitbelow and "below" or "above",
})
end
return buf
end
---Run `git <args>` async. On success, drop the output into a named
---scratch split (creating or reusing as needed). On failure, `util.exec`
---notifies and the split is never opened, so a bad ref doesn't leave a
---stray buffer behind.
---@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
-- `cat-file -p <sha>` routes to the gitobject viewer so commits get
-- the full message + diff view and other types render via cat-file.
-- Other modes (-t, -s, -e) fall through to the generic dump.
if args[1] == "cat-file" and vim.list_contains(args, "-p") then
local ref = first_positional(args, 2)
if ref then
require("git.object").open_object(worktree, ref)
return
end
end
local name = "[git " .. table.concat(args, " ") .. "]"
local buf = vim.fn.bufnr("\\V" .. name)
if buf == -1 or not vim.api.nvim_buf_is_loaded(buf) then
buf = git.new_scratch()
pcall(vim.api.nvim_buf_set_name, buf, name)
else
local win_id = vim.fn.bufwinid(buf)
if win_id ~= -1 then
vim.api.nvim_set_current_win(win_id)
else
vim.api.nvim_open_win(buf, true, {
split = vim.o.splitbelow and "below" or "above",
})
end
vim.bo[buf].modifiable = true
vim.api.nvim_buf_set_lines(buf, 0, -1, false, {})
end
vim.b[buf].git_worktree = worktree
vim.b[buf].git_ref = nil
vim.b[buf].git_parent_ref = nil
if conf.needs_ref then
local user_ref = first_positional(args, 2) or "HEAD"
local sha = repo.rev_parse(worktree, user_ref, true)
if sha then
vim.b[buf].git_ref = sha
vim.b[buf].git_parent_ref =
repo.rev_parse(worktree, user_ref .. "^", true)
end
end
vim.bo[buf].filetype = conf.ft
local cmd = { "git" }
vim.list_extend(cmd, args)
vim.system(
cmd,
{ cwd = worktree, text = true },
vim.schedule_wrap(function(obj)
if not vim.api.nvim_buf_is_valid(buf) then
util.exec(cmd, {
cwd = worktree,
on_done = function(stdout)
if not stdout then
return
end
local content = (obj.stdout or "") .. (obj.stderr or "")
local name = "[git " .. table.concat(args, " ") .. "]"
local buf = place_split(name)
vim.b[buf].git_worktree = worktree
vim.b[buf].git_ref = nil
vim.b[buf].git_parent_ref = nil
if conf.needs_ref then
local user_ref = first_positional(args, 2) or "HEAD"
local sha = repo.rev_parse(worktree, user_ref, true)
if sha then
vim.b[buf].git_ref = sha
vim.b[buf].git_parent_ref =
repo.rev_parse(worktree, user_ref .. "^", true)
end
end
vim.bo[buf].filetype = conf.ft
vim.bo[buf].modifiable = true
vim.api.nvim_buf_set_lines(
buf,
0,
-1,
false,
util.split_lines(content)
util.split_lines(stdout)
)
vim.bo[buf].modifiable = false
vim.bo[buf].modified = false
if obj.code ~= 0 then
util.error(
"git %s failed: %s",
args[1] or "",
vim.trim(obj.stderr or "")
)
end
end)
)
end,
})
end
---@param worktree string
@@ -244,7 +207,35 @@ function M.run(args)
local sub = args[1]
if sub == "commit" and not has_message(args) then
require("git.commit").commit({ amend = has_flag(args, "--amend") })
commit.commit({ amend = has_flag(args, "--amend") })
return
end
-- `:G show <ref>:<path>` opens the blob via the BufReadCmd loader
-- so the URI carries the path and filetype detection has something
-- to match against. Other show invocations dump output to a buffer.
if sub == "show" then
local arg = first_positional(args, 2)
if arg and arg:find(":", 1, true) then
object.open_object(worktree, arg)
return
end
run_in_split(worktree, args, { ft = "git", needs_ref = true })
return
end
-- `:G cat-file -p <sha>` routes to the gitobject viewer so commits
-- get the full message + diff view and other types render via
-- cat-file. Other modes (-t, -s, -e) dump to a buffer.
if sub == "cat-file" then
if vim.list_contains(args, "-p") then
local ref = first_positional(args, 2)
if ref then
object.open_object(worktree, ref)
return
end
end
run_in_split(worktree, args, { ft = "git", needs_ref = true })
return
end