Make FakeGitRepository behave more like a real git repository (#26961)

This PR reworks the `FakeGitRepository` type that we use for testing git
interactions, to make it more realistic. In particular, the `status`
method now derives the Git status from the differences between HEAD, the
index, and the working copy. This way, if you modify a file in the
`FakeFs`, the Git repository's `status` method will reflect that
modification.

Release Notes:

- N/A

---------

Co-authored-by: Junkui Zhang <364772080@qq.com>
This commit is contained in:
Max Brunsfeld 2025-03-19 09:04:27 -07:00 committed by GitHub
parent 5f398071b2
commit 74a39c7263
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 790 additions and 679 deletions

View file

@ -103,7 +103,6 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
}),
)
.await;
client_a.fs().recalculate_git_status(Path::new("/a/.git"));
cx_b.run_until_parked();
project_b.update(cx_b, |project, cx| {

View file

@ -2958,15 +2958,38 @@ async fn test_git_status_sync(
.insert_tree(
"/dir",
json!({
".git": {},
"a.txt": "a",
"b.txt": "b",
".git": {},
"a.txt": "a",
"b.txt": "b",
"c.txt": "c",
}),
)
.await;
const A_TXT: &str = "a.txt";
const B_TXT: &str = "b.txt";
// Initially, a.txt is uncommitted, but present in the index,
// and b.txt is unmerged.
client_a.fs().set_head_for_repo(
"/dir/.git".as_ref(),
&[("b.txt".into(), "B".into()), ("c.txt".into(), "c".into())],
);
client_a.fs().set_index_for_repo(
"/dir/.git".as_ref(),
&[
("a.txt".into(), "".into()),
("b.txt".into(), "B".into()),
("c.txt".into(), "c".into()),
],
);
client_a.fs().set_unmerged_paths_for_repo(
"/dir/.git".as_ref(),
&[(
"b.txt".into(),
UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Deleted,
},
)],
);
const A_STATUS_START: FileStatus = FileStatus::Tracked(TrackedStatus {
index_status: StatusCode::Added,
@ -2977,14 +3000,6 @@ async fn test_git_status_sync(
second_head: UnmergedStatusCode::Deleted,
});
client_a.fs().set_status_for_repo_via_git_operation(
Path::new("/dir/.git"),
&[
(Path::new(A_TXT), A_STATUS_START),
(Path::new(B_TXT), B_STATUS_START),
],
);
let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| {
@ -3000,7 +3015,7 @@ async fn test_git_status_sync(
#[track_caller]
fn assert_status(
file: &impl AsRef<Path>,
file: impl AsRef<Path>,
status: Option<FileStatus>,
project: &Project,
cx: &App,
@ -3014,13 +3029,15 @@ async fn test_git_status_sync(
}
project_local.read_with(cx_a, |project, cx| {
assert_status(&Path::new(A_TXT), Some(A_STATUS_START), project, cx);
assert_status(&Path::new(B_TXT), Some(B_STATUS_START), project, cx);
assert_status("a.txt", Some(A_STATUS_START), project, cx);
assert_status("b.txt", Some(B_STATUS_START), project, cx);
assert_status("c.txt", None, project, cx);
});
project_remote.read_with(cx_b, |project, cx| {
assert_status(&Path::new(A_TXT), Some(A_STATUS_START), project, cx);
assert_status(&Path::new(B_TXT), Some(B_STATUS_START), project, cx);
assert_status("a.txt", Some(A_STATUS_START), project, cx);
assert_status("b.txt", Some(B_STATUS_START), project, cx);
assert_status("c.txt", None, project, cx);
});
const A_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus {
@ -3029,30 +3046,42 @@ async fn test_git_status_sync(
});
const B_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus {
index_status: StatusCode::Deleted,
worktree_status: StatusCode::Unmodified,
worktree_status: StatusCode::Added,
});
const C_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus {
index_status: StatusCode::Unmodified,
worktree_status: StatusCode::Modified,
});
client_a.fs().set_status_for_repo_via_working_copy_change(
Path::new("/dir/.git"),
&[
(Path::new(A_TXT), A_STATUS_END),
(Path::new(B_TXT), B_STATUS_END),
],
// Delete b.txt from the index, mark conflict as resolved,
// and modify c.txt in the working copy.
client_a.fs().set_index_for_repo(
"/dir/.git".as_ref(),
&[("a.txt".into(), "a".into()), ("c.txt".into(), "c".into())],
);
client_a
.fs()
.set_unmerged_paths_for_repo("/dir/.git".as_ref(), &[]);
client_a
.fs()
.atomic_write("/dir/c.txt".into(), "CC".into())
.await
.unwrap();
// Wait for buffer_local_a to receive it
executor.run_until_parked();
// Smoke test status reading
project_local.read_with(cx_a, |project, cx| {
assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx);
assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx);
assert_status("a.txt", Some(A_STATUS_END), project, cx);
assert_status("b.txt", Some(B_STATUS_END), project, cx);
assert_status("c.txt", Some(C_STATUS_END), project, cx);
});
project_remote.read_with(cx_b, |project, cx| {
assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx);
assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx);
assert_status("a.txt", Some(A_STATUS_END), project, cx);
assert_status("b.txt", Some(B_STATUS_END), project, cx);
assert_status("c.txt", Some(C_STATUS_END), project, cx);
});
// And synchronization while joining
@ -3060,8 +3089,9 @@ async fn test_git_status_sync(
executor.run_until_parked();
project_remote_c.read_with(cx_c, |project, cx| {
assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx);
assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx);
assert_status("a.txt", Some(A_STATUS_END), project, cx);
assert_status("b.txt", Some(B_STATUS_END), project, cx);
assert_status("c.txt", Some(C_STATUS_END), project, cx);
});
}

