Represent git statuses more faithfully (#23082)

First, parse the output of `git status --porcelain=v1` into a
representation that can handle the full "grammar" and doesn't lose
information.

Second, as part of pushing this throughout the codebase, expand the use
of the existing `GitSummary` type to all the places where status
propagation is in play (i.e., anywhere we're dealing with a mix of files
and directories), and get rid of the previous `GitSummary ->
GitFileStatus` conversion.

- [x] Synchronize new representation over collab
  - [x] Update zed.proto
  - [x] Update DB models
- [x] Update `GitSummary` and summarization for the new `FileStatus`
- [x] Fix all tests
  - [x] worktree
  - [x] collab
- [x] Clean up `FILE_*` constants
- [x] New collab tests to exercise syncing of complex statuses
- [x] Run it locally and make sure it looks good

Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
Cole Miller 2025-01-15 19:01:38 -05:00 committed by GitHub
parent 224f3d4746
commit a41d72ee81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1015 additions and 552 deletions

View file

@ -4,7 +4,12 @@ use crate::{
};
use anyhow::Result;
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
use git::{repository::GitFileStatus, GITIGNORE};
use git::{
status::{
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
},
GITIGNORE,
};
use gpui::{BorrowAppContext, ModelContext, Task, TestAppContext};
use parking_lot::Mutex;
use postage::stream::Stream;
@ -738,7 +743,10 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_working_copy_change(
Path::new("/root/tree/.git"),
&[(Path::new("tracked-dir/tracked-file2"), GitFileStatus::Added)],
&[(
Path::new("tracked-dir/tracked-file2"),
FileStatus::worktree(StatusCode::Added),
)],
);
fs.create_file(
@ -766,7 +774,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
assert_entry_git_state(
tree,
"tracked-dir/tracked-file2",
Some(GitFileStatus::Added),
Some(StatusCode::Added),
false,
);
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, false);
@ -822,14 +830,14 @@ async fn test_update_gitignore(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_working_copy_change(
Path::new("/root/.git"),
&[(Path::new("b.txt"), GitFileStatus::Added)],
&[(Path::new("b.txt"), FileStatus::worktree(StatusCode::Added))],
);
cx.executor().run_until_parked();
cx.read(|cx| {
let tree = tree.read(cx);
assert_entry_git_state(tree, "a.xml", None, true);
assert_entry_git_state(tree, "b.txt", Some(GitFileStatus::Added), false);
assert_entry_git_state(tree, "b.txt", Some(StatusCode::Added), false);
});
}
@ -1492,7 +1500,10 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
// detected.
fs.set_status_for_repo_via_git_operation(
Path::new("/root/.git"),
&[(Path::new("b/c.txt"), GitFileStatus::Modified)],
&[(
Path::new("b/c.txt"),
FileStatus::worktree(StatusCode::Modified),
)],
);
cx.executor().run_until_parked();
@ -1501,9 +1512,9 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
check_git_statuses(
&snapshot,
&[
(Path::new(""), Some(GitFileStatus::Modified)),
(Path::new("a.txt"), None),
(Path::new("b/c.txt"), Some(GitFileStatus::Modified)),
(Path::new(""), GitSummary::MODIFIED),
(Path::new("a.txt"), GitSummary::UNCHANGED),
(Path::new("b/c.txt"), GitSummary::MODIFIED),
],
);
}
@ -2142,6 +2153,11 @@ fn random_filename(rng: &mut impl Rng) -> String {
.collect()
}
const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
});
#[gpui::test]
async fn test_rename_work_directory(cx: &mut TestAppContext) {
init_test(cx);
@ -2183,11 +2199,11 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
assert_eq!(
tree.status_for_file(Path::new("projects/project1/a")),
Some(GitFileStatus::Modified)
Some(FileStatus::worktree(StatusCode::Modified)),
);
assert_eq!(
tree.status_for_file(Path::new("projects/project1/b")),
Some(GitFileStatus::Untracked)
Some(FileStatus::Untracked),
);
});
@ -2204,11 +2220,11 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
assert_eq!(
tree.status_for_file(Path::new("projects/project2/a")),
Some(GitFileStatus::Modified)
Some(FileStatus::worktree(StatusCode::Modified)),
);
assert_eq!(
tree.status_for_file(Path::new("projects/project2/b")),
Some(GitFileStatus::Untracked)
Some(FileStatus::Untracked),
);
});
}
@ -2387,11 +2403,11 @@ async fn test_file_status(cx: &mut TestAppContext) {
assert_eq!(
snapshot.status_for_file(project_path.join(B_TXT)),
Some(GitFileStatus::Untracked)
Some(FileStatus::Untracked),
);
assert_eq!(
snapshot.status_for_file(project_path.join(F_TXT)),
Some(GitFileStatus::Untracked)
Some(FileStatus::Untracked),
);
});
@ -2405,7 +2421,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
let snapshot = tree.snapshot();
assert_eq!(
snapshot.status_for_file(project_path.join(A_TXT)),
Some(GitFileStatus::Modified)
Some(FileStatus::worktree(StatusCode::Modified)),
);
});
@ -2421,7 +2437,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
let snapshot = tree.snapshot();
assert_eq!(
snapshot.status_for_file(project_path.join(F_TXT)),
Some(GitFileStatus::Untracked)
Some(FileStatus::Untracked),
);
assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
@ -2443,11 +2459,11 @@ async fn test_file_status(cx: &mut TestAppContext) {
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
assert_eq!(
snapshot.status_for_file(project_path.join(B_TXT)),
Some(GitFileStatus::Untracked)
Some(FileStatus::Untracked),
);
assert_eq!(
snapshot.status_for_file(project_path.join(E_TXT)),
Some(GitFileStatus::Modified)
Some(FileStatus::worktree(StatusCode::Modified)),
);
});
@ -2482,7 +2498,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
let snapshot = tree.snapshot();
assert_eq!(
snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
Some(GitFileStatus::Untracked)
Some(FileStatus::Untracked),
);
});
@ -2506,7 +2522,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
.join(Path::new(renamed_dir_name))
.join(RENAMED_FILE)
),
Some(GitFileStatus::Untracked)
Some(FileStatus::Untracked),
);
});
}
@ -2559,11 +2575,14 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(entries[0].worktree_status(), Some(GitFileStatus::Modified));
assert_eq!(
entries[0].status,
FileStatus::worktree(StatusCode::Modified)
);
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
assert_eq!(entries[1].worktree_status(), Some(GitFileStatus::Untracked));
assert_eq!(entries[1].status, FileStatus::Untracked);
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
assert_eq!(entries[2].worktree_status(), Some(GitFileStatus::Deleted));
assert_eq!(entries[2].status, FileStatus::worktree(StatusCode::Deleted));
});
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
@ -2581,14 +2600,20 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(entries[0].worktree_status(), Some(GitFileStatus::Modified));
assert_eq!(
entries[0].status,
FileStatus::worktree(StatusCode::Modified)
);
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
assert_eq!(entries[1].worktree_status(), Some(GitFileStatus::Untracked));
assert_eq!(entries[1].status, FileStatus::Untracked);
// Status updated
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
assert_eq!(entries[2].worktree_status(), Some(GitFileStatus::Modified));
assert_eq!(
entries[2].status,
FileStatus::worktree(StatusCode::Modified)
);
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
assert_eq!(entries[3].worktree_status(), Some(GitFileStatus::Deleted));
assert_eq!(entries[3].status, FileStatus::worktree(StatusCode::Deleted));
});
git_add("a.txt", &repo);
@ -2621,7 +2646,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
&entries
);
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(entries[0].worktree_status(), Some(GitFileStatus::Deleted));
assert_eq!(entries[0].status, FileStatus::worktree(StatusCode::Deleted));
});
}
@ -2692,7 +2717,7 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
assert_eq!(snapshot.status_for_file("c.txt"), None);
assert_eq!(
snapshot.status_for_file("d/e.txt"),
Some(GitFileStatus::Untracked)
Some(FileStatus::Untracked)
);
});
@ -2744,17 +2769,20 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
&[
(Path::new("x2.txt"), GitFileStatus::Modified),
(Path::new("z.txt"), GitFileStatus::Added),
(
Path::new("x2.txt"),
FileStatus::worktree(StatusCode::Modified),
),
(Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/y/.git"),
&[(Path::new("y1.txt"), GitFileStatus::Conflict)],
&[(Path::new("y1.txt"), CONFLICT)],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
&[(Path::new("z2.txt"), GitFileStatus::Added)],
&[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
);
let tree = Worktree::local(
@ -2780,25 +2808,25 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
assert_eq!(entry.git_status, None);
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
assert_eq!(entry.git_status, Some(GitFileStatus::Modified));
assert_eq!(entry.git_summary, GitSummary::MODIFIED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt"));
assert_eq!(entry.git_status, Some(GitFileStatus::Conflict));
assert_eq!(entry.git_summary, GitSummary::CONFLICT);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
assert_eq!(entry.git_status, None);
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
assert_eq!(entry.git_status, Some(GitFileStatus::Added));
assert_eq!(entry.git_summary, GitSummary::ADDED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
assert_eq!(entry.git_status, None);
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
assert_eq!(entry.git_status, Some(GitFileStatus::Added));
assert_eq!(entry.git_summary, GitSummary::ADDED);
}
#[gpui::test]
@ -2834,9 +2862,15 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_git_operation(
Path::new("/root/.git"),
&[
(Path::new("a/b/c1.txt"), GitFileStatus::Added),
(Path::new("a/d/e2.txt"), GitFileStatus::Modified),
(Path::new("g/h2.txt"), GitFileStatus::Conflict),
(
Path::new("a/b/c1.txt"),
FileStatus::worktree(StatusCode::Added),
),
(
Path::new("a/d/e2.txt"),
FileStatus::worktree(StatusCode::Modified),
),
(Path::new("g/h2.txt"), CONFLICT),
],
);
@ -2859,52 +2893,58 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
check_git_statuses(
&snapshot,
&[
(Path::new(""), Some(GitFileStatus::Conflict)),
(Path::new("g"), Some(GitFileStatus::Conflict)),
(Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
(
Path::new(""),
GitSummary::CONFLICT + GitSummary::MODIFIED + GitSummary::ADDED,
),
(Path::new("g"), GitSummary::CONFLICT),
(Path::new("g/h2.txt"), GitSummary::CONFLICT),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new(""), Some(GitFileStatus::Conflict)),
(Path::new("a"), Some(GitFileStatus::Modified)),
(Path::new("a/b"), Some(GitFileStatus::Added)),
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
(Path::new("a/b/c2.txt"), None),
(Path::new("a/d"), Some(GitFileStatus::Modified)),
(Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
(Path::new("f"), None),
(Path::new("f/no-status.txt"), None),
(Path::new("g"), Some(GitFileStatus::Conflict)),
(Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
(
Path::new(""),
GitSummary::CONFLICT + GitSummary::ADDED + GitSummary::MODIFIED,
),
(Path::new("a"), GitSummary::ADDED + GitSummary::MODIFIED),
(Path::new("a/b"), GitSummary::ADDED),
(Path::new("a/b/c1.txt"), GitSummary::ADDED),
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
(Path::new("a/d"), GitSummary::MODIFIED),
(Path::new("a/d/e2.txt"), GitSummary::MODIFIED),
(Path::new("f"), GitSummary::UNCHANGED),
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
(Path::new("g"), GitSummary::CONFLICT),
(Path::new("g/h2.txt"), GitSummary::CONFLICT),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("a/b"), Some(GitFileStatus::Added)),
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
(Path::new("a/b/c2.txt"), None),
(Path::new("a/d"), Some(GitFileStatus::Modified)),
(Path::new("a/d/e1.txt"), None),
(Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
(Path::new("f"), None),
(Path::new("f/no-status.txt"), None),
(Path::new("g"), Some(GitFileStatus::Conflict)),
(Path::new("a/b"), GitSummary::ADDED),
(Path::new("a/b/c1.txt"), GitSummary::ADDED),
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
(Path::new("a/d"), GitSummary::MODIFIED),
(Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
(Path::new("a/d/e2.txt"), GitSummary::MODIFIED),
(Path::new("f"), GitSummary::UNCHANGED),
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
(Path::new("g"), GitSummary::CONFLICT),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
(Path::new("a/b/c2.txt"), None),
(Path::new("a/d/e1.txt"), None),
(Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
(Path::new("f/no-status.txt"), None),
(Path::new("a/b/c1.txt"), GitSummary::ADDED),
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
(Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
(Path::new("a/d/e2.txt"), GitSummary::MODIFIED),
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
],
);
}
@ -2937,18 +2977,24 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
&[(Path::new("x1.txt"), GitFileStatus::Added)],
&[(Path::new("x1.txt"), FileStatus::worktree(StatusCode::Added))],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/y/.git"),
&[
(Path::new("y1.txt"), GitFileStatus::Conflict),
(Path::new("y2.txt"), GitFileStatus::Modified),
(Path::new("y1.txt"), CONFLICT),
(
Path::new("y2.txt"),
FileStatus::worktree(StatusCode::Modified),
),
],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
&[(Path::new("z2.txt"), GitFileStatus::Modified)],
&[(
Path::new("z2.txt"),
FileStatus::worktree(StatusCode::Modified),
)],
);
let tree = Worktree::local(
@ -2971,48 +3017,48 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext
check_git_statuses(
&snapshot,
&[
(Path::new("x"), Some(GitFileStatus::Added)),
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
(Path::new("x"), GitSummary::ADDED),
(Path::new("x/x1.txt"), GitSummary::ADDED),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("y"), Some(GitFileStatus::Conflict)),
(Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)),
(Path::new("y/y2.txt"), Some(GitFileStatus::Modified)),
(Path::new("y"), GitSummary::CONFLICT + GitSummary::MODIFIED),
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
(Path::new("y/y2.txt"), GitSummary::MODIFIED),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("z"), Some(GitFileStatus::Modified)),
(Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
(Path::new("z"), GitSummary::MODIFIED),
(Path::new("z/z2.txt"), GitSummary::MODIFIED),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("x"), Some(GitFileStatus::Added)),
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
(Path::new("x"), GitSummary::ADDED),
(Path::new("x/x1.txt"), GitSummary::ADDED),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("x"), Some(GitFileStatus::Added)),
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
(Path::new("x/x2.txt"), None),
(Path::new("y"), Some(GitFileStatus::Conflict)),
(Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)),
(Path::new("y/y2.txt"), Some(GitFileStatus::Modified)),
(Path::new("z"), Some(GitFileStatus::Modified)),
(Path::new("z/z1.txt"), None),
(Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
(Path::new("x"), GitSummary::ADDED),
(Path::new("x/x1.txt"), GitSummary::ADDED),
(Path::new("x/x2.txt"), GitSummary::UNCHANGED),
(Path::new("y"), GitSummary::CONFLICT + GitSummary::MODIFIED),
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
(Path::new("y/y2.txt"), GitSummary::MODIFIED),
(Path::new("z"), GitSummary::MODIFIED),
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
(Path::new("z/z2.txt"), GitSummary::MODIFIED),
],
);
}
@ -3047,18 +3093,21 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
&[
(Path::new("x2.txt"), GitFileStatus::Modified),
(Path::new("z.txt"), GitFileStatus::Added),
(
Path::new("x2.txt"),
FileStatus::worktree(StatusCode::Modified),
),
(Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/y/.git"),
&[(Path::new("y1.txt"), GitFileStatus::Conflict)],
&[(Path::new("y1.txt"), CONFLICT)],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
&[(Path::new("z2.txt"), GitFileStatus::Added)],
&[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
);
let tree = Worktree::local(
@ -3082,17 +3131,17 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
check_git_statuses(
&snapshot,
&[
(Path::new("x/y"), Some(GitFileStatus::Conflict)), // the y git repository has conflict file in it, and so should have a conflict status
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
(Path::new("x/y/y2.txt"), None),
(Path::new("x/y"), GitSummary::CONFLICT),
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("z"), Some(GitFileStatus::Added)),
(Path::new("z/z1.txt"), None),
(Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
(Path::new("z"), GitSummary::ADDED),
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
(Path::new("z/z2.txt"), GitSummary::ADDED),
],
);
@ -3100,9 +3149,9 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
check_git_statuses(
&snapshot,
&[
(Path::new("x"), Some(GitFileStatus::Modified)),
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
(Path::new("x"), GitSummary::MODIFIED + GitSummary::ADDED),
(Path::new("x/y"), GitSummary::CONFLICT),
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
],
);
@ -3110,13 +3159,13 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
check_git_statuses(
&snapshot,
&[
(Path::new("x"), Some(GitFileStatus::Modified)),
(Path::new("x/x1.txt"), None),
(Path::new("x/x2.txt"), Some(GitFileStatus::Modified)),
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
(Path::new("x/y/y2.txt"), None),
(Path::new("x/z.txt"), Some(GitFileStatus::Added)),
(Path::new("x"), GitSummary::MODIFIED + GitSummary::ADDED),
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
(Path::new("x/x2.txt"), GitSummary::MODIFIED),
(Path::new("x/y"), GitSummary::CONFLICT),
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
(Path::new("x/z.txt"), GitSummary::ADDED),
],
);
@ -3124,9 +3173,9 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
check_git_statuses(
&snapshot,
&[
(Path::new(""), None),
(Path::new("x"), Some(GitFileStatus::Modified)),
(Path::new("x/x1.txt"), None),
(Path::new(""), GitSummary::UNCHANGED),
(Path::new("x"), GitSummary::MODIFIED + GitSummary::ADDED),
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
],
);
@ -3134,17 +3183,17 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
check_git_statuses(
&snapshot,
&[
(Path::new(""), None),
(Path::new("x"), Some(GitFileStatus::Modified)),
(Path::new("x/x1.txt"), None),
(Path::new("x/x2.txt"), Some(GitFileStatus::Modified)),
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
(Path::new("x/y/y2.txt"), None),
(Path::new("x/z.txt"), Some(GitFileStatus::Added)),
(Path::new("z"), Some(GitFileStatus::Added)),
(Path::new("z/z1.txt"), None),
(Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
(Path::new(""), GitSummary::UNCHANGED),
(Path::new("x"), GitSummary::MODIFIED + GitSummary::ADDED),
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
(Path::new("x/x2.txt"), GitSummary::MODIFIED),
(Path::new("x/y"), GitSummary::CONFLICT),
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
(Path::new("x/z.txt"), GitSummary::ADDED),
(Path::new("z"), GitSummary::ADDED),
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
(Path::new("z/z2.txt"), GitSummary::ADDED),
],
);
}
@ -3173,7 +3222,7 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
}
#[track_caller]
fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, Option<GitFileStatus>)]) {
fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, GitSummary)]) {
let mut traversal = snapshot
.traverse_from_path(true, true, false, "".as_ref())
.with_git_statuses();
@ -3182,8 +3231,8 @@ fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, Option<G
.map(|&(path, _)| {
let git_entry = traversal
.find(|git_entry| &*git_entry.path == path)
.expect("Traversal has no entry for {path:?}");
(path, git_entry.git_status)
.unwrap_or_else(|| panic!("Traversal has no entry for {path:?}"));
(path, git_entry.git_summary)
})
.collect::<Vec<_>>();
assert_eq!(found_statuses, expected_statuses);
@ -3330,14 +3379,21 @@ fn init_test(cx: &mut gpui::TestAppContext) {
fn assert_entry_git_state(
tree: &Worktree,
path: &str,
git_status: Option<GitFileStatus>,
worktree_status: Option<StatusCode>,
is_ignored: bool,
) {
let entry = tree.entry_for_path(path).expect("entry {path} not found");
let status = tree.status_for_file(Path::new(path));
let expected = worktree_status.map(|worktree_status| {
TrackedStatus {
worktree_status,
index_status: StatusCode::Unmodified,
}
.into()
});
assert_eq!(
tree.status_for_file(Path::new(path)),
git_status,
"expected {path} to have git status: {git_status:?}"
status, expected,
"expected {path} to have git status: {expected:?}"
);
assert_eq!(
entry.is_ignored, is_ignored,