feat: edit working-checkout comparisons with normal editor actions

This commit is contained in:
2026-10-11 02:13:16 +02:00
parent 704dce2539
commit 9a34669c08
16 changed files with 2277 additions and 647 deletions
+123 -15
View File
@@ -237,9 +237,15 @@ fn git_working_reads_disk_and_handles_ranges_ignored_files_binary_links_and_odd_
&& r.category == Category::Tags
&& r.target == RevisionTarget::Commit(id.clone())));
let comparison = repository
.compare(&revision(&id, RepoKind::Git), &working(RepoKind::Git))
.compare(&revision(&id, RepoKind::Git), &working(RepoKind::Git), &[])
.unwrap();
let text = changed(&comparison, Path::new("text"));
assert_eq!(comparison.root, fs::canonicalize(&root).unwrap());
assert!(comparison.editable_paths.contains(Path::new("text")));
assert!(comparison.editable_paths.contains(Path::new("empty-added")));
for path in ["binary", "link", "removed", "type-switch"] {
assert!(!comparison.editable_paths.contains(Path::new(path)));
}
assert_eq!(text.after, ["one", "two", "three"]);
assert_eq!(
(
@@ -338,15 +344,16 @@ fn git_working_reads_disk_and_handles_ranges_ignored_files_binary_links_and_odd_
assert_eq!(moved.before, moved.after);
assert!(moved.hunks.is_empty());
let reverse = repository
.compare(&working(RepoKind::Git), &revision(&id, RepoKind::Git))
.compare(&working(RepoKind::Git), &revision(&id, RepoKind::Git), &[])
.unwrap();
assert!(reverse.editable_paths.is_empty());
assert_eq!(
changed(&reverse, Path::new("added")).status,
FileStatus::Removed
);
assert!(
repository
.compare(&working(RepoKind::Git), &working(RepoKind::Git))
.compare(&working(RepoKind::Git), &working(RepoKind::Git), &[])
.unwrap()
.files
.is_empty()
@@ -433,17 +440,22 @@ fn git_remote_refs_and_annotated_tags_are_discovered_before_fetch_and_history_st
.success()
);
let comparison = repository
.compare(&revision(&local, RepoKind::Git), &remote)
.compare(
&revision(&local, RepoKind::Git),
&remote,
&[(active.join("file"), "unsaved local text\n".into())],
)
.unwrap();
assert!(comparison.editable_paths.is_empty());
assert_eq!(changed(&comparison, Path::new("file")).before, ["local"]);
assert_eq!(
changed(&comparison, Path::new("file")).after,
["remote tip"]
);
let comparison = repository.compare(&tag, &remote).unwrap();
let comparison = repository.compare(&tag, &remote, &[]).unwrap();
assert_eq!(changed(&comparison, Path::new("file")).before, ["ancestor"]);
let comparison = repository
.compare(&remote, &working(RepoKind::Git))
.compare(&remote, &working(RepoKind::Git), &[])
.unwrap();
assert_eq!(changed(&comparison, Path::new("file")).after, ["local"]);
let loaded = repository.fetch_remote("origin").unwrap();
@@ -457,7 +469,7 @@ fn git_remote_refs_and_annotated_tags_are_discovered_before_fetch_and_history_st
assert_eq!(historic.label, ancestor);
assert_eq!(historic.description, "remote ancestor needle");
let comparison = repository
.compare(historic, &revision(&local, RepoKind::Git))
.compare(historic, &revision(&local, RepoKind::Git), &[])
.unwrap();
assert_eq!(changed(&comparison, Path::new("file")).before, ["ancestor"]);
assert!(
@@ -530,7 +542,7 @@ fn jj_catalog_and_working_comparisons_prefer_jj_and_never_snapshot_or_merge_oper
.any(|r| r.label == "baseline" && r.category == Category::Bookmarks)
);
let comparison = repository
.compare(baseline, &working(RepoKind::Jujutsu))
.compare(baseline, &working(RepoKind::Jujutsu), &[])
.unwrap();
assert_eq!(changed(&comparison, Path::new("file")).after, ["disk"]);
assert_eq!(
@@ -652,7 +664,7 @@ fn jj_private_fetch_discovers_never_fetched_bookmark_and_searchable_remote_chang
.any(|r| r.target == RevisionTarget::Commit(ancestor.clone()))
);
let comparison = repository
.compare(&remote, &working(RepoKind::Jujutsu))
.compare(&remote, &working(RepoKind::Jujutsu), &[])
.unwrap();
assert_eq!(
changed(&comparison, Path::new("file")).before,
@@ -670,7 +682,7 @@ fn jj_private_fetch_discovers_never_fetched_bookmark_and_searchable_remote_chang
.unwrap();
assert!(historic.label.contains(&ancestor) && historic.label.contains(&change));
assert_eq!(historic.description, "remote historic needle");
let comparison = repository.compare(historic, &remote).unwrap();
let comparison = repository.compare(historic, &remote, &[]).unwrap();
assert_eq!(
changed(&comparison, Path::new("file")).before,
["remote ancestor"]
@@ -733,7 +745,11 @@ fn errors_are_explicit_and_do_not_modify_active_repository() {
);
assert!(
repository
.compare(&revision("--bad", RepoKind::Git), &working(RepoKind::Git))
.compare(
&revision("--bad", RepoKind::Git),
&working(RepoKind::Git),
&[]
)
.unwrap_err()
.contains("Invalid immutable commit")
);
@@ -741,7 +757,8 @@ fn errors_are_explicit_and_do_not_modify_active_repository() {
repository
.compare(
&revision(&"f".repeat(40), RepoKind::Git),
&working(RepoKind::Git)
&working(RepoKind::Git),
&[],
)
.unwrap_err()
.contains("unavailable")
@@ -752,7 +769,7 @@ fn errors_are_explicit_and_do_not_modify_active_repository() {
};
assert!(
repository
.compare(&loader, &revision(&id, RepoKind::Git))
.compare(&loader, &revision(&id, RepoKind::Git), &[])
.unwrap_err()
.contains("history loader")
);
@@ -792,7 +809,11 @@ fn unchanged_submodules_are_not_false_changes_and_changed_pointers_are_explicit(
let mut repository = open(&root);
assert!(
repository
.compare(&revision(&baseline, RepoKind::Git), &working(RepoKind::Git))
.compare(
&revision(&baseline, RepoKind::Git),
&working(RepoKind::Git),
&[]
)
.unwrap()
.files
.is_empty()
@@ -804,7 +825,11 @@ fn unchanged_submodules_are_not_false_changes_and_changed_pointers_are_explicit(
git(&root.join("nested"), &["checkout", "--detach", &second]);
let state = disk_state(&root);
let comparison = repository
.compare(&revision(&baseline, RepoKind::Git), &working(RepoKind::Git))
.compare(
&revision(&baseline, RepoKind::Git),
&working(RepoKind::Git),
&[],
)
.unwrap();
let submodule = changed(&comparison, Path::new("nested"));
assert_eq!(submodule.status, FileStatus::Modified);
@@ -876,6 +901,7 @@ fn multiple_remotes_retain_tag_only_history_even_when_tag_names_collide() {
.compare(
&revision(&ids[0], RepoKind::Git),
&revision(&ids[1], RepoKind::Git),
&[],
)
.unwrap();
assert_eq!(changed(&comparison, Path::new("file")).before, ["one"]);
@@ -989,6 +1015,7 @@ fn jj_conflicted_revisions_show_cli_projection_and_keep_ordinary_files_navigable
.compare(
&revision(&base, RepoKind::Jujutsu),
&revision(&merged, RepoKind::Jujutsu),
&[],
)
.unwrap();
let conflict = changed(&comparison, Path::new("conflict"));
@@ -1009,6 +1036,7 @@ fn jj_conflicted_revisions_show_cli_projection_and_keep_ordinary_files_navigable
.compare(
&revision(&merged, RepoKind::Jujutsu),
&working(RepoKind::Jujutsu),
&[],
)
.unwrap();
assert!(
@@ -1020,3 +1048,83 @@ fn jj_conflicted_revisions_show_cli_projection_and_keep_ordinary_files_navigable
);
assert_eq!(disk_state(&root), state);
}
#[test]
fn unsaved_overlays_are_working_after_only_and_cannot_follow_nonregular_paths() {
let fixture = Fixture::new();
let root = fs::canonicalize(fixture.dir("overlay")).unwrap();
init_git(&root);
fs::create_dir(root.join("sub")).unwrap();
fs::write(root.join("file"), "base\n").unwrap();
fs::write(root.join("sub/file"), "nested\n").unwrap();
fs::write(root.join("binary"), b"\0binary").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-m", "base"]);
let id = git(&root, &["rev-parse", "HEAD"]);
let outside = fixture.dir("outside");
fs::write(outside.join("file"), "outside\n").unwrap();
symlink(&outside, root.join("linked")).unwrap();
symlink("file", root.join("link")).unwrap();
let state = disk_state(&root);
let buffers = vec![
(root.join("file"), "unsaved\n".into()),
(root.join("binary"), "not binary\n".into()),
(root.join("missing"), "not on disk\n".into()),
(root.join("link"), "not a regular file\n".into()),
(root.join("linked/file"), "outside via link\n".into()),
(outside.join("file"), "outside\n".into()),
(root.join("sub/../../outside/file"), "escaped\n".into()),
(root.join(".git/config"), "metadata\n".into()),
];
let mut repository = open(&root.join("sub"));
let base = revision(&id, RepoKind::Git);
let checkout = working(RepoKind::Git);
let comparison = repository.compare(&base, &checkout, &buffers).unwrap();
assert_eq!(comparison.root, fs::canonicalize(&root).unwrap());
assert_eq!(changed(&comparison, Path::new("file")).after, ["unsaved"]);
assert!(comparison.editable_paths.contains(Path::new("file")));
for path in [
"binary",
"missing",
"link",
"linked",
"linked/file",
".git/config",
] {
assert!(!comparison.editable_paths.contains(Path::new(path)));
}
assert!(
!comparison
.files
.iter()
.any(|file| file.path == Path::new("binary")
|| file.path == Path::new("missing")
|| file.path == Path::new("linked/file"))
);
let immutable = repository.compare(&checkout, &base, &buffers).unwrap();
assert!(immutable.editable_paths.is_empty());
assert!(
!immutable
.files
.iter()
.any(|file| file.path == Path::new("file"))
);
assert!(
repository
.compare(&base, &base, &buffers)
.unwrap()
.files
.is_empty()
);
let same_checkout = repository.compare(&checkout, &checkout, &buffers).unwrap();
assert_eq!(changed(&same_checkout, Path::new("file")).before, ["base"]);
assert_eq!(
changed(&same_checkout, Path::new("file")).after,
["unsaved"]
);
assert_eq!(disk_state(&root), state);
assert_eq!(
fs::read_to_string(outside.join("file")).unwrap(),
"outside\n"
);
}