Move git status out of Entry (#22224)

- [x] Rewrite worktree git handling
- [x] Fix tests
- [x] Fix `test_propagate_statuses_for_repos_under_project`
- [x] Replace `WorkDirectoryEntry` with `WorkDirectory` in
`RepositoryEntry`
- [x] Add a worktree event for capturing git status changes
- [x] Confirm that the local repositories are correctly updating the new
WorkDirectory field
- [x] Implement the git statuses query as a join when pulling entries
out of worktree
- [x] Use this new join to implement the project panel and outline
panel.
- [x] Synchronize git statuses over the wire for collab and remote dev
(use the existing `worktree_repository_statuses` table, adjust as
needed)
- [x] Only send changed statuses to collab

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.com>
Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Mikayla Maki 2025-01-03 17:00:16 -08:00 committed by GitHub
parent 72057e5716
commit 9613084f59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 2824 additions and 1254 deletions

File diff suppressed because it is too large Load diff

View file

@ -1497,7 +1497,8 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
check_propagated_statuses(
check_git_statuses(
&snapshot,
&[
(Path::new(""), Some(GitFileStatus::Modified)),
@ -2178,15 +2179,15 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
cx.read(|cx| {
let tree = tree.read(cx);
let (work_dir, _) = tree.repositories().next().unwrap();
assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
let repo = tree.repositories().next().unwrap();
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
assert_eq!(
tree.status_for_file(Path::new("projects/project1/a")),
Some(GitFileStatus::Modified)
);
assert_eq!(
tree.status_for_file(Path::new("projects/project1/b")),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
});
@ -2199,15 +2200,15 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
cx.read(|cx| {
let tree = tree.read(cx);
let (work_dir, _) = tree.repositories().next().unwrap();
assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
let repo = tree.repositories().next().unwrap();
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
assert_eq!(
tree.status_for_file(Path::new("projects/project2/a")),
Some(GitFileStatus::Modified)
);
assert_eq!(
tree.status_for_file(Path::new("projects/project2/b")),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
});
}
@ -2253,23 +2254,13 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
assert_eq!(
entry
.work_directory(tree)
.map(|directory| directory.as_ref().to_owned()),
Some(Path::new("dir1").to_owned())
);
let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
assert_eq!(repo.path.as_ref(), Path::new("dir1"));
let entry = tree
let repo = tree
.repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
.unwrap();
assert_eq!(
entry
.work_directory(tree)
.map(|directory| directory.as_ref().to_owned()),
Some(Path::new("dir1/deps/dep1").to_owned())
);
assert_eq!(repo.path.as_ref(), Path::new("dir1/deps/dep1"));
let entries = tree.files(false, 0);
@ -2278,10 +2269,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
.map(|(entry, repo)| {
(
entry.path.as_ref(),
repo.and_then(|repo| {
repo.work_directory(tree)
.map(|work_directory| work_directory.0.to_path_buf())
}),
repo.map(|repo| repo.path.to_path_buf()),
)
})
.collect::<Vec<_>>();
@ -2334,7 +2322,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
}
#[gpui::test]
async fn test_git_status(cx: &mut TestAppContext) {
async fn test_file_status(cx: &mut TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
const IGNORE_RULE: &str = "**/target";
@ -2393,17 +2381,17 @@ async fn test_git_status(cx: &mut TestAppContext) {
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
assert_eq!(snapshot.repositories().count(), 1);
let (dir, repo_entry) = snapshot.repositories().next().unwrap();
assert_eq!(dir.as_ref(), Path::new("project"));
let repo_entry = snapshot.repositories().next().unwrap();
assert_eq!(repo_entry.path.as_ref(), Path::new("project"));
assert!(repo_entry.location_in_repo.is_none());
assert_eq!(
snapshot.status_for_file(project_path.join(B_TXT)),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
assert_eq!(
snapshot.status_for_file(project_path.join(F_TXT)),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
});
@ -2433,7 +2421,7 @@ async fn test_git_status(cx: &mut TestAppContext) {
let snapshot = tree.snapshot();
assert_eq!(
snapshot.status_for_file(project_path.join(F_TXT)),
Some(GitFileStatus::Added)
Some(GitFileStatus::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);
@ -2455,7 +2443,7 @@ async fn test_git_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::Added)
Some(GitFileStatus::Untracked)
);
assert_eq!(
snapshot.status_for_file(project_path.join(E_TXT)),
@ -2494,7 +2482,7 @@ async fn test_git_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::Added)
Some(GitFileStatus::Untracked)
);
});
@ -2518,11 +2506,125 @@ async fn test_git_status(cx: &mut TestAppContext) {
.join(Path::new(renamed_dir_name))
.join(RENAMED_FILE)
),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
});
}
#[gpui::test]
async fn test_git_repository_status(cx: &mut TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
let root = temp_tree(json!({
"project": {
"a.txt": "a", // Modified
"b.txt": "bb", // Added
"c.txt": "ccc", // Unchanged
"d.txt": "dddd", // Deleted
},
}));
// Set up git repository before creating the worktree.
let work_dir = root.path().join("project");
let repo = git_init(work_dir.as_path());
git_add("a.txt", &repo);
git_add("c.txt", &repo);
git_add("d.txt", &repo);
git_commit("Initial commit", &repo);
std::fs::remove_file(work_dir.join("d.txt")).unwrap();
std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
let tree = Worktree::local(
root.path(),
true,
Arc::new(RealFs::default()),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
// Check that the right git state is observed on startup
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
let repo = snapshot.repositories().next().unwrap();
let entries = repo.status().collect::<Vec<_>>();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(entries[0].status, GitFileStatus::Modified);
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
assert_eq!(entries[1].status, GitFileStatus::Untracked);
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
assert_eq!(entries[2].status, GitFileStatus::Deleted);
});
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
eprintln!("File c.txt has been modified");
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
let repository = snapshot.repositories().next().unwrap();
let entries = repository.status().collect::<Vec<_>>();
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(entries[0].status, GitFileStatus::Modified);
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
assert_eq!(entries[1].status, GitFileStatus::Untracked);
// Status updated
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
assert_eq!(entries[2].status, GitFileStatus::Modified);
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
assert_eq!(entries[3].status, GitFileStatus::Deleted);
});
git_add("a.txt", &repo);
git_add("c.txt", &repo);
git_remove_index(Path::new("d.txt"), &repo);
git_commit("Another commit", &repo);
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
std::fs::remove_file(work_dir.join("a.txt")).unwrap();
std::fs::remove_file(work_dir.join("b.txt")).unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
let repo = snapshot.repositories().next().unwrap();
let entries = repo.status().collect::<Vec<_>>();
// Deleting an untracked entry, b.txt, should leave no status
// a.txt was tracked, and so should have a status
assert_eq!(
entries.len(),
1,
"Entries length was incorrect\n{:#?}",
&entries
);
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(entries[0].status, GitFileStatus::Deleted);
});
}
#[gpui::test]
async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
init_test(cx);
@ -2575,22 +2677,22 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
assert_eq!(snapshot.repositories().count(), 1);
let (dir, repo_entry) = snapshot.repositories().next().unwrap();
let repo = snapshot.repositories().next().unwrap();
// Path is blank because the working directory of
// the git repository is located at the root of the project
assert_eq!(dir.as_ref(), Path::new(""));
assert_eq!(repo.path.as_ref(), Path::new(""));
// This is the missing path between the root of the project (sub-folder-2) and its
// location relative to the root of the repository.
assert_eq!(
repo_entry.location_in_repo,
repo.location_in_repo,
Some(Arc::from(Path::new("sub-folder-1/sub-folder-2")))
);
assert_eq!(snapshot.status_for_file("c.txt"), None);
assert_eq!(
snapshot.status_for_file("d/e.txt"),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
});
@ -2612,6 +2714,93 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/root",
json!({
"x": {
".git": {},
"x1.txt": "foo",
"x2.txt": "bar",
"y": {
".git": {},
"y1.txt": "baz",
"y2.txt": "qux"
},
"z.txt": "sneaky..."
},
"z": {
".git": {},
"z1.txt": "quux",
"z2.txt": "quuux"
}
}),
)
.await;
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),
],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/y/.git"),
&[(Path::new("y1.txt"), GitFileStatus::Conflict)],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
&[(Path::new("z2.txt"), GitFileStatus::Added)],
);
let tree = Worktree::local(
Path::new("/root"),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
let mut traversal = snapshot
.traverse_from_path(true, false, true, Path::new("x"))
.with_git_statuses();
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
assert_eq!(entry.git_status, None);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
assert_eq!(entry.git_status, Some(GitFileStatus::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));
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
assert_eq!(entry.git_status, None);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
assert_eq!(entry.git_status, Some(GitFileStatus::Added));
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
assert_eq!(entry.git_status, None);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
assert_eq!(entry.git_status, Some(GitFileStatus::Added));
}
#[gpui::test]
async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
init_test(cx);
@ -2638,7 +2827,6 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
"h1.txt": "",
"h2.txt": ""
},
}),
)
.await;
@ -2668,7 +2856,16 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
check_propagated_statuses(
check_git_statuses(
&snapshot,
&[
(Path::new(""), Some(GitFileStatus::Conflict)),
(Path::new("g"), Some(GitFileStatus::Conflict)),
(Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new(""), Some(GitFileStatus::Conflict)),
@ -2685,7 +2882,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
],
);
check_propagated_statuses(
check_git_statuses(
&snapshot,
&[
(Path::new("a/b"), Some(GitFileStatus::Added)),
@ -2700,7 +2897,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
],
);
check_propagated_statuses(
check_git_statuses(
&snapshot,
&[
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
@ -2712,6 +2909,246 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/root",
json!({
"x": {
".git": {},
"x1.txt": "foo",
"x2.txt": "bar"
},
"y": {
".git": {},
"y1.txt": "baz",
"y2.txt": "qux"
},
"z": {
".git": {},
"z1.txt": "quux",
"z2.txt": "quuux"
}
}),
)
.await;
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
&[(Path::new("x1.txt"), GitFileStatus::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),
],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
&[(Path::new("z2.txt"), GitFileStatus::Modified)],
);
let tree = Worktree::local(
Path::new("/root"),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
check_git_statuses(
&snapshot,
&[
(Path::new("x"), Some(GitFileStatus::Added)),
(Path::new("x/x1.txt"), Some(GitFileStatus::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)),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("z"), Some(GitFileStatus::Modified)),
(Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("x"), Some(GitFileStatus::Added)),
(Path::new("x/x1.txt"), Some(GitFileStatus::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)),
],
);
}
#[gpui::test]
async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/root",
json!({
"x": {
".git": {},
"x1.txt": "foo",
"x2.txt": "bar",
"y": {
".git": {},
"y1.txt": "baz",
"y2.txt": "qux"
},
"z.txt": "sneaky..."
},
"z": {
".git": {},
"z1.txt": "quux",
"z2.txt": "quuux"
}
}),
)
.await;
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),
],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/y/.git"),
&[(Path::new("y1.txt"), GitFileStatus::Conflict)],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
&[(Path::new("z2.txt"), GitFileStatus::Added)],
);
let tree = Worktree::local(
Path::new("/root"),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
// Sanity check the propagation for x/y and z
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),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("z"), Some(GitFileStatus::Added)),
(Path::new("z/z1.txt"), None),
(Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
],
);
// Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
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)),
],
);
// Sanity check everything around it
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)),
],
);
// Test the other fundamental case, transitioning from git repository to non-git repository
check_git_statuses(
&snapshot,
&[
(Path::new(""), None),
(Path::new("x"), Some(GitFileStatus::Modified)),
(Path::new("x/x1.txt"), None),
],
);
// And all together now
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)),
],
);
}
#[gpui::test]
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
init_test(cx);
@ -2736,22 +3173,20 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
}
#[track_caller]
fn check_propagated_statuses(
snapshot: &Snapshot,
expected_statuses: &[(&Path, Option<GitFileStatus>)],
) {
let mut entries = expected_statuses
fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, Option<GitFileStatus>)]) {
let mut traversal = snapshot
.traverse_from_path(true, true, false, "".as_ref())
.with_git_statuses();
let found_statuses = expected_statuses
.iter()
.map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
.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)
})
.collect::<Vec<_>>();
snapshot.propagate_git_statuses(&mut entries);
assert_eq!(
entries
.iter()
.map(|e| (e.path.as_ref(), e.git_status))
.collect::<Vec<_>>(),
expected_statuses
);
assert_eq!(found_statuses, expected_statuses);
}
#[track_caller]
@ -2763,14 +3198,14 @@ fn git_init(path: &Path) -> git2::Repository {
fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
let path = path.as_ref();
let mut index = repo.index().expect("Failed to get index");
index.add_path(path).expect("Failed to add a.txt");
index.add_path(path).expect("Failed to add file");
index.write().expect("Failed to write index");
}
#[track_caller]
fn git_remove_index(path: &Path, repo: &git2::Repository) {
let mut index = repo.index().expect("Failed to get index");
index.remove_path(path).expect("Failed to add a.txt");
index.remove_path(path).expect("Failed to add file");
index.write().expect("Failed to write index");
}
@ -2900,7 +3335,8 @@ fn assert_entry_git_state(
) {
let entry = tree.entry_for_path(path).expect("entry {path} not found");
assert_eq!(
entry.git_status, git_status,
tree.status_for_file(Path::new(path)),
git_status,
"expected {path} to have git status: {git_status:?}"
);
assert_eq!(