235 lines
7.8 KiB
Lua
235 lines
7.8 KiB
Lua
local repo = require("git.repo")
|
|
local util = require("git.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.exec(
|
|
{ "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.exec(
|
|
{ "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.exec({
|
|
"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 addressed by a git revspec. The
|
|
---URI is `git://<revspec>` and BufReadCmd loads via `git cat-file -p`.
|
|
---@param worktree string
|
|
---@param revspec string any revspec git understands (e.g. `HEAD:foo`, `:foo`, `:1:foo`, `<sha>`, `<sha>:foo`)
|
|
---@return integer
|
|
function M.git_show_buf(worktree, revspec)
|
|
local name = "git://" .. revspec
|
|
local buf = vim.fn.bufadd(name)
|
|
vim.b[buf].git_worktree = worktree
|
|
vim.fn.bufload(buf)
|
|
return buf
|
|
end
|
|
|
|
---BufReadCmd handler for `git://<revspec>` URIs. Loads content via
|
|
---`git cat-file -p <revspec>`. Worktree comes from `vim.b[buf]
|
|
---.git_worktree` if set, else from cwd. Index entries (revspec form
|
|
---`:<path>` for stage 0) are made writable via `attach_index_writer`,
|
|
---so `:w` updates the index. Other revspecs are read-only.
|
|
---@param buf integer
|
|
function M.read_uri(buf)
|
|
local name = vim.api.nvim_buf_get_name(buf)
|
|
local revspec = name:match("^git://(.+)$")
|
|
if not revspec then
|
|
return
|
|
end
|
|
|
|
local worktree = vim.b[buf].git_worktree or select(2, repo.resolve_cwd())
|
|
if not worktree then
|
|
util.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"
|
|
|
|
local stdout = util.exec(
|
|
{ "git", "cat-file", "-p", revspec },
|
|
{ cwd = worktree }
|
|
)
|
|
if stdout then
|
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout))
|
|
end
|
|
|
|
-- Stage-0 index entries (`:<path>` with no further `:`) are
|
|
-- editable; `:w` rewrites the index entry via `attach_index_writer`.
|
|
-- Anything else (HEAD:, <sha>:, :1:, :2:, :3:, bare object refs)
|
|
-- is read-only.
|
|
local index_path = revspec:match("^:([^:]+)$")
|
|
if index_path 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, index_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
|
|
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
|
|
local _, worktree = repo.resolve(cur_path)
|
|
if not worktree then
|
|
util.warning("not in a git repository")
|
|
return
|
|
end
|
|
local rel = vim.fs.relpath(worktree, cur_path)
|
|
if not rel then
|
|
util.warning("file is outside the worktree")
|
|
return
|
|
end
|
|
|
|
-- Stage 0 (index) is `:<path>`; named refs are `<ref>:<path>`.
|
|
local revspec = opts.ref == "" and (":" .. rel) or (opts.ref .. ":" .. rel)
|
|
local uri = "git://" .. revspec
|
|
-- 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
|