// 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::StreamExt as _;use itertools::Itertools as _;use jj_lib::backend::CommitId;use jj_lib::backend::CopyRecord;use jj_lib::backend::FileId;use jj_lib::backend::MergedTreeId;use jj_lib::backend::TreeValue;use jj_lib::copies::CopiesTreeDiffEntryPath;use jj_lib::copies::CopyOperation;use jj_lib::copies::CopyRecords;use jj_lib::files::MergeResult;use jj_lib::matchers::EverythingMatcher;use jj_lib::matchers::FilesMatcher;use jj_lib::matchers::Matcher;use jj_lib::matchers::PrefixMatcher;use jj_lib::merge::Merge;use jj_lib::merge::MergeBuilder;use jj_lib::merge::MergedTreeValue;use jj_lib::merged_tree::MergedTree;use jj_lib::merged_tree::MergedTreeBuilder;use jj_lib::merged_tree::TreeDiffEntry;use jj_lib::merged_tree::TreeDiffIterator;use jj_lib::merged_tree::TreeDiffStreamImpl;use jj_lib::repo::Repo as _;use jj_lib::repo_path::RepoPath;use jj_lib::repo_path::RepoPathBuf;use jj_lib::repo_path::RepoPathComponent;use pollster::FutureExt as _;use pretty_assertions::assert_eq;use testutils::create_single_tree;use testutils::write_file;use testutils::TestRepo; fn file_value(file_id: &FileId) -> TreeValue { TreeValue::File { id: file_id.clone(), executable: false, }} fn diff_entry_tuple(diff: TreeDiffEntry) -> (RepoPathBuf, (MergedTreeValue, MergedTreeValue)) { (diff.path, diff.values.unwrap())} fn diff_stream_equals_iter(tree1: &MergedTree, tree2: &MergedTree, matcher: &dyn Matcher) { let trees1 = tree1.as_merge(); let trees2 = tree2.as_merge(); let iter_diff: Vec<_> = TreeDiffIterator::new(trees1, trees2, matcher) .map(|diff| (diff.path, diff.values.unwrap())) .collect(); let max_concurrent_reads = 10; let stream_diff: Vec<_> = TreeDiffStreamImpl::new(trees1, trees2, matcher, max_concurrent_reads) .map(|diff| (diff.path, diff.values.unwrap())) .collect() .block_on(); 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.clone(), file_value(&dir1_filename_id)); let tree_id = tree_builder.write_tree().unwrap(); let tree = store.get_tree(RepoPathBuf::root(), &tree_id).unwrap(); let merged_tree = MergedTree::from_legacy_tree(tree.clone()).unwrap(); assert_eq!( merged_tree.value(RepoPathComponent::new("missing")), Merge::absent() ); // file1: regular file without conflicts assert_eq!( merged_tree .value(file1_path.components().next().unwrap()) .cloned(), Merge::normal(TreeValue::File { id: file1_id.clone(), executable: false, }) ); // file2: 3-way conflict assert_eq!( merged_tree .value(file2_path.components().next().unwrap()) .cloned(), 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()) .cloned(), 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()) .cloned(), 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()) .cloned(), 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), Merge::resolved(tree.value(dir1_basename)) ); // 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_n(store.empty_tree_id(), 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.unwrap()); } 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.clone(), 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 that a tree built with no changes on top of an add/add conflict gets/// resolved.#[test]fn test_merged_tree_builder_resolves_conflict() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let store = repo.store(); let path1 = RepoPathBuf::from_internal_string("dir/file"); let tree1 = create_single_tree(repo, &[(&path1, "foo")]); let tree2 = create_single_tree(repo, &[(&path1, "bar")]); let tree3 = create_single_tree(repo, &[(&path1, "bar")]); let base_tree_id = MergedTreeId::Merge(Merge::from_removes_adds( [tree1.id().clone()], [tree2.id().clone(), tree3.id().clone()], )); let tree_builder = MergedTreeBuilder::new(base_tree_id); let tree_id = tree_builder.write_tree(store).unwrap(); assert_eq!(tree_id, MergedTreeId::resolved(tree2.id().clone()));} #[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::new(Merge::from_removes_adds( vec![tree1.clone()], vec![tree2.clone(), tree3.clone()], )); // Get the root tree assert_eq!( merged_tree.path_value(RepoPath::root()).unwrap(), 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).unwrap(), Merge::resolved(tree1.path_value(resolved_file_path).unwrap()), ); // Get directory path without conflict assert_eq!( merged_tree.path_value(resolved_dir_path).unwrap(), Merge::resolved(tree1.path_value(resolved_dir_path).unwrap()), ); // Get missing path assert_eq!( merged_tree.path_value(missing_path).unwrap(), Merge::absent() ); // Get modify/delete conflict (some None values) assert_eq!( merged_tree.path_value(modify_delete_path).unwrap(), Merge::from_removes_adds( vec![tree1.path_value(modify_delete_path).unwrap()], vec![tree2.path_value(modify_delete_path).unwrap(), None] ), ); // Get file/dir conflict path assert_eq!( merged_tree.path_value(file_dir_conflict_path).unwrap(), Merge::from_removes_adds( vec![tree1.path_value(file_dir_conflict_path).unwrap()], vec![ tree2.path_value(file_dir_conflict_path).unwrap(), tree3.path_value(file_dir_conflict_path).unwrap() ] ), ); // 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).unwrap(), Merge::resolved(tree3.path_value(file_dir_conflict_sub_path).unwrap()), ); // Test entries() let actual_entries = merged_tree .entries() .map(|(path, result)| (path, result.unwrap())) .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).unwrap())) .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, ])) .map(|(path, result)| (path, result.unwrap())) .collect_vec(); let expected_entries = [resolved_file_path, modify_delete_path] .iter() .sorted() .map(|&path| (path.to_owned(), merged_tree.path_value(path).unwrap())) .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().take(); 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() );} Lines 1–500 of 1569. Load more
// 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::StreamExt as _; use itertools::Itertools as _; use jj_lib::backend::CommitId; use jj_lib::backend::CopyRecord; use jj_lib::backend::FileId; use jj_lib::backend::MergedTreeId; use jj_lib::backend::TreeValue; use jj_lib::copies::CopiesTreeDiffEntryPath; use jj_lib::copies::CopyOperation; use jj_lib::copies::CopyRecords; use jj_lib::files::MergeResult; use jj_lib::matchers::EverythingMatcher; use jj_lib::matchers::FilesMatcher; use jj_lib::matchers::Matcher; use jj_lib::matchers::PrefixMatcher; use jj_lib::merge::Merge; use jj_lib::merge::MergeBuilder; use jj_lib::merge::MergedTreeValue; use jj_lib::merged_tree::MergedTree; use jj_lib::merged_tree::MergedTreeBuilder; use jj_lib::merged_tree::TreeDiffEntry; use jj_lib::merged_tree::TreeDiffIterator; use jj_lib::merged_tree::TreeDiffStreamImpl; use jj_lib::repo::Repo as _; use jj_lib::repo_path::RepoPath; use jj_lib::repo_path::RepoPathBuf; use jj_lib::repo_path::RepoPathComponent; use pollster::FutureExt as _; use pretty_assertions::assert_eq; use testutils::create_single_tree; use testutils::write_file; use testutils::TestRepo; fn file_value(file_id: &FileId) -> TreeValue { TreeValue::File { id: file_id.clone(), executable: false, } } fn diff_entry_tuple(diff: TreeDiffEntry) -> (RepoPathBuf, (MergedTreeValue, MergedTreeValue)) { (diff.path, diff.values.unwrap()) } fn diff_stream_equals_iter(tree1: &MergedTree, tree2: &MergedTree, matcher: &dyn Matcher) { let trees1 = tree1.as_merge(); let trees2 = tree2.as_merge(); let iter_diff: Vec<_> = TreeDiffIterator::new(trees1, trees2, matcher) .map(|diff| (diff.path, diff.values.unwrap())) .collect(); let max_concurrent_reads = 10; let stream_diff: Vec<_> = TreeDiffStreamImpl::new(trees1, trees2, matcher, max_concurrent_reads) .map(|diff| (diff.path, diff.values.unwrap())) .collect() .block_on(); 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.clone(), file_value(&dir1_filename_id)); let tree_id = tree_builder.write_tree().unwrap(); let tree = store.get_tree(RepoPathBuf::root(), &tree_id).unwrap(); let merged_tree = MergedTree::from_legacy_tree(tree.clone()).unwrap(); assert_eq!( merged_tree.value(RepoPathComponent::new("missing")), Merge::absent() ); // file1: regular file without conflicts assert_eq!( merged_tree .value(file1_path.components().next().unwrap()) .cloned(), Merge::normal(TreeValue::File { id: file1_id.clone(), executable: false, }) ); // file2: 3-way conflict assert_eq!( merged_tree .value(file2_path.components().next().unwrap()) .cloned(), 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()) .cloned(), 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()) .cloned(), 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()) .cloned(), 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), Merge::resolved(tree.value(dir1_basename)) ); // 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_n(store.empty_tree_id(), 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.unwrap()); } 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.clone(), 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 that a tree built with no changes on top of an add/add conflict gets /// resolved. #[test] fn test_merged_tree_builder_resolves_conflict() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let store = repo.store(); let path1 = RepoPathBuf::from_internal_string("dir/file"); let tree1 = create_single_tree(repo, &[(&path1, "foo")]); let tree2 = create_single_tree(repo, &[(&path1, "bar")]); let tree3 = create_single_tree(repo, &[(&path1, "bar")]); let base_tree_id = MergedTreeId::Merge(Merge::from_removes_adds( [tree1.id().clone()], [tree2.id().clone(), tree3.id().clone()], )); let tree_builder = MergedTreeBuilder::new(base_tree_id); let tree_id = tree_builder.write_tree(store).unwrap(); assert_eq!(tree_id, MergedTreeId::resolved(tree2.id().clone())); } #[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::new(Merge::from_removes_adds( vec![tree1.clone()], vec![tree2.clone(), tree3.clone()], )); // Get the root tree assert_eq!( merged_tree.path_value(RepoPath::root()).unwrap(), 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).unwrap(), Merge::resolved(tree1.path_value(resolved_file_path).unwrap()), ); // Get directory path without conflict assert_eq!( merged_tree.path_value(resolved_dir_path).unwrap(), Merge::resolved(tree1.path_value(resolved_dir_path).unwrap()), ); // Get missing path assert_eq!( merged_tree.path_value(missing_path).unwrap(), Merge::absent() ); // Get modify/delete conflict (some None values) assert_eq!( merged_tree.path_value(modify_delete_path).unwrap(), Merge::from_removes_adds( vec![tree1.path_value(modify_delete_path).unwrap()], vec![tree2.path_value(modify_delete_path).unwrap(), None] ), ); // Get file/dir conflict path assert_eq!( merged_tree.path_value(file_dir_conflict_path).unwrap(), Merge::from_removes_adds( vec![tree1.path_value(file_dir_conflict_path).unwrap()], vec![ tree2.path_value(file_dir_conflict_path).unwrap(), tree3.path_value(file_dir_conflict_path).unwrap() ] ), ); // 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).unwrap(), Merge::resolved(tree3.path_value(file_dir_conflict_sub_path).unwrap()), ); // Test entries() let actual_entries = merged_tree .entries() .map(|(path, result)| (path, result.unwrap())) .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).unwrap())) .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, ])) .map(|(path, result)| (path, result.unwrap())) .collect_vec(); let expected_entries = [resolved_file_path, modify_delete_path] .iter() .sorted() .map(|&path| (path.to_owned(), merged_tree.path_value(path).unwrap())) .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().take(); 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() ); }