Files
nvim/lua/git/object.lua
T

367 lines
10 KiB
Lua

local Revision = require("git.revision")
local repo = require("git.repo")
local util = require("git.util")
local M = {}
---@class ow.Git.DiffSection
---@field path_a string
---@field path_b string
---@field blob_a string?
---@field blob_b string?
---@class ow.Git.BufContext
---@field worktree string
---@field sha string
---@field parent_sha string?
---@return ow.Git.BufContext?
local function context()
local worktree = vim.b.git_worktree
local sha = vim.b.git_sha
if not worktree or not sha then
return nil
end
return { worktree = worktree, sha = sha, parent_sha = vim.b.git_parent_sha }
end
---@return ow.Git.DiffSection?
local function diff_section()
local diff_lnum = vim.fn.search("^diff --git ", "bcnW")
if diff_lnum == 0 then
return nil
end
local diff_line =
vim.api.nvim_buf_get_lines(0, diff_lnum - 1, diff_lnum, false)[1]
if not diff_line then
return nil
end
local path_a, path_b = diff_line:match("^diff %-%-git a/(.-) b/(.+)$")
if not path_a or not path_b then
return nil
end
local header =
vim.api.nvim_buf_get_lines(0, diff_lnum, diff_lnum + 20, false)
local blob_a, blob_b
for _, l in ipairs(header) do
if l:sub(1, 3) == "@@ " or l:match("^diff %-%-git ") then
break
end
local pre, post = l:match("^index (%x+)%.%.(%x+)")
if pre then
blob_a = pre
blob_b = post
break
end
end
return {
path_a = path_a,
path_b = path_b,
blob_a = blob_a,
blob_b = blob_b,
}
end
---@param sha string?
---@return boolean
local function is_zero(sha)
return sha == nil or sha:match("^0+$") ~= nil
end
---@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
---@type table<integer, string>
local pending_content = {}
---@param worktree string
---@param rev ow.Git.Revision
---@param content string?
---@return integer
function M.buf_for(worktree, rev, content)
local buf = vim.fn.bufadd(rev:uri())
vim.b[buf].git_worktree = worktree
if content then
pending_content[buf] = content
end
vim.fn.bufload(buf)
return buf
end
---@param buf integer
function M.read_uri(buf)
local name = vim.api.nvim_buf_get_name(buf)
local rev = Revision.from_uri(name)
if not rev then
return
end
local rev_str = rev:format()
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 = "hide"
---@type string?
local stdout = pending_content[buf]
pending_content[buf] = nil
if stdout == nil then
stdout = util.exec(
{ "git", "cat-file", "-p", rev_str },
{ cwd = worktree }
)
end
if stdout and rev.path == nil then
local commit_sha =
repo.rev_parse(worktree, rev_str .. "^{commit}", true)
if commit_sha then
local patch = util.exec({
"git",
"diff-tree",
"-p",
"-m",
"--first-parent",
"--root",
"--no-commit-id",
commit_sha,
}, { cwd = worktree })
if patch then
stdout = (stdout:gsub("\n*$", "\n\n")) .. patch
end
vim.b[buf].git_parent_sha =
repo.rev_parse(worktree, commit_sha .. "^", true)
end
end
if stdout then
vim.bo[buf].modifiable = true
vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout))
end
local rev_sha = repo.rev_parse(worktree, rev_str, true)
if rev_sha then
vim.b[buf].git_sha = rev_sha
end
if rev.stage == 0 and rev.path then
vim.bo[buf].buftype = "acwrite"
if not vim.b[buf].git_index_writer then
attach_index_writer(buf, worktree, rev.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
-- Match on the inner path directly. `vim.filetype.add` patterns
-- don't work because Vim normalises `git://` filenames (cwd-prefix
-- + `://` -> `:/`) before matching, breaking any pattern keyed on
-- the raw scheme.
if rev.path then
local ft = vim.filetype.match({ filename = rev.path, buf = buf })
if ft then
vim.bo[buf].filetype = ft
end
else
vim.bo[buf].filetype = "git"
end
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf })
end
---@param worktree string
---@param blob string?
---@param path string
---@param sha string
---@return integer?
local function blob_buf(worktree, blob, path, sha)
if is_zero(blob) then
return nil
end
return M.buf_for(worktree, Revision.new({ base = sha, path = path }))
end
---@param worktree string
---@param blob string?
---@param path string
---@param sha string
local function load_blob(worktree, blob, path, sha)
local buf = blob_buf(worktree, blob, path, sha)
if not buf then
util.warning("no content for %s at %s", path, sha)
return
end
vim.cmd.normal({ "m'", bang = true })
vim.api.nvim_set_current_buf(buf)
end
---@param ctx ow.Git.BufContext
---@param section ow.Git.DiffSection
local function open_section(ctx, section)
if not section.blob_a or not section.blob_b then
util.warning("no index line, cannot determine blob SHAs")
return
end
local parent = ctx.parent_sha or "0"
local left = blob_buf(ctx.worktree, section.blob_a, section.path_a, parent)
local right =
blob_buf(ctx.worktree, section.blob_b, section.path_b, ctx.sha)
if left and right then
require("git.diff").open(left, right, true)
return
end
if not left and not right then
util.warning("no content for %s", section.path_b)
return
end
local buf = left or right
---@cast buf -nil
vim.cmd.normal({ "m'", bang = true })
vim.api.nvim_set_current_buf(buf)
end
---@class ow.Git.OpenObjectOpts
---@field split (false|"above"|"below"|"left"|"right")?
---@param worktree string
---@param rev string
---@param opts ow.Git.OpenObjectOpts?
function M.open_object(worktree, rev, opts)
local parsed = Revision.parse(rev)
if parsed.base then
local sha = repo.rev_parse(worktree, parsed.base, true)
if sha then
parsed.base = sha
end
end
local content = util.exec(
{ "git", "cat-file", "-p", parsed:format() },
{ cwd = worktree, silent = true }
)
if not content then
util.warning("not a git object: %s", rev)
return
end
local buf = M.buf_for(worktree, parsed, content)
util.place_buf(buf, opts and opts.split)
end
---@return boolean dispatched
function M.open_under_cursor()
local ctx = context()
if not ctx then
return false
end
local line = vim.api.nvim_get_current_line()
local sha = line:match("^commit (%x+)$")
or line:match("^parent (%x+)$")
or line:match("^tree (%x+)$")
or line:match("^object (%x+)$")
if sha then
M.open_object(ctx.worktree, sha, { split = false })
return true
end
local entry_type, entry_sha, entry_name =
line:match("^%d+ (%w+) (%x+)\t(.+)$")
if entry_sha then
local nav_rev = entry_type == "blob"
and Revision.new({ base = ctx.sha, path = entry_name }):format()
or entry_sha
M.open_object(ctx.worktree, nav_rev, { split = false })
return true
end
local section = diff_section()
if not section then
return false
end
local parent = ctx.parent_sha or "0"
if line:match("^diff %-%-git ") then
open_section(ctx, section)
return true
end
if line:match("^%-%-%- ") then
load_blob(ctx.worktree, section.blob_a, section.path_a, parent)
return true
end
if line:match("^%+%+%+ ") then
load_blob(ctx.worktree, section.blob_b, section.path_b, ctx.sha)
return true
end
local prefix = line:sub(1, 1)
if prefix == "+" then
load_blob(ctx.worktree, section.blob_b, section.path_b, ctx.sha)
return true
elseif prefix == "-" then
load_blob(ctx.worktree, section.blob_a, section.path_a, parent)
return true
end
return false
end
return M