local diff = require("git.diff") 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 ---Walk upward from the cursor to the enclosing `diff --git` line and parse ---the section's pre/post paths plus the pre/post blob SHAs from the `index` ---line. ---@param cursor_lnum integer 1-indexed ---@return ow.Git.DiffSection? local function diff_section(cursor_lnum) local lines = vim.api.nvim_buf_get_lines(0, 0, cursor_lnum, false) local diff_lnum, diff_line for i = #lines, 1, -1 do if lines[i]:match("^diff %-%-git ") then diff_lnum = i diff_line = lines[i] break end end if not diff_lnum or 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 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 ---Build a buffer holding the file's content at a given blob, named after the ---commit ref it corresponds to (so the name lines up with `git log` output ---instead of an opaque blob hash). ---@param worktree string ---@param blob string? ---@param path string ---@param ref string the commit ref the blob represents (e.g. `` or `^`) ---@return integer local function blob_buf(worktree, blob, path, ref) if is_zero(blob) then return diff.empty_buf({ name = "git://" .. ref .. "/" .. path, bufhidden = "hide", }) end ---@cast blob string local buf = diff.git_show_blob(worktree, blob) diff.set_buf_name_and_filetype(buf, "git://" .. ref .. "/" .. path) return buf 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'") vim.cmd("buffer " .. 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'") vim.cmd("buffer " .. left) vim.cmd("diffthis") vim.cmd("rightbelow vertical sbuffer " .. right) vim.cmd("diffthis") vim.cmd("wincmd p") end ---@param worktree string ---@param ref string function M.open_commit(worktree, ref) local result = vim.system( { "git", "show", ref }, { cwd = worktree, text = true } ) :wait() if result.code ~= 0 then log.error("git show %s failed: %s", ref, result.stderr or "") return end local lines = util.split_lines(result.stdout or "") local sha = repo.rev_parse(worktree, ref, true) or ref local parent = repo.rev_parse(worktree, ref .. "^", true) local buf = vim.api.nvim_create_buf(false, true) vim.bo[buf].buftype = "nofile" vim.bo[buf].bufhidden = "hide" vim.bo[buf].swapfile = false vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) vim.bo[buf].modifiable = false vim.bo[buf].modified = false pcall(vim.api.nvim_buf_set_name, buf, "git://" .. sha .. "/") vim.b[buf].git_worktree = worktree vim.b[buf].git_ref = sha vim.b[buf].git_parent_ref = parent vim.bo[buf].filetype = "git" vim.cmd("normal! m'") vim.cmd("buffer " .. buf) 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 cursor_lnum = vim.api.nvim_win_get_cursor(0)[1] local section = diff_section(cursor_lnum) 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