// Copyright 2020 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 assert_matches::assert_matches; use itertools::Itertools; use jj_lib::backend::TreeValue; use jj_lib::repo::Repo; use jj_lib::repo_path::{RepoPath, RepoPathComponent}; use jj_lib::rewrite::rebase_commit; use jj_lib::tree::{merge_trees, Tree}; use test_case::test_case; use testutils::TestRepo; #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_same_type(use_git: bool) { // Tests all possible cases where the entry type is unchanged, specifically // using only normal files in all trees (no symlinks, no trees, etc.). let test_repo = TestRepo::init(use_git); let repo = &test_repo.repo; let store = repo.store(); // The file name encodes the state in the base and in each side ("_" means // missing) let files = vec![ "__a", // side 2 added "_a_", // side 1 added "_aa", // both sides added, same content "_ab", // both sides added, different content "a__", // both sides removed "a_a", // side 1 removed "a_b", // side 1 removed, side 2 modified "aa_", // side 2 removed "aaa", // no changes "aab", // side 2 modified "ab_", // side 1 modified, side 2 removed "aba", // side 1 modified "abb", // both sides modified, same content "abc", // both sides modified, different content ]; let write_tree = |index: usize| -> Tree { let mut tree_builder = store.tree_builder(store.empty_tree_id().clone()); for path in &files { let contents = &path[index..][..1]; if contents != "_" { testutils::write_normal_file( &mut tree_builder, &RepoPath::from_internal_string(path), contents, ); } } let tree_id = tree_builder.write_tree(); store.get_tree(&RepoPath::root(), &tree_id).unwrap() }; let base_tree = write_tree(0); let side1_tree = write_tree(1); let side2_tree = write_tree(2); // Create the merged tree let merged_tree = merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap(); // Check that we have exactly the paths we expect in the merged tree let names = merged_tree .entries_non_recursive() .map(|entry| entry.name().as_str()) .collect_vec(); assert_eq!( names, vec!["__a", "_a_", "_aa", "_ab", "a_b", "aaa", "aab", "ab_", "aba", "abb", "abc",] ); // Check that the simple, non-conflicting cases were resolved correctly assert_eq!( merged_tree.value(&RepoPathComponent::from("__a")), side2_tree.value(&RepoPathComponent::from("__a")) ); assert_eq!( merged_tree.value(&RepoPathComponent::from("_a_")), side1_tree.value(&RepoPathComponent::from("_a_")) ); assert_eq!( merged_tree.value(&RepoPathComponent::from("_aa")), side1_tree.value(&RepoPathComponent::from("_aa")) ); assert_eq!( merged_tree.value(&RepoPathComponent::from("aaa")), side1_tree.value(&RepoPathComponent::from("aaa")) ); assert_eq!( merged_tree.value(&RepoPathComponent::from("aab")), side2_tree.value(&RepoPathComponent::from("aab")) ); assert_eq!( merged_tree.value(&RepoPathComponent::from("aba")), side1_tree.value(&RepoPathComponent::from("aba")) ); assert_eq!( merged_tree.value(&RepoPathComponent::from("abb")), side1_tree.value(&RepoPathComponent::from("abb")) ); // Check the conflicting cases let component = RepoPathComponent::from("_ab"); match merged_tree.value(&component).unwrap() { TreeValue::Conflict(id) => { let conflict = store .read_conflict(&RepoPath::from_internal_string("_ab"), id) .unwrap(); assert_eq!( conflict.adds(), vec![ side1_tree.value(&component).cloned(), side2_tree.value(&component).cloned(), ] ); assert_eq!(conflict.removes(), vec![None]); } _ => panic!("unexpected value"), }; let component = RepoPathComponent::from("a_b"); match merged_tree.value(&component).unwrap() { TreeValue::Conflict(id) => { let conflict = store .read_conflict(&RepoPath::from_internal_string("a_b"), id) .unwrap(); assert_eq!( conflict.removes(), vec![base_tree.value(&component).cloned()] ); assert_eq!( conflict.adds(), vec![side2_tree.value(&component).cloned(), None] ); } _ => panic!("unexpected value"), }; let component = RepoPathComponent::from("ab_"); match merged_tree.value(&component).unwrap() { TreeValue::Conflict(id) => { let conflict = store .read_conflict(&RepoPath::from_internal_string("ab_"), id) .unwrap(); assert_eq!( conflict.removes(), vec![base_tree.value(&component).cloned()] ); assert_eq!( conflict.adds(), vec![side1_tree.value(&component).cloned(), None] ); } _ => panic!("unexpected value"), }; let component = RepoPathComponent::from("abc"); match merged_tree.value(&component).unwrap() { TreeValue::Conflict(id) => { let conflict = store .read_conflict(&RepoPath::from_internal_string("abc"), id) .unwrap(); assert_eq!( conflict.removes(), vec![base_tree.value(&component).cloned()] ); assert_eq!( conflict.adds(), vec![ side1_tree.value(&component).cloned(), side2_tree.value(&component).cloned(), ] ); } _ => panic!("unexpected value"), }; } #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_executable(use_git: bool) { let test_repo = TestRepo::init(use_git); let repo = &test_repo.repo; let store = repo.store(); // The file name encodes whether the file was executable or normal in the base // and in each side let files = vec!["nnn", "nnx", "nxn", "nxx", "xnn", "xnx", "xxn", "xxx"]; let write_tree = |files: &[(&str, bool)]| -> Tree { let mut tree_builder = store.tree_builder(store.empty_tree_id().clone()); for (path, executable) in files { let repo_path = RepoPath::from_internal_string(path); if *executable { testutils::write_executable_file(&mut tree_builder, &repo_path, "contents"); } else { testutils::write_normal_file(&mut tree_builder, &repo_path, "contents"); } } let tree_id = tree_builder.write_tree(); store.get_tree(&RepoPath::root(), &tree_id).unwrap() }; fn contents_in_tree<'a>(files: &[&'a str], index: usize) -> Vec<(&'a str, bool)> { files .iter() .map(|f| (*f, &f[index..][..1] == "x")) .collect() } let base_tree = write_tree(&contents_in_tree(&files, 0)); let side1_tree = write_tree(&contents_in_tree(&files, 1)); let side2_tree = write_tree(&contents_in_tree(&files, 2)); // Create the merged tree let merged_tree = merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap(); // Check that the merged tree has the correct executable bits let norm = base_tree.value(&RepoPathComponent::from("nnn")); let exec = base_tree.value(&RepoPathComponent::from("xxx")); assert_eq!(merged_tree.value(&RepoPathComponent::from("nnn")), norm); assert_eq!(merged_tree.value(&RepoPathComponent::from("nnx")), exec); assert_eq!(merged_tree.value(&RepoPathComponent::from("nxn")), exec); assert_eq!(merged_tree.value(&RepoPathComponent::from("nxx")), exec); assert_eq!(merged_tree.value(&RepoPathComponent::from("xnn")), norm); assert_eq!(merged_tree.value(&RepoPathComponent::from("xnx")), norm); assert_eq!(merged_tree.value(&RepoPathComponent::from("xxn")), norm); assert_eq!(merged_tree.value(&RepoPathComponent::from("xxx")), exec); } #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_subtrees(use_git: bool) { // Tests that subtrees are merged. let test_repo = TestRepo::init(use_git); let repo = &test_repo.repo; let store = repo.store(); let write_tree = |paths: Vec<&str>| -> Tree { let mut tree_builder = store.tree_builder(store.empty_tree_id().clone()); for path in paths { testutils::write_normal_file( &mut tree_builder, &RepoPath::from_internal_string(path), &format!("contents of {path:?}"), ); } let tree_id = tree_builder.write_tree(); store.get_tree(&RepoPath::root(), &tree_id).unwrap() }; let base_tree = write_tree(vec!["f1", "d1/f1", "d1/d1/f1", "d1/d1/d1/f1"]); let side1_tree = write_tree(vec![ "f1", "f2", "d1/f1", "d1/f2", "d1/d1/f1", "d1/d1/d1/f1", ]); let side2_tree = write_tree(vec![ "f1", "d1/f1", "d1/d1/f1", "d1/d1/d1/f1", "d1/d1/d1/f2", ]); let merged_tree = merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap(); let entries = merged_tree.entries().collect_vec(); let expected_tree = write_tree(vec![ "f1", "f2", "d1/f1", "d1/f2", "d1/d1/f1", "d1/d1/d1/f1", "d1/d1/d1/f2", ]); let expected_entries = expected_tree.entries().collect_vec(); assert_eq!(entries, expected_entries); } #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_subtree_becomes_empty(use_git: bool) { // Tests that subtrees that become empty are removed from the parent tree. let test_repo = TestRepo::init(use_git); let repo = &test_repo.repo; let store = repo.store(); let write_tree = |paths: Vec<&str>| -> Tree { let mut tree_builder = store.tree_builder(store.empty_tree_id().clone()); for path in paths { testutils::write_normal_file( &mut tree_builder, &RepoPath::from_internal_string(path), &format!("contents of {path:?}"), ); } let tree_id = tree_builder.write_tree(); store.get_tree(&RepoPath::root(), &tree_id).unwrap() }; let base_tree = write_tree(vec!["f1", "d1/f1", "d1/d1/d1/f1", "d1/d1/d1/f2"]); let side1_tree = write_tree(vec!["f1", "d1/f1", "d1/d1/d1/f1"]); let side2_tree = write_tree(vec!["d1/d1/d1/f2"]); let merged_tree = merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap(); assert_eq!(merged_tree.id(), store.empty_tree_id()); } #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_subtree_one_missing(use_git: bool) { // Tests that merging trees where one side is missing is resolved as if the // missing side was empty. let test_repo = TestRepo::init(use_git); let repo = &test_repo.repo; let store = repo.store(); let write_tree = |paths: Vec<&str>| -> Tree { let mut tree_builder = store.tree_builder(store.empty_tree_id().clone()); for path in paths { testutils::write_normal_file( &mut tree_builder, &RepoPath::from_internal_string(path), &format!("contents of {path:?}"), ); } let tree_id = tree_builder.write_tree(); store.get_tree(&RepoPath::root(), &tree_id).unwrap() }; let tree1 = write_tree(vec![]); let tree2 = write_tree(vec!["d1/f1"]); let tree3 = write_tree(vec!["d1/f1", "d1/f2"]); // The two sides add different trees let merged_tree = merge_trees(&tree2, &tree1, &tree3).unwrap(); let expected_entries = write_tree(vec!["d1/f1", "d1/f2"]).entries().collect_vec(); assert_eq!(merged_tree.entries().collect_vec(), expected_entries); // Same tree other way let reverse_merged_tree = merge_trees(&tree3, &tree1, &tree2).unwrap(); assert_eq!(reverse_merged_tree.id(), merged_tree.id()); // One side removes, the other side modifies let merged_tree = merge_trees(&tree1, &tree2, &tree3).unwrap(); let expected_entries = write_tree(vec!["d1/f2"]).entries().collect_vec(); assert_eq!(merged_tree.entries().collect_vec(), expected_entries); // Same tree other way let reverse_merged_tree = merge_trees(&tree3, &tree2, &tree1).unwrap(); assert_eq!(reverse_merged_tree.id(), merged_tree.id()); } #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_types(use_git: bool) { // Tests conflicts between different types. This is mostly to test that the // conflicts survive the roundtrip to the store. let test_repo = TestRepo::init(use_git); let repo = &test_repo.repo; let store = repo.store(); let mut base_tree_builder = store.tree_builder(store.empty_tree_id().clone()); let mut side1_tree_builder = store.tree_builder(store.empty_tree_id().clone()); let mut side2_tree_builder = store.tree_builder(store.empty_tree_id().clone()); testutils::write_normal_file( &mut base_tree_builder, &RepoPath::from_internal_string("normal_executable_symlink"), "contents", ); testutils::write_executable_file( &mut side1_tree_builder, &RepoPath::from_internal_string("normal_executable_symlink"), "contents", ); testutils::write_symlink( &mut side2_tree_builder, &RepoPath::from_internal_string("normal_executable_symlink"), "contents", ); let tree_id = store.empty_tree_id().clone(); base_tree_builder.set( RepoPath::from_internal_string("tree_normal_symlink"), TreeValue::Tree(tree_id), ); testutils::write_normal_file( &mut side1_tree_builder, &RepoPath::from_internal_string("tree_normal_symlink"), "contents", ); testutils::write_symlink( &mut side2_tree_builder, &RepoPath::from_internal_string("tree_normal_symlink"), "contents", ); let base_tree_id = base_tree_builder.write_tree(); let base_tree = store.get_tree(&RepoPath::root(), &base_tree_id).unwrap(); let side1_tree_id = side1_tree_builder.write_tree(); let side1_tree = store.get_tree(&RepoPath::root(), &side1_tree_id).unwrap(); let side2_tree_id = side2_tree_builder.write_tree(); let side2_tree = store.get_tree(&RepoPath::root(), &side2_tree_id).unwrap(); // Created the merged tree let merged_tree = merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap(); // Check the conflicting cases let component = RepoPathComponent::from("normal_executable_symlink"); match merged_tree.value(&component).unwrap() { TreeValue::Conflict(id) => { let conflict = store .read_conflict( &RepoPath::from_internal_string("normal_executable_symlink"), id, ) .unwrap(); assert_eq!( conflict.removes(), vec![base_tree.value(&component).cloned()] ); assert_eq!( conflict.adds(), vec![ side1_tree.value(&component).cloned(), side2_tree.value(&component).cloned(), ] ); } _ => panic!("unexpected value"), }; let component = RepoPathComponent::from("tree_normal_symlink"); match merged_tree.value(&component).unwrap() { TreeValue::Conflict(id) => { let conflict = store .read_conflict(&RepoPath::from_internal_string("tree_normal_symlink"), id) .unwrap(); assert_eq!( conflict.removes(), vec![base_tree.value(&component).cloned()] ); assert_eq!( conflict.adds(), vec![ side1_tree.value(&component).cloned(), side2_tree.value(&component).cloned(), ] ); } _ => panic!("unexpected value"), }; } #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_simplify_conflict(use_git: bool) { let test_repo = TestRepo::init(use_git); let repo = &test_repo.repo; let store = repo.store(); let component = RepoPathComponent::from("file"); let path = RepoPath::from_internal_string("file"); let write_tree = |contents: &str| -> Tree { testutils::create_tree(repo, &[(&path, contents)]) }; let base_tree = write_tree("base contents"); let branch_tree = write_tree("branch contents"); let upstream1_tree = write_tree("upstream1 contents"); let upstream2_tree = write_tree("upstream2 contents"); // Rebase the branch tree to the first upstream tree let rebased1_tree = merge_trees(&branch_tree, &base_tree, &upstream1_tree).unwrap(); // Make sure we have a conflict (testing the test setup) match rebased1_tree.value(&component).unwrap() { TreeValue::Conflict(_) => { // expected } _ => panic!("unexpected value"), }; // Rebase the rebased tree back to the base. The conflict should be gone. Try // both directions. let rebased_back_tree = merge_trees(&rebased1_tree, &upstream1_tree, &base_tree).unwrap(); assert_eq!( rebased_back_tree.value(&component), branch_tree.value(&component) ); let rebased_back_tree = merge_trees(&base_tree, &upstream1_tree, &rebased1_tree).unwrap(); assert_eq!( rebased_back_tree.value(&component), branch_tree.value(&component) ); // Rebase the rebased tree further upstream. The conflict should be simplified // to not mention the contents from the first rebase. let further_rebased_tree = merge_trees(&rebased1_tree, &upstream1_tree, &upstream2_tree).unwrap(); match further_rebased_tree.value(&component).unwrap() { TreeValue::Conflict(id) => { let conflict = store .read_conflict(&RepoPath::from_components(vec![component.clone()]), id) .unwrap(); assert_eq!( conflict.removes(), vec![base_tree.value(&component).cloned()] ); assert_eq!( conflict.adds(), vec![ branch_tree.value(&component).cloned(), upstream2_tree.value(&component).cloned(), ] ); } _ => panic!("unexpected value"), }; let further_rebased_tree = merge_trees(&upstream2_tree, &upstream1_tree, &rebased1_tree).unwrap(); match further_rebased_tree.value(&component).unwrap() { TreeValue::Conflict(id) => { let conflict = store.read_conflict(&path, id).unwrap(); assert_eq!( conflict.removes(), vec![base_tree.value(&component).cloned()] ); assert_eq!( conflict.adds(), vec![ upstream2_tree.value(&component).cloned(), branch_tree.value(&component).cloned(), ] ); } _ => panic!("unexpected value"), }; } #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_simplify_conflict_after_resolving_parent(use_git: bool) { let settings = testutils::user_settings(); let test_repo = TestRepo::init(use_git); let repo = &test_repo.repo; // Set up a repo like this: // D // | C // | B // |/ // A // // Commit A has a file with 3 lines. B and D make conflicting changes to the // first line. C changes the third line. We then rebase B and C onto D, // which creates a conflict. We resolve the conflict in the first line and // rebase C2 (the rebased C) onto the resolved conflict. C3 should not have // a conflict since it changed an unrelated line. let path = RepoPath::from_internal_string("dir/file"); let mut tx = repo.start_transaction(&settings, "test"); let tree_a = testutils::create_tree(repo, &[(&path, "abc\ndef\nghi\n")]); let commit_a = tx .mut_repo() .new_commit( &settings, vec![repo.store().root_commit_id().clone()], tree_a.id().clone(), ) .write() .unwrap(); let tree_b = testutils::create_tree(repo, &[(&path, "Abc\ndef\nghi\n")]); let commit_b = tx .mut_repo() .new_commit(&settings, vec![commit_a.id().clone()], tree_b.id().clone()) .write() .unwrap(); let tree_c = testutils::create_tree(repo, &[(&path, "Abc\ndef\nGhi\n")]); let commit_c = tx .mut_repo() .new_commit(&settings, vec![commit_b.id().clone()], tree_c.id().clone()) .write() .unwrap(); let tree_d = testutils::create_tree(repo, &[(&path, "abC\ndef\nghi\n")]); let commit_d = tx .mut_repo() .new_commit(&settings, vec![commit_a.id().clone()], tree_d.id().clone()) .write() .unwrap(); let commit_b2 = rebase_commit(&settings, tx.mut_repo(), &commit_b, &[commit_d]).unwrap(); let commit_c2 = rebase_commit(&settings, tx.mut_repo(), &commit_c, &[commit_b2.clone()]).unwrap(); // Test the setup: Both B and C should have conflicts. assert_matches!( commit_b2.tree().path_value(&path), Some(TreeValue::Conflict(_)) ); assert_matches!( commit_c2.tree().path_value(&path), Some(TreeValue::Conflict(_)) ); // Create the resolved B and rebase C on top. let tree_b3 = testutils::create_tree(repo, &[(&path, "AbC\ndef\nghi\n")]); let commit_b3 = tx .mut_repo() .rewrite_commit(&settings, &commit_b2) .set_tree(tree_b3.id().clone()) .write() .unwrap(); let commit_c3 = rebase_commit(&settings, tx.mut_repo(), &commit_c2, &[commit_b3]).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); let repo = tx.commit(); // The conflict should now be resolved. let resolved_value = commit_c3.tree().path_value(&path); match resolved_value { Some(TreeValue::File { id, executable: false, }) => { assert_eq!( testutils::read_file(repo.store(), &path, &id), b"AbC\ndef\nGhi\n" ); } other => { panic!("unexpected value: {other:#?}"); } } } // TODO: Add tests for simplification of multi-way conflicts. Both the content // and the executable bit need testing.