refactor(git): unify diff/object dispatch, codify naming, add :Gdiffsplit
This commit is contained in:
+198
-74
@@ -1,5 +1,4 @@
|
||||
local diff = require("git.diff")
|
||||
local git = require("git")
|
||||
local repo = require("git.repo")
|
||||
local util = require("git.util")
|
||||
|
||||
@@ -11,12 +10,12 @@ local M = {}
|
||||
---@field pre_blob string?
|
||||
---@field post_blob string?
|
||||
|
||||
---@class ow.Git.ShowContext
|
||||
---@class ow.Git.BufContext
|
||||
---@field worktree string
|
||||
---@field ref string resolved commit SHA of the gitobject buffer
|
||||
---@field parent_ref string? resolved parent commit SHA, nil for root commits
|
||||
|
||||
---@return ow.Git.ShowContext?
|
||||
---@return ow.Git.BufContext?
|
||||
local function context()
|
||||
local worktree = vim.b.git_worktree
|
||||
local ref = vim.b.git_ref
|
||||
@@ -79,6 +78,130 @@ local function is_zero(sha)
|
||||
return sha == nil or sha:match("^0+$") ~= nil
|
||||
end
|
||||
|
||||
---Stage-0 (`:<path>`) index entries are writable through the buffer:
|
||||
---`:w` rewrites the entry via `hash-object` + `update-index`. All other
|
||||
---revspecs (HEAD:, <sha>:, :1:, bare object refs) stay read-only.
|
||||
---@param buf integer
|
||||
---@param worktree string
|
||||
---@param path string
|
||||
local function attach_index_writer(buf, worktree, path)
|
||||
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
local body = table.concat(
|
||||
vim.api.nvim_buf_get_lines(buf, 0, -1, false),
|
||||
"\n"
|
||||
) .. "\n"
|
||||
local hash_stdout = util.exec(
|
||||
{ "git", "hash-object", "-w", "--stdin" },
|
||||
{ cwd = worktree, stdin = body }
|
||||
)
|
||||
if not hash_stdout then
|
||||
return
|
||||
end
|
||||
local sha = vim.trim(hash_stdout)
|
||||
local mode = vim.b[buf].git_index_mode
|
||||
if not mode then
|
||||
mode = "100644"
|
||||
local ls = util.exec(
|
||||
{ "git", "ls-files", "-s", "--", path },
|
||||
{ cwd = worktree, silent = true }
|
||||
)
|
||||
if ls then
|
||||
local m = ls:match("^(%d+)")
|
||||
if m then
|
||||
mode = m
|
||||
end
|
||||
end
|
||||
vim.b[buf].git_index_mode = mode
|
||||
end
|
||||
-- Use the 3-arg form (mode sha path) instead of the comma form
|
||||
-- (mode,sha,path), which doesn't survive paths containing a
|
||||
-- comma.
|
||||
if
|
||||
not util.exec({
|
||||
"git",
|
||||
"update-index",
|
||||
"--cacheinfo",
|
||||
mode,
|
||||
sha,
|
||||
path,
|
||||
}, { cwd = worktree })
|
||||
then
|
||||
return
|
||||
end
|
||||
vim.bo[buf].modified = false
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---Return a buffer holding the content addressed by a git revspec. The
|
||||
---URI is `git://<revspec>` and BufReadCmd routes through `M.read_uri`,
|
||||
---which loads via `git cat-file -p`.
|
||||
---@param worktree string
|
||||
---@param revspec string any revspec git understands (e.g. `HEAD:foo`, `:foo`, `:1:foo`, `<sha>`, `<sha>:foo`)
|
||||
---@return integer
|
||||
function M.buf_for(worktree, revspec)
|
||||
local name = "git://" .. revspec
|
||||
local buf = vim.fn.bufadd(name)
|
||||
vim.b[buf].git_worktree = worktree
|
||||
vim.fn.bufload(buf)
|
||||
return buf
|
||||
end
|
||||
|
||||
---BufReadCmd handler for `git://<revspec>` URIs. Loads content via
|
||||
---`git cat-file -p <revspec>`. Worktree comes from `vim.b[buf]
|
||||
---.git_worktree` if set, else from cwd. Stage-0 index entries (revspec
|
||||
---form `:<path>`) are made writable via `attach_index_writer` so `:w`
|
||||
---updates the index. Other revspecs are read-only.
|
||||
---@param buf integer
|
||||
function M.read_uri(buf)
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
local revspec = name:match("^git://(.+)$")
|
||||
if not revspec then
|
||||
return
|
||||
end
|
||||
|
||||
local worktree = vim.b[buf].git_worktree or select(2, repo.resolve_cwd())
|
||||
if not worktree then
|
||||
util.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"
|
||||
|
||||
local stdout = util.exec(
|
||||
{ "git", "cat-file", "-p", revspec },
|
||||
{ cwd = worktree }
|
||||
)
|
||||
if stdout then
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout))
|
||||
end
|
||||
|
||||
local parsed = util.parse_revspec(revspec)
|
||||
local index_path = parsed.stage == 0 and parsed.path or nil
|
||||
if index_path then
|
||||
vim.bo[buf].buftype = "acwrite"
|
||||
-- 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, index_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
|
||||
|
||||
-- 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
|
||||
|
||||
---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
|
||||
@@ -94,22 +217,22 @@ local function blob_buf(worktree, blob, path, ref)
|
||||
bufhidden = "hide",
|
||||
})
|
||||
end
|
||||
return diff.git_show_buf(worktree, revspec)
|
||||
return M.buf_for(worktree, revspec)
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param blob string?
|
||||
---@param path string
|
||||
---@param ref string
|
||||
local function show_blob(worktree, blob, path, ref)
|
||||
local function load_blob(worktree, blob, path, ref)
|
||||
local buf = blob_buf(worktree, blob, path, ref)
|
||||
vim.cmd.normal({ "m'", bang = true })
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
end
|
||||
|
||||
---@param ctx ow.Git.ShowContext
|
||||
---@param ctx ow.Git.BufContext
|
||||
---@param section ow.Git.DiffSection
|
||||
local function show_diff(ctx, section)
|
||||
local function open_section(ctx, section)
|
||||
if not section.pre_blob or not section.post_blob then
|
||||
util.warning("no index line; cannot determine blob SHAs")
|
||||
return
|
||||
@@ -122,18 +245,41 @@ local function show_diff(ctx, section)
|
||||
blob_buf(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
||||
local right =
|
||||
blob_buf(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
||||
vim.cmd.normal({ "m'", bang = true })
|
||||
vim.api.nvim_set_current_buf(right)
|
||||
-- `:diffsplit` is the same path `M.split` uses; Vim's built-in diff
|
||||
-- machinery handles the diff option setup on both windows.
|
||||
vim.cmd(
|
||||
"leftabove vert diffsplit "
|
||||
.. vim.fn.fnameescape(vim.api.nvim_buf_get_name(left))
|
||||
)
|
||||
diff.open(left, right, true)
|
||||
end
|
||||
|
||||
---@class ow.Git.OpenCommitOpts
|
||||
---@field split (false|"above"|"below"|"left"|"right")? forwarded to `git.new_scratch`. Default opens a new horizontal split.
|
||||
---@class ow.Git.OpenObjectOpts
|
||||
---@field split (false|"above"|"below"|"left"|"right")? forwarded to `util.new_scratch`. Default opens a new horizontal split.
|
||||
|
||||
---Place a `git://<revspec>` URI buffer in a window per `opts.split`.
|
||||
---`bufadd` dedups against existing buffers, so re-opening the same URI
|
||||
---reuses the buffer (and `bufload` no-ops). `read_uri` defaults to
|
||||
---`bufhidden=wipe` (right for diff sides), but navigation buffers
|
||||
---should persist across window closes, so override to `hide`.
|
||||
---@param worktree string
|
||||
---@param uri string
|
||||
---@param sha string written to `b:git_ref` so `<CR>` navigation in the buffer can resolve relative paths
|
||||
---@param opts ow.Git.OpenObjectOpts?
|
||||
---@param default_ft string? applied if filetype detection didn't pick anything (bare-sha URIs have no path for the `filetype.add` pattern to match)
|
||||
local function open_uri(worktree, uri, sha, opts, default_ft)
|
||||
local buf = vim.fn.bufadd(uri)
|
||||
vim.b[buf].git_worktree = worktree
|
||||
vim.b[buf].git_ref = sha
|
||||
vim.fn.bufload(buf)
|
||||
vim.bo[buf].bufhidden = "hide"
|
||||
if default_ft and vim.bo[buf].filetype == "" then
|
||||
vim.bo[buf].filetype = default_ft
|
||||
end
|
||||
local split = opts and opts.split
|
||||
if split == false then
|
||||
vim.cmd.normal({ "m'", bang = true })
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
return
|
||||
end
|
||||
vim.api.nvim_open_win(buf, true, {
|
||||
split = split or (vim.o.splitbelow and "below" or "above"),
|
||||
})
|
||||
end
|
||||
|
||||
---Open a commit's body via `git cat-file -p` for the header (raw object
|
||||
---form, flush-left message) plus `git diff-tree -p` for the patch. The
|
||||
@@ -144,7 +290,7 @@ end
|
||||
---`<CR>` flow.
|
||||
---@param worktree string
|
||||
---@param ref string
|
||||
---@param opts ow.Git.OpenCommitOpts?
|
||||
---@param opts ow.Git.OpenObjectOpts?
|
||||
function M.open_commit(worktree, ref, opts)
|
||||
local split = opts and opts.split
|
||||
local sha = repo.rev_parse(worktree, ref, true) or ref
|
||||
@@ -185,7 +331,7 @@ function M.open_commit(worktree, ref, opts)
|
||||
end
|
||||
|
||||
local parent = repo.rev_parse(worktree, ref .. "^", true)
|
||||
local buf, _ = git.new_scratch({ name = name, split = split })
|
||||
local buf, _ = util.new_scratch({ name = name, split = split })
|
||||
vim.b[buf].git_worktree = worktree
|
||||
vim.b[buf].git_ref = sha
|
||||
vim.b[buf].git_parent_ref = parent
|
||||
@@ -199,14 +345,24 @@ function M.open_commit(worktree, ref, opts)
|
||||
vim.bo[buf].filetype = "git"
|
||||
end
|
||||
|
||||
---Open any git object in a buffer. Commits get the full
|
||||
---`M.open_commit` view (cat-file + diff-tree). Trees, blobs, and tags
|
||||
---dump `git cat-file -p` output as-is. The object type is detected via
|
||||
---`git cat-file -t`.
|
||||
---Open any git object. Accepts either a bare ref (commit/tree/blob/tag
|
||||
---SHA, branch name, etc.) or `<commit-ref>:<path>` form. Commits route
|
||||
---to `M.open_commit` for the message + diff view; everything else flows
|
||||
---through the BufReadCmd loader at the `git://<revspec>` URI.
|
||||
---@param worktree string
|
||||
---@param ref string
|
||||
---@param opts ow.Git.OpenCommitOpts?
|
||||
---@param opts ow.Git.OpenObjectOpts?
|
||||
function M.open_object(worktree, ref, opts)
|
||||
-- Path-form: resolve the commit-ref to a sha so the URI stays stable
|
||||
-- if the ref later moves, and the `filetype.add` pattern can pick the
|
||||
-- ft from the path segment.
|
||||
local commit_ref, path = ref:match("^(.-):(.+)$")
|
||||
if commit_ref then
|
||||
local sha = repo.rev_parse(worktree, commit_ref, true) or commit_ref
|
||||
open_uri(worktree, "git://" .. sha .. ":" .. path, sha, opts)
|
||||
return
|
||||
end
|
||||
|
||||
local type_out = util.exec(
|
||||
{ "git", "cat-file", "-t", ref },
|
||||
{ cwd = worktree, silent = true }
|
||||
@@ -216,44 +372,16 @@ function M.open_object(worktree, ref, opts)
|
||||
util.warning("not a git object: %s", ref)
|
||||
return
|
||||
end
|
||||
|
||||
if obj_type == "commit" then
|
||||
M.open_commit(worktree, ref, opts)
|
||||
return
|
||||
end
|
||||
|
||||
local split = opts and opts.split
|
||||
-- Trees, blobs, tags. The bare-sha URI has no path, so the
|
||||
-- `filetype.add` pattern doesn't match; default to `git` so
|
||||
-- tree / tag header lines syntax-highlight.
|
||||
local sha = repo.rev_parse(worktree, ref, true) or ref
|
||||
local name = "git://" .. sha
|
||||
local existing = vim.fn.bufnr(name)
|
||||
if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then
|
||||
if split == false then
|
||||
vim.cmd.normal({ "m'", bang = true })
|
||||
vim.api.nvim_set_current_buf(existing)
|
||||
else
|
||||
vim.api.nvim_open_win(existing, true, {
|
||||
split = split or (vim.o.splitbelow and "below" or "above"),
|
||||
})
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local stdout = util.exec(
|
||||
{ "git", "cat-file", "-p", ref },
|
||||
{ cwd = worktree }
|
||||
)
|
||||
if not stdout then
|
||||
return
|
||||
end
|
||||
|
||||
local buf, _ = git.new_scratch({ name = name, split = split })
|
||||
vim.b[buf].git_worktree = worktree
|
||||
vim.b[buf].git_ref = sha
|
||||
vim.bo[buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout))
|
||||
vim.bo[buf].modifiable = false
|
||||
vim.bo[buf].modified = false
|
||||
vim.bo[buf].filetype = "git"
|
||||
open_uri(worktree, "git://" .. sha, sha, opts, "git")
|
||||
end
|
||||
|
||||
---@return boolean dispatched true if the cursor was on an actionable line
|
||||
@@ -277,22 +405,18 @@ function M.open_under_cursor()
|
||||
return true
|
||||
end
|
||||
|
||||
-- Tree-entry navigation: `<mode> <type> <sha>\t<name>`. Blob entries
|
||||
-- route through `diff.git_show_buf` so the buffer URI carries the
|
||||
-- entry name and BufReadCmd / BufReadPost resolve filetype from it.
|
||||
-- Other types (subtrees, submodule commit refs, tags) fall through
|
||||
-- to the generic SHA-based opener.
|
||||
-- Tree-entry navigation: `<mode> <type> <sha>\t<name>`. Blobs are
|
||||
-- routed by path so the URI carries the entry name and filetype
|
||||
-- detection picks it up. Subtrees navigate by sha so the resulting
|
||||
-- buffer's `git_ref` is the subtree's own sha (correct anchor for
|
||||
-- relative path navigation within it). Other types (submodule
|
||||
-- commit refs, tags) also navigate by sha.
|
||||
local entry_type, entry_sha, entry_name =
|
||||
line:match("^%d+ (%w+) (%x+)\t(.+)$")
|
||||
if entry_sha then
|
||||
if entry_type == "blob" then
|
||||
local buf =
|
||||
diff.git_show_buf(ctx.worktree, ctx.ref .. ":" .. entry_name)
|
||||
vim.cmd.normal({ "m'", bang = true })
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
else
|
||||
M.open_object(ctx.worktree, entry_sha, { split = false })
|
||||
end
|
||||
local nav_ref = entry_type == "blob" and (ctx.ref .. ":" .. entry_name)
|
||||
or entry_sha
|
||||
M.open_object(ctx.worktree, nav_ref, { split = false })
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -303,23 +427,23 @@ function M.open_under_cursor()
|
||||
local parent = ctx.parent_ref or "0"
|
||||
|
||||
if line:match("^diff %-%-git ") then
|
||||
show_diff(ctx, section)
|
||||
open_section(ctx, section)
|
||||
return true
|
||||
end
|
||||
if line:match("^%-%-%- ") then
|
||||
show_blob(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
||||
load_blob(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
||||
return true
|
||||
end
|
||||
if line:match("^%+%+%+ ") then
|
||||
show_blob(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
||||
load_blob(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
||||
return true
|
||||
end
|
||||
local prefix = line:sub(1, 1)
|
||||
if prefix == "+" then
|
||||
show_blob(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
||||
load_blob(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
||||
return true
|
||||
elseif prefix == "-" then
|
||||
show_blob(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
||||
load_blob(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user