Skip to content

Commit 54fdf58

Browse files
authored
git_panel: Show staged and unstaged diff stats (#60815)
# Objective - Show accurate diff stats for each staged and unstaged projection of a partially staged file in the Git panel. - This was originally considered for #59884, but was scoped out of that already-large PR and is being submitted separately as discussed there. ## Solution - Collect HEAD-to-index and index-to-worktree diff stats alongside the existing combined HEAD-to-worktree stats. - Carry the staged and unstaged stats through repository status snapshots and remote status serialization. - Use the stat matching the projected Git panel section while preserving the combined stat for the other grouping modes. - Update the fake Git repository and add regression coverage with deliberately different staged and unstaged counts. ## Testing - `cargo check -p git_ui` - `cargo check -p collab` - `cargo test -p git_ui test_group_by_staging_section_membership_and_order --lib` - `cargo test -p project --lib --no-run` - `cargo fmt --all -- --check` - `git diff --check` ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Fixed diff stats for partially staged files in the Git panel
1 parent eb96279 commit 54fdf58

7 files changed

Lines changed: 241 additions & 47 deletions

File tree

crates/collab/src/db.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,10 @@ fn db_status_to_proto(
732732
}),
733733
diff_stat_added: entry.lines_added.map(|v| v as u32),
734734
diff_stat_deleted: entry.lines_deleted.map(|v| v as u32),
735+
staged_diff_stat_added: None,
736+
staged_diff_stat_deleted: None,
737+
unstaged_diff_stat_added: None,
738+
unstaged_diff_stat_deleted: None,
735739
})
736740
}
737741

