diff --git a/ftplugin/gitlog.lua b/ftplugin/gitlog.lua index e37f80d..60aed0f 100644 --- a/ftplugin/gitlog.lua +++ b/ftplugin/gitlog.lua @@ -9,7 +9,7 @@ vim.keymap.set("n", "", function() .nvim_get_current_line() :match("^[*|/\\_ ]*(%x%x%x%x%x%x%x+)") if sha then - require("git.show").open_commit(worktree, sha, { split = false }) + require("git.show").open_commit_object(worktree, sha, { split = false }) else -- "n" mode = no remap, so this doesn't recurse into our mapping. vim.api.nvim_feedkeys(cr, "n", false) diff --git a/lua/git/show.lua b/lua/git/show.lua index 87cc1b0..cfdc25f 100644 --- a/lua/git/show.lua +++ b/lua/git/show.lua @@ -168,19 +168,94 @@ function M.open_commit(worktree, ref, opts) vim.bo[buf].filetype = "git" 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 +---`-m --first-parent` flags collapse merges and stashes into single +---`diff --git` blocks per file, so `M.open_at_cursor`'s `` parser +---can navigate them. (`git show` would emit `diff --cc` combined diffs +---in those cases, which the parser can't follow.) Used by the gitlog +---`` flow. +---@param worktree string +---@param ref string +---@param opts ow.Git.OpenCommitOpts? +function M.open_commit_object(worktree, ref, opts) + local split = opts and opts.split + 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 header = util.exec( + { "git", "cat-file", "-p", ref }, + { cwd = worktree } + ) + if not header then + return + end + -- `--root` lets initial commits show their full tree. + local patch = util.exec({ + "git", + "diff-tree", + "-p", + "-m", + "--first-parent", + "--root", + "--no-commit-id", + ref, + }, { cwd = worktree }) + if not patch then + return + end + + local parent = repo.rev_parse(worktree, ref .. "^", true) + local buf, _ = git.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 + vim.bo[buf].modifiable = true + -- Normalise to exactly one blank line between the message body and + -- the patch, regardless of trailing newlines on the header. + local content = (header:gsub("\n*$", "\n\n")) .. patch + vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(content)) + vim.bo[buf].modifiable = false + vim.bo[buf].modified = false + vim.bo[buf].filetype = "git" +end + ---@return boolean dispatched true if the cursor was on an actionable line function M.open_at_cursor() local ctx = context() if not ctx then return false end + + local line = vim.api.nvim_get_current_line() + + -- Cat-file header navigation. `parent ` opens the referenced + -- commit. (`git show` doesn't emit a `parent` line, so this only + -- fires inside `M.open_commit_object` buffers.) + local parent_sha = line:match("^parent (%x+)$") + if parent_sha then + M.open_commit_object(ctx.worktree, parent_sha, { split = false }) + return true + end + local section = diff_section() if not section then return false end local parent = ctx.parent_ref or "0" - local line = vim.api.nvim_get_current_line() if line:match("^diff %-%-git ") then show_diff(ctx, section) return true