// Copyright 2023 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use futures::executor::block_on; use futures::StreamExt; use itertools::Itertools; use jj_lib::backend::{FileId, MergedTreeId, TreeValue}; use jj_lib::files::MergeResult; use jj_lib::matchers::{EverythingMatcher, FilesMatcher, Matcher, PrefixMatcher}; use jj_lib::merge::{Merge, MergeBuilder}; use jj_lib::merged_tree::{ MergedTree, MergedTreeBuilder, MergedTreeVal, TreeDiffIterator, TreeDiffStreamImpl, }; use jj_lib::repo::Repo; use jj_lib::repo_path::{RepoPath, RepoPathComponent}; use jj_lib::tree::merge_trees; use pretty_assertions::assert_eq; use testutils::{create_single_tree, write_file, TestRepo}; fn file_value(file_id: &FileId) -> TreeValue { TreeValue::File { id: file_id.clone(), executable: false, } } fn diff_stream_equals_iter(tree1: &MergedTree, tree2: &MergedTree, matcher: &dyn Matcher) { let iter_diff: Vec<_> = TreeDiffIterator::new(tree1.clone(), tree2.clone(), matcher) .map(|(path, diff)| (path, diff.unwrap())) .collect(); let max_concurrent_reads = 10; let stream_diff: Vec<_> = block_on( TreeDiffStreamImpl::new(tree1.clone(), tree2.clone(), matcher, max_concurrent_reads) .map(|(path, diff)| (path, diff.unwrap())) .collect(), ); assert_eq!(stream_diff, iter_diff); } #[test] fn test_from_legacy_tree() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let store = repo.store(); let mut tree_builder = store.tree_builder(repo.store().empty_tree_id().clone()); // file1: regular file without conflicts let file1_path = RepoPath::from_internal_string("no_conflict"); let file1_id = write_file(store.as_ref(), file1_path, "foo"); tree_builder.set(file1_path.to_owned(), file_value(&file1_id)); // file2: 3-way conflict let file2_path = RepoPath::from_internal_string("3way"); let file2_v1_id = write_file(store.as_ref(), file2_path, "file2_v1"); let file2_v2_id = write_file(store.as_ref(), file2_path, "file2_v2"); let file2_v3_id = write_file(store.as_ref(), file2_path, "file2_v3"); let file2_conflict = Merge::from_removes_adds( vec![Some(file_value(&file2_v1_id))], vec![ Some(file_value(&file2_v2_id)), Some(file_value(&file2_v3_id)), ], ); let file2_conflict_id = store.write_conflict(file2_path, &file2_conflict).unwrap(); tree_builder.set( file2_path.to_owned(), TreeValue::Conflict(file2_conflict_id), ); // file3: modify/delete conflict let file3_path = RepoPath::from_internal_string("modify_delete"); let file3_v1_id = write_file(store.as_ref(), file3_path, "file3_v1"); let file3_v2_id = write_file(store.as_ref(), file3_path, "file3_v2"); let file3_conflict = Merge::from_removes_adds( vec![Some(file_value(&file3_v1_id))], vec![Some(file_value(&file3_v2_id)), None], ); let file3_conflict_id = store.write_conflict(file3_path, &file3_conflict).unwrap(); tree_builder.set( file3_path.to_owned(), TreeValue::Conflict(file3_conflict_id), ); // file4: add/add conflict let file4_path = RepoPath::from_internal_string("add_add"); let file4_v1_id = write_file(store.as_ref(), file4_path, "file4_v1"); let file4_v2_id = write_file(store.as_ref(), file4_path, "file4_v2"); let file4_conflict = Merge::from_removes_adds( vec![None], vec![ Some(file_value(&file4_v1_id)), Some(file_value(&file4_v2_id)), ], ); let file4_conflict_id = store.write_conflict(file4_path, &file4_conflict).unwrap(); tree_builder.set( file4_path.to_owned(), TreeValue::Conflict(file4_conflict_id), ); // file5: 5-way conflict let file5_path = RepoPath::from_internal_string("5way"); let file5_v1_id = write_file(store.as_ref(), file5_path, "file5_v1"); let file5_v2_id = write_file(store.as_ref(), file5_path, "file5_v2"); let file5_v3_id = write_file(store.as_ref(), file5_path, "file5_v3"); let file5_v4_id = write_file(store.as_ref(), file5_path, "file5_v4"); let file5_v5_id = write_file(store.as_ref(), file5_path, "file5_v5"); let file5_conflict = Merge::from_removes_adds( vec![ Some(file_value(&file5_v1_id)), Some(file_value(&file5_v2_id)), ], vec![ Some(file_value(&file5_v3_id)), Some(file_value(&file5_v4_id)), Some(file_value(&file5_v5_id)), ], ); let file5_conflict_id = store.write_conflict(file5_path, &file5_conflict).unwrap(); tree_builder.set( file5_path.to_owned(), TreeValue::Conflict(file5_conflict_id), ); // dir1: directory without conflicts let dir1_basename = RepoPathComponent::new("dir1"); let dir1_filename = &RepoPath::root() .join(dir1_basename) .join(RepoPathComponent::new("file")); let dir1_filename_id = write_file(store.as_ref(), dir1_filename, "file5_v2"); tree_builder.set(dir1_filename.to_owned(), file_value(&dir1_filename_id)); let tree_id = tree_builder.write_tree(); let tree = store.get_tree(RepoPath::root(), &tree_id).unwrap(); let merged_tree = MergedTree::from_legacy_tree(tree.clone()).unwrap(); assert_eq!( merged_tree.value(RepoPathComponent::new("missing")), MergedTreeVal::Resolved(None) ); // file1: regular file without conflicts assert_eq!( merged_tree.value(file1_path.components().next().unwrap()), MergedTreeVal::Resolved(Some(&TreeValue::File { id: file1_id.clone(), executable: false, })) ); // file2: 3-way conflict assert_eq!( merged_tree.value(file2_path.components().next().unwrap()), MergedTreeVal::Conflict(Merge::from_removes_adds( vec![Some(file_value(&file2_v1_id)), None], vec![ Some(file_value(&file2_v2_id)), Some(file_value(&file2_v3_id)), None, ], )) ); // file3: modify/delete conflict assert_eq!( merged_tree.value(file3_path.components().next().unwrap()), MergedTreeVal::Conflict(Merge::from_removes_adds( vec![Some(file_value(&file3_v1_id)), None], vec![Some(file_value(&file3_v2_id)), None, None], )) ); // file4: add/add conflict assert_eq!( merged_tree.value(file4_path.components().next().unwrap()), MergedTreeVal::Conflict(Merge::from_removes_adds( vec![None, None], vec![ Some(file_value(&file4_v1_id)), Some(file_value(&file4_v2_id)), None ], )) ); // file5: 5-way conflict assert_eq!( merged_tree.value(file5_path.components().next().unwrap()), MergedTreeVal::Conflict(Merge::from_removes_adds( vec![ Some(file_value(&file5_v1_id)), Some(file_value(&file5_v2_id)), ], vec![ Some(file_value(&file5_v3_id)), Some(file_value(&file5_v4_id)), Some(file_value(&file5_v5_id)), ], )) ); // file6: directory without conflicts assert_eq!( merged_tree.value(dir1_basename), MergedTreeVal::Resolved(tree.value(dir1_basename)) ); // Also test that MergedTreeBuilder can create the same tree by starting from an // empty legacy tree. let mut tree_builder = MergedTreeBuilder::new(store.empty_merged_tree_id()); for (path, value) in tree.entries() { tree_builder.set_or_remove(path, Merge::normal(value)); } let recreated_legacy_id = tree_builder.write_tree(store).unwrap(); assert_eq!(recreated_legacy_id, MergedTreeId::Legacy(tree_id.clone())); // Create the merged tree by starting from an empty merged tree and adding // entries from the merged tree we created before let empty_merged_id_builder: MergeBuilder<_> = std::iter::repeat(store.empty_tree_id()) .take(5) .cloned() .collect(); let empty_merged_id = MergedTreeId::Merge(empty_merged_id_builder.build()); let mut tree_builder = MergedTreeBuilder::new(empty_merged_id); for (path, value) in merged_tree.entries() { tree_builder.set_or_remove(path, value); } let recreated_merged_id = tree_builder.write_tree(store).unwrap(); assert_eq!(recreated_merged_id, merged_tree.id()); // Create the merged tree by adding the same (variable-arity) entries as we // added to the single-tree TreeBuilder. let mut tree_builder = MergedTreeBuilder::new(MergedTreeId::Merge(Merge::resolved( store.empty_tree_id().clone(), ))); // Add the entries out of order, so we test both increasing and reducing the // arity (going up from 1-way to 3-way to 5-way, then to 3-way again) tree_builder.set_or_remove(file1_path.to_owned(), Merge::normal(file_value(&file1_id))); tree_builder.set_or_remove(file2_path.to_owned(), file2_conflict); tree_builder.set_or_remove(file5_path.to_owned(), file5_conflict); tree_builder.set_or_remove(file3_path.to_owned(), file3_conflict); tree_builder.set_or_remove(file4_path.to_owned(), file4_conflict); tree_builder.set_or_remove( dir1_filename.to_owned(), Merge::normal(file_value(&dir1_filename_id)), ); let recreated_merged_id = tree_builder.write_tree(store).unwrap(); assert_eq!(recreated_merged_id, merged_tree.id()); } #[test] fn test_path_value_and_entries() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; // Create a MergedTree let resolved_file_path = RepoPath::from_internal_string("dir1/subdir/resolved"); let resolved_dir_path = &resolved_file_path.parent().unwrap(); let conflicted_file_path = RepoPath::from_internal_string("dir2/conflicted"); let missing_path = RepoPath::from_internal_string("dir2/missing_file"); let modify_delete_path = RepoPath::from_internal_string("dir2/modify_delete"); let file_dir_conflict_path = RepoPath::from_internal_string("file_dir"); let file_dir_conflict_sub_path = RepoPath::from_internal_string("file_dir/file"); let tree1 = create_single_tree( repo, &[ (resolved_file_path, "unchanged"), (conflicted_file_path, "1"), (modify_delete_path, "1"), (file_dir_conflict_path, "1"), ], ); let tree2 = create_single_tree( repo, &[ (resolved_file_path, "unchanged"), (conflicted_file_path, "2"), (modify_delete_path, "2"), (file_dir_conflict_path, "2"), ], ); let tree3 = create_single_tree( repo, &[ (resolved_file_path, "unchanged"), (conflicted_file_path, "3"), // No modify_delete_path in this tree (file_dir_conflict_sub_path, "1"), ], ); let merged_tree = MergedTree::Merge(Merge::from_removes_adds( vec![tree1.clone()], vec![tree2.clone(), tree3.clone()], )); // Get the root tree assert_eq!( merged_tree.path_value(RepoPath::root()), Merge::from_removes_adds( vec![Some(TreeValue::Tree(tree1.id().clone()))], vec![ Some(TreeValue::Tree(tree2.id().clone())), Some(TreeValue::Tree(tree3.id().clone())), ] ) ); // Get file path without conflict assert_eq!( merged_tree.path_value(resolved_file_path), Merge::resolved(tree1.path_value(resolved_file_path)), ); // Get directory path without conflict assert_eq!( merged_tree.path_value(resolved_dir_path), Merge::resolved(tree1.path_value(resolved_dir_path)), ); // Get missing path assert_eq!(merged_tree.path_value(missing_path), Merge::absent()); // Get modify/delete conflict (some None values) assert_eq!( merged_tree.path_value(modify_delete_path), Merge::from_removes_adds( vec![tree1.path_value(modify_delete_path)], vec![tree2.path_value(modify_delete_path), None] ), ); // Get file/dir conflict path assert_eq!( merged_tree.path_value(file_dir_conflict_path), Merge::from_removes_adds( vec![tree1.path_value(file_dir_conflict_path)], vec![ tree2.path_value(file_dir_conflict_path), tree3.path_value(file_dir_conflict_path) ] ), ); // Get file inside file/dir conflict // There is a conflict in the parent directory, but this file is still resolved assert_eq!( merged_tree.path_value(file_dir_conflict_sub_path), Merge::resolved(tree3.path_value(file_dir_conflict_sub_path)), ); // Test entries() let actual_entries = merged_tree.entries().collect_vec(); // missing_path, resolved_dir_path, and file_dir_conflict_sub_path should not // appear let expected_entries = [ resolved_file_path, conflicted_file_path, modify_delete_path, file_dir_conflict_path, ] .iter() .sorted() .map(|&path| (path.to_owned(), merged_tree.path_value(path))) .collect_vec(); assert_eq!(actual_entries, expected_entries); let actual_entries = merged_tree .entries_matching(&FilesMatcher::new([ &resolved_file_path, &modify_delete_path, &file_dir_conflict_sub_path, ])) .collect_vec(); let expected_entries = [resolved_file_path, modify_delete_path] .iter() .sorted() .map(|&path| (path.to_owned(), merged_tree.path_value(path))) .collect_vec(); assert_eq!(actual_entries, expected_entries); } #[test] fn test_resolve_success() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let unchanged_path = RepoPath::from_internal_string("unchanged"); let trivial_file_path = RepoPath::from_internal_string("trivial-file"); let trivial_hunk_path = RepoPath::from_internal_string("trivial-hunk"); let both_added_dir_path = RepoPath::from_internal_string("added-dir"); let both_added_dir_file1_path = &both_added_dir_path.join(RepoPathComponent::new("file1")); let both_added_dir_file2_path = &both_added_dir_path.join(RepoPathComponent::new("file2")); let emptied_dir_path = RepoPath::from_internal_string("to-become-empty"); let emptied_dir_file1_path = &emptied_dir_path.join(RepoPathComponent::new("file1")); let emptied_dir_file2_path = &emptied_dir_path.join(RepoPathComponent::new("file2")); let base1 = create_single_tree( repo, &[ (unchanged_path, "unchanged"), (trivial_file_path, "base1"), (trivial_hunk_path, "line1\nline2\nline3\n"), (emptied_dir_file1_path, "base1"), (emptied_dir_file2_path, "base1"), ], ); let side1 = create_single_tree( repo, &[ (unchanged_path, "unchanged"), (trivial_file_path, "base1"), (trivial_hunk_path, "line1 side1\nline2\nline3\n"), (both_added_dir_file1_path, "side1"), (emptied_dir_file2_path, "base1"), ], ); let side2 = create_single_tree( repo, &[ (unchanged_path, "unchanged"), (trivial_file_path, "side2"), (trivial_hunk_path, "line1\nline2\nline3 side2\n"), (both_added_dir_file2_path, "side2"), (emptied_dir_file1_path, "base1"), ], ); let expected = create_single_tree( repo, &[ (unchanged_path, "unchanged"), (trivial_file_path, "side2"), (trivial_hunk_path, "line1 side1\nline2\nline3 side2\n"), (both_added_dir_file1_path, "side1"), (both_added_dir_file2_path, "side2"), ], ); let tree = MergedTree::new(Merge::from_removes_adds(vec![base1], vec![side1, side2])); let resolved = tree.resolve().unwrap(); let resolved_tree = resolved.as_resolved().unwrap().clone(); assert_eq!( resolved_tree, expected, "actual entries: {:#?}, expected entries {:#?}", resolved_tree.entries().collect_vec(), expected.entries().collect_vec() ); } #[test] fn test_resolve_root_becomes_empty() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let store = repo.store(); let path1 = RepoPath::from_internal_string("dir1/file"); let path2 = RepoPath::from_internal_string("dir2/file"); let base1 = create_single_tree(repo, &[(path1, "base1"), (path2, "base1")]); let side1 = create_single_tree(repo, &[(path2, "base1")]); let side2 = create_single_tree(repo, &[(path1, "base1")]); let tree = MergedTree::new(Merge::from_removes_adds(vec![base1], vec![side1, side2])); let resolved = tree.resolve().unwrap(); assert_eq!(resolved.as_resolved().unwrap().id(), store.empty_tree_id()); } #[test] fn test_resolve_with_conflict() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; // The trivial conflict should be resolved but the non-trivial should not (and // cannot) let trivial_path = RepoPath::from_internal_string("dir1/trivial"); let conflict_path = RepoPath::from_internal_string("dir2/file_conflict"); let base1 = create_single_tree(repo, &[(trivial_path, "base1"), (conflict_path, "base1")]); let side1 = create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "side1")]); let side2 = create_single_tree(repo, &[(trivial_path, "base1"), (conflict_path, "side2")]); let expected_base1 = create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "base1")]); let expected_side1 = create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "side1")]); let expected_side2 = create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "side2")]); let tree = MergedTree::new(Merge::from_removes_adds(vec![base1], vec![side1, side2])); let resolved_tree = tree.resolve().unwrap(); assert_eq!( resolved_tree, Merge::from_removes_adds(vec![expected_base1], vec![expected_side1, expected_side2]) ) } #[test] fn test_resolve_with_conflict_containing_empty_subtree() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; // Since "dir" in side2 is absent, the root tree should be empty as well. // If it were added to the root tree, side2.id() would differ. let conflict_path = RepoPath::from_internal_string("dir/file_conflict"); let base1 = create_single_tree(repo, &[(conflict_path, "base1")]); let side1 = create_single_tree(repo, &[(conflict_path, "side1")]); let side2 = create_single_tree(repo, &[]); let original_tree = Merge::from_removes_adds(vec![base1], vec![side1, side2]); let tree = MergedTree::new(original_tree.clone()); let resolved_tree = tree.resolve().unwrap(); assert_eq!(resolved_tree, original_tree); } #[test] fn test_conflict_iterator() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let unchanged_path = RepoPath::from_internal_string("dir/subdir/unchanged"); let trivial_path = RepoPath::from_internal_string("dir/subdir/trivial"); let trivial_hunk_path = RepoPath::from_internal_string("dir/non_trivial"); let file_conflict_path = RepoPath::from_internal_string("dir/subdir/file_conflict"); let modify_delete_path = RepoPath::from_internal_string("dir/subdir/modify_delete"); let same_add_path = RepoPath::from_internal_string("dir/subdir/same_add"); let different_add_path = RepoPath::from_internal_string("dir/subdir/different_add"); let dir_file_path = RepoPath::from_internal_string("dir/subdir/dir_file"); let added_dir_path = RepoPath::from_internal_string("dir/new_dir"); let modify_delete_dir_path = RepoPath::from_internal_string("dir/modify_delete_dir"); let base1 = create_single_tree( repo, &[ (unchanged_path, "unchanged"), (trivial_path, "base"), (trivial_hunk_path, "line1\nline2\nline3\n"), (file_conflict_path, "base"), (modify_delete_path, "base"), // no same_add_path // no different_add_path (dir_file_path, "base"), // no added_dir_path ( &modify_delete_dir_path.join(RepoPathComponent::new("base")), "base", ), ], ); let side1 = create_single_tree( repo, &[ (unchanged_path, "unchanged"), (trivial_path, "base"), (file_conflict_path, "side1"), (trivial_hunk_path, "line1 side1\nline2\nline3\n"), (modify_delete_path, "modified"), (same_add_path, "same"), (different_add_path, "side1"), (dir_file_path, "side1"), ( &added_dir_path.join(RepoPathComponent::new("side1")), "side1", ), ( &modify_delete_dir_path.join(RepoPathComponent::new("side1")), "side1", ), ], ); let side2 = create_single_tree( repo, &[ (unchanged_path, "unchanged"), (trivial_path, "side2"), (file_conflict_path, "side2"), (trivial_hunk_path, "line1\nline2\nline3 side2\n"), // no modify_delete_path (same_add_path, "same"), (different_add_path, "side2"), (&dir_file_path.join(RepoPathComponent::new("dir")), "new"), ( &added_dir_path.join(RepoPathComponent::new("side2")), "side2", ), // no modify_delete_dir_path ], ); let tree = MergedTree::new(Merge::from_removes_adds( vec![base1.clone()], vec![side1.clone(), side2.clone()], )); let conflicts = tree.conflicts().collect_vec(); let conflict_at = |path: &RepoPath| { Merge::from_removes_adds( vec![base1.path_value(path)], vec![side1.path_value(path), side2.path_value(path)], ) }; // We initially also get a conflict in trivial_hunk_path because we had // forgotten to resolve conflicts assert_eq!( conflicts, vec![ (trivial_hunk_path.to_owned(), conflict_at(trivial_hunk_path)), ( different_add_path.to_owned(), conflict_at(different_add_path) ), (dir_file_path.to_owned(), conflict_at(dir_file_path)), ( file_conflict_path.to_owned(), conflict_at(file_conflict_path) ), ( modify_delete_path.to_owned(), conflict_at(modify_delete_path) ), ] ); // After we resolve conflicts, there are only non-trivial conflicts left let tree = MergedTree::Merge(tree.resolve().unwrap()); let conflicts = tree.conflicts().collect_vec(); assert_eq!( conflicts, vec![ ( different_add_path.to_owned(), conflict_at(different_add_path) ), (dir_file_path.to_owned(), conflict_at(dir_file_path)), ( file_conflict_path.to_owned(), conflict_at(file_conflict_path) ), ( modify_delete_path.to_owned(), conflict_at(modify_delete_path) ), ] ); let merged_legacy_tree = merge_trees(&side1, &base1, &side2).unwrap(); let legacy_conflicts = MergedTree::legacy(merged_legacy_tree) .conflicts() .collect_vec(); assert_eq!(legacy_conflicts, conflicts); } #[test] fn test_conflict_iterator_higher_arity() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let two_sided_path = RepoPath::from_internal_string("dir/2-sided"); let three_sided_path = RepoPath::from_internal_string("dir/3-sided"); let base1 = create_single_tree( repo, &[(two_sided_path, "base1"), (three_sided_path, "base1")], ); let base2 = create_single_tree( repo, &[(two_sided_path, "base2"), (three_sided_path, "base2")], ); let side1 = create_single_tree( repo, &[(two_sided_path, "side1"), (three_sided_path, "side1")], ); let side2 = create_single_tree( repo, &[(two_sided_path, "base1"), (three_sided_path, "side2")], ); let side3 = create_single_tree( repo, &[(two_sided_path, "side3"), (three_sided_path, "side3")], ); let tree = MergedTree::new(Merge::from_removes_adds( vec![base1.clone(), base2.clone()], vec![side1.clone(), side2.clone(), side3.clone()], )); let conflicts = tree.conflicts().collect_vec(); let conflict_at = |path: &RepoPath| { Merge::from_removes_adds( vec![base1.path_value(path), base2.path_value(path)], vec![ side1.path_value(path), side2.path_value(path), side3.path_value(path), ], ) }; // Both paths have the full, unsimplified conflict (3-sided) assert_eq!( conflicts, vec![ (two_sided_path.to_owned(), conflict_at(two_sided_path)), (three_sided_path.to_owned(), conflict_at(three_sided_path)) ] ); // Iterating over conflicts in a legacy tree yields the simplified conflict at // each path let merged_legacy_tree = merge_trees(&side1, &base1, &side2).unwrap(); let merged_legacy_tree = merge_trees(&merged_legacy_tree, &base2, &side3).unwrap(); let legacy_conflicts = MergedTree::legacy(merged_legacy_tree) .conflicts() .collect_vec(); assert_eq!( legacy_conflicts, vec![ ( two_sided_path.to_owned(), Merge::from_removes_adds( vec![base2.path_value(two_sided_path)], vec![ side1.path_value(two_sided_path), side3.path_value(two_sided_path), ], ) ), (three_sided_path.to_owned(), conflict_at(three_sided_path)) ] ); } /// Diff two resolved trees #[test] fn test_diff_resolved() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let clean_path = RepoPath::from_internal_string("dir1/file"); let modified_path = RepoPath::from_internal_string("dir2/file"); let removed_path = RepoPath::from_internal_string("dir3/file"); let added_path = RepoPath::from_internal_string("dir4/file"); let before = create_single_tree( repo, &[ (clean_path, "clean"), (modified_path, "before"), (removed_path, "before"), ], ); let after = create_single_tree( repo, &[ (clean_path, "clean"), (modified_path, "after"), (added_path, "after"), ], ); let before_merged = MergedTree::new(Merge::resolved(before.clone())); let after_merged = MergedTree::new(Merge::resolved(after.clone())); let diff = before_merged .diff(&after_merged, &EverythingMatcher) .map(|(path, diff)| (path, diff.unwrap())) .collect_vec(); assert_eq!(diff.len(), 3); assert_eq!( diff[0].clone(), ( modified_path.to_owned(), ( Merge::resolved(before.path_value(modified_path)), Merge::resolved(after.path_value(modified_path)) ), ) ); assert_eq!( diff[1].clone(), ( removed_path.to_owned(), ( Merge::resolved(before.path_value(removed_path)), Merge::absent() ), ) ); assert_eq!( diff[2].clone(), ( added_path.to_owned(), ( Merge::absent(), Merge::resolved(after.path_value(added_path)) ), ) ); diff_stream_equals_iter(&before_merged, &after_merged, &EverythingMatcher); } /// Diff two conflicted trees #[test] fn test_diff_conflicted() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; // path1 is a clean (unchanged) conflict // path2 is a conflict before and different conflict after // path3 is resolved before and a conflict after // path4 is missing before and a conflict after let path1 = RepoPath::from_internal_string("dir1/file"); let path2 = RepoPath::from_internal_string("dir2/file"); let path3 = RepoPath::from_internal_string("dir4/file"); let path4 = RepoPath::from_internal_string("dir6/file"); let left_base = create_single_tree( repo, &[(path1, "clean-base"), (path2, "left-base"), (path3, "left")], ); let left_side1 = create_single_tree( repo, &[ (path1, "clean-side1"), (path2, "left-side1"), (path3, "left"), ], ); let left_side2 = create_single_tree( repo, &[ (path1, "clean-side2"), (path2, "left-side2"), (path3, "left"), ], ); let right_base = create_single_tree( repo, &[ (path1, "clean-base"), (path2, "right-base"), (path3, "right-base"), (path4, "right-base"), ], ); let right_side1 = create_single_tree( repo, &[ (path1, "clean-side1"), (path2, "right-side1"), (path3, "right-side1"), (path4, "right-side1"), ], ); let right_side2 = create_single_tree( repo, &[ (path1, "clean-side2"), (path2, "right-side2"), (path3, "right-side2"), (path4, "right-side2"), ], ); let left_merged = MergedTree::new(Merge::from_removes_adds( vec![left_base.clone()], vec![left_side1.clone(), left_side2.clone()], )); let right_merged = MergedTree::new(Merge::from_removes_adds( vec![right_base.clone()], vec![right_side1.clone(), right_side2.clone()], )); // Test the forwards diff let actual_diff = left_merged .diff(&right_merged, &EverythingMatcher) .map(|(path, diff)| (path, diff.unwrap())) .collect_vec(); let expected_diff = [path2, path3, path4] .iter() .map(|&path| { ( path.to_owned(), (left_merged.path_value(path), right_merged.path_value(path)), ) }) .collect_vec(); assert_eq!(actual_diff, expected_diff); diff_stream_equals_iter(&left_merged, &right_merged, &EverythingMatcher); // Test the reverse diff let actual_diff = right_merged .diff(&left_merged, &EverythingMatcher) .map(|(path, diff)| (path, diff.unwrap())) .collect_vec(); let expected_diff = [path2, path3, path4] .iter() .map(|&path| { ( path.to_owned(), (right_merged.path_value(path), left_merged.path_value(path)), ) }) .collect_vec(); assert_eq!(actual_diff, expected_diff); diff_stream_equals_iter(&right_merged, &left_merged, &EverythingMatcher); } #[test] fn test_diff_dir_file() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; // path1: file1 -> directory1 // path2: file1 -> directory1+(directory2-absent) // path3: file1 -> directory1+(file1-absent) // path4: file1+(file2-file3) -> directory1+(directory2-directory3) // path5: directory1 -> file1+(file2-absent) // path6: directory1 -> file1+(directory1-absent) let path1 = RepoPath::from_internal_string("path1"); let path2 = RepoPath::from_internal_string("path2"); let path3 = RepoPath::from_internal_string("path3"); let path4 = RepoPath::from_internal_string("path4"); let path5 = RepoPath::from_internal_string("path5"); let path6 = RepoPath::from_internal_string("path6"); let file = RepoPathComponent::new("file"); let left_base = create_single_tree( repo, &[ (path1, "left"), (path2, "left"), (path3, "left"), (path4, "left-base"), (&path5.join(file), "left"), (&path6.join(file), "left"), ], ); let left_side1 = create_single_tree( repo, &[ (path1, "left"), (path2, "left"), (path3, "left"), (path4, "left-side1"), (&path5.join(file), "left"), (&path6.join(file), "left"), ], ); let left_side2 = create_single_tree( repo, &[ (path1, "left"), (path2, "left"), (path3, "left"), (path4, "left-side2"), (&path5.join(file), "left"), (&path6.join(file), "left"), ], ); let right_base = create_single_tree( repo, &[ (&path1.join(file), "right"), // path2 absent // path3 absent (&path4.join(file), "right-base"), // path5 is absent // path6 is absent ], ); let right_side1 = create_single_tree( repo, &[ (&path1.join(file), "right"), (&path2.join(file), "right"), (&path3.join(file), "right-side1"), (&path4.join(file), "right-side1"), (path5, "right-side1"), (path6, "right"), ], ); let right_side2 = create_single_tree( repo, &[ (&path1.join(file), "right"), (&path2.join(file), "right"), (path3, "right-side2"), (&path4.join(file), "right-side2"), (path5, "right-side2"), (&path6.join(file), "right"), ], ); let left_merged = MergedTree::new(Merge::from_removes_adds( vec![left_base], vec![left_side1, left_side2], )); let right_merged = MergedTree::new(Merge::from_removes_adds( vec![right_base], vec![right_side1, right_side2], )); // Test the forwards diff { let actual_diff = left_merged .diff(&right_merged, &EverythingMatcher) .map(|(path, diff)| (path, diff.unwrap())) .collect_vec(); let expected_diff = vec![ // path1: file1 -> directory1 ( path1.to_owned(), (left_merged.path_value(path1), Merge::absent()), ), ( path1.join(file), (Merge::absent(), right_merged.path_value(&path1.join(file))), ), // path2: file1 -> directory1+(directory2-absent) ( path2.to_owned(), (left_merged.path_value(path2), Merge::absent()), ), ( path2.join(file), (Merge::absent(), right_merged.path_value(&path2.join(file))), ), // path3: file1 -> directory1+(file1-absent) ( path3.to_owned(), ( left_merged.path_value(path3), right_merged.path_value(path3), ), ), // path4: file1+(file2-file3) -> directory1+(directory2-directory3) ( path4.to_owned(), (left_merged.path_value(path4), Merge::absent()), ), ( path4.join(file), (Merge::absent(), right_merged.path_value(&path4.join(file))), ), // path5: directory1 -> file1+(file2-absent) ( path5.join(file), (left_merged.path_value(&path5.join(file)), Merge::absent()), ), ( path5.to_owned(), (Merge::absent(), right_merged.path_value(path5)), ), // path6: directory1 -> file1+(directory1-absent) ( path6.join(file), (left_merged.path_value(&path6.join(file)), Merge::absent()), ), ( path6.to_owned(), (Merge::absent(), right_merged.path_value(path6)), ), ]; assert_eq!(actual_diff, expected_diff); diff_stream_equals_iter(&left_merged, &right_merged, &EverythingMatcher); } // Test the reverse diff { let actual_diff = right_merged .diff(&left_merged, &EverythingMatcher) .map(|(path, diff)| (path, diff.unwrap())) .collect_vec(); let expected_diff = vec![ // path1: file1 -> directory1 ( path1.join(file), (right_merged.path_value(&path1.join(file)), Merge::absent()), ), ( path1.to_owned(), (Merge::absent(), left_merged.path_value(path1)), ), // path2: file1 -> directory1+(directory2-absent) ( path2.join(file), (right_merged.path_value(&path2.join(file)), Merge::absent()), ), ( path2.to_owned(), (Merge::absent(), left_merged.path_value(path2)), ), // path3: file1 -> directory1+(file1-absent) ( path3.to_owned(), ( right_merged.path_value(path3), left_merged.path_value(path3), ), ), // path4: file1+(file2-file3) -> directory1+(directory2-directory3) ( path4.join(file), (right_merged.path_value(&path4.join(file)), Merge::absent()), ), ( path4.to_owned(), (Merge::absent(), left_merged.path_value(path4)), ), // path5: directory1 -> file1+(file2-absent) ( path5.to_owned(), (right_merged.path_value(path5), Merge::absent()), ), ( path5.join(file), (Merge::absent(), left_merged.path_value(&path5.join(file))), ), // path6: directory1 -> file1+(directory1-absent) ( path6.to_owned(), (right_merged.path_value(path6), Merge::absent()), ), ( path6.join(file), (Merge::absent(), left_merged.path_value(&path6.join(file))), ), ]; assert_eq!(actual_diff, expected_diff); diff_stream_equals_iter(&right_merged, &left_merged, &EverythingMatcher); } // Diff while filtering by `path1` (file1 -> directory1) as a file { let matcher = FilesMatcher::new([&path1]); let actual_diff = left_merged .diff(&right_merged, &matcher) .map(|(path, diff)| (path, diff.unwrap())) .collect_vec(); let expected_diff = vec![ // path1: file1 -> directory1 ( path1.to_owned(), (left_merged.path_value(path1), Merge::absent()), ), ]; assert_eq!(actual_diff, expected_diff); diff_stream_equals_iter(&left_merged, &right_merged, &matcher); } // Diff while filtering by `path1/file` (file1 -> directory1) as a file { let matcher = FilesMatcher::new([path1.join(file)]); let actual_diff = left_merged .diff(&right_merged, &matcher) .map(|(path, diff)| (path, diff.unwrap())) .collect_vec(); let expected_diff = vec![ // path1: file1 -> directory1 ( path1.join(file), (Merge::absent(), right_merged.path_value(&path1.join(file))), ), ]; assert_eq!(actual_diff, expected_diff); diff_stream_equals_iter(&left_merged, &right_merged, &matcher); } // Diff while filtering by `path1` (file1 -> directory1) as a prefix { let matcher = PrefixMatcher::new([&path1]); let actual_diff = left_merged .diff(&right_merged, &matcher) .map(|(path, diff)| (path, diff.unwrap())) .collect_vec(); let expected_diff = vec![ ( path1.to_owned(), (left_merged.path_value(path1), Merge::absent()), ), ( path1.join(file), (Merge::absent(), right_merged.path_value(&path1.join(file))), ), ]; assert_eq!(actual_diff, expected_diff); diff_stream_equals_iter(&left_merged, &right_merged, &matcher); } // Diff while filtering by `path6` (directory1 -> file1+(directory1-absent)) as // a file. We don't see the directory at `path6` on the left side, but we // do see the directory that's included in the conflict with a file on the right // side. { let matcher = FilesMatcher::new([&path6]); let actual_diff = left_merged .diff(&right_merged, &matcher) .map(|(path, diff)| (path, diff.unwrap())) .collect_vec(); let expected_diff = vec![( path6.to_owned(), (Merge::absent(), right_merged.path_value(path6)), )]; assert_eq!(actual_diff, expected_diff); diff_stream_equals_iter(&left_merged, &right_merged, &matcher); } } /// Merge 3 resolved trees that can be resolved #[test] fn test_merge_simple() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let path1 = RepoPath::from_internal_string("dir1/file"); let path2 = RepoPath::from_internal_string("dir2/file"); let base1 = create_single_tree(repo, &[(path1, "base"), (path2, "base")]); let side1 = create_single_tree(repo, &[(path1, "side1"), (path2, "base")]); let side2 = create_single_tree(repo, &[(path1, "base"), (path2, "side2")]); let expected = create_single_tree(repo, &[(path1, "side1"), (path2, "side2")]); let base1_merged = MergedTree::new(Merge::resolved(base1)); let side1_merged = MergedTree::new(Merge::resolved(side1)); let side2_merged = MergedTree::new(Merge::resolved(side2)); let expected_merged = MergedTree::new(Merge::resolved(expected)); let merged = side1_merged.merge(&base1_merged, &side2_merged).unwrap(); assert_eq!(merged, expected_merged); } /// Merge 3 resolved trees that can be partially resolved #[test] fn test_merge_partial_resolution() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; // path1 can be resolved, path2 cannot let path1 = RepoPath::from_internal_string("dir1/file"); let path2 = RepoPath::from_internal_string("dir2/file"); let base1 = create_single_tree(repo, &[(path1, "base"), (path2, "base")]); let side1 = create_single_tree(repo, &[(path1, "side1"), (path2, "side1")]); let side2 = create_single_tree(repo, &[(path1, "base"), (path2, "side2")]); let expected_base1 = create_single_tree(repo, &[(path1, "side1"), (path2, "base")]); let expected_side1 = create_single_tree(repo, &[(path1, "side1"), (path2, "side1")]); let expected_side2 = create_single_tree(repo, &[(path1, "side1"), (path2, "side2")]); let base1_merged = MergedTree::new(Merge::resolved(base1)); let side1_merged = MergedTree::new(Merge::resolved(side1)); let side2_merged = MergedTree::new(Merge::resolved(side2)); let expected_merged = MergedTree::new(Merge::from_removes_adds( vec![expected_base1], vec![expected_side1, expected_side2], )); let merged = side1_merged.merge(&base1_merged, &side2_merged).unwrap(); assert_eq!(merged, expected_merged); } /// Merge 3 resolved trees, including one empty legacy tree #[test] fn test_merge_with_empty_legacy_tree() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let path1 = RepoPath::from_internal_string("dir1/file"); let path2 = RepoPath::from_internal_string("dir2/file"); let base1 = repo .store() .get_tree(RepoPath::root(), repo.store().empty_tree_id()) .unwrap(); let side1 = create_single_tree(repo, &[(path1, "side1")]); let side2 = create_single_tree(repo, &[(path2, "side2")]); let expected = create_single_tree(repo, &[(path1, "side1"), (path2, "side2")]); let base1_merged = MergedTree::legacy(base1); let side1_merged = MergedTree::new(Merge::resolved(side1)); let side2_merged = MergedTree::new(Merge::resolved(side2)); let expected_merged = MergedTree::new(Merge::resolved(expected)); let merged = side1_merged.merge(&base1_merged, &side2_merged).unwrap(); assert_eq!(merged, expected_merged); } /// Merge 3 trees where each one is a 3-way conflict and the result is arrived /// at by only simplifying the conflict (no need to recurse) #[test] fn test_merge_simplify_only() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let path = RepoPath::from_internal_string("dir1/file"); let tree1 = create_single_tree(repo, &[(path, "1")]); let tree2 = create_single_tree(repo, &[(path, "2")]); let tree3 = create_single_tree(repo, &[(path, "3")]); let tree4 = create_single_tree(repo, &[(path, "4")]); let tree5 = create_single_tree(repo, &[(path, "5")]); let expected = tree5.clone(); let base1_merged = MergedTree::new(Merge::from_removes_adds( vec![tree1.clone()], vec![tree2.clone(), tree3.clone()], )); let side1_merged = MergedTree::new(Merge::from_removes_adds( vec![tree1.clone()], vec![tree4.clone(), tree2.clone()], )); let side2_merged = MergedTree::new(Merge::from_removes_adds( vec![tree4.clone()], vec![tree5.clone(), tree3.clone()], )); let expected_merged = MergedTree::new(Merge::resolved(expected)); let merged = side1_merged.merge(&base1_merged, &side2_merged).unwrap(); assert_eq!(merged, expected_merged); } /// Merge 3 trees with 3+1+1 terms (i.e. a 5-way conflict) such that resolving /// the conflict between the trees leads to two trees being the same, so the /// result is a 3-way conflict. #[test] fn test_merge_simplify_result() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; // The conflict in path1 cannot be resolved, but the conflict in path2 can. let path1 = RepoPath::from_internal_string("dir1/file"); let path2 = RepoPath::from_internal_string("dir2/file"); let tree1 = create_single_tree(repo, &[(path1, "1"), (path2, "1")]); let tree2 = create_single_tree(repo, &[(path1, "2"), (path2, "2")]); let tree3 = create_single_tree(repo, &[(path1, "3"), (path2, "3")]); let tree4 = create_single_tree(repo, &[(path1, "4"), (path2, "2")]); let tree5 = create_single_tree(repo, &[(path1, "4"), (path2, "1")]); let expected_base1 = create_single_tree(repo, &[(path1, "1"), (path2, "3")]); let expected_side1 = create_single_tree(repo, &[(path1, "2"), (path2, "3")]); let expected_side2 = create_single_tree(repo, &[(path1, "3"), (path2, "3")]); let side1_merged = MergedTree::new(Merge::from_removes_adds( vec![tree1.clone()], vec![tree2.clone(), tree3.clone()], )); let base1_merged = MergedTree::new(Merge::resolved(tree4.clone())); let side2_merged = MergedTree::new(Merge::resolved(tree5.clone())); let expected_merged = MergedTree::new(Merge::from_removes_adds( vec![expected_base1], vec![expected_side1, expected_side2], )); let merged = side1_merged.merge(&base1_merged, &side2_merged).unwrap(); assert_eq!(merged, expected_merged); } /// Test that we simplify content-level conflicts before passing them to /// files::merge(). /// /// This is what happens when you squash a conflict resolution into a conflict /// and it gets propagated to a child where the conflict is different. #[test] fn test_merge_simplify_file_conflict() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let conflict_path = RepoPath::from_internal_string("CHANGELOG.md"); let other_path = RepoPath::from_internal_string("other"); let prefix = r#"### New features * The `ancestors()` revset function now takes an optional `depth` argument to limit the depth of the ancestor set. For example, use `jj log -r 'ancestors(@, 5)` to view the last 5 commits. * Support for the Watchman filesystem monitor is now bundled by default. Set `core.fsmonitor = "watchman"` in your repo to enable. "#; let suffix = r#" ### Fixed bugs "#; let parent_base_text = format!(r#"{prefix}{suffix}"#); let parent_left_text = format!( r#"{prefix} * `jj op log` now supports `--no-graph`. {suffix}"# ); let parent_right_text = format!( r#"{prefix} * You can now configure the set of immutable commits via `revsets.immutable-heads`. For example, set it to `"main"` to prevent rewriting commits on the `main` branch. {suffix}"# ); let child1_right_text = format!( r#"{prefix} * You can now configure the set of immutable commits via `revsets.immutable-heads`. For example, set it to `"main"` to prevent rewriting commits on the `main` branch. The new `immutable()` revset resolves to these immutable commits. {suffix}"# ); let child2_text = format!( r#"{prefix} * You can now configure the set of immutable commits via `revsets.immutable-heads`. For example, set it to `"main"` to prevent rewriting commits on the `main` branch. * `jj op log` now supports `--no-graph`. {suffix}"# ); let expected_text = format!( r#"{prefix} * You can now configure the set of immutable commits via `revsets.immutable-heads`. For example, set it to `"main"` to prevent rewriting commits on the `main` branch. The new `immutable()` revset resolves to these immutable commits. * `jj op log` now supports `--no-graph`. {suffix}"# ); // conflict in parent commit let parent_base = create_single_tree(repo, &[(conflict_path, &parent_base_text)]); let parent_left = create_single_tree(repo, &[(conflict_path, &parent_left_text)]); let parent_right = create_single_tree(repo, &[(conflict_path, &parent_right_text)]); let parent_merged = MergedTree::new(Merge::from_removes_adds( vec![parent_base], vec![parent_left, parent_right], )); // different conflict in child let child1_base = create_single_tree( repo, &[(other_path, "child1"), (conflict_path, &parent_base_text)], ); let child1_left = create_single_tree( repo, &[(other_path, "child1"), (conflict_path, &parent_left_text)], ); let child1_right = create_single_tree( repo, &[(other_path, "child1"), (conflict_path, &child1_right_text)], ); let child1_merged = MergedTree::new(Merge::from_removes_adds( vec![child1_base], vec![child1_left, child1_right], )); // resolved state let child2 = create_single_tree(repo, &[(conflict_path, &child2_text)]); let child2_merged = MergedTree::resolved(child2.clone()); // expected result let expected = create_single_tree( repo, &[(other_path, "child1"), (conflict_path, &expected_text)], ); let expected_merged = MergedTree::resolved(expected); let merged = child1_merged.merge(&parent_merged, &child2_merged).unwrap(); assert_eq!(merged, expected_merged); // Also test the setup by checking that the unsimplified content conflict cannot // be resolved. If we later change files::merge() so this no longer fails, it // probably means that we can delete this whole test (the Merge::simplify() call // in try_resolve_file_conflict() is just an optimization then). let text_merge = Merge::from_removes_adds( vec![Merge::from_removes_adds( vec![parent_base_text.as_bytes()], vec![parent_left_text.as_bytes(), parent_right_text.as_bytes()], )], vec![ Merge::from_removes_adds( vec![parent_base_text.as_bytes()], vec![parent_left_text.as_bytes(), child1_right_text.as_bytes()], ), Merge::resolved(child2_text.as_bytes()), ], ); assert!(matches!( jj_lib::files::merge(&text_merge.flatten()), MergeResult::Conflict(_) )); } /// Like `test_merge_simplify_file_conflict()`, but some of the conflicts are /// absent. #[test] fn test_merge_simplify_file_conflict_with_absent() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; // conflict_path doesn't exist in parent and child2_left, and these two // trees can't be canceled out at the root level. Still the file merge // should succeed by eliminating absent entries. let child2_path = RepoPath::from_internal_string("file_child2"); let conflict_path = RepoPath::from_internal_string("dir/file_conflict"); let child1 = create_single_tree(repo, &[(conflict_path, "1\n0\n")]); let parent = create_single_tree(repo, &[]); let child2_left = create_single_tree(repo, &[(child2_path, "")]); let child2_base = create_single_tree(repo, &[(child2_path, ""), (conflict_path, "0\n")]); let child2_right = create_single_tree(repo, &[(child2_path, ""), (conflict_path, "0\n2\n")]); let child1_merged = MergedTree::resolved(child1); let parent_merged = MergedTree::resolved(parent); let child2_merged = MergedTree::new(Merge::from_removes_adds( vec![child2_base], vec![child2_left, child2_right], )); let expected = create_single_tree(repo, &[(child2_path, ""), (conflict_path, "1\n0\n2\n")]); let expected_merged = MergedTree::resolved(expected); let merged = child1_merged.merge(&parent_merged, &child2_merged).unwrap(); assert_eq!(merged, expected_merged); }