crates/fs/src/fake_git_repo.rs

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,7 @@ impl GitRepository for FakeGitRepository {
11561156

11571157
fn diff_stat(
11581158
&self,
1159+
diff: git::repository::DiffStatType,
11591160
path_prefixes: &[RepoPath],
11601161
) -> BoxFuture<'static, Result<git::status::GitDiffStat>> {
11611162
fn count_lines(s: &str) -> u32 {
@@ -1203,22 +1204,43 @@ impl GitRepository for FakeGitRepository {
12031204

12041205
self.with_state_async(false, move |state| {
12051206
let mut entries = Vec::new();
1206-
let all_paths: HashSet<&RepoPath> = state
1207-
.head_contents
1208-
.keys()
1209-
.chain(
1210-
worktree_files
1211-
.keys()
1212-
.filter(|p| state.index_contents.contains_key(*p)),
1213-
)
1214-
.collect();
1207+
let (old_files, new_files) = match diff {
1208+
git::repository::DiffStatType::HeadToIndex => {
1209+
(&state.head_contents, &state.index_contents)
1210+
}
1211+
git::repository::DiffStatType::HeadToWorktree => {
1212+
(&state.head_contents, &worktree_files)
1213+
}
1214+
git::repository::DiffStatType::IndexToWorktree => {
1215+
(&state.index_contents, &worktree_files)
1216+
}
1217+
};
1218+
let all_paths: HashSet<&RepoPath> = match diff {
1219+
git::repository::DiffStatType::HeadToIndex => state
1220+
.head_contents
1221+
.keys()
1222+
.chain(state.index_contents.keys())
1223+
.collect(),
1224+
git::repository::DiffStatType::HeadToWorktree => state
1225+
.head_contents
1226+
.keys()
1227+
.chain(
1228+
worktree_files
1229+
.keys()
1230+
.filter(|path| state.index_contents.contains_key(*path)),
1231+
)
1232+
.collect(),
1233+
git::repository::DiffStatType::IndexToWorktree => {
1234+
state.index_contents.keys().collect()
1235+
}
1236+
};
12151237
for path in all_paths {
12161238
if !matches_prefixes(path, &path_prefixes) {
12171239
continue;
12181240
}
1219-
let head = state.head_contents.get(path);
1220-
let worktree = worktree_files.get(path);
1221-
match (head, worktree) {
1241+
let old_file = old_files.get(path);
1242+
let new_file = new_files.get(path);
1243+
match (old_file, new_file) {
12221244
(Some(old), Some(new)) if old != new => {
12231245
entries.push((
12241246
path.clone(),

crates/git/src/repository.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,7 @@ pub trait GitRepository: Send + Sync {
11091109

11101110
fn diff_stat(
11111111
&self,
1112+
diff: DiffStatType,
11121113
path_prefixes: &[RepoPath],
11131114
) -> BoxFuture<'static, Result<crate::status::GitDiffStat>>;
11141115

@@ -1193,6 +1194,13 @@ pub enum DiffType {
11931194
MergeBase { base_ref: SharedString },
11941195
}
11951196

1197+
#[derive(Clone, Copy)]
1198+
pub enum DiffStatType {
1199+
HeadToIndex,
1200+
HeadToWorktree,
1201+
IndexToWorktree,
1202+
}
1203+
11961204
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
11971205
pub enum PushOptions {
11981206
SetUpstream,
@@ -2333,6 +2341,7 @@ impl GitRepository for RealGitRepository {
23332341

23342342
fn diff_stat(
23352343
&self,
2344+
diff: DiffStatType,
23362345
path_prefixes: &[RepoPath],
23372346
) -> BoxFuture<'static, Result<crate::status::GitDiffStat>> {
23382347
let path_prefixes = path_prefixes.to_vec();
@@ -2341,12 +2350,13 @@ impl GitRepository for RealGitRepository {
23412350
self.executor
23422351
.spawn(async move {
23432352
let git_binary = git_binary?;
2344-
let mut args: Vec<String> = vec![
2345-
"diff".into(),
2346-
"--numstat".into(),
2347-
"--no-renames".into(),
2348-
"HEAD".into(),
2349-
];
2353+
let mut args: Vec<String> =
2354+
vec!["diff".into(), "--numstat".into(), "--no-renames".into()];
2355+
match diff {
2356+
DiffStatType::HeadToIndex => args.extend(["--cached".into(), "HEAD".into()]),
2357+
DiffStatType::HeadToWorktree => args.push("HEAD".into()),
2358+
DiffStatType::IndexToWorktree => {}
2359+
}
23502360
if !path_prefixes.is_empty() {
23512361
args.push("--".into());
23522362
args.extend(

crates/git_ui/src/git_panel.rs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4548,13 +4548,13 @@ impl GitPanel {
45484548

45494549
self.stash_entries = repo.cached_stash();
45504550

4551-
for entry in repo.cached_status() {
4551+
for status_entry in repo.cached_status() {
45524552
self.changes_count += 1;
4553-
let is_conflict = repo.had_conflict_on_last_merge_head_change(&entry.repo_path);
4554-
let is_new = entry.status.is_created();
4555-
let staging = entry.status.staging();
4553+
let is_conflict = repo.had_conflict_on_last_merge_head_change(&status_entry.repo_path);
4554+
let is_new = status_entry.status.is_created();
4555+
let staging = status_entry.status.staging();
45564556

4557-
if let Some(pending) = repo.pending_ops_for_path(&entry.repo_path)
4557+
if let Some(pending) = repo.pending_ops_for_path(&status_entry.repo_path)
45584558
&& pending
45594559
.ops
45604560
.iter()
@@ -4564,10 +4564,10 @@ impl GitPanel {
45644564
}
45654565

45664566
let entry = GitStatusEntry {
4567-
repo_path: entry.repo_path.clone(),
4568-
status: entry.status,
4567+
repo_path: status_entry.repo_path.clone(),
4568+
status: status_entry.status,
45694569
staging,
4570-
diff_stat: entry.diff_stat,
4570+
diff_stat: status_entry.diff_stat,
45714571
};
45724572

45734573
if !is_conflict && !is_new {
@@ -4583,10 +4583,16 @@ impl GitPanel {
45834583
conflict_entries.push(entry);
45844584
} else if group_by_staging_state {
45854585
if staging.has_staged() {
4586-
staged_entries.push(entry.clone());
4586+
staged_entries.push(GitStatusEntry {
4587+
diff_stat: status_entry.staged_diff_stat,
4588+
..entry.clone()
4589+
});
45874590
}
45884591
if staging.has_unstaged() {
4589-
unstaged_entries.push(entry);
4592+
unstaged_entries.push(GitStatusEntry {
4593+
diff_stat: status_entry.unstaged_diff_stat,
4594+
..entry
4595+
});
45904596
}
45914597
} else if group_by_file_status && is_conflict {
45924598
conflict_entries.push(entry);
@@ -9339,7 +9345,6 @@ mod tests {
93399345
("crates/util/util.rs", StatusCode::Modified.worktree()),
93409346
],
93419347
);
9342-
93439348
let project =
93449349
Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await;
93459350
let window_handle =
@@ -9552,6 +9557,16 @@ mod tests {
95529557
("unstaged.rs", StatusCode::Modified.worktree()),
95539558
],
95549559
);
9560+
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
9561+
state.head_contents.insert(
9562+
repo_path("partial.rs"),
9563+
"head one\nhead two\nhead three\nhead four".into(),
9564+
);
9565+
state
9566+
.index_contents
9567+
.insert(repo_path("partial.rs"), "index one\nindex two".into());
9568+
})
9569+
.expect("fake repository should exist");
95559570

95569571
let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await;
95579572
let window_handle =
@@ -9626,6 +9641,24 @@ mod tests {
96269641
panel.stage_intent_for_entry_index(projections[1].index),
96279642
StageIntent::Stage
96289643
);
9644+
assert_eq!(
9645+
panel.entries[projections[0].index]
9646+
.status_entry()
9647+
.and_then(|entry| entry.diff_stat),
9648+
Some(DiffStat {
9649+
added: 2,
9650+
deleted: 4,
9651+
})
9652+
);
9653+
assert_eq!(
9654+
panel.entries[projections[1].index]
9655+
.status_entry()
9656+
.and_then(|entry| entry.diff_stat),
9657+
Some(DiffStat {
9658+
added: 1,
9659+
deleted: 2,
9660+
})
9661+
);
96299662
panel.entries.clone()
96309663
});
96319664

0 commit comments

Comments
 (0)