feat: follow forward working-checkout changes in diffs

This commit is contained in:
Valdemar Lorenzen
2026-10-13 13:38:46 +02:00
parent b16232aa1d
commit bfdb7c544f
12 changed files with 1118 additions and 62 deletions
+346
View File
@@ -1305,6 +1305,352 @@ fn deferred_file_details_keep_frozen_checkout_and_unsaved_after() {
assert_eq!(again.after, details.after);
}
#[test]
fn git_working_after_advances_only_to_descendants_with_new_paths_and_unsaved_text() {
let fixture = Fixture::new();
let root = fixture.dir("follow").canonicalize().unwrap();
init_git(&root);
fs::write(root.join("file"), "base\n").unwrap();
fs::write(root.join("removed"), "remove me\n").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-m", "base"]);
let base = git(&root, &["rev-parse", "HEAD"]);
fs::write(root.join("file"), "original disk\n").unwrap();
let mut repository = open(&root);
let original = compare_loaded(
&mut repository,
&revision(&base, RepoKind::Git),
&working(RepoKind::Git),
&[],
)
.unwrap();
fs::write(root.join("file"), "later disk\n").unwrap();
assert!(
repository
.refresh_working_after(1, 2, &[])
.unwrap()
.is_none()
);
assert_eq!(repository.load_file(1, 0).unwrap().after, ["original disk"]);
fs::remove_file(root.join("removed")).unwrap();
fs::write(root.join("added"), "new file\n").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-m", "descendant"]);
let tip = git(&root, &["rev-parse", "HEAD"]);
let state = disk_state(&root);
assert!(
repository
.refresh_working_after(99, 2, &[])
.unwrap()
.is_none()
);
let mut refreshed = repository
.refresh_working_after(1, 2, &[(root.join("file"), "unsaved\n".into())])
.unwrap()
.unwrap();
assert_eq!(refreshed.before, original.before);
assert_eq!(refreshed.after, original.after);
for (index, file) in refreshed.files.iter_mut().enumerate() {
*file = repository.load_file(2, index).unwrap();
}
assert_eq!(changed(&refreshed, Path::new("file")).before, ["base"]);
assert_eq!(changed(&refreshed, Path::new("file")).after, ["unsaved"]);
assert_eq!(
changed(&refreshed, Path::new("added")).status,
FileStatus::Added
);
assert_eq!(
changed(&refreshed, Path::new("removed")).status,
FileStatus::Removed
);
assert!(refreshed.editable_paths.contains(Path::new("file")));
assert!(refreshed.editable_paths.contains(Path::new("added")));
assert!(!refreshed.editable_paths.contains(Path::new("removed")));
assert!(repository.load_file(1, 0).is_err());
assert!(
repository
.refresh_working_after(2, 3, &[])
.unwrap()
.is_none()
);
assert_eq!(disk_state(&root), state);
git(&root, &["checkout", "--detach", &base]);
assert!(
repository
.refresh_working_after(2, 3, &[])
.unwrap()
.is_none()
);
git(&root, &["checkout", "-b", "other"]);
fs::write(root.join("file"), "other branch\n").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-m", "unrelated to tip"]);
assert!(
repository
.refresh_working_after(2, 3, &[])
.unwrap()
.is_none()
);
let index = refreshed
.files
.iter()
.position(|file| file.path == Path::new("file"))
.unwrap();
assert_eq!(repository.load_file(2, index).unwrap().after, ["unsaved"]);
git(&root, &["checkout", "--detach", &tip]);
assert!(
repository
.refresh_working_after(2, 3, &[])
.unwrap()
.is_none()
);
}
#[test]
fn working_after_refresh_preserves_frozen_working_before_and_sources_on_failure() {
let fixture = Fixture::new();
let root = fixture.dir("frozen-follow").canonicalize().unwrap();
init_git(&root);
fs::write(root.join("file"), "base\n").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-m", "base"]);
let base = revision(&git(&root, &["rev-parse", "HEAD"]), RepoKind::Git);
fs::write(root.join("file"), "frozen before\n").unwrap();
let cancelled = Arc::new(AtomicBool::new(false));
let mut repository = Repository::open(&root, cancelled.clone()).unwrap();
repository
.compare(
10,
&working(RepoKind::Git),
&working(RepoKind::Git),
ComparisonMode::Snapshots,
&[(root.join("file"), "frozen after\n".into())],
)
.unwrap();
fs::write(root.join("file"), "next checkout\n").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-m", "next"]);
cancelled.store(true, Ordering::Release);
assert!(repository.refresh_working_after(10, 11, &[]).is_err());
cancelled.store(false, Ordering::Release);
assert_eq!(repository.load_file(10, 0).unwrap().after, ["frozen after"]);
fs::write(root.join("oversized"), vec![b'x'; 17 * 1024 * 1024]).unwrap();
assert!(repository.refresh_working_after(10, 11, &[]).is_err());
let original = repository.load_file(10, 0).unwrap();
assert_eq!(original.before, ["frozen before"]);
assert_eq!(original.after, ["frozen after"]);
assert!(repository.load_file(11, 0).is_err());
fs::remove_file(root.join("oversized")).unwrap();
let refreshed = repository
.refresh_working_after(10, 11, &[])
.unwrap()
.unwrap();
assert_eq!(refreshed.files.len(), 1);
let file = repository.load_file(11, 0).unwrap();
assert_eq!(file.before, ["frozen before"]);
assert_eq!(file.after, ["next checkout"]);
let tip = revision(&git(&root, &["rev-parse", "HEAD"]), RepoKind::Git);
repository
.compare(12, &base, &tip, ComparisonMode::Snapshots, &[])
.unwrap();
fs::write(root.join("file"), "newer checkout\n").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-m", "newer"]);
assert!(
repository
.refresh_working_after(12, 13, &[])
.unwrap()
.is_none()
);
assert_eq!(
repository.load_file(12, 0).unwrap().after,
["next checkout"]
);
}
#[test]
fn refresh_materialization_failure_restores_prepared_file_sources() {
let fixture = Fixture::new();
let root = fixture.dir("failed-materialization");
init_git(&root);
fs::write(root.join("a-file"), "frozen source\n").unwrap();
fs::write(root.join("z-large"), vec![b'x'; 17 * 1024 * 1024]).unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-m", "base"]);
let base = revision(&git(&root, &["rev-parse", "HEAD"]), RepoKind::Git);
fs::remove_file(root.join("a-file")).unwrap();
fs::remove_file(root.join("z-large")).unwrap();
let mut repository = open(&root);
let original = repository
.compare(
1,
&base,
&working(RepoKind::Git),
ComparisonMode::Snapshots,
&[],
)
.unwrap();
let index = original
.files
.iter()
.position(|file| file.path == Path::new("a-file"))
.unwrap();
fs::write(root.join("a-file"), "new checkout\n").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-m", "descendant"]);
assert!(repository.refresh_working_after(1, 2, &[]).is_err());
let file = repository.load_file(1, index).unwrap();
assert_eq!(file.before, ["frozen source"]);
assert!(file.after.is_empty());
assert_eq!(file.status, FileStatus::Removed);
assert!(repository.load_file(2, index).is_err());
}
#[test]
fn unborn_git_working_after_safely_advances_to_its_first_commit() {
let fixture = Fixture::new();
let root = fixture.dir("unborn");
init_git(&root);
let mut repository = open(&root);
repository
.compare(
1,
&working(RepoKind::Git),
&working(RepoKind::Git),
ComparisonMode::Snapshots,
&[],
)
.unwrap();
assert!(
repository
.refresh_working_after(1, 2, &[])
.unwrap()
.is_none()
);
fs::write(root.join("first"), "initial\n").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-m", "initial"]);
let comparison = repository
.refresh_working_after(1, 2, &[])
.unwrap()
.unwrap();
assert_eq!(comparison.files.len(), 1);
assert_eq!(repository.load_file(2, 0).unwrap().after, ["initial"]);
}
#[test]
fn jj_working_after_follows_changes_not_rewrites_and_rejects_other_history() {
for colocate in [false, true] {
let fixture = Fixture::new();
let root = fixture.dir("jj-follow").canonicalize().unwrap();
init_jj(&root, colocate);
fs::write(root.join("file"), "base\n").unwrap();
fs::write(root.join("removed"), "remove me\n").unwrap();
jj(&root, &["describe", "-m", "base"]);
let base = jj(&root, &["log", "--no-graph", "-r", "@", "-T", "commit_id"]);
jj(&root, &["new"]);
fs::write(root.join("file"), "original disk\n").unwrap();
let mut repository = open(&root);
compare_loaded(
&mut repository,
&revision(&base, RepoKind::Jujutsu),
&working(RepoKind::Jujutsu),
&[],
)
.unwrap();
fs::write(root.join("file"), "same-change rewrite\n").unwrap();
jj(&root, &["describe", "-m", "rewritten"]);
let state = disk_state(&root);
assert!(
repository
.refresh_working_after(1, 2, &[])
.unwrap()
.is_none()
);
assert_eq!(repository.load_file(1, 0).unwrap().after, ["original disk"]);
assert_eq!(disk_state(&root), state);
jj(&root, &["new"]);
fs::remove_file(root.join("removed")).unwrap();
fs::write(root.join("added"), "new file\n").unwrap();
let state = disk_state(&root);
let mut comparison = repository
.refresh_working_after(1, 2, &[(root.join("file"), "unsaved\n".into())])
.unwrap()
.unwrap();
for (index, file) in comparison.files.iter_mut().enumerate() {
*file = repository.load_file(2, index).unwrap();
}
assert_eq!(changed(&comparison, Path::new("file")).before, ["base"]);
assert_eq!(changed(&comparison, Path::new("file")).after, ["unsaved"]);
assert_eq!(
changed(&comparison, Path::new("added")).status,
FileStatus::Added
);
assert_eq!(
changed(&comparison, Path::new("removed")).status,
FileStatus::Removed
);
assert!(repository.load_file(1, 0).is_err());
assert_eq!(disk_state(&root), state);
assert!(
repository
.refresh_working_after(2, 3, &[])
.unwrap()
.is_none()
);
jj(&root, &["edit", &base]);
assert!(
repository
.refresh_working_after(2, 3, &[])
.unwrap()
.is_none()
);
jj(&root, &["new", &base]);
assert!(
repository
.refresh_working_after(2, 3, &[])
.unwrap()
.is_none()
);
let index = comparison
.files
.iter()
.position(|file| file.path == Path::new("file"))
.unwrap();
assert_eq!(repository.load_file(2, index).unwrap().after, ["unsaved"]);
repository
.compare(
10,
&working(RepoKind::Jujutsu),
&working(RepoKind::Jujutsu),
ComparisonMode::Snapshots,
&[(root.join("file"), "frozen after\n".into())],
)
.unwrap();
fs::write(root.join("file"), "new checkout\n").unwrap();
jj(&root, &["new"]);
let refreshed = repository
.refresh_working_after(10, 11, &[])
.unwrap()
.unwrap();
assert_eq!(refreshed.files.len(), 1);
let file = repository.load_file(11, 0).unwrap();
assert_eq!(file.before, ["base"]);
assert_eq!(file.after, ["new checkout"]);
let historical = revision(&base, RepoKind::Jujutsu);
repository
.compare(12, &historical, &historical, ComparisonMode::Snapshots, &[])
.unwrap();
jj(&root, &["new"]);
assert!(
repository
.refresh_working_after(12, 13, &[])
.unwrap()
.is_none()
);
}
}
#[test]
fn branch_changes_resolves_local_and_private_remote_ancestry_without_active_mutation() {
let fixture = Fixture::new();