local Revision = require("git.revision") local repo = require("git.repo") local util = require("git.util") local M = {} ---@class ow.Git.Diff.Side ---@field buf integer ---@field name string? ---@class ow.Git.Diff.Pair ---@field left ow.Git.Diff.Side ---@field right ow.Git.Diff.Side ---@param win integer ---@param enabled boolean function M.set_diff(win, enabled) vim.api.nvim_win_call(win, function() vim.cmd(enabled and "diffthis" or "diffoff") end) end ---@param left integer ---@param right integer ---@param vertical boolean function M.open(left, right, vertical) local left_name = vim.api.nvim_buf_get_name(left) vim.cmd.normal({ "m'", bang = true }) vim.api.nvim_set_current_buf(right) vim.cmd.diffsplit({ args = { left_name }, mods = { split = "aboveleft", vertical = vertical, keepjumps = true }, magic = { file = false }, }) end ---@param left_win integer ---@param right_win integer ---@param pair ow.Git.Diff.Pair function M.update_pair(left_win, right_win, pair) M.set_diff(left_win, false) M.set_diff(right_win, false) vim.api.nvim_win_set_buf(left_win, pair.left.buf) vim.api.nvim_win_set_buf(right_win, pair.right.buf) for _, side in ipairs({ pair.left, pair.right }) do if side.name then util.set_buf_name(side.buf, side.name) end end M.set_diff(left_win, true) M.set_diff(right_win, true) vim.api.nvim_win_set_cursor(left_win, { 1, 0 }) vim.api.nvim_win_set_cursor(right_win, { 1, 0 }) vim.cmd.syncbind() end ---@param buf_a integer ---@param buf_b integer ---@param a_left boolean ---@param vertical boolean local function place_pair(buf_a, buf_b, a_left, vertical) if a_left then M.open(buf_a, buf_b, vertical) else M.open(buf_b, buf_a, vertical) end end ---@param opts ow.Git.Diff.SplitOpts ---@param buf integer ---@param rev ow.Git.Revision local function uri_split(opts, buf, rev) local r = repo.resolve(buf) if not r then util.error("git URI buffer has no worktree") return end if not rev.path then util.error("git URI has no path, cannot diff against worktree") return end local object = require("git.object") if opts.rev and opts.rev:find(":", 1, true) then local content = util.exec( { "git", "cat-file", "-p", opts.rev }, { cwd = r.worktree, silent = true } ) if not content then util.error("invalid rev: %s", opts.rev) return end place_pair( buf, object.buf_for(r, Revision.parse(opts.rev), content), false, opts.vertical ) return end if not opts.rev then local worktree_path = vim.fs.joinpath(r.worktree, rev.path) if not vim.uv.fs_stat(worktree_path) then util.error("worktree file does not exist: %s", rev.path) return end local worktree_buf = vim.fn.bufadd(worktree_path) vim.fn.bufload(worktree_buf) place_pair(buf, worktree_buf, true, opts.vertical) return end if rev.stage == 1 then util.error("gD on merge base is ambiguous, use :Gdiffsplit ") return end local mapping = { [2] = { Revision.new({ stage = 3, path = rev.path }), true }, [3] = { Revision.new({ stage = 2, path = rev.path }), false }, [0] = { Revision.new({ base = "HEAD", path = rev.path }), false }, } local m = mapping[rev.stage] or { Revision.new({ stage = 0, path = rev.path }), true } local other_rev, left = m[1], m[2] local content = util.exec( { "git", "cat-file", "-p", other_rev:format() }, { cwd = r.worktree, silent = true } ) if not content then util.error("invalid rev: %s", other_rev:format()) return end place_pair(buf, object.buf_for(r, other_rev, content), left, opts.vertical) end ---@class ow.Git.Diff.SplitOpts ---@field rev string? ---@field vertical boolean ---@param opts ow.Git.Diff.SplitOpts function M.split(opts) local cur_buf = vim.api.nvim_get_current_buf() local cur_path = vim.api.nvim_buf_get_name(cur_buf) local cur_rev = require("git.object").parse_uri(cur_path) if cur_rev then return uri_split(opts, cur_buf, cur_rev) end if cur_path == "" then util.error("no file in current buffer") return end if vim.bo[cur_buf].buftype ~= "" then util.error("cannot diff this buffer (not a worktree file)") return end cur_path = vim.fn.resolve(cur_path) local r = repo.resolve(cur_path) if not r then util.error("not in a git repository") return end local rel = vim.fs.relpath(r.worktree, cur_path) local rev if not opts.rev then rev = Revision.new({ stage = 0, path = rel }) elseif opts.rev:find(":", 1, true) then rev = Revision.parse(opts.rev) else rev = Revision.new({ base = opts.rev, path = rel }) end local content = util.exec( { "git", "cat-file", "-p", rev:format() }, { cwd = r.worktree, silent = true } ) if not content then util.error("invalid rev: %s", rev:format()) return end local buf = require("git.object").buf_for(r, rev, content) place_pair(buf, cur_buf, true, opts.vertical) end return M