refactor(git): unify around the Repo abstraction
This commit is contained in:
+96
-75
@@ -4,27 +4,29 @@ local util = require("git.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.URI_PREFIX = "git://"
|
||||
|
||||
---@param rev ow.Git.Revision
|
||||
---@return string
|
||||
function M.format_uri(rev)
|
||||
return M.URI_PREFIX .. rev:format()
|
||||
end
|
||||
|
||||
---@param str string
|
||||
---@return ow.Git.Revision?
|
||||
function M.parse_uri(str)
|
||||
local raw = str:match("^" .. M.URI_PREFIX .. "(.+)$")
|
||||
if raw then
|
||||
return Revision.parse(raw)
|
||||
end
|
||||
end
|
||||
|
||||
---@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")
|
||||
@@ -70,9 +72,9 @@ local function is_zero(sha)
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param worktree string
|
||||
---@param r ow.Git.Repo
|
||||
---@param path string
|
||||
local function attach_index_writer(buf, worktree, path)
|
||||
local function attach_index_writer(buf, r, path)
|
||||
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
@@ -82,18 +84,19 @@ local function attach_index_writer(buf, worktree, path)
|
||||
) .. "\n"
|
||||
local hash_stdout = util.exec(
|
||||
{ "git", "hash-object", "-w", "--stdin" },
|
||||
{ cwd = worktree, stdin = body }
|
||||
{ cwd = r.worktree, stdin = body }
|
||||
)
|
||||
if not hash_stdout then
|
||||
return
|
||||
end
|
||||
local sha = vim.trim(hash_stdout)
|
||||
local mode = vim.b[buf].git_index_mode
|
||||
local state = r:state(buf)
|
||||
local mode = state and state.index_mode
|
||||
if not mode then
|
||||
mode = "100644"
|
||||
local ls = util.exec(
|
||||
{ "git", "ls-files", "-s", "--", path },
|
||||
{ cwd = worktree, silent = true }
|
||||
{ cwd = r.worktree, silent = true }
|
||||
)
|
||||
if ls then
|
||||
local m = ls:match("^(%d+)")
|
||||
@@ -101,7 +104,9 @@ local function attach_index_writer(buf, worktree, path)
|
||||
mode = m
|
||||
end
|
||||
end
|
||||
vim.b[buf].git_index_mode = mode
|
||||
if state then
|
||||
state.index_mode = mode
|
||||
end
|
||||
end
|
||||
-- Use the 3-arg form (mode sha path) instead of the comma
|
||||
-- form (mode,sha,path), which doesn't survive paths
|
||||
@@ -114,7 +119,7 @@ local function attach_index_writer(buf, worktree, path)
|
||||
mode,
|
||||
sha,
|
||||
path,
|
||||
}, { cwd = worktree })
|
||||
}, { cwd = r.worktree })
|
||||
then
|
||||
return
|
||||
end
|
||||
@@ -123,18 +128,27 @@ local function attach_index_writer(buf, worktree, path)
|
||||
})
|
||||
end
|
||||
|
||||
---@type table<integer, string>
|
||||
local pending_content = {}
|
||||
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
|
||||
|
||||
---@param worktree string
|
||||
---@param buf integer
|
||||
function M.attach_dispatch(buf)
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
if not M.open_under_cursor() then
|
||||
vim.api.nvim_feedkeys(cr, "n", false)
|
||||
end
|
||||
end, { buffer = buf, silent = true, desc = "Open file at commit" })
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@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
|
||||
function M.buf_for(r, rev, content)
|
||||
local buf = vim.fn.bufadd(M.format_uri(rev))
|
||||
repo.attach(buf, r)
|
||||
if content then
|
||||
pending_content[buf] = content
|
||||
local state = r:state(buf) --[[@as -nil]]
|
||||
state.pending_content = content
|
||||
end
|
||||
vim.fn.bufload(buf)
|
||||
return buf
|
||||
@@ -143,35 +157,43 @@ end
|
||||
---@param buf integer
|
||||
function M.read_uri(buf)
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
local rev = Revision.from_uri(name)
|
||||
local rev = M.parse_uri(name)
|
||||
if not rev then
|
||||
return
|
||||
end
|
||||
local rev_str = rev:format()
|
||||
|
||||
local worktree = vim.b[buf].git_worktree or select(2, repo.current_repo())
|
||||
if not worktree then
|
||||
local r = repo.find(buf)
|
||||
if not r then
|
||||
util.error("git BufReadCmd %s: cannot resolve worktree", name)
|
||||
return
|
||||
end
|
||||
vim.b[buf].git_worktree = worktree
|
||||
repo.attach(buf, r)
|
||||
local state = r:state(buf) --[[@as -nil]]
|
||||
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].bufhidden = "hide"
|
||||
|
||||
---@type string?
|
||||
local stdout = pending_content[buf]
|
||||
pending_content[buf] = nil
|
||||
local stdout = state.pending_content
|
||||
state.pending_content = nil
|
||||
-- On a refresh tick (no caller-provided content), skip the re-read
|
||||
-- when the rev still resolves to the same sha. Avoids re-firing
|
||||
-- BufReadPost (and the LSP/treesitter re-attach storm) on every
|
||||
-- fs-event for buffers whose content can't have changed.
|
||||
if stdout == nil then
|
||||
local rev_sha = r:rev_parse(rev_str, true)
|
||||
if rev_sha and rev_sha == state.sha then
|
||||
return
|
||||
end
|
||||
stdout = util.exec(
|
||||
{ "git", "cat-file", "-p", rev_str },
|
||||
{ cwd = worktree }
|
||||
{ cwd = r.worktree }
|
||||
)
|
||||
end
|
||||
|
||||
if stdout and rev.path == nil then
|
||||
local commit_sha =
|
||||
repo.rev_parse(worktree, rev_str .. "^{commit}", true)
|
||||
local commit_sha = r:rev_parse(rev_str .. "^{commit}", true)
|
||||
if commit_sha then
|
||||
local patch = util.exec({
|
||||
"git",
|
||||
@@ -182,12 +204,11 @@ function M.read_uri(buf)
|
||||
"--root",
|
||||
"--no-commit-id",
|
||||
commit_sha,
|
||||
}, { cwd = worktree })
|
||||
}, { cwd = r.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)
|
||||
state.parent_sha = r:rev_parse(commit_sha .. "^", true)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -196,16 +217,13 @@ function M.read_uri(buf)
|
||||
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
|
||||
state.sha = r:rev_parse(rev_str, true)
|
||||
|
||||
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
|
||||
if not state.index_writer then
|
||||
attach_index_writer(buf, r, rev.path)
|
||||
state.index_writer = true
|
||||
end
|
||||
else
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
@@ -226,27 +244,29 @@ function M.read_uri(buf)
|
||||
vim.bo[buf].filetype = "git"
|
||||
end
|
||||
|
||||
M.attach_dispatch(buf)
|
||||
|
||||
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf })
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param r ow.Git.Repo
|
||||
---@param blob string?
|
||||
---@param path string
|
||||
---@param sha string
|
||||
---@return integer?
|
||||
local function blob_buf(worktree, blob, path, sha)
|
||||
local function blob_buf(r, blob, path, sha)
|
||||
if is_zero(blob) then
|
||||
return nil
|
||||
end
|
||||
return M.buf_for(worktree, Revision.new({ base = sha, path = path }))
|
||||
return M.buf_for(r, Revision.new({ base = sha, path = path }))
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param r ow.Git.Repo
|
||||
---@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)
|
||||
local function load_blob(r, blob, path, sha)
|
||||
local buf = blob_buf(r, blob, path, sha)
|
||||
if not buf then
|
||||
util.warning("no content for %s at %s", path, sha)
|
||||
return
|
||||
@@ -255,17 +275,17 @@ local function load_blob(worktree, blob, path, sha)
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
end
|
||||
|
||||
---@param ctx ow.Git.BufContext
|
||||
---@param s ow.Git.BufState
|
||||
---@param section ow.Git.DiffSection
|
||||
local function open_section(ctx, section)
|
||||
local function open_section(s, 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 parent = s.parent_sha or "0"
|
||||
local left = blob_buf(s.repo, section.blob_a, section.path_a, parent)
|
||||
local right =
|
||||
blob_buf(ctx.worktree, section.blob_b, section.path_b, ctx.sha)
|
||||
blob_buf(s.repo, section.blob_b, section.path_b, s.sha --[[@as string]])
|
||||
if left and right then
|
||||
require("git.diff").open(left, right, true)
|
||||
return
|
||||
@@ -283,44 +303,45 @@ end
|
||||
---@class ow.Git.OpenObjectOpts
|
||||
---@field split (false|"above"|"below"|"left"|"right")?
|
||||
|
||||
---@param worktree string
|
||||
---@param r ow.Git.Repo
|
||||
---@param rev string
|
||||
---@param opts ow.Git.OpenObjectOpts?
|
||||
function M.open_object(worktree, rev, opts)
|
||||
function M.open_object(r, rev, opts)
|
||||
local parsed = Revision.parse(rev)
|
||||
if parsed.base then
|
||||
local sha = repo.rev_parse(worktree, parsed.base, true)
|
||||
local sha = r:rev_parse(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 }
|
||||
{ cwd = r.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)
|
||||
local buf = M.buf_for(r, 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
|
||||
local s = repo.state()
|
||||
if not s or not s.sha then
|
||||
return false
|
||||
end
|
||||
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
local r = s.repo
|
||||
|
||||
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 })
|
||||
M.open_object(r, sha, { split = false })
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -328,9 +349,9 @@ function M.open_under_cursor()
|
||||
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()
|
||||
and Revision.new({ base = s.sha, path = entry_name }):format()
|
||||
or entry_sha
|
||||
M.open_object(ctx.worktree, nav_rev, { split = false })
|
||||
M.open_object(r, nav_rev, { split = false })
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -338,26 +359,26 @@ function M.open_under_cursor()
|
||||
if not section then
|
||||
return false
|
||||
end
|
||||
local parent = ctx.parent_sha or "0"
|
||||
local parent = s.parent_sha or "0"
|
||||
|
||||
if line:match("^diff %-%-git ") then
|
||||
open_section(ctx, section)
|
||||
open_section(s, section)
|
||||
return true
|
||||
end
|
||||
if line:match("^%-%-%- ") then
|
||||
load_blob(ctx.worktree, section.blob_a, section.path_a, parent)
|
||||
load_blob(r, 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)
|
||||
load_blob(r, section.blob_b, section.path_b, s.sha --[[@as string]])
|
||||
return true
|
||||
end
|
||||
local prefix = line:sub(1, 1)
|
||||
if prefix == "+" then
|
||||
load_blob(ctx.worktree, section.blob_b, section.path_b, ctx.sha)
|
||||
load_blob(r, section.blob_b, section.path_b, s.sha --[[@as string]])
|
||||
return true
|
||||
elseif prefix == "-" then
|
||||
load_blob(ctx.worktree, section.blob_a, section.path_a, parent)
|
||||
load_blob(r, section.blob_a, section.path_a, parent)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user