From 2ef9cb7c9e000014ba0b1042af18939994881d51 Mon Sep 17 00:00:00 2001 From: Oscar Wallberg Date: Mon, 27 Apr 2026 14:09:33 +0200 Subject: [PATCH] perf(git): piggyback sidebar status on the indicator's git status call --- lua/git/repo.lua | 28 +++-- lua/git/status_win.lua | 270 +++++++++++++++++++++++------------------ 2 files changed, 173 insertions(+), 125 deletions(-) diff --git a/lua/git/repo.lua b/lua/git/repo.lua index 8f6d521..5e9bad3 100644 --- a/lua/git/repo.lua +++ b/lua/git/repo.lua @@ -142,25 +142,33 @@ end ---@param repo ow.Git.Repo local function do_refresh(repo) + -- `--branch` adds the `## branch...upstream [ahead/behind]` line that + -- the sidebar parses; the per-buffer indicator only needs the XY + + -- path lines, so it ignores `##` lines below. Running with `--branch` + -- lets the sidebar reuse this single subprocess via the GitRefresh + -- data payload instead of spawning its own. vim.system({ "git", "-c", "core.quotePath=false", "status", "--porcelain=v1", + "--branch", }, { cwd = repo.worktree, text = true }, function(obj) vim.schedule(function() local statuses = {} if obj.code == 0 then for line in (obj.stdout or ""):gmatch("[^\r\n]+") do - local code = line:sub(1, 2) - local path_part = line:sub(4) - local arrow = path_part:find(" -> ", 1, true) - if arrow then - path_part = path_part:sub(arrow + 4) + if line:sub(1, 2) ~= "##" then + local code = line:sub(1, 2) + local path_part = line:sub(4) + local arrow = path_part:find(" -> ", 1, true) + if arrow then + path_part = path_part:sub(arrow + 4) + end + statuses[vim.fs.joinpath(repo.worktree, path_part)] = + format(code) end - statuses[vim.fs.joinpath(repo.worktree, path_part)] = - format(code) end else log.warning("git status failed: %s", vim.trim(obj.stderr or "")) @@ -182,7 +190,11 @@ local function do_refresh(repo) end vim.api.nvim_exec_autocmds("User", { pattern = "GitRefresh", - data = { gitdir = repo.gitdir, worktree = repo.worktree }, + data = { + gitdir = repo.gitdir, + worktree = repo.worktree, + porcelain_stdout = obj.code == 0 and obj.stdout or nil, + }, }) end) end) diff --git a/lua/git/status_win.lua b/lua/git/status_win.lua index 13cf6fc..9cf57a9 100644 --- a/lua/git/status_win.lua +++ b/lua/git/status_win.lua @@ -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 +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 ---@param callback fun(branch: ow.Git.BranchInfo, groups: table) -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) +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, })