jj/lib/tests/test_merge_trees.rs
Martin von Zweigbergk f16d2a237b backend: pass in path when reading/writing conflicts as well
We do it for all the other kinds of objects already. It's useful to
have the path for backends that store objects by path (we don't have
any such backends yet). I think the reason I didn't do it from the
beginning was because we had separate `RepoPath` types for files and
directories back then.
2022-03-31 10:23:33 -07:00

665 lines
24 KiB
Rust

// Copyright 2020 Google LLC
//
// 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 jujutsu_lib::backend::{ConflictPart, TreeValue};
use jujutsu_lib::commit_builder::CommitBuilder;
use jujutsu_lib::repo_path::{RepoPath, RepoPathComponent};
use jujutsu_lib::rewrite::rebase_commit;
use jujutsu_lib::tree::Tree;
use jujutsu_lib::{testutils, tree};
use test_case::test_case;
#[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 settings = testutils::user_settings();
let test_repo = testutils::init_repo(&settings, 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..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_id = tree::merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap();
let merged_tree = store.get_tree(&RepoPath::root(), &merged_tree_id).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
match merged_tree.value(&RepoPathComponent::from("_ab")).unwrap() {
TreeValue::Conflict(id) => {
let conflict = store
.read_conflict(&RepoPath::from_internal_string("_ab"), id)
.unwrap();
assert_eq!(
conflict.adds,
vec![
ConflictPart {
value: side1_tree
.value(&RepoPathComponent::from("_ab"))
.cloned()
.unwrap()
},
ConflictPart {
value: side2_tree
.value(&RepoPathComponent::from("_ab"))
.cloned()
.unwrap()
}
]
);
assert!(conflict.removes.is_empty());
}
_ => panic!("unexpected value"),
};
match merged_tree.value(&RepoPathComponent::from("a_b")).unwrap() {
TreeValue::Conflict(id) => {
let conflict = store
.read_conflict(&RepoPath::from_internal_string("a_b"), id)
.unwrap();
assert_eq!(
conflict.removes,
vec![ConflictPart {
value: base_tree
.value(&RepoPathComponent::from("a_b"))
.cloned()
.unwrap()
}]
);
assert_eq!(
conflict.adds,
vec![ConflictPart {
value: side2_tree
.value(&RepoPathComponent::from("a_b"))
.cloned()
.unwrap()
}]
);
}
_ => panic!("unexpected value"),
};
match merged_tree.value(&RepoPathComponent::from("ab_")).unwrap() {
TreeValue::Conflict(id) => {
let conflict = store
.read_conflict(&RepoPath::from_internal_string("ab_"), id)
.unwrap();
assert_eq!(
conflict.removes,
vec![ConflictPart {
value: base_tree
.value(&RepoPathComponent::from("ab_"))
.cloned()
.unwrap()
}]
);
assert_eq!(
conflict.adds,
vec![ConflictPart {
value: side1_tree
.value(&RepoPathComponent::from("ab_"))
.cloned()
.unwrap()
}]
);
}
_ => panic!("unexpected value"),
};
match merged_tree.value(&RepoPathComponent::from("abc")).unwrap() {
TreeValue::Conflict(id) => {
let conflict = store
.read_conflict(&RepoPath::from_internal_string("abc"), id)
.unwrap();
assert_eq!(
conflict.removes,
vec![ConflictPart {
value: base_tree
.value(&RepoPathComponent::from("abc"))
.cloned()
.unwrap()
}]
);
assert_eq!(
conflict.adds,
vec![
ConflictPart {
value: side1_tree
.value(&RepoPathComponent::from("abc"))
.cloned()
.unwrap()
},
ConflictPart {
value: side2_tree
.value(&RepoPathComponent::from("abc"))
.cloned()
.unwrap()
}
]
);
}
_ => panic!("unexpected value"),
};
}
#[test_case(false ; "local backend")]
#[test_case(true ; "git backend")]
fn test_subtrees(use_git: bool) {
// Tests that subtrees are merged.
let settings = testutils::user_settings();
let test_repo = testutils::init_repo(&settings, 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_id = tree::merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap();
let merged_tree = store.get_tree(&RepoPath::root(), &merged_tree_id).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 settings = testutils::user_settings();
let test_repo = testutils::init_repo(&settings, 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_id = tree::merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap();
let merged_tree = store.get_tree(&RepoPath::root(), &merged_tree_id).unwrap();
assert_eq!(merged_tree.id(), store.empty_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 settings = testutils::user_settings();
let test_repo = testutils::init_repo(&settings, 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_id = tree::merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap();
let merged_tree = store.get_tree(&RepoPath::root(), &merged_tree_id).unwrap();
// Check the conflicting cases
match merged_tree
.value(&RepoPathComponent::from("normal_executable_symlink"))
.unwrap()
{
TreeValue::Conflict(id) => {
let conflict = store
.read_conflict(
&RepoPath::from_internal_string("normal_executable_symlink"),
id,
)
.unwrap();
assert_eq!(
conflict.removes,
vec![ConflictPart {
value: base_tree
.value(&RepoPathComponent::from("normal_executable_symlink"))
.cloned()
.unwrap()
}]
);
assert_eq!(
conflict.adds,
vec![
ConflictPart {
value: side1_tree
.value(&RepoPathComponent::from("normal_executable_symlink"))
.cloned()
.unwrap()
},
ConflictPart {
value: side2_tree
.value(&RepoPathComponent::from("normal_executable_symlink"))
.cloned()
.unwrap()
},
]
);
}
_ => panic!("unexpected value"),
};
match merged_tree
.value(&RepoPathComponent::from("tree_normal_symlink"))
.unwrap()
{
TreeValue::Conflict(id) => {
let conflict = store
.read_conflict(&RepoPath::from_internal_string("tree_normal_symlink"), id)
.unwrap();
assert_eq!(
conflict.removes,
vec![ConflictPart {
value: base_tree
.value(&RepoPathComponent::from("tree_normal_symlink"))
.cloned()
.unwrap()
}]
);
assert_eq!(
conflict.adds,
vec![
ConflictPart {
value: side1_tree
.value(&RepoPathComponent::from("tree_normal_symlink"))
.cloned()
.unwrap()
},
ConflictPart {
value: side2_tree
.value(&RepoPathComponent::from("tree_normal_symlink"))
.cloned()
.unwrap()
},
]
);
}
_ => panic!("unexpected value"),
};
}
#[test_case(false ; "local backend")]
#[test_case(true ; "git backend")]
fn test_simplify_conflict(use_git: bool) {
let settings = testutils::user_settings();
let test_repo = testutils::init_repo(&settings, use_git);
let repo = &test_repo.repo;
let store = repo.store();
let write_tree = |contents: &str| -> Tree {
testutils::create_tree(repo, &[(&RepoPath::from_internal_string("file"), 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");
let merge_trees = |base: &Tree, side1: &Tree, side2: &Tree| -> Tree {
let tree_id = tree::merge_trees(side1, base, side2).unwrap();
store.get_tree(&RepoPath::root(), &tree_id).unwrap()
};
// Rebase the branch tree to the first upstream tree
let rebased1_tree = merge_trees(&base_tree, &branch_tree, &upstream1_tree);
// Make sure we have a conflict (testing the test setup)
match rebased1_tree
.value(&RepoPathComponent::from("file"))
.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(&upstream1_tree, &rebased1_tree, &base_tree);
assert_eq!(
rebased_back_tree.value(&RepoPathComponent::from("file")),
branch_tree.value(&RepoPathComponent::from("file"))
);
let rebased_back_tree = merge_trees(&upstream1_tree, &base_tree, &rebased1_tree);
assert_eq!(
rebased_back_tree.value(&RepoPathComponent::from("file")),
branch_tree.value(&RepoPathComponent::from("file"))
);
// 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(&upstream1_tree, &rebased1_tree, &upstream2_tree);
match further_rebased_tree
.value(&RepoPathComponent::from("file"))
.unwrap()
{
TreeValue::Conflict(id) => {
let conflict = store
.read_conflict(&RepoPath::from_internal_string("file"), id)
.unwrap();
assert_eq!(
conflict.removes,
vec![ConflictPart {
value: base_tree
.value(&RepoPathComponent::from("file"))
.cloned()
.unwrap()
}]
);
assert_eq!(
conflict.adds,
vec![
ConflictPart {
value: branch_tree
.value(&RepoPathComponent::from("file"))
.cloned()
.unwrap()
},
ConflictPart {
value: upstream2_tree
.value(&RepoPathComponent::from("file"))
.cloned()
.unwrap()
},
]
);
}
_ => panic!("unexpected value"),
};
let further_rebased_tree = merge_trees(&upstream1_tree, &upstream2_tree, &rebased1_tree);
match further_rebased_tree
.value(&RepoPathComponent::from("file"))
.unwrap()
{
TreeValue::Conflict(id) => {
let conflict = store
.read_conflict(&RepoPath::from_internal_string("file"), id)
.unwrap();
assert_eq!(
conflict.removes,
vec![ConflictPart {
value: base_tree
.value(&RepoPathComponent::from("file"))
.cloned()
.unwrap()
}]
);
assert_eq!(
conflict.adds,
vec![
ConflictPart {
value: upstream2_tree
.value(&RepoPathComponent::from("file"))
.cloned()
.unwrap()
},
ConflictPart {
value: branch_tree
.value(&RepoPathComponent::from("file"))
.cloned()
.unwrap()
},
]
);
}
_ => 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 = testutils::init_repo(&settings, 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("test");
let tree_a = testutils::create_tree(repo, &[(&path, "abc\ndef\nghi\n")]);
let commit_a = CommitBuilder::for_new_commit(&settings, repo.store(), tree_a.id().clone())
.write_to_repo(tx.mut_repo());
let tree_b = testutils::create_tree(repo, &[(&path, "Abc\ndef\nghi\n")]);
let commit_b = CommitBuilder::for_new_commit(&settings, repo.store(), tree_b.id().clone())
.set_parents(vec![commit_a.id().clone()])
.write_to_repo(tx.mut_repo());
let tree_c = testutils::create_tree(repo, &[(&path, "Abc\ndef\nGhi\n")]);
let commit_c = CommitBuilder::for_new_commit(&settings, repo.store(), tree_c.id().clone())
.set_parents(vec![commit_b.id().clone()])
.write_to_repo(tx.mut_repo());
let tree_d = testutils::create_tree(repo, &[(&path, "abC\ndef\nghi\n")]);
let commit_d = CommitBuilder::for_new_commit(&settings, repo.store(), tree_d.id().clone())
.set_parents(vec![commit_a.id().clone()])
.write_to_repo(tx.mut_repo());
let commit_b2 = rebase_commit(&settings, tx.mut_repo(), &commit_b, &[commit_d]);
let commit_c2 = rebase_commit(&settings, tx.mut_repo(), &commit_c, &[commit_b2.clone()]);
// 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 = CommitBuilder::for_rewrite_from(&settings, repo.store(), &commit_b2)
.set_tree(tree_b3.id().clone())
.write_to_repo(tx.mut_repo());
let commit_c3 = rebase_commit(&settings, tx.mut_repo(), &commit_c2, &[commit_b3]);
tx.mut_repo().rebase_descendants(&settings);
let repo = tx.commit();
// The conflict should now be resolved.
let resolved_value = commit_c3.tree().path_value(&path);
match resolved_value {
Some(TreeValue::Normal {
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.