View file

@ -128,7 +128,6 @@ enum GitOperation {
WriteGitStatuses {
repo_path: PathBuf,
statuses: Vec<(PathBuf, FileStatus)>,
git_operation: bool,
},
}
@ -987,7 +986,6 @@ impl RandomizedTest for ProjectCollaborationTest {
GitOperation::WriteGitStatuses {
repo_path,
statuses,
git_operation,
} => {
if !client.fs().directories(false).contains(&repo_path) {
return Err(TestError::Inapplicable);
@ -1016,17 +1014,9 @@ impl RandomizedTest for ProjectCollaborationTest {
client.fs().create_dir(&dot_git_dir).await?;
}
if git_operation {
client.fs().set_status_for_repo_via_git_operation(
&dot_git_dir,
statuses.as_slice(),
);
} else {
client.fs().set_status_for_repo_via_working_copy_change(
&dot_git_dir,
statuses.as_slice(),
);
}
client
.fs()
.set_status_for_repo(&dot_git_dir, statuses.as_slice());
}
},
}
@ -1455,18 +1445,13 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
}
64..=100 => {
let file_paths = generate_file_paths(&repo_path, rng, client);
let statuses = file_paths
.into_iter()
.map(|path| (path, gen_status(rng)))
.collect::<Vec<_>>();
let git_operation = rng.gen::<bool>();
GitOperation::WriteGitStatuses {
repo_path,
statuses,
git_operation,
}
}
_ => unreachable!(),
@ -1605,15 +1590,24 @@ fn gen_file_name(rng: &mut StdRng) -> String {
}
fn gen_status(rng: &mut StdRng) -> FileStatus {
fn gen_status_code(rng: &mut StdRng) -> StatusCode {
match rng.gen_range(0..7) {
0 => StatusCode::Modified,
1 => StatusCode::TypeChanged,
2 => StatusCode::Added,
3 => StatusCode::Deleted,
4 => StatusCode::Renamed,
5 => StatusCode::Copied,
6 => StatusCode::Unmodified,
fn gen_tracked_status(rng: &mut StdRng) -> TrackedStatus {
match rng.gen_range(0..3) {
0 => TrackedStatus {
index_status: StatusCode::Unmodified,
worktree_status: StatusCode::Unmodified,
},
1 => TrackedStatus {
index_status: StatusCode::Modified,
worktree_status: StatusCode::Modified,
},
2 => TrackedStatus {
index_status: StatusCode::Added,
worktree_status: StatusCode::Modified,
},
3 => TrackedStatus {
index_status: StatusCode::Added,
worktree_status: StatusCode::Unmodified,
},
_ => unreachable!(),
}
}
@ -1627,17 +1621,12 @@ fn gen_status(rng: &mut StdRng) -> FileStatus {
}
}
match rng.gen_range(0..4) {
0 => FileStatus::Untracked,
1 => FileStatus::Ignored,
2 => FileStatus::Unmerged(UnmergedStatus {
match rng.gen_range(0..2) {
0 => FileStatus::Unmerged(UnmergedStatus {
first_head: gen_unmerged_status_code(rng),
second_head: gen_unmerged_status_code(rng),
}),
3 => FileStatus::Tracked(TrackedStatus {
index_status: gen_status_code(rng),
worktree_status: gen_status_code(rng),
}),
1 => FileStatus::Tracked(gen_tracked_status(rng)),
_ => unreachable!(),
}
}