Files
nvim/lua/git/diff.lua
T

240 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_stdout = util.system_sync(
{ "git", "hash-object", "-w", "--stdin" },
{ cwd = worktree, stdin = body }
)
if not hash_stdout then
return
end
local sha = vim.trim(hash_stdout)
local mode = vim.b[buf].git_index_mode
if not mode then
mode = "100644"
local ls = util.system_sync(
{ "git", "ls-files", "-s", "--", path },
{ cwd = worktree, silent = true }
)
if ls then
local m = ls: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.
if
not util.system_sync({
"git",
"update-index",
"--cacheinfo",
mode,
sha,
path,
}, { cwd = worktree })
then
return
end
vim.bo[buf].modified = false
end,
})
end
---Return a buffer holding the content at `<ref>:<path>`. `ref` is
---"index", "HEAD", or a commit revspec.
---@param worktree string
---@param ref string
---@param path string
---@return integer
function M.git_show_buf(worktree, ref, path)
local name = "git://" .. ref .. "//" .. path
local buf = vim.fn.bufadd(name)
vim.b[buf].git_worktree = worktree
vim.fn.bufload(buf)
return buf
end
---BufReadCmd handler for `git://<ref>//<path>` URIs. Worktree comes from
---`vim.b[buf].git_worktree` if set, else from cwd. Ref of "index" maps
---to `git show :<path>`; "worktree" leaves the buffer empty (placeholder
---for missing files); anything else is a revspec.
---@param buf integer
function M.read_uri(buf)
local name = vim.api.nvim_buf_get_name(buf)
local ref, path = name:match("^git://(.-)//(.*)$")
if not ref or path == "" then
return
end
---@cast path -nil
local worktree = vim.b[buf].git_worktree or select(2, repo.resolve_cwd())
if not worktree then
log.error("git BufReadCmd %s: cannot resolve worktree", name)
return
end
vim.b[buf].git_worktree = worktree
vim.bo[buf].swapfile = false
vim.bo[buf].bufhidden = "wipe"
if ref == "worktree" then
vim.bo[buf].buftype = "nofile"
vim.bo[buf].modifiable = false
vim.bo[buf].modified = false
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf })
return
end
local revspec = ref == "index" and (":" .. path) or (ref .. ":" .. path)
local stdout = util.system_sync(
{ "git", "show", revspec },
{ cwd = worktree }
)
if stdout then
vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout))
end
if ref == "index" then
vim.bo[buf].buftype = "acwrite"
-- Re-running BufReadCmd (e.g. on `:edit`) would otherwise stack
-- another BufWriteCmd on the same buffer, so each `:w` runs
-- hash-object + update-index N times.
if not vim.b[buf].git_index_writer then
attach_index_writer(buf, worktree, path)
vim.b[buf].git_index_writer = true
end
else
vim.bo[buf].buftype = "nofile"
vim.bo[buf].modifiable = false
end
vim.bo[buf].modified = false
-- BufReadCmd suppresses the normal BufReadPost dispatch, so filetype
-- detection and modeline parsing don't run unless we fire it ourselves.
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf })
end
---@class ow.Git.EmptyBufOpts
---@field name string?
---@field bufhidden ("hide"|"wipe")? defaults to "wipe"
---Build a read-only scratch buffer, optionally naming it. When `opts.name`
---is set and a loaded buffer with that name already exists, returns it
---instead of creating a duplicate.
---@param opts ow.Git.EmptyBufOpts?
---@return integer
function M.empty_buf(opts)
opts = opts or {}
if opts.name then
local existing = vim.fn.bufnr(opts.name)
if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then
return existing
end
end
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
---Toggle a window into or out of Vim's diff mode. Goes through the
---`:diffthis` / `:diffoff` commands rather than `vim.wo[win].diff = X`.
---The command path runs Vim's full `diff_win_options` setup, which sets
---an internal flag that prevents subsequently-created floats from
---inheriting `'diff' = 1` when opened from a focused diff pane. The raw
---option setter skips that setup, so floats (oil, fzf-lua, etc) end up
---joining the tabpage's diff group and corrupting its render.
---@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
---@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 label = opts.ref == "" and "index" or opts.ref
local uri = "git://" .. label .. "//" .. rel
-- Stash the worktree on the buffer so the BufReadCmd handler doesn't
-- fall back to cwd resolution (wrong when cwd != worktree).
local buf = vim.fn.bufadd(uri)
vim.b[buf].git_worktree = worktree
local prefix = opts.vertical and "leftabove vert " or "leftabove "
vim.cmd(prefix .. "diffsplit " .. vim.fn.fnameescape(uri))
end
return M