Files
nvim/lua/git/diff.lua
T

245 lines
7.7 KiB
Lua

local log = require("log")
local repo = require("git.repo")
local util = require("util")
local M = {}
---@param buf integer
---@param worktree string
---@param path string
local function attach_index_writer(buf, worktree, path)
vim.api.nvim_create_autocmd("BufWriteCmd", {
buffer = buf,
callback = function()
local body = table.concat(
vim.api.nvim_buf_get_lines(buf, 0, -1, false),
"\n"
) .. "\n"
local hash = vim.system(
{ "git", "hash-object", "-w", "--stdin" },
{ cwd = worktree, stdin = body, text = true }
):wait()
if hash.code ~= 0 then
log.error("git hash-object failed: %s", hash.stderr or "")
return
end
local sha = vim.trim(hash.stdout or "")
local mode = vim.b[buf].git_index_mode
if not mode then
mode = "100644"
local ls = vim.system(
{ "git", "ls-files", "-s", "--", path },
{ cwd = worktree, text = true }
):wait()
if ls.code == 0 and ls.stdout then
local m = ls.stdout:match("^(%d+)")
if m then
mode = m
end
end
vim.b[buf].git_index_mode = mode
end
-- Use the 3-arg form (mode sha path) instead of the comma form
-- (mode,sha,path), which doesn't survive paths containing a
-- comma.
local upd = vim.system({
"git",
"update-index",
"--cacheinfo",
mode,
sha,
path,
}, { cwd = worktree, text = true }):wait()
if upd.code ~= 0 then
log.error("git update-index failed: %s", upd.stderr or "")
return
end
vim.bo[buf].modified = false
end,
})
end
---Internal builder: create a scratch buffer and fill it with the output of
---`git show <revspec>`. Synchronous so the buffer is ready by the time the
---caller wires up windows / `:diffthis`. An empty buffer briefly visible
---to the diff engine produces a spurious whole-file diff.
---@param worktree string
---@param revspec string
---@param is_index boolean
---@param index_path string? required when is_index is true
---@return integer
local function build_show_buf(worktree, revspec, is_index, index_path)
local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].buftype = "nofile"
vim.bo[buf].bufhidden = "wipe"
vim.bo[buf].swapfile = false
local result = vim.system(
{ "git", "show", revspec },
{ cwd = worktree, text = true }
)
:wait()
if result.code ~= 0 then
log.error(
"git show %s failed: %s",
revspec,
vim.trim(result.stderr or "")
)
else
vim.api.nvim_buf_set_lines(
buf,
0,
-1,
false,
util.split_lines(result.stdout or "")
)
end
if is_index then
vim.bo[buf].buftype = "acwrite"
attach_index_writer(buf, worktree, assert(index_path))
else
vim.bo[buf].modifiable = false
end
vim.bo[buf].modified = false
return buf
end
---@param worktree string
---@param ref string '' for index, 'HEAD' or a sha for committed refs
---@param path string
---@param is_index boolean? true to hook :w to update the git index
---@return integer
function M.git_show_buf(worktree, ref, path, is_index)
return build_show_buf(worktree, ref .. ":" .. path, is_index or false, path)
end
---@param worktree string
---@param blob string the blob SHA (full or abbreviated)
---@return integer
function M.git_show_blob(worktree, blob)
return build_show_buf(worktree, blob, false, nil)
end
---@class ow.Git.EmptyBufOpts
---@field name string?
---@field bufhidden ("hide"|"wipe")? defaults to "wipe"
---Build a read-only scratch buffer, optionally naming it via
---`nvim_buf_set_name` (silently no-op if a buffer with that name
---already exists).
---@param opts ow.Git.EmptyBufOpts?
---@return integer
function M.empty_buf(opts)
opts = opts or {}
local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].buftype = "nofile"
vim.bo[buf].bufhidden = opts.bufhidden or "wipe"
vim.bo[buf].swapfile = false
vim.bo[buf].modifiable = false
vim.bo[buf].modified = false
if opts.name then
pcall(vim.api.nvim_buf_set_name, buf, opts.name)
end
return buf
end
---@param abs_path string
---@return integer
function M.load_file_buf(abs_path)
local buf = vim.fn.bufadd(abs_path)
vim.fn.bufload(buf)
return buf
end
---Name a scratch buffer with a `git://...` URI and apply the filetype
---inferred from the inner path segment. The `nvim_buf_set_name` call is
---wrapped in pcall because a buffer with that name may already exist
---(E95).
---@param buf integer
---@param name string
function M.set_buf_name_and_filetype(buf, name)
pcall(vim.api.nvim_buf_set_name, buf, name)
local ft = vim.filetype.match({ buf = buf })
if ft then
vim.bo[buf].filetype = ft
end
end
---Apply (or remove) the window-local options that `:diffthis` would
---normally set. Used by both `M.split` here and the sidebar so the two
---diff entry points behave consistently. Vim is supposed to save and
---restore these around the `'diff'` flag flip, but that round-trip is
---fragile when buffers are swapped under an already-diff window.
---@param win integer
---@param enabled boolean
function M.set_diff(win, enabled)
vim.wo[win].diff = enabled
if enabled then
vim.wo[win].foldmethod = "diff"
vim.wo[win].foldenable = true
vim.wo[win].foldlevel = 0
vim.wo[win].scrollbind = true
vim.wo[win].cursorbind = true
vim.wo[win].wrap = false
else
vim.wo[win].scrollbind = false
vim.wo[win].cursorbind = false
end
end
---@class ow.Git.SplitOpts
---@field ref string '' for index, 'HEAD' for HEAD
---@field vertical boolean
---@param opts ow.Git.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)
if cur_path == "" then
log.warning("no file in current buffer")
return
end
if vim.bo[cur_buf].buftype ~= "" then
log.warning("cannot diff this buffer (not a worktree file)")
return
end
local _, worktree = repo.resolve(cur_path)
if not worktree then
log.warning("not in a git repository")
return
end
local rel = vim.fs.relpath(worktree, cur_path)
if not rel then
log.warning("file is outside the worktree")
return
end
local is_index = opts.ref == ""
local other = M.git_show_buf(worktree, opts.ref, rel, is_index)
local label = is_index and "index" or opts.ref
M.set_buf_name_and_filetype(other, "git://" .. label .. "/" .. rel)
local cur_win = vim.api.nvim_get_current_win()
local other_win = vim.api.nvim_open_win(other, true, {
split = opts.vertical and "left" or "above",
win = cur_win,
})
-- The synthetic index/HEAD buffer can't run BufRead, so its filetype
-- detection in `set_buf_name_and_filetype` only catches
-- filename-pattern matches. Mirror cur_buf's filetype, since this is
-- the same logical file at a different version. Done after the
-- window opens so any BufWinEnter / BufEnter autocmds that fire on
-- nvim_open_win can't undo it.
local cur_ft = vim.bo[cur_buf].filetype
if cur_ft ~= "" then
vim.bo[other].filetype = cur_ft
end
M.set_diff(cur_win, true)
M.set_diff(other_win, true)
end
return M