refactor(git): unify around the Repo abstraction

This commit is contained in:
2026-05-02 22:45:44 +02:00
parent 8bd674622e
commit be1d7ace50
14 changed files with 671 additions and 586 deletions
-17
View File
@@ -1,17 +0,0 @@
-- The built-in `git` filetype is reused for our `:G show` / `:G cat-file -p`
-- output buffers and for commits opened from the log window. We set
-- `vim.b.git_worktree` before assigning the filetype on those buffers; the
-- guard below keeps the <CR> dispatcher off any unrelated git buffer (a
-- real `.git/HEAD` file, etc.) so the default normal-mode <CR> still works.
if not vim.b.git_worktree then
return
end
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
vim.keymap.set("n", "<CR>", function()
if not require("git.object").open_under_cursor() then
-- "n" mode = no remap, so this doesn't recurse into our mapping.
vim.api.nvim_feedkeys(cr, "n", false)
end
end, { buffer = 0, silent = true, desc = "Open file at commit" })
-17
View File
@@ -1,17 +0,0 @@
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
vim.keymap.set("n", "<CR>", function()
local worktree = vim.b.git_worktree
-- Anchor past the leading graph chars (matches the leading sha column,
-- not any hex word that happens to appear later in the subject).
local sha = worktree
and vim.api
.nvim_get_current_line()
:match("^[*|/\\_ ]*(%x%x%x%x%x%x%x+)")
if sha then
require("git.object").open_object(worktree, sha, { split = false })
else
-- "n" mode = no remap, so this doesn't recurse into our mapping.
vim.api.nvim_feedkeys(cr, "n", false)
end
end, { buffer = 0, silent = true, desc = "Open commit" })
+122 -23
View File
@@ -23,8 +23,10 @@ local function git_cmds()
if cached_cmds then
return cached_cmds
end
local result = vim
.system({ "git", "--list-cmds=main,others,alias" }, { text = true })
local result = vim.system(
{ "git", "--list-cmds=main,others,alias" },
{ text = true }
)
:wait()
if result.code ~= 0 then
util.error("git --list-cmds failed: %s", vim.trim(result.stderr or ""))
@@ -70,30 +72,31 @@ local function place_split(name)
return buf
end
---@param worktree string
---@param r ow.Git.Repo
---@param args string[]
---@param conf ow.Git.SplitHandler
local function run_in_split(worktree, args, conf)
local function run_in_split(r, args, conf)
local cmd = { "git" }
vim.list_extend(cmd, args)
util.exec(cmd, {
cwd = worktree,
cwd = r.worktree,
on_done = function(stdout)
if not stdout then
return
end
local name = "[git " .. table.concat(args, " ") .. "]"
local buf = place_split(name)
vim.b[buf].git_worktree = worktree
vim.b[buf].git_sha = nil
vim.b[buf].git_parent_sha = nil
repo.attach(buf, r)
object.attach_dispatch(buf)
local state = r:state(buf) --[[@as -nil]]
state.sha = nil
state.parent_sha = nil
if conf.needs_rev then
local user_rev = first_positional(args, 2) or "HEAD"
local sha = repo.rev_parse(worktree, user_rev, true)
local sha = r:rev_parse(user_rev, true)
if sha then
vim.b[buf].git_sha = sha
vim.b[buf].git_parent_sha =
repo.rev_parse(worktree, user_rev .. "^", true)
state.sha = sha
state.parent_sha = r:rev_parse(user_rev .. "^", true)
end
end
vim.bo[buf].filetype = conf.ft
@@ -111,14 +114,14 @@ local function run_in_split(worktree, args, conf)
})
end
---@param worktree string
---@param r ow.Git.Repo
---@param args string[]
local function run_to_messages(worktree, args)
local function run_to_messages(r, args)
local cmd = { "git" }
vim.list_extend(cmd, args)
vim.system(
cmd,
{ cwd = worktree, text = true },
{ cwd = r.worktree, text = true },
vim.schedule_wrap(function(obj)
local out = vim.trim(obj.stdout or "")
local err = vim.trim(obj.stderr or "")
@@ -175,8 +178,8 @@ end
---@param args string[]
function M.run(args)
local _, worktree = repo.current_repo()
if not worktree then
local r = repo.find()
if not r then
util.warning("not in a git repository")
return
end
@@ -190,10 +193,10 @@ function M.run(args)
if sub == "show" then
local arg = first_positional(args, 2)
if arg and arg:find(":", 1, true) then
object.open_object(worktree, arg)
object.open_object(r, arg)
return
end
run_in_split(worktree, args, { ft = "git", needs_rev = true })
run_in_split(r, args, { ft = "git", needs_rev = true })
return
end
@@ -201,22 +204,118 @@ function M.run(args)
if vim.list_contains(args, "-p") then
local rev = first_positional(args, 2)
if rev then
object.open_object(worktree, rev)
object.open_object(r, rev)
return
end
end
run_in_split(worktree, args, { ft = "git", needs_rev = true })
run_in_split(r, args, { ft = "git", needs_rev = true })
return
end
local conf = sub and SPLIT_HANDLERS[sub]
if conf then
run_in_split(worktree, args, conf)
run_in_split(r, args, conf)
else
run_to_messages(worktree, args)
run_to_messages(r, args)
end
end
---@param arg_lead string
---@return string[]
function M.complete_rev(arg_lead)
local r = repo.find()
if not r then
return {}
end
local stage, stage_path_lead = arg_lead:match("^:([0-3]):(.*)$")
if stage then
local out = util.exec(
{ "git", "ls-files", "--stage" },
{ cwd = r.worktree, silent = true }
)
if not out then
return {}
end
local matches = {}
for _, line in ipairs(util.split_lines(out)) do
local row_stage, row_path = line:match("^%S+ %S+ (%d)\t(.*)$")
if
row_stage == stage
and row_path
and row_path:sub(1, #stage_path_lead) == stage_path_lead
then
table.insert(matches, ":" .. stage .. ":" .. row_path)
end
end
return matches
end
local colon = arg_lead:find(":", 1, true)
if not colon then
local matches = {}
for _, ref in ipairs(r:list_refs()) do
if ref:sub(1, #arg_lead) == arg_lead then
table.insert(matches, ref)
end
end
return matches
end
local rev = arg_lead:sub(1, colon - 1)
local path_lead = arg_lead:sub(colon + 1)
local dir, name_lead = path_lead:match("^(.*/)([^/]*)$")
dir = dir or ""
name_lead = name_lead or path_lead
if rev ~= "" then
local cmd = { "git", "ls-tree", rev }
if dir ~= "" then
table.insert(cmd, dir)
end
local out = util.exec(cmd, { cwd = r.worktree, silent = true })
if not out then
return {}
end
local matches = {}
for _, line in ipairs(util.split_lines(out)) do
local typ, full_path = line:match("^%S+ (%S+) %S+\t(.*)$")
if typ and full_path then
local basename = dir == "" and full_path
or full_path:sub(#dir + 1)
if typ == "tree" then
basename = basename .. "/"
end
if basename:sub(1, #name_lead) == name_lead then
table.insert(matches, rev .. ":" .. dir .. basename)
end
end
end
return matches
end
local cmd = { "git", "ls-files" }
if dir ~= "" then
table.insert(cmd, dir)
end
local out = util.exec(cmd, { cwd = r.worktree, silent = true })
if not out then
return {}
end
local matches = {}
local seen = {}
for _, full_path in ipairs(util.split_lines(out)) do
local rel = dir == "" and full_path or full_path:sub(#dir + 1)
local slash = rel:find("/", 1, true)
local segment = slash and rel:sub(1, slash) or rel
if not seen[segment] and segment:sub(1, #name_lead) == name_lead then
seen[segment] = true
table.insert(matches, ":" .. dir .. segment)
end
end
return matches
end
---@param arg_lead string
---@param cmd_line string
---@return string[]
+3 -3
View File
@@ -7,8 +7,8 @@ local M = {}
---@param opts { amend: boolean? }?
function M.commit(opts)
local amend = opts and opts.amend or false
local _, worktree = repo.current_repo()
if not worktree then
local r = repo.find()
if not r then
util.warning("not in a git repository")
return
end
@@ -19,7 +19,7 @@ function M.commit(opts)
end
local proxy_buf, proxy_win
editor.run(cmd, { cwd = worktree }, function(file_path, done)
editor.run(cmd, { cwd = r.worktree }, function(file_path, done)
local lines = {}
local f = io.open(file_path, "r")
if f then
+14 -19
View File
@@ -62,8 +62,8 @@ end
---@param buf integer
---@param rev ow.Git.Revision
local function uri_split(opts, buf, rev)
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.warning("git URI buffer has no worktree")
return
end
@@ -76,7 +76,7 @@ local function uri_split(opts, buf, rev)
if opts.rev and opts.rev:find(":", 1, true) then
local content = util.exec(
{ "git", "cat-file", "-p", opts.rev },
{ cwd = worktree, silent = true }
{ cwd = r.worktree, silent = true }
)
if not content then
util.warning("invalid rev: %s", opts.rev)
@@ -84,7 +84,7 @@ local function uri_split(opts, buf, rev)
end
place_pair(
buf,
object.buf_for(worktree, Revision.parse(opts.rev), content),
object.buf_for(r, Revision.parse(opts.rev), content),
false,
opts.vertical
)
@@ -92,7 +92,7 @@ local function uri_split(opts, buf, rev)
end
if not opts.rev then
local worktree_path = vim.fs.joinpath(worktree, rev.path)
local worktree_path = vim.fs.joinpath(r.worktree, rev.path)
if not vim.uv.fs_stat(worktree_path) then
util.warning("worktree file does not exist: %s", rev.path)
return
@@ -118,18 +118,13 @@ local function uri_split(opts, buf, rev)
local other_rev, left = m[1], m[2]
local content = util.exec(
{ "git", "cat-file", "-p", other_rev:format() },
{ cwd = worktree, silent = true }
{ cwd = r.worktree, silent = true }
)
if not content then
util.warning("invalid rev: %s", other_rev:format())
return
end
place_pair(
buf,
object.buf_for(worktree, other_rev, content),
left,
opts.vertical
)
place_pair(buf, object.buf_for(r, other_rev, content), left, opts.vertical)
end
---@class ow.Git.SplitOpts
@@ -141,7 +136,7 @@ function M.split(opts)
local cur_buf = vim.api.nvim_get_current_buf()
local cur_path = vim.api.nvim_buf_get_name(cur_buf)
local cur_rev = Revision.from_uri(cur_path)
local cur_rev = require("git.object").parse_uri(cur_path)
if cur_rev then
return uri_split(opts, cur_buf, cur_rev)
end
@@ -154,13 +149,13 @@ function M.split(opts)
util.warning("cannot diff this buffer (not a worktree file)")
return
end
local _, worktree, cur_path = repo.resolve(cur_path)
if not worktree then
cur_path = vim.fn.resolve(cur_path)
local r = repo.resolve(cur_path)
if not r then
util.warning("not in a git repository")
return
end
---@cast cur_path -nil
local rel = vim.fs.relpath(worktree, cur_path)
local rel = vim.fs.relpath(r.worktree, cur_path)
local rev
if not opts.rev then
@@ -172,13 +167,13 @@ function M.split(opts)
end
local content = util.exec(
{ "git", "cat-file", "-p", rev:format() },
{ cwd = worktree, silent = true }
{ cwd = r.worktree, silent = true }
)
if not content then
util.warning("invalid rev: %s", rev:format())
return
end
local buf = require("git.object").buf_for(worktree, rev, content)
local buf = require("git.object").buf_for(r, rev, content)
place_pair(buf, cur_buf, true, opts.vertical)
end
+6 -6
View File
@@ -25,26 +25,26 @@ function M.init()
{
group = group,
callback = function(args)
require("git.watcher").refresh_buf(args.buf)
require("git.repo").refresh(args.buf)
end,
}
)
vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, {
group = group,
callback = function(args)
require("git.watcher").unregister(args.buf)
require("git.repo").unregister(args.buf)
end,
})
vim.api.nvim_create_autocmd("FocusGained", {
group = group,
callback = function(args)
require("git.watcher").refresh_buf(args.buf)
require("git.repo").refresh(args.buf)
end,
})
vim.api.nvim_create_autocmd("VimLeavePre", {
group = group,
callback = function()
require("git.watcher").stop_all()
require("git.repo").stop_all()
end,
})
@@ -82,7 +82,7 @@ function M.init()
end
end
local function complete_rev(...)
return require("git.repo").complete_rev(...)
return require("git.cmd").complete_rev(...)
end
vim.api.nvim_create_user_command("Gdiffsplit", diff_split_cmd(true), {
nargs = "?",
@@ -101,7 +101,7 @@ function M.init()
})
vim.api.nvim_create_user_command("Gedit", function(opts)
vim.cmd.edit({
args = { "git://" .. opts.args },
args = { require("git.object").URI_PREFIX .. opts.args },
magic = { file = false },
})
end, {
+43 -10
View File
@@ -3,8 +3,31 @@ local util = require("git.util")
local M = {}
M.URI_PREFIX = "gitlog://"
local LOG_FORMAT = "%h %ad {%an}%d %s"
local URI_PREFIX = "gitlog://"
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
---@param buf integer
local function attach_dispatch(buf)
vim.keymap.set("n", "<CR>", function()
local r = repo.find(buf)
-- Anchor past the leading graph chars (matches the leading sha
-- column, not any hex word that happens to appear later in the
-- subject).
local sha = r
and vim.api
.nvim_get_current_line()
:match("^[*|/\\_ ]*(%x%x%x%x%x%x%x+)")
if sha then
---@cast r -nil
require("git.object").open_object(r, sha, { split = false })
else
vim.api.nvim_feedkeys(cr, "n", false)
end
end, { buffer = buf, silent = true, desc = "Open commit" })
end
---@param worktree string
---@param max_count integer?
@@ -27,8 +50,12 @@ end
---@param buf integer
local function populate(buf)
local worktree = vim.b[buf].git_worktree
local stdout = fetch(worktree, vim.b[buf].git_log_max_count)
local r = repo.find(buf)
local state = r and r:state(buf)
if not r or not state then
return
end
local stdout = fetch(r.worktree, state.log_max_count)
if not stdout then
return
end
@@ -65,12 +92,16 @@ end
---@param buf integer
function M.read_uri(buf)
local name = vim.api.nvim_buf_get_name(buf)
local worktree = name:sub(#URI_PREFIX + 1)
local worktree = name:sub(#M.URI_PREFIX + 1)
if worktree == "" then
return
end
local r = repo.resolve(worktree)
if not r then
return
end
repo.attach(buf, r)
vim.b[buf].git_worktree = worktree
vim.bo[buf].swapfile = false
vim.bo[buf].bufhidden = "hide"
vim.bo[buf].buftype = "nofile"
@@ -78,6 +109,7 @@ function M.read_uri(buf)
vim.bo[buf].filetype = "gitlog"
end
attach_dispatch(buf)
populate(buf)
end
@@ -92,15 +124,16 @@ M.opt_parsers = {
---@param opts ow.Git.LogOpts?
function M.open(opts)
opts = opts or {}
local _, worktree = repo.current_repo()
if not worktree then
local r = repo.find()
if not r then
util.warning("not in a git repository")
return
end
local buf = vim.fn.bufadd(URI_PREFIX .. worktree)
vim.b[buf].git_worktree = worktree
vim.b[buf].git_log_max_count = opts.max_count
local buf = vim.fn.bufadd(M.URI_PREFIX .. r.worktree)
repo.attach(buf, r)
local state = r:state(buf) --[[@as -nil]]
state.log_max_count = opts.max_count
local was_loaded = vim.api.nvim_buf_is_loaded(buf)
local win = vim.fn.bufwinid(buf)
+96 -75
View File
@@ -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
+338 -154
View File
@@ -1,61 +1,205 @@
local status = require("git.status")
local util = require("git.util")
local M = {}
---@param path string
---@return string? gitdir
---@return string? worktree
---@return string? path
function M.resolve(path)
path = vim.fn.resolve(path)
local found = vim.fs.find(".git", { upward = true, path = path })[1]
if not found then
return nil
---@param buf integer?
---@return integer
local function expand_buf(buf)
if not buf or buf == 0 then
return vim.api.nvim_get_current_buf()
end
local worktree = vim.fs.dirname(found)
local stat = vim.uv.fs_stat(found)
if not stat then
return nil
end
if stat.type == "directory" then
return found, worktree, path
end
local f = io.open(found, "r")
if not f then
return nil
end
local content = f:read("*a")
f:close()
local gitdir = content:match("gitdir:%s*(%S+)")
if not gitdir then
util.warning(".git file at %s has no `gitdir:` line", found)
return nil
end
if not gitdir:match("^/") then
gitdir = vim.fs.joinpath(worktree, gitdir)
end
return vim.fs.normalize(gitdir), worktree, path
return buf
end
---@return string? gitdir
---@return string? worktree
function M.current_repo()
local path = vim.api.nvim_buf_get_name(0)
if path == "" or path:match("^%a+://") then
path = vim.fn.getcwd()
---@class ow.Git.BufState
---@field repo ow.Git.Repo
---@field sha string?
---@field parent_sha string?
---@field index_writer boolean?
---@field index_mode string?
---@field log_max_count integer?
---@field pending_content string?
---@class ow.Git.Repo
---@field gitdir string
---@field worktree string
---@field buffers table<integer, ow.Git.BufState>
---@field watcher? uv.uv_fs_event_t
---@field refresh fun(self: ow.Git.Repo)
---@field refresh_handle ow.Git.Util.DebounceHandle
---@field private refresh_listeners (fun(r: ow.Git.Repo, porcelain_stdout: string?))[]
local Repo = {}
Repo.__index = Repo
---@param r ow.Git.Repo
local function do_refresh(r)
vim.system(
{
"git",
"-c",
"core.quotePath=false",
"status",
"--porcelain=v1",
"--branch",
},
{ cwd = r.worktree, text = true },
vim.schedule_wrap(function(obj)
local statuses = {}
if obj.code == 0 then
for line in (obj.stdout or ""):gmatch("[^\r\n]+") do
if line:sub(1, 2) ~= "##" then
local code = line:sub(1, 2)
local x = code:sub(1, 1)
local y = code:sub(2, 2)
local path_part = line:sub(4)
if x == "R" or x == "C" or y == "R" or y == "C" then
local arrow = path_part:find(" -> ", 1, true)
if arrow then
path_part = path_part:sub(arrow + 4)
end
end
statuses[vim.fs.joinpath(r.worktree, path_part)] =
status.format(code)
end
end
else
util.warning(
"git status failed: %s",
vim.trim(obj.stderr or "")
)
end
local dirty = false
for buf in pairs(r.buffers) do
if not vim.api.nvim_buf_is_valid(buf) then
r.buffers[buf] = nil
else
local name = vim.api.nvim_buf_get_name(buf)
local object = require("git.object")
local log = require("git.log")
if name:sub(1, #object.URI_PREFIX) == object.URI_PREFIX then
object.read_uri(buf)
elseif name:sub(1, #log.URI_PREFIX) == log.URI_PREFIX then
log.read_uri(buf)
else
local s = statuses[vim.fn.resolve(name)]
if vim.b[buf].git_status ~= s then
vim.b[buf].git_status = s
dirty = true
end
end
end
end
if dirty then
vim.cmd.redrawstatus({ bang = true })
end
r:notify_refresh(obj.code == 0 and obj.stdout or nil)
end)
)
end
---@param gitdir string
---@param worktree string
---@return ow.Git.Repo
function Repo.new(gitdir, worktree)
local self = setmetatable({
gitdir = gitdir,
worktree = worktree,
buffers = {},
refresh_listeners = {},
}, Repo)
self.refresh, self.refresh_handle = util.debounce(do_refresh, 50)
self:start_watcher()
return self
end
function Repo:start_watcher()
local watcher, err = vim.uv.new_fs_event()
if not watcher then
util.warning(
"git: failed to create fs_event for %s: %s",
self.gitdir,
err
)
return
end
local ok, err = watcher:start(
self.gitdir,
{ recursive = true },
function(err_, filename)
if
err_
or filename:match("^objects/")
or filename:match("^logs/")
then
return
end
self:refresh()
end
)
if not ok then
util.warning("failed to watch %s: %s", self.gitdir, tostring(err))
watcher:close()
return
end
self.watcher = watcher
end
function Repo:stop_watcher()
if self.watcher then
self.watcher:stop()
self.watcher:close()
self.watcher = nil
end
self.refresh_handle.close()
end
---@param buf integer
function Repo:add_buffer(buf)
buf = expand_buf(buf)
if not self.buffers[buf] then
self.buffers[buf] = { repo = self }
end
end
---@param buf integer
function Repo:remove_buffer(buf)
self.buffers[expand_buf(buf)] = nil
end
---@param buf integer
---@return ow.Git.BufState?
function Repo:state(buf)
return self.buffers[expand_buf(buf)]
end
---@return boolean
function Repo:has_buffers()
return next(self.buffers) ~= nil
end
---@param fn fun(r: ow.Git.Repo, porcelain_stdout: string?)
---@return fun() unsubscribe
function Repo:on_refresh(fn)
table.insert(self.refresh_listeners, fn)
return function()
for i, f in ipairs(self.refresh_listeners) do
if f == fn then
table.remove(self.refresh_listeners, i)
return
end
end
end
end
---@param porcelain_stdout string?
function Repo:notify_refresh(porcelain_stdout)
for _, fn in ipairs(self.refresh_listeners) do
fn(self, porcelain_stdout)
end
local gitdir, worktree, _ = M.resolve(path)
return gitdir, worktree
end
---@param path string
---@return string?
function M.head(path)
local gitdir = M.resolve(path)
if not gitdir then
return nil
end
local f = io.open(vim.fs.joinpath(gitdir, "HEAD"), "r")
function Repo:head()
local f = io.open(vim.fs.joinpath(self.gitdir, "HEAD"), "r")
if not f then
return nil
end
@@ -75,9 +219,8 @@ function M.head(path)
return nil
end
---@param worktree string
---@return string[]
function M.list_refs(worktree)
function Repo:list_refs()
local out = util.exec({
"git",
"for-each-ref",
@@ -85,7 +228,7 @@ function M.list_refs(worktree)
"refs/heads",
"refs/tags",
"refs/remotes",
}, { cwd = worktree, silent = true })
}, { cwd = self.worktree, silent = true })
if not out then
return {}
end
@@ -94,118 +237,159 @@ function M.list_refs(worktree)
return refs
end
---@param arg_lead string
---@return string[]
function M.complete_rev(arg_lead)
local _, worktree = M.current_repo()
if not worktree then
return {}
end
local stage, stage_path_lead = arg_lead:match("^:([0-3]):(.*)$")
if stage then
local out = util.exec(
{ "git", "ls-files", "--stage" },
{ cwd = worktree, silent = true }
)
if not out then
return {}
end
local matches = {}
for _, line in ipairs(util.split_lines(out)) do
local row_stage, row_path = line:match("^%S+ %S+ (%d)\t(.*)$")
if
row_stage == stage
and row_path
and row_path:sub(1, #stage_path_lead) == stage_path_lead
then
table.insert(matches, ":" .. stage .. ":" .. row_path)
end
end
return matches
end
local colon = arg_lead:find(":", 1, true)
if not colon then
local matches = {}
for _, ref in ipairs(M.list_refs(worktree)) do
if ref:sub(1, #arg_lead) == arg_lead then
table.insert(matches, ref)
end
end
return matches
end
local rev = arg_lead:sub(1, colon - 1)
local path_lead = arg_lead:sub(colon + 1)
local dir, name_lead = path_lead:match("^(.*/)([^/]*)$")
dir = dir or ""
name_lead = name_lead or path_lead
if rev ~= "" then
local cmd = { "git", "ls-tree", rev }
if dir ~= "" then
table.insert(cmd, dir)
end
local out = util.exec(cmd, { cwd = worktree, silent = true })
if not out then
return {}
end
local matches = {}
for _, line in ipairs(util.split_lines(out)) do
local typ, full_path = line:match("^%S+ (%S+) %S+\t(.*)$")
if typ and full_path then
local basename = dir == "" and full_path
or full_path:sub(#dir + 1)
if typ == "tree" then
basename = basename .. "/"
end
if basename:sub(1, #name_lead) == name_lead then
table.insert(matches, rev .. ":" .. dir .. basename)
end
end
end
return matches
end
local cmd = { "git", "ls-files" }
if dir ~= "" then
table.insert(cmd, dir)
end
local out = util.exec(cmd, { cwd = worktree, silent = true })
if not out then
return {}
end
local matches = {}
local seen = {}
for _, full_path in ipairs(util.split_lines(out)) do
local rel = dir == "" and full_path or full_path:sub(#dir + 1)
local slash = rel:find("/", 1, true)
local segment = slash and rel:sub(1, slash) or rel
if
not seen[segment]
and segment:sub(1, #name_lead) == name_lead
then
seen[segment] = true
table.insert(matches, ":" .. dir .. segment)
end
end
return matches
end
---@param worktree string
---@param rev string
---@param short boolean
---@return string?
function M.rev_parse(worktree, rev, short)
function Repo:rev_parse(rev, short)
local cmd = { "git", "rev-parse", "--verify", "--quiet" }
if short then
table.insert(cmd, "--short")
end
table.insert(cmd, rev)
local stdout = util.exec(cmd, { cwd = worktree, silent = true })
local stdout = util.exec(cmd, { cwd = self.worktree, silent = true })
local trimmed = stdout and vim.trim(stdout) or ""
return trimmed ~= "" and trimmed or nil
end
local M = {}
---@type table<string, ow.Git.Repo>
local repo_by_gitdir = {}
---@type table<integer, ow.Git.Repo>
local repo_by_buf = {}
---@param path string
---@return ow.Git.Repo?
function M.resolve(path)
path = vim.fn.resolve(path)
local found = vim.fs.find(".git", { upward = true, path = path })[1]
if not found then
return nil
end
local stat = vim.uv.fs_stat(found)
if not stat then
return nil
end
local worktree = vim.fs.dirname(found)
local gitdir
if stat.type == "directory" then
gitdir = found
else
local f = io.open(found, "r")
if not f then
return nil
end
local content = f:read("*a")
f:close()
local rel = content:match("gitdir:%s*(%S+)")
if not rel then
util.warning(".git file at %s has no `gitdir:` line", found)
return nil
end
if rel:match("^/") then
gitdir = rel
else
gitdir = vim.fs.joinpath(worktree, rel)
end
gitdir = vim.fs.normalize(gitdir)
end
local r = repo_by_gitdir[gitdir]
if not r then
r = Repo.new(gitdir, worktree)
repo_by_gitdir[gitdir] = r
end
return r
end
---@param buf integer?
---@return ow.Git.Repo?
function M.find(buf)
buf = expand_buf(buf)
local existing = repo_by_buf[buf]
if existing then
return existing
end
local path = vim.api.nvim_buf_get_name(buf)
if path == "" or path:match("^%a+://") then
path = vim.fn.getcwd()
end
return M.resolve(path)
end
---@param buf integer?
---@return ow.Git.BufState?
function M.state(buf)
buf = expand_buf(buf)
local r = repo_by_buf[buf]
return r and r.buffers[buf]
end
---@param buf integer
---@param r ow.Git.Repo
function M.attach(buf, r)
buf = expand_buf(buf)
if repo_by_buf[buf] == r then
return
end
if repo_by_buf[buf] then
repo_by_buf[buf]:remove_buffer(buf)
end
r:add_buffer(buf)
repo_by_buf[buf] = r
end
---@param buf integer
---@return ow.Git.Repo?
function M.register(buf)
buf = expand_buf(buf)
local r = M.find(buf)
if not r then
return nil
end
if repo_by_buf[buf] ~= r then
M.attach(buf, r)
end
return r
end
---@param buf integer
function M.unregister(buf)
buf = expand_buf(buf)
local r = repo_by_buf[buf]
if not r then
return
end
repo_by_buf[buf] = nil
r:remove_buffer(buf)
if not r:has_buffers() then
r:stop_watcher()
repo_by_gitdir[r.gitdir] = nil
end
end
---@param buf integer
function M.refresh(buf)
buf = expand_buf(buf)
if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then
return
end
local path = vim.api.nvim_buf_get_name(buf)
if path == "" or path:match("^%a+://") then
return
end
local r = M.register(buf)
if not r then
vim.b[buf].git_status = nil
return
end
r:refresh()
end
function M.stop_all()
for _, r in pairs(repo_by_gitdir) do
r:stop_watcher()
end
end
return M
-16
View File
@@ -5,8 +5,6 @@
local Revision = {}
Revision.__index = Revision
local URI_PREFIX = "git://"
---@return string
function Revision:format()
if self.stage then
@@ -17,11 +15,6 @@ function Revision:format()
return self.base or error("Revision:format: empty Revision")
end
---@return string
function Revision:uri()
return URI_PREFIX .. self:format()
end
---@param parts { stage?: integer, base?: string, path?: string }
---@return ow.Git.Revision
function Revision.new(parts)
@@ -49,13 +42,4 @@ function Revision.parse(str)
return Revision.new({ base = str })
end
---@param str string
---@return ow.Git.Revision?
function Revision.from_uri(str)
local raw = str:match("^" .. URI_PREFIX .. "(.+)$")
if raw then
return Revision.parse(raw)
end
end
return Revision
+32 -45
View File
@@ -32,14 +32,13 @@ local SIDEBAR_WIDTH = 50
---@alias ow.Git.SidebarEntry ow.Git.FileEntry | ow.Git.CommitEntry
---@class ow.Git.SidebarState
---@field gitdir string
---@field worktree string
---@field repo ow.Git.Repo
---@field lines table<integer, ow.Git.SidebarEntry>
---@field sidebar_win integer?
---@field invocation_win integer?
---@field diff_left_win integer?
---@field diff_right_win integer?
---@field user_aucmd integer?
---@field unsubscribe fun()?
---@field last_shown_key string?
---@field last_render_key string?
@@ -407,7 +406,7 @@ local function refresh(bufnr, prefetched_stdout)
end
end
fetch_status(s.worktree, prefetched_stdout, function(branch, groups)
fetch_status(s.repo.worktree, prefetched_stdout, function(branch, groups)
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
@@ -456,22 +455,22 @@ end
---@field left ow.Git.DiffSide
---@field right ow.Git.DiffSide
---@param worktree string
---@param r ow.Git.Repo
---@param path string
---@return ow.Git.DiffSide
local function head_pane(worktree, path)
local function head_pane(r, path)
local rev = Revision.new({ base = "HEAD", path = path })
return {
buf = object.buf_for(worktree, rev),
name = rev:uri(),
buf = object.buf_for(r, rev),
name = object.format_uri(rev),
}
end
---@param worktree string
---@param r ow.Git.Repo
---@param path string
---@return ow.Git.DiffSide
local function worktree_pane(worktree, path)
local buf = vim.fn.bufadd(vim.fs.joinpath(worktree, path))
local function worktree_pane(r, path)
local buf = vim.fn.bufadd(vim.fs.joinpath(r.worktree, path))
vim.fn.bufload(buf)
return { buf = buf, name = nil }
end
@@ -482,8 +481,8 @@ end
local function index_pane(s, entry)
local rev = Revision.new({ stage = 0, path = entry.path })
return {
buf = object.buf_for(s.worktree, rev),
name = rev:uri(),
buf = object.buf_for(s.repo, rev),
name = object.format_uri(rev),
}
end
@@ -495,7 +494,7 @@ local function older_pane(s, entry)
if entry.x == "A" then
return nil
end
return head_pane(s.worktree, entry.orig or entry.path)
return head_pane(s.repo, entry.orig or entry.path)
end
if entry.section == "Unstaged" then
return index_pane(s, entry)
@@ -517,10 +516,10 @@ local function newer_pane(s, entry)
if entry.y == "D" then
return nil
end
return worktree_pane(s.worktree, entry.path)
return worktree_pane(s.repo, entry.path)
end
if entry.section == "Untracked" then
return worktree_pane(s.worktree, entry.path)
return worktree_pane(s.repo, entry.path)
end
return nil
end
@@ -695,8 +694,9 @@ local function view_entry(s, entry, focus_left)
+ vim.api.nvim_win_get_width(right_win)
vim.api.nvim_win_set_width(left_win, math.floor(combined / 2))
end
---@cast left_win -nil
---@cast right_win -nil
assert(left_win and right_win, "diff windows must be set")
vim.w[left_win].git_diff_role = "left"
vim.w[right_win].git_diff_role = "right"
s.diff_left_win = left_win
@@ -728,7 +728,7 @@ local function action_stage()
end
vim.system(
{ "git", "add", "--", entry.path },
{ cwd = s.worktree },
{ cwd = s.repo.worktree },
vim.schedule_wrap(function(obj)
if obj.code ~= 0 then
util.error("git add failed: %s", vim.trim(obj.stderr or ""))
@@ -753,7 +753,7 @@ local function action_unstage()
table.insert(cmd, entry.path)
vim.system(
cmd,
{ cwd = s.worktree },
{ cwd = s.repo.worktree },
vim.schedule_wrap(function(obj)
if obj.code ~= 0 then
util.error(
@@ -785,7 +785,7 @@ local function action_discard()
entry.path
)
action = function()
local target = vim.fs.joinpath(s.worktree, entry.path)
local target = vim.fs.joinpath(s.repo.worktree, entry.path)
local rc = vim.fn.delete(target, is_dir and "rf" or "")
if rc ~= 0 then
util.error("failed to delete %s", entry.path)
@@ -797,7 +797,7 @@ local function action_discard()
action = function()
vim.system(
{ "git", "checkout", "--", entry.path },
{ cwd = s.worktree },
{ cwd = s.repo.worktree },
vim.schedule_wrap(function(obj)
if obj.code ~= 0 then
util.error(
@@ -829,20 +829,14 @@ local function action_help()
}, "\n"))
end
---@param worktree string
local function open(worktree)
---@param r ow.Git.Repo
local function open(r)
local existing = find_sidebar()
if existing then
vim.api.nvim_set_current_win(existing)
return
end
local gitdir, worktree = repo.resolve(worktree)
if not gitdir then
return
end
---@cast worktree -nil
local previous_win = vim.api.nvim_get_current_win()
local bufnr, win = util.new_scratch({ split = "left" })
vim.bo[bufnr].filetype = "gitsidebar"
@@ -856,8 +850,7 @@ local function open(worktree)
vim.api.nvim_win_set_width(win, SIDEBAR_WIDTH)
state[bufnr] = {
gitdir = gitdir,
worktree = worktree,
repo = r,
lines = {},
sidebar_win = win,
invocation_win = previous_win,
@@ -882,15 +875,9 @@ local function open(worktree)
k("X", action_discard, "Discard worktree changes")
k("g?", action_help, "Help")
state[bufnr].user_aucmd = vim.api.nvim_create_autocmd("User", {
pattern = "GitRefresh",
group = group,
callback = function(args)
if args.data and args.data.gitdir == gitdir then
refresh(bufnr, args.data.porcelain_stdout)
end
end,
})
state[bufnr].unsubscribe = r:on_refresh(function(_, porcelain_stdout)
refresh(bufnr, porcelain_stdout)
end)
vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, {
buffer = bufnr,
group = group,
@@ -899,8 +886,8 @@ local function open(worktree)
if not s then
return
end
if s.user_aucmd then
pcall(vim.api.nvim_del_autocmd, s.user_aucmd)
if s.unsubscribe then
s.unsubscribe()
end
state[bufnr] = nil
end,
@@ -916,12 +903,12 @@ function M.toggle()
vim.api.nvim_win_close(sidebar_win, false)
return
end
local _, worktree = repo.current_repo()
if not worktree then
local r = repo.find()
if not r then
util.warning("not in a git repository")
return
end
open(worktree)
open(r)
end
return M
+14 -1
View File
@@ -101,7 +101,20 @@ end
---@param delay integer
---@return F, ow.Git.Util.DebounceHandle
function M.debounce(fn, delay)
local timer = assert(vim.uv.new_timer())
local timer, err = vim.uv.new_timer()
if not timer then
M.warning("git: failed to create timer: %s", err)
local noop = function() end
return fn,
{
cancel = noop,
flush = noop,
pending = function()
return false
end,
close = noop,
}
end
local args ---@type table?
local gen = 0
local fired_gen = 0
-198
View File
@@ -1,198 +0,0 @@
local repo = require("git.repo")
local status = require("git.status")
local util = require("git.util")
local M = {}
---@class ow.Git.Repo
---@field gitdir string
---@field worktree string
---@field buffers table<integer, true>
---@field watcher? uv.uv_fs_event_t
---@field refresh fun(self: ow.Git.Repo)
---@field refresh_handle ow.Git.Util.DebounceHandle
local Repo = {}
Repo.__index = Repo
function Repo:start_watcher()
local watcher = assert(vim.uv.new_fs_event())
assert(watcher:start(self.gitdir, {}, function(err, filename)
if err or (filename ~= "index" and filename ~= "HEAD") then
return
end
self:refresh()
end))
self.watcher = watcher
end
function Repo:stop_watcher()
-- Stop the libuv watcher first so no further fs-events can trigger
-- self:refresh(). Tearing down the debounce handle first leaves a
-- window where an in-flight watcher callback hits a closed timer.
if self.watcher then
self.watcher:stop()
self.watcher:close()
self.watcher = nil
end
self.refresh_handle.close()
end
---@param buf integer
function Repo:add_buffer(buf)
self.buffers[buf] = true
end
---@param buf integer
function Repo:remove_buffer(buf)
self.buffers[buf] = nil
end
---@return boolean
function Repo:has_buffers()
return next(self.buffers) ~= nil
end
---@param r ow.Git.Repo
local function do_refresh(r)
vim.system(
{
"git",
"-c",
"core.quotePath=false",
"status",
"--porcelain=v1",
"--branch",
},
{ cwd = r.worktree, text = true },
vim.schedule_wrap(function(obj)
local statuses = {}
if obj.code == 0 then
for line in (obj.stdout or ""):gmatch("[^\r\n]+") do
if line:sub(1, 2) ~= "##" then
local code = line:sub(1, 2)
local x = code:sub(1, 1)
local y = code:sub(2, 2)
local path_part = line:sub(4)
if x == "R" or x == "C" or y == "R" or y == "C" then
local arrow = path_part:find(" -> ", 1, true)
if arrow then
path_part = path_part:sub(arrow + 4)
end
end
statuses[vim.fs.joinpath(r.worktree, path_part)] =
status.format(code)
end
end
else
util.warning(
"git status failed: %s",
vim.trim(obj.stderr or "")
)
end
local dirty = false
for buf in pairs(r.buffers) do
if not vim.api.nvim_buf_is_valid(buf) then
r.buffers[buf] = nil
else
local status =
statuses[vim.fn.resolve(vim.api.nvim_buf_get_name(buf))]
if vim.b[buf].git_status ~= status then
vim.b[buf].git_status = status
dirty = true
end
end
end
if dirty then
vim.cmd.redrawstatus({ bang = true })
end
vim.api.nvim_exec_autocmds("User", {
pattern = "GitRefresh",
data = {
gitdir = r.gitdir,
worktree = r.worktree,
porcelain_stdout = obj.code == 0 and obj.stdout or nil,
},
})
end)
)
end
---@param gitdir string
---@param worktree string
---@return ow.Git.Repo
function Repo.new(gitdir, worktree)
local self = setmetatable({
gitdir = gitdir,
worktree = worktree,
buffers = {},
}, Repo)
self.refresh, self.refresh_handle = util.debounce(do_refresh, 50)
self:start_watcher()
return self
end
---@type table<string, ow.Git.Repo>
local repo_by_gitdir = {}
---@type table<integer, ow.Git.Repo>
local repo_by_buf = {}
---@param buf integer
---@return ow.Git.Repo?
local function register(buf)
local existing = repo_by_buf[buf]
if existing then
return existing
end
local path = vim.api.nvim_buf_get_name(buf)
if path == "" then
return nil
end
local gitdir, worktree = repo.resolve(path)
if not gitdir or not worktree then
return nil
end
local r = repo_by_gitdir[gitdir]
if not r then
r = Repo.new(gitdir, worktree)
repo_by_gitdir[gitdir] = r
end
r:add_buffer(buf)
repo_by_buf[buf] = r
return r
end
---@param buf integer
function M.unregister(buf)
local r = repo_by_buf[buf]
if not r then
return
end
repo_by_buf[buf] = nil
r:remove_buffer(buf)
if not r:has_buffers() then
r:stop_watcher()
repo_by_gitdir[r.gitdir] = nil
end
end
---@param buf integer
function M.refresh_buf(buf)
if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then
return
end
local r = register(buf)
if not r then
vim.b[buf].git_status = nil
return
end
r:refresh()
end
function M.stop_all()
for _, r in pairs(repo_by_gitdir) do
r:stop_watcher()
end
end
return M
+2 -1
View File
@@ -111,7 +111,8 @@ require("nvim-tree").setup({
full_name = true,
root_folder_label = function(path)
local label = vim.fn.fnamemodify(path, ":~")
local git_head = require("git.repo").head(path)
local r = require("git.repo").resolve(path)
local git_head = r and r:head()
if git_head then
label = label .. ("  %s"):format(git_head)
end