Files
nvim/lua/git/show.lua
T

228 lines
7.6 KiB
Lua

local diff = require("git.diff")
local git = require("git")
local log = require("log")
local repo = require("git.repo")
local util = require("util")
local M = {}
---@class ow.Git.DiffSection
---@field pre_path string path on the parent side (`a/...`)
---@field post_path string path on the current side (`b/...`)
---@field pre_blob string?
---@field post_blob string?
---@class ow.Git.ShowContext
---@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?
local function context()
local worktree = vim.b.git_worktree
local ref = vim.b.git_ref
if not worktree or not ref then
return nil
end
return { worktree = worktree, ref = ref, parent_ref = vim.b.git_parent_ref }
end
---Find the enclosing `diff --git` line and parse the section's pre/post
---paths plus the pre/post blob SHAs from the `index` line.
---
---Uses `vim.fn.search('bcnW')` (backward, accept cursor pos, no move, no
---wrap) so a giant `git show <merge>` buffer doesn't pay an O(cursor_lnum)
---array allocation on every <CR>.
---@return ow.Git.DiffSection?
local function diff_section()
local diff_lnum = vim.fn.search("^diff --git ", "bcnW")
if diff_lnum == 0 then
return nil
end
local diff_line =
vim.api.nvim_buf_get_lines(0, diff_lnum - 1, diff_lnum, false)[1]
if not diff_line then
return nil
end
local pre_path, post_path = diff_line:match("^diff %-%-git a/(.-) b/(.+)$")
if not pre_path or not post_path then
return nil
end
-- Header lines (mode/index/oldfile/newfile/etc) sit between the
-- `diff --git` line and the first `@@` hunk; cap the read at 20 to
-- bound work even for unusual diff headers.
local header =
vim.api.nvim_buf_get_lines(0, diff_lnum, diff_lnum + 20, false)
local pre_blob, post_blob
for _, l in ipairs(header) do
if l:sub(1, 3) == "@@ " or l:match("^diff %-%-git ") then
break
end
local pre, post = l:match("^index (%x+)%.%.(%x+)")
if pre then
pre_blob = pre
post_blob = post
break
end
end
return {
pre_path = pre_path,
post_path = post_path,
pre_blob = pre_blob,
post_blob = post_blob,
}
end
---@param sha string?
---@return boolean
local function is_zero(sha)
return sha == nil or sha:match("^0+$") ~= nil
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
---@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
if is_zero(blob) then
return diff.empty_buf({ name = name, bufhidden = "hide" })
end
return diff.git_show_buf(worktree, ref, path)
end
---@param worktree string
---@param blob string?
---@param path string
---@param ref string
local function show_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 section ow.Git.DiffSection
local function show_diff(ctx, section)
if not section.pre_blob or not section.post_blob then
log.warning("no index line; cannot determine blob SHAs")
return
end
local parent = ctx.parent_ref or "0"
local left =
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 })
local left_win = vim.api.nvim_get_current_win()
vim.api.nvim_set_current_buf(left)
vim.wo[left_win].diff = true
local right_win =
vim.api.nvim_open_win(right, true, { split = "right", win = left_win })
vim.wo[right_win].diff = true
vim.api.nvim_set_current_win(left_win)
end
---@param worktree string
---@param ref string
function M.open_commit(worktree, ref)
repo.rev_parse(worktree, ref, true, function(resolved)
local sha = resolved or ref
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)
if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then
vim.api.nvim_open_win(existing, true, {
split = vim.o.splitbelow and "below" or "above",
})
return
end
local buf, win = git.new_scratch({ name = name })
vim.b[buf].git_worktree = worktree
vim.b[buf].git_ref = sha
vim.system(
{ "git", "show", ref },
{ cwd = worktree, text = true },
vim.schedule_wrap(function(result)
if result.code ~= 0 then
log.error(
"git show %s failed: %s",
ref,
vim.trim(result.stderr or "")
)
-- Tear down the empty placeholder window+buffer so a
-- retry runs a fresh fetch instead of hitting the
-- cached-buffer branch and reopening an empty pane.
if vim.api.nvim_win_is_valid(win) then
pcall(vim.api.nvim_win_close, win, true)
end
if vim.api.nvim_buf_is_valid(buf) then
vim.api.nvim_buf_delete(buf, { force = true })
end
return
end
if not vim.api.nvim_buf_is_valid(buf) then
return
end
local lines = util.split_lines(result.stdout or "")
repo.rev_parse(worktree, ref .. "^", true, function(parent)
if not vim.api.nvim_buf_is_valid(buf) then
return
end
vim.b[buf].git_parent_ref = parent
vim.bo[buf].modifiable = true
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.bo[buf].modifiable = false
vim.bo[buf].modified = false
vim.bo[buf].filetype = "git"
end)
end)
)
end)
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 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
end
if line:match("^%-%-%- ") then
show_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)
return true
end
local prefix = line:sub(1, 1)
if prefix == "+" then
show_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)
return true
end
return false
end
return M