local Revision = require("git.core.revision") local object = require("git.object") local repo = require("git.core.repo") local util = require("git.core.util") local M = {} ---@class ow.Git.Diffsplit.OpenOpts ---@field other string? ---@field layout ("vertical"|"horizontal")? ---@field mods vim.api.keyset.cmd.mods? ---@field focus ("current"|"other")? ---@param cur_buf integer ---@return string? other ---@return string? err local function infer_other(cur_buf) local cur_name = vim.api.nvim_buf_get_name(cur_buf) local cur_rev = object.parse_uri(cur_name) if cur_rev then local r = repo.resolve(cur_buf) if not r then return nil, "git URI buffer has no worktree" end if not cur_rev.path then return nil, "git URI has no path, cannot diff against worktree" end local worktree_path = vim.fs.joinpath(r.worktree, cur_rev.path) if not vim.uv.fs_stat(worktree_path) then return nil, "worktree file does not exist: " .. cur_rev.path end return worktree_path, nil end if cur_name == "" then return nil, "no file in current buffer" end if vim.bo[cur_buf].buftype ~= "" then return nil, "cannot diff this buffer (not a worktree file)" end local resolved = vim.fn.resolve(cur_name) local r = repo.resolve(resolved) if not r then return nil, "not in a git repository" end local rel = vim.fs.relpath(r.worktree, resolved) if not rel then return nil, "current buffer is outside the worktree" end return object.format_uri(Revision.new({ stage = 0, path = rel })), nil end ---@param other string ---@param cur_buf integer ---@return string? resolved ---@return string? err local function resolve_other(other, cur_buf) if vim.startswith(other, object.URI_PREFIX) then return other, nil end if vim.fn.filereadable(other) == 1 then return other, nil end local cur_name = vim.api.nvim_buf_get_name(cur_buf) local cur_rev = object.parse_uri(cur_name) local r, rel if cur_rev and cur_rev.path then r = repo.resolve(cur_buf) rel = cur_rev.path elseif cur_name ~= "" then local resolved = vim.fn.resolve(cur_name) r = repo.resolve(resolved) if r then rel = vim.fs.relpath(r.worktree, resolved) end end if not r then return nil, "not in a git repository" end if not rel then return nil, "current buffer has no path" end if not r:rev_parse(other, true) then return nil, "invalid rev: " .. other end return object.format_uri(Revision.new({ base = other, path = rel })), nil end ---@param cur_buf integer ---@param other string ---@return 'aboveleft'|'belowright'|nil local function default_split(cur_buf, other) local cur_rev = object.parse_uri(vim.api.nvim_buf_get_name(cur_buf)) local other_rev = object.parse_uri(other) if not cur_rev and other_rev then return "aboveleft" end if cur_rev and not other_rev then return "belowright" end if cur_rev and other_rev then if cur_rev.stage == 0 and other_rev.base then return "aboveleft" end if cur_rev.base and other_rev.stage == 0 then return "belowright" end end return nil end ---@alias ow.Git.Diffsplit.Side string|integer ---@class ow.Git.Diffsplit.OpenPairOpts ---@field layout ("vertical"|"horizontal")? ---@field mods vim.api.keyset.cmd.mods? ---@field focus ("old"|"new")? ---@param mods vim.api.keyset.cmd.mods? ---@param layout ("vertical"|"horizontal")? ---@return vim.api.keyset.cmd.mods local function layout_mods(mods, layout) mods = vim.tbl_extend("force", {}, mods or {}) if mods.vertical == nil then mods.vertical = layout ~= "horizontal" end return mods end ---@param side ow.Git.Diffsplit.Side ---@param cur_buf integer ---@return string? name ---@return integer? buf ---@return string? err local function resolve_side(side, cur_buf) if type(side) == "number" then local name = vim.api.nvim_buf_get_name(side) if name == "" then return nil, nil, "diff side buffer has no name" end return name, side, nil end local name, err = resolve_other(side, cur_buf) return name, nil, err end ---@param side ow.Git.Diffsplit.Side ---@param cur_buf integer ---@return integer? buf ---@return string? err local function buf_for_side(side, cur_buf) local name, buf, err = resolve_side(side, cur_buf) if not name then return nil, err end if buf then return buf, nil end buf = vim.fn.bufadd(name) vim.fn.bufload(buf) return buf, nil end ---@param opts? ow.Git.Diffsplit.OpenOpts function M.open(opts) opts = opts or {} local cur_buf = vim.api.nvim_get_current_buf() local other, err if opts.other then other, err = resolve_other(opts.other, cur_buf) else other, err = infer_other(cur_buf) end if not other then util.warning("%s", err or "no diff side") return end local mods = layout_mods(opts.mods, opts.layout) if mods.split == nil then local placement = default_split(cur_buf, other) if placement then mods = vim.tbl_extend("force", mods or {}, { split = placement }) end end local cur_win = vim.api.nvim_get_current_win() vim.cmd.diffsplit({ args = { other }, mods = mods }) if opts.focus == "current" and vim.api.nvim_win_is_valid(cur_win) then vim.api.nvim_set_current_win(cur_win) end end ---@param old ow.Git.Diffsplit.Side ---@param new ow.Git.Diffsplit.Side ---@param opts? ow.Git.Diffsplit.OpenPairOpts function M.open_pair(old, new, opts) opts = opts or {} local cur_buf = vim.api.nvim_get_current_buf() local new_buf, err = buf_for_side(new, cur_buf) if not new_buf then util.warning("%s", err or "no new diff side") return end local old_name, _, old_err = resolve_side(old, cur_buf) if not old_name then util.warning("%s", old_err or "no old diff side") return end vim.cmd.normal({ "m'", bang = true }) vim.api.nvim_set_current_buf(new_buf) local mods = layout_mods(opts.mods, opts.layout) mods.split = mods.split or "aboveleft" vim.cmd.diffsplit({ args = { old_name }, mods = mods }) if opts.focus ~= "old" then vim.cmd.wincmd("p") end end return M