perf(git): piggyback sidebar status on the indicator's git status call

This commit is contained in:
2026-04-27 14:09:33 +02:00
parent 26b12a4371
commit 2ef9cb7c9e
2 changed files with 173 additions and 125 deletions
+153 -117
View File
@@ -142,9 +142,146 @@ local function parse_branch_line(line)
return info
end
---Parse `git status --porcelain=v1 --branch` output into a (branch, groups)
---pair. `Unpushed` and `Unpulled` start empty here; ahead/behind commits are
---filled in by a follow-up `git log` once we know the upstream is set.
---@param stdout string
---@return ow.Git.BranchInfo, table<string, ow.Git.StatusEntry[]>
local function parse_porcelain(stdout)
local branch = { ahead = 0, behind = 0 }
local groups = {
Untracked = {},
Unstaged = {},
Staged = {},
Unmerged = {},
Unpushed = {},
Unpulled = {},
}
for line in stdout:gmatch("[^\r\n]+") do
if line:sub(1, 2) == "##" then
branch = parse_branch_line(line)
else
local x = line:sub(1, 1)
local y = line:sub(2, 2)
local rest = line:sub(4)
local orig
local arrow = rest:find(" -> ", 1, true)
if arrow then
orig = rest:sub(1, arrow - 1)
rest = rest:sub(arrow + 4)
end
local entry = {
section = nil,
path = rest,
orig = orig,
x = x,
y = y,
}
if x == "?" and y == "?" then
entry.section = "Untracked"
table.insert(groups.Untracked, entry)
elseif repo.UNMERGED[x .. y] then
entry.section = "Unmerged"
table.insert(groups.Unmerged, entry)
else
if x ~= " " then
table.insert(groups.Staged, {
section = "Staged",
path = entry.path,
orig = entry.orig,
x = entry.x,
y = entry.y,
})
end
if y ~= " " then
table.insert(groups.Unstaged, {
section = "Unstaged",
path = entry.path,
orig = entry.orig,
x = entry.x,
y = entry.y,
})
end
end
end
end
return branch, groups
end
---Run the ahead/behind `git log` calls for any non-zero counters and call
---`callback(branch, groups)` once they all finish (or immediately when
---there's nothing to fetch).
---@param worktree string
---@param branch ow.Git.BranchInfo
---@param groups table<string, ow.Git.StatusEntry[]>
---@param callback fun(branch: ow.Git.BranchInfo, groups: table<string, ow.Git.StatusEntry[]>)
local function fetch_status(worktree, callback)
local function enrich_with_log(worktree, branch, groups, callback)
local fetches = {}
if branch.upstream and branch.ahead > 0 then
table.insert(
fetches,
{ section = "Unpushed", range = "@{upstream}..HEAD" }
)
end
if branch.upstream and branch.behind > 0 then
table.insert(
fetches,
{ section = "Unpulled", range = "HEAD..@{upstream}" }
)
end
if #fetches == 0 then
callback(branch, groups)
return
end
local pending = #fetches
for _, f in ipairs(fetches) do
vim.system({
"git",
"log",
"--format=%h %s",
f.range,
}, { cwd = worktree, text = true }, function(log_obj)
vim.schedule(function()
if log_obj.code == 0 then
for line in (log_obj.stdout or ""):gmatch("[^\r\n]+") do
local sha, subject = line:match("^(%S+)%s+(.+)$")
if sha then
table.insert(groups[f.section], {
section = f.section,
sha = sha,
subject = subject,
})
end
end
else
log.error(
"git log %s failed: %s",
f.range,
vim.trim(log_obj.stderr or "")
)
end
pending = pending - 1
if pending == 0 then
callback(branch, groups)
end
end)
end)
end
end
---Build the (branch, groups) tuple for the sidebar. When `prefetched_stdout`
---is provided (typical case: dispatched via the `User GitRefresh` autocmd
---that already ran `git status --porcelain=v1 --branch` for the indicator),
---we skip the duplicate subprocess. Otherwise the sidebar fetches its own.
---@param worktree string
---@param prefetched_stdout string?
---@param callback fun(branch: ow.Git.BranchInfo, groups: table<string, ow.Git.StatusEntry[]>)
local function fetch_status(worktree, prefetched_stdout, callback)
if prefetched_stdout then
local branch, groups = parse_porcelain(prefetched_stdout)
enrich_with_log(worktree, branch, groups, callback)
return
end
vim.system({
"git",
"-c",
@@ -154,124 +291,22 @@ local function fetch_status(worktree, callback)
"--branch",
}, { cwd = worktree, text = true }, function(obj)
vim.schedule(function()
local branch = { ahead = 0, behind = 0 }
local groups = {
Untracked = {},
Unstaged = {},
Staged = {},
Unmerged = {},
Unpushed = {},
Unpulled = {},
}
if obj.code ~= 0 then
log.error("git status failed: %s", vim.trim(obj.stderr or ""))
end
if obj.code == 0 then
for line in (obj.stdout or ""):gmatch("[^\r\n]+") do
if line:sub(1, 2) == "##" then
branch = parse_branch_line(line)
else
local x = line:sub(1, 1)
local y = line:sub(2, 2)
local rest = line:sub(4)
local orig
local arrow = rest:find(" -> ", 1, true)
if arrow then
orig = rest:sub(1, arrow - 1)
rest = rest:sub(arrow + 4)
end
local entry = {
section = nil,
path = rest,
orig = orig,
x = x,
y = y,
}
if x == "?" and y == "?" then
entry.section = "Untracked"
table.insert(groups.Untracked, entry)
elseif repo.UNMERGED[x .. y] then
entry.section = "Unmerged"
table.insert(groups.Unmerged, entry)
else
if x ~= " " then
table.insert(groups.Staged, {
section = "Staged",
path = entry.path,
orig = entry.orig,
x = entry.x,
y = entry.y,
})
end
if y ~= " " then
table.insert(groups.Unstaged, {
section = "Unstaged",
path = entry.path,
orig = entry.orig,
x = entry.x,
y = entry.y,
})
end
end
end
end
end
local fetches = {}
if branch.upstream and branch.ahead > 0 then
table.insert(
fetches,
{ section = "Unpushed", range = "@{upstream}..HEAD" }
)
end
if branch.upstream and branch.behind > 0 then
table.insert(
fetches,
{ section = "Unpulled", range = "HEAD..@{upstream}" }
)
end
if #fetches == 0 then
local branch = { ahead = 0, behind = 0 }
local groups = {
Untracked = {},
Unstaged = {},
Staged = {},
Unmerged = {},
Unpushed = {},
Unpulled = {},
}
callback(branch, groups)
return
end
local pending = #fetches
for _, f in ipairs(fetches) do
vim.system({
"git",
"log",
"--format=%h %s",
f.range,
}, { cwd = worktree, text = true }, function(
log_obj
)
vim.schedule(function()
if log_obj.code == 0 then
for line in
(log_obj.stdout or ""):gmatch("[^\r\n]+")
do
local sha, subject =
line:match("^(%S+)%s+(.+)$")
if sha then
table.insert(groups[f.section], {
section = f.section,
sha = sha,
subject = subject,
})
end
end
else
log.error(
"git log %s failed: %s",
f.range,
vim.trim(log_obj.stderr or "")
)
end
pending = pending - 1
if pending == 0 then
callback(branch, groups)
end
end)
end)
end
local branch, groups = parse_porcelain(obj.stdout or "")
enrich_with_log(worktree, branch, groups, callback)
end)
end)
end
@@ -367,7 +402,8 @@ local function fingerprint(branch, groups)
end
---@param bufnr integer
local function refresh(bufnr)
---@param prefetched_stdout string? porcelain output from a piggybacked GitRefresh
local function refresh(bufnr, prefetched_stdout)
local s = state[bufnr]
if not s then
return
@@ -384,7 +420,7 @@ local function refresh(bufnr)
end
end
fetch_status(s.worktree, function(branch, groups)
fetch_status(s.worktree, prefetched_stdout, function(branch, groups)
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
@@ -874,7 +910,7 @@ local function open(worktree)
group = group,
callback = function(args)
if args.data and args.data.gitdir == gitdir then
refresh(bufnr)
refresh(bufnr, args.data.porcelain_stdout)
end
end,
})