From 342b65ae5e9ed6dd0c4045796bb476bb98164f06 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:26:57 +0700 Subject: [PATCH] fix(git): stop the sidebar count disagreeing with the diff it opens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The +N −N on a tab row is refreshed on an edge — the directory changing, a command finishing, an agent turn ending, the window coming back — and never polled. That is the right shape for it, but it means a working tree edited from anywhere else keeps the old number until one of those happens. Click the number and the overlay reads the tree there and then: the row said +27 −8 while the overlay it had just opened said +3 −1. A diff read is a fresher answer to the same question, so it now hands its totals back to the status cache. The Changes panel feeds the same path, so watching a diff shrink there keeps the row honest too. Only the numbers move — the branch, the root and the probe's own schedule are untouched, and a diff for a repo nothing has probed is dropped rather than inventing a row with no branch on it. The totals are exact even when the diff bodies were truncated by the repo-wide budget, which `repo_wide_budget_keeps_totals_exact` holds. --- src/terminal/git_status.rs | 54 ++++++++++++++++++++++++++++++++++++++ src/ui/diff_overlay.rs | 15 ++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/terminal/git_status.rs b/src/terminal/git_status.rs index f53fa4be..7070d169 100644 --- a/src/terminal/git_status.rs +++ b/src/terminal/git_status.rs @@ -70,6 +70,35 @@ impl GitStatusCache { } } + /// Corrects a repo's line counts from a working-tree diff that was just + /// read. + /// + /// The counts here are refreshed on an edge — a command finishing, the + /// directory changing, the window coming back — and a diff read by the + /// Changes panel or the diff overlay is a fresher answer to the same + /// question. Without this the sidebar can say +27 −8 while the overlay it + /// opens says +3 −1. Only the numbers move; the branch, the root and the + /// probe's own schedule are left alone. + pub fn note_counts(&mut self, host: HostId, root: &Path, added: u32, removed: u32) -> bool { + let Some(status) = self.status.get(host, root) else { + return false; + }; + if status.added == added && status.removed == removed { + return false; + } + let branch = status.branch.clone(); + self.status.insert( + host, + root.to_path_buf(), + GitStatus { + branch, + added, + removed, + }, + ); + true + } + pub fn finish_probe( &mut self, host: HostId, @@ -133,6 +162,31 @@ mod tests { counts: Some((0, 0)), } } + #[test] + fn a_diff_read_corrects_the_counts_without_touching_the_branch() { + let mut cache = GitStatusCache::default(); + let cwd = Path::new("/repo/sub"); + cache.finish_probe(L, cwd, Some(snap("/repo", "main", Some((27, 8))))); + + assert!(cache.note_counts(L, Path::new("/repo"), 3, 1)); + let got = cache.status_for(L, cwd).unwrap(); + assert_eq!((got.added, got.removed), (3, 1)); + assert_eq!(got.branch, "main", "only the numbers move"); + + // Nothing to say when the diff agrees with what is already there. + assert!(!cache.note_counts(L, Path::new("/repo"), 3, 1)); + } + + #[test] + fn a_diff_for_a_repo_nobody_probed_is_dropped() { + // The counts hang off a root the probe established. Without one there + // is no row to correct, and inventing an entry would leave it with no + // branch to show. + let mut cache = GitStatusCache::default(); + assert!(!cache.note_counts(L, Path::new("/elsewhere"), 3, 1)); + assert!(cache.status_for(L, Path::new("/elsewhere")).is_none()); + } + #[test] fn cwds_in_one_repo_share_a_snapshot() { let mut cache = GitStatusCache::default(); diff --git a/src/ui/diff_overlay.rs b/src/ui/diff_overlay.rs index da52eaa7..95259152 100644 --- a/src/ui/diff_overlay.rs +++ b/src/ui/diff_overlay.rs @@ -179,7 +179,20 @@ impl Tty7App { snap: Option>, cx: &mut Context, ) { - let mut landed = false; + // A diff read is a fresher answer to the question the sidebar's + // +N −N asks, and it is the one the reader is looking at. Hand the + // numbers back before anything renders, or the row can disagree with + // the overlay it just opened. + let mut landed = if let Some(snap) = snap.as_ref() { + let (added, removed) = snap.totals(); + let root = snap.root.clone(); + cx.default_global::(); + cx.update_global::(|cache, _| { + cache.note_counts(host, &root, added, removed) + }) + } else { + false + }; for tab in self.tabs.iter_mut() { let Some(overlay) = tab .diff_overlay