fix(git): stop the sidebar count disagreeing with the diff it opens

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.
This commit is contained in:
l0ng-ai
2026-08-08 17:26:57 +07:00
parent d1f61e109f
commit 342b65ae5e
2 changed files with 68 additions and 1 deletions
+54
View File
@@ -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();
+14 -1
View File
@@ -179,7 +179,20 @@ impl Tty7App {
snap: Option<Arc<DiffSnapshot>>,
cx: &mut Context<Self>,
) {
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::<crate::terminal::git_status::GitStatusCache>();
cx.update_global::<crate::terminal::git_status::GitStatusCache, _>(|cache, _| {
cache.note_counts(host, &root, added, removed)
})
} else {
false
};
for tab in self.tabs.iter_mut() {
let Some(overlay) = tab
.diff_overlay