Files
nvim/lua/git/diff.lua
T

189 lines
5.4 KiB
Lua

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.warning("git URI buffer has no worktree")
return
end
if not rev.path then
util.warning("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.warning("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.warning("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.warning("gD on merge base is ambiguous, use :Gdiffsplit <rev>")
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.warning("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.warning("no file in current buffer")
return
end
if vim.bo[cur_buf].buftype ~= "" then
util.warning("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.warning("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.warning("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