// 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;use jj_lib::matchers::EverythingMatcher;use jj_lib::matchers::FilesMatcher;use jj_lib::matchers::Matcher;use jj_lib::matchers::PrefixMatcher;use jj_lib::merge::Diff;use jj_lib::merge::Merge;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 pollster::FutureExt as _;use pretty_assertions::assert_eq;use testutils::TestRepo;use testutils::create_single_tree;use testutils::create_tree;use testutils::repo_path;use testutils::repo_path_buf;use testutils::repo_path_component; fn diff_entry_tuple(diff: TreeDiffEntry) -> (RepoPathBuf, (MergedTreeValue, MergedTreeValue)) { let values = diff.values.unwrap(); (diff.path, (values.before, values.after))} 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; tree1.store().clear_caches(); 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 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 = repo_path("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::new(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 = repo_path("dir1/subdir/resolved"); let resolved_dir_path = &resolved_file_path.parent().unwrap(); let conflicted_file_path = repo_path("dir2/conflicted"); let missing_path = repo_path("dir2/missing_file"); let modify_delete_path = repo_path("dir2/modify_delete"); let file_dir_conflict_path = repo_path("file_dir"); let file_dir_conflict_sub_path = repo_path("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, so it is considered to not be a // directory in the merged tree, making the file hidden until the directory // conflict has been resolved. assert_eq!( merged_tree.path_value(file_dir_conflict_sub_path).unwrap(), Merge::absent(), ); // 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 = repo_path("unchanged"); let trivial_file_path = repo_path("trivial-file"); let trivial_hunk_path = repo_path("trivial-hunk"); let both_added_dir_path = repo_path("added-dir"); let both_added_dir_file1_path = &both_added_dir_path.join(repo_path_component("file1")); let both_added_dir_file2_path = &both_added_dir_path.join(repo_path_component("file2")); let emptied_dir_path = repo_path("to-become-empty"); let emptied_dir_file1_path = &emptied_dir_path.join(repo_path_component("file1")); let emptied_dir_file2_path = &emptied_dir_path.join(repo_path_component("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_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 = tree.resolve().block_on().unwrap(); assert!(resolved_tree.as_merge().is_resolved()); 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 = repo_path("dir1/file"); let path2 = repo_path("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().block_on().unwrap(); assert_eq!(resolved.id(), store.empty_merged_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 = repo_path("dir1/trivial"); let conflict_path = repo_path("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().block_on().unwrap(); assert_eq!( resolved_tree, MergedTree::new(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, side2's root tree should be empty as // well. If it were added to the root tree, side2.id() would differ. let conflict_path = repo_path("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 tree = MergedTree::new(Merge::from_removes_adds(vec![base1], vec![side1, side2])); let resolved_tree = tree.clone().resolve().block_on().unwrap(); assert_eq!(resolved_tree, tree);} #[test]fn test_conflict_iterator() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let unchanged_path = repo_path("dir/subdir/unchanged"); let trivial_path = repo_path("dir/subdir/trivial"); let trivial_hunk_path = repo_path("dir/non_trivial"); let file_conflict_path = repo_path("dir/subdir/file_conflict"); let modify_delete_path = repo_path("dir/subdir/modify_delete"); let same_add_path = repo_path("dir/subdir/same_add"); let different_add_path = repo_path("dir/subdir/different_add"); let dir_file_path = repo_path("dir/subdir/dir_file"); let added_dir_path = repo_path("dir/new_dir"); let modify_delete_dir_path = repo_path("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(repo_path_component("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(repo_path_component("side1")), "side1"), ( &modify_delete_dir_path.join(repo_path_component("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(repo_path_component("dir")), "new"), (&added_dir_path.join(repo_path_component("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() .map(|(path, conflict)| (path, conflict.unwrap())) .collect_vec(); let conflict_at = |path: &RepoPath| { Merge::from_removes_adds( vec![base1.path_value(path).unwrap()], vec![ side1.path_value(path).unwrap(), side2.path_value(path).unwrap(), ], ) }; // 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 = tree.resolve().block_on().unwrap(); let conflicts = tree .conflicts() .map(|(path, conflict)| (path, conflict.unwrap())) .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) ), ] );} #[test]fn test_conflict_iterator_higher_arity() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let two_sided_path = repo_path("dir/2-sided"); let three_sided_path = repo_path("dir/3-sided"); let base1 = create_single_tree( repo, &[(two_sided_path, "base1"), (three_sided_path, "base1")], );Lines 1–500 of 1448. 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; use jj_lib::matchers::EverythingMatcher; use jj_lib::matchers::FilesMatcher; use jj_lib::matchers::Matcher; use jj_lib::matchers::PrefixMatcher; use jj_lib::merge::Diff; use jj_lib::merge::Merge; 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 pollster::FutureExt as _; use pretty_assertions::assert_eq; use testutils::TestRepo; use testutils::create_single_tree; use testutils::create_tree; use testutils::repo_path; use testutils::repo_path_buf; use testutils::repo_path_component; fn diff_entry_tuple(diff: TreeDiffEntry) -> (RepoPathBuf, (MergedTreeValue, MergedTreeValue)) { let values = diff.values.unwrap(); (diff.path, (values.before, values.after)) } 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; tree1.store().clear_caches(); 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 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 = repo_path("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::new(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 = repo_path("dir1/subdir/resolved"); let resolved_dir_path = &resolved_file_path.parent().unwrap(); let conflicted_file_path = repo_path("dir2/conflicted"); let missing_path = repo_path("dir2/missing_file"); let modify_delete_path = repo_path("dir2/modify_delete"); let file_dir_conflict_path = repo_path("file_dir"); let file_dir_conflict_sub_path = repo_path("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, so it is considered to not be a // directory in the merged tree, making the file hidden until the directory // conflict has been resolved. assert_eq!( merged_tree.path_value(file_dir_conflict_sub_path).unwrap(), Merge::absent(), ); // 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 = repo_path("unchanged"); let trivial_file_path = repo_path("trivial-file"); let trivial_hunk_path = repo_path("trivial-hunk"); let both_added_dir_path = repo_path("added-dir"); let both_added_dir_file1_path = &both_added_dir_path.join(repo_path_component("file1")); let both_added_dir_file2_path = &both_added_dir_path.join(repo_path_component("file2")); let emptied_dir_path = repo_path("to-become-empty"); let emptied_dir_file1_path = &emptied_dir_path.join(repo_path_component("file1")); let emptied_dir_file2_path = &emptied_dir_path.join(repo_path_component("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_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 = tree.resolve().block_on().unwrap(); assert!(resolved_tree.as_merge().is_resolved()); 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 = repo_path("dir1/file"); let path2 = repo_path("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().block_on().unwrap(); assert_eq!(resolved.id(), store.empty_merged_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 = repo_path("dir1/trivial"); let conflict_path = repo_path("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().block_on().unwrap(); assert_eq!( resolved_tree, MergedTree::new(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, side2's root tree should be empty as // well. If it were added to the root tree, side2.id() would differ. let conflict_path = repo_path("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 tree = MergedTree::new(Merge::from_removes_adds(vec![base1], vec![side1, side2])); let resolved_tree = tree.clone().resolve().block_on().unwrap(); assert_eq!(resolved_tree, tree); } #[test] fn test_conflict_iterator() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let unchanged_path = repo_path("dir/subdir/unchanged"); let trivial_path = repo_path("dir/subdir/trivial"); let trivial_hunk_path = repo_path("dir/non_trivial"); let file_conflict_path = repo_path("dir/subdir/file_conflict"); let modify_delete_path = repo_path("dir/subdir/modify_delete"); let same_add_path = repo_path("dir/subdir/same_add"); let different_add_path = repo_path("dir/subdir/different_add"); let dir_file_path = repo_path("dir/subdir/dir_file"); let added_dir_path = repo_path("dir/new_dir"); let modify_delete_dir_path = repo_path("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(repo_path_component("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(repo_path_component("side1")), "side1"), ( &modify_delete_dir_path.join(repo_path_component("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(repo_path_component("dir")), "new"), (&added_dir_path.join(repo_path_component("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() .map(|(path, conflict)| (path, conflict.unwrap())) .collect_vec(); let conflict_at = |path: &RepoPath| { Merge::from_removes_adds( vec![base1.path_value(path).unwrap()], vec![ side1.path_value(path).unwrap(), side2.path_value(path).unwrap(), ], ) }; // 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 = tree.resolve().block_on().unwrap(); let conflicts = tree .conflicts() .map(|(path, conflict)| (path, conflict.unwrap())) .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) ), ] ); } #[test] fn test_conflict_iterator_higher_arity() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let two_sided_path = repo_path("dir/2-sided"); let three_sided_path = repo_path("dir/3-sided"); let base1 = create_single_tree( repo, &[(two_sided_path, "base1"), (three_sided_path, "base1")], );