feat: default diffs to branch changes

This commit is contained in:
Valdemar Lorenzen
2026-10-13 10:41:37 +02:00
parent fafec2a6b0
commit 6c254f9b0d
10 changed files with 562 additions and 33 deletions
+202 -5
View File
@@ -16,7 +16,7 @@ use std::{
},
};
use vamp::{
diff::{Category, Comparison, FileStatus, RepoKind, Revision, RevisionTarget},
diff::{Category, Comparison, ComparisonMode, FileStatus, RepoKind, Revision, RevisionTarget},
repository::Repository,
};
@@ -134,7 +134,8 @@ fn compare_loaded(
after: &Revision,
buffers: &[(PathBuf, String)],
) -> Result<Comparison, String> {
let mut comparison = repository.compare(1, before, after, buffers)?;
let mut comparison =
repository.compare(1, before, after, ComparisonMode::Snapshots, buffers)?;
for (index, file) in comparison.files.iter_mut().enumerate() {
*file = repository.load_file(1, index)?;
if file
@@ -381,7 +382,13 @@ fn git_working_reads_disk_and_handles_ranges_ignored_files_binary_links_and_odd_
);
assert!(
repository
.compare(1, &working(RepoKind::Git), &working(RepoKind::Git), &[])
.compare(
1,
&working(RepoKind::Git),
&working(RepoKind::Git),
ComparisonMode::Snapshots,
&[]
)
.unwrap()
.files
.is_empty()
@@ -574,6 +581,40 @@ fn jj_catalog_and_working_comparisons_prefer_jj_and_never_snapshot_or_merge_oper
.iter()
.any(|f| f.path == Path::new("ignored-untracked"))
);
let synthetic_root = revision(&"0".repeat(40), RepoKind::Jujutsu);
let branch = repository
.compare(
20,
&synthetic_root,
baseline,
ComparisonMode::BranchChanges,
&[],
)
.unwrap();
assert!(branch.before.ends_with(&"0".repeat(40)));
assert!(
branch
.files
.iter()
.any(|file| file.path == Path::new("file"))
);
let reversed = repository
.compare(
21,
baseline,
&synthetic_root,
ComparisonMode::BranchChanges,
&[],
)
.unwrap();
assert!(reversed.files.is_empty());
assert!(
repository
.compare(22, baseline, baseline, ComparisonMode::BranchChanges, &[])
.unwrap()
.files
.is_empty()
);
assert_eq!(disk_state(&active), state);
}
}
@@ -657,6 +698,15 @@ fn jj_private_fetch_discovers_never_fetched_bookmark_and_searchable_remote_chang
.iter()
.any(|r| r.target == RevisionTarget::Commit(ancestor.clone()))
);
let local = catalog.entries.iter().find(|revision| {
matches!(&revision.target, RevisionTarget::Commit(id) if id.bytes().any(|byte| byte != b'0'))
}).unwrap();
let branch = repository
.compare(60, local, &remote, ComparisonMode::BranchChanges, &[])
.unwrap();
assert!(branch.before.ends_with(&"0".repeat(40)));
assert_eq!(branch.files.len(), 1);
assert_eq!(repository.load_file(60, 0).unwrap().after, ["remote tip"]);
let comparison =
compare_loaded(&mut repository, &remote, &working(RepoKind::Jujutsu), &[]).unwrap();
assert_eq!(
@@ -682,6 +732,14 @@ fn jj_private_fetch_discovers_never_fetched_bookmark_and_searchable_remote_chang
changed(&comparison, Path::new("file")).after,
["remote tip"]
);
let branch = repository
.compare(61, historic, &remote, ComparisonMode::BranchChanges, &[])
.unwrap();
assert!(branch.before.ends_with(&ancestor));
assert_eq!(
repository.load_file(61, 0).unwrap().before,
["remote ancestor"]
);
let store = active.join(".jj/repo/store");
let active_store =
fs::canonicalize(store.join(fs::read_to_string(store.join("git_target")).unwrap().trim()))
@@ -747,6 +805,7 @@ fn errors_are_explicit_and_do_not_modify_active_repository() {
1,
&revision("--bad", RepoKind::Git),
&working(RepoKind::Git),
ComparisonMode::Snapshots,
&[]
)
.unwrap_err()
@@ -758,6 +817,7 @@ fn errors_are_explicit_and_do_not_modify_active_repository() {
1,
&revision(&"f".repeat(40), RepoKind::Git),
&working(RepoKind::Git),
ComparisonMode::Snapshots,
&[],
)
.unwrap_err()
@@ -769,7 +829,13 @@ fn errors_are_explicit_and_do_not_modify_active_repository() {
};
assert!(
repository
.compare(1, &loader, &revision(&id, RepoKind::Git), &[])
.compare(
1,
&loader,
&revision(&id, RepoKind::Git),
ComparisonMode::Snapshots,
&[]
)
.unwrap_err()
.contains("history loader")
);
@@ -813,6 +879,7 @@ fn unchanged_submodules_are_not_false_changes_and_changed_pointers_are_explicit(
1,
&revision(&baseline, RepoKind::Git),
&working(RepoKind::Git),
ComparisonMode::Snapshots,
&[]
)
.unwrap()
@@ -1033,6 +1100,29 @@ fn jj_conflicted_revisions_show_cli_projection_and_keep_ordinary_files_navigable
assert_eq!(ordinary.before, ["old"]);
assert_eq!(ordinary.after, ["left ordinary"]);
assert!(ordinary.notice.is_none());
let branch = repository
.compare(
30,
&revision(&base, RepoKind::Jujutsu),
&revision(&merged, RepoKind::Jujutsu),
ComparisonMode::BranchChanges,
&[],
)
.unwrap();
let index = branch
.files
.iter()
.position(|file| file.path == Path::new("conflict"))
.unwrap();
let conflict = repository.load_file(30, index).unwrap();
assert_eq!(conflict.after, expected);
assert!(
conflict
.notice
.as_deref()
.unwrap()
.contains("conflict projection")
);
let working_comparison = compare_loaded(
&mut repository,
&revision(&merged, RepoKind::Jujutsu),
@@ -1112,7 +1202,7 @@ fn unsaved_overlays_are_working_after_only_and_cannot_follow_nonregular_paths()
);
assert!(
repository
.compare(1, &base, &base, &buffers)
.compare(1, &base, &base, ComparisonMode::Snapshots, &buffers)
.unwrap()
.files
.is_empty()
@@ -1152,6 +1242,7 @@ fn immutable_catalog_skips_unchanged_blobs_and_prepares_only_requested_details()
100,
&revision(&before, RepoKind::Git),
&revision(&after, RepoKind::Git),
ComparisonMode::Snapshots,
&[],
)
.unwrap();
@@ -1176,6 +1267,7 @@ fn immutable_catalog_skips_unchanged_blobs_and_prepares_only_requested_details()
101,
&revision(&after, RepoKind::Git),
&revision(&after, RepoKind::Git),
ComparisonMode::Snapshots,
&[],
)
.unwrap();
@@ -1198,6 +1290,7 @@ fn deferred_file_details_keep_frozen_checkout_and_unsaved_after() {
50,
&working(RepoKind::Git),
&working(RepoKind::Git),
ComparisonMode::Snapshots,
&[(root.join("file"), "unsaved after\n".into())],
)
.unwrap();
@@ -1211,3 +1304,107 @@ fn deferred_file_details_keep_frozen_checkout_and_unsaved_after() {
assert_eq!(again.before, details.before);
assert_eq!(again.after, details.after);
}
#[test]
fn branch_changes_resolves_local_and_private_remote_ancestry_without_active_mutation() {
let fixture = Fixture::new();
let active = fixture.dir("active\nquoted\"\\");
init_git(&active);
fs::write(active.join("common"), "base\n").unwrap();
git(&active, &["add", "."]);
git(&active, &["commit", "-m", "base"]);
let base = git(&active, &["rev-parse", "HEAD"]);
let source = fixture.0.join("source");
git(
&fixture.0,
&["clone", active.to_str().unwrap(), source.to_str().unwrap()],
);
git(&source, &["config", "user.name", "Diff Test"]);
git(&source, &["config", "user.email", "diff@example.invalid"]);
fs::write(source.join("remote-only"), "remote\n").unwrap();
git(&source, &["add", "."]);
git(&source, &["commit", "-m", "remote"]);
fs::write(active.join("local-only"), "local\n").unwrap();
git(&active, &["add", "."]);
git(&active, &["commit", "-m", "local"]);
let local = revision(&git(&active, &["rev-parse", "HEAD"]), RepoKind::Git);
git(
&active,
&["remote", "add", "origin", source.to_str().unwrap()],
);
let state = disk_state(&active);
let mut repository = open(&active);
let catalog = repository.list_remote("origin").unwrap();
let remote = catalog
.entries
.iter()
.find(|r| matches!(r.target, RevisionTarget::Remote { .. }))
.unwrap();
let branch = repository
.compare(40, &local, remote, ComparisonMode::BranchChanges, &[])
.unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].path, Path::new("remote-only"));
assert!(branch.before.ends_with(&base));
assert_eq!(repository.load_file(40, 0).unwrap().after, ["remote"]);
let reverse = repository
.compare(41, remote, &local, ComparisonMode::BranchChanges, &[])
.unwrap();
assert_eq!(reverse.files.len(), 1);
assert_eq!(reverse.files[0].path, Path::new("local-only"));
assert_eq!(repository.load_file(41, 0).unwrap().after, ["local"]);
assert!(
repository
.compare(42, &local, &local, ComparisonMode::BranchChanges, &[])
.unwrap()
.files
.is_empty()
);
assert_eq!(disk_state(&active), state);
}
#[test]
fn branch_changes_rejects_missing_and_multiple_merge_bases_without_snapshot_fallback() {
let fixture = Fixture::new();
let root = fixture.dir("graphs");
init_git(&root);
fs::write(root.join("file"), "base\n").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-m", "base"]);
let base = git(&root, &["rev-parse", "HEAD"]);
let tree = git(&root, &["rev-parse", "HEAD^{tree}"]);
let left = git(&root, &["commit-tree", &tree, "-p", &base, "-m", "left"]);
let right = git(&root, &["commit-tree", &tree, "-p", &base, "-m", "right"]);
let one = git(
&root,
&["commit-tree", &tree, "-p", &left, "-p", &right, "-m", "one"],
);
let two = git(
&root,
&["commit-tree", &tree, "-p", &right, "-p", &left, "-m", "two"],
);
let unrelated = git(&root, &["commit-tree", &tree, "-m", "unrelated"]);
let state = disk_state(&root);
let mut repository = open(&root);
for (before, after, message) in [
(&base, &unrelated, "no merge base"),
(&one, &two, "multiple merge bases"),
] {
let before = revision(before, RepoKind::Git);
let after = revision(after, RepoKind::Git);
assert!(
repository
.compare(50, &before, &after, ComparisonMode::BranchChanges, &[])
.unwrap_err()
.contains(message)
);
assert!(
repository
.compare(51, &before, &after, ComparisonMode::Snapshots, &[])
.unwrap()
.files
.is_empty()
);
}
assert_eq!(disk_state(&root), state);
}