jj/lib/tests/test_merged_tree.rs
2023-08-15 07:56:55 -07:00

577 lines
21 KiB
Rust

// 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 itertools::Itertools;
use jj_lib::backend::{FileId, TreeValue};
use jj_lib::merge::Merge;
use jj_lib::merged_tree::{MergedTree, MergedTreeValue};
use jj_lib::repo::Repo;
use jj_lib::repo_path::{RepoPath, RepoPathComponent, RepoPathJoin};
use jj_lib::tree::merge_trees;
use testutils::{create_tree, write_file, write_normal_file, TestRepo};
fn file_value(file_id: &FileId) -> TreeValue {
TreeValue::File {
id: file_id.clone(),
executable: false,
}
}
#[test]
fn test_from_legacy_tree() {
let test_repo = TestRepo::init(true);
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_normal_file(&mut tree_builder, &file1_path, "foo");
// 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::new(
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.clone(), 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::new(
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.clone(), 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::new(
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.clone(), 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::new(
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.clone(), TreeValue::Conflict(file5_conflict_id));
// dir1: directory without conflicts
let dir1_basename = RepoPathComponent::from("dir1");
write_normal_file(
&mut tree_builder,
&RepoPath::root()
.join(&dir1_basename)
.join(&RepoPathComponent::from("file")),
"foo",
);
let tree_id = tree_builder.write_tree();
let tree = store.get_tree(&RepoPath::root(), &tree_id).unwrap();
let merged_tree = MergedTree::from_legacy_tree(tree.clone());
assert_eq!(
merged_tree.value(&RepoPathComponent::from("missing")),
MergedTreeValue::Resolved(None)
);
// file1: regular file without conflicts
assert_eq!(
merged_tree.value(&file1_path.components()[0]),
MergedTreeValue::Resolved(Some(&TreeValue::File {
id: file1_id,
executable: false,
}))
);
// file2: 3-way conflict
assert_eq!(
merged_tree.value(&file2_path.components()[0]),
MergedTreeValue::Conflict(Merge::new(
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()[0]),
MergedTreeValue::Conflict(Merge::new(
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()[0]),
MergedTreeValue::Conflict(Merge::new(
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()[0]),
MergedTreeValue::Conflict(Merge::new(
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),
MergedTreeValue::Resolved(tree.value(&dir1_basename))
);
}
#[test]
fn test_path_value() {
let test_repo = TestRepo::init(true);
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_tree(
repo,
&[
(&resolved_file_path, "unchanged"),
(&conflicted_file_path, "1"),
(&modify_delete_path, "1"),
(&file_dir_conflict_path, "1"),
],
);
let tree2 = create_tree(
repo,
&[
(&resolved_file_path, "unchanged"),
(&conflicted_file_path, "2"),
(&modify_delete_path, "2"),
(&file_dir_conflict_path, "2"),
],
);
let tree3 = create_tree(
repo,
&[
(&resolved_file_path, "unchanged"),
(&conflicted_file_path, "3"),
// No modify_delete_path in this tree
(&file_dir_conflict_sub_path, "1"),
],
);
let merged_tree = MergedTree::Merge(Merge::new(
vec![tree1.clone()],
vec![tree2.clone(), tree3.clone()],
));
// Get the root tree
assert_eq!(
merged_tree.path_value(&RepoPath::root()),
Merge::new(
vec![Some(TreeValue::Tree(tree1.id().clone()))],
vec![
Some(TreeValue::Tree(tree2.id().clone())),
Some(TreeValue::Tree(tree3.id().clone())),
]
)
);
// Get file path without conflict
assert_eq!(
merged_tree.path_value(&resolved_file_path),
Merge::resolved(tree1.path_value(&resolved_file_path)),
);
// Get directory path without conflict
assert_eq!(
merged_tree.path_value(&resolved_dir_path),
Merge::resolved(tree1.path_value(&resolved_dir_path)),
);
// Get missing path
assert_eq!(merged_tree.path_value(&missing_path), Merge::absent());
// Get modify/delete conflict (some None values)
assert_eq!(
merged_tree.path_value(&modify_delete_path),
Merge::new(
vec![tree1.path_value(&modify_delete_path)],
vec![tree2.path_value(&modify_delete_path), None]
),
);
// Get file/dir conflict path
assert_eq!(
merged_tree.path_value(&file_dir_conflict_path),
Merge::new(
vec![tree1.path_value(&file_dir_conflict_path)],
vec![
tree2.path_value(&file_dir_conflict_path),
tree3.path_value(&file_dir_conflict_path)
]
),
);
// Get file inside file/dir conflict
// There is a conflict in the parent directory, but this file is still resolved
assert_eq!(
merged_tree.path_value(&file_dir_conflict_sub_path),
Merge::resolved(tree3.path_value(&file_dir_conflict_sub_path)),
);
}
#[test]
fn test_resolve_success() {
let test_repo = TestRepo::init(true);
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::from("file1"));
let both_added_dir_file2_path = both_added_dir_path.join(&RepoPathComponent::from("file2"));
let emptied_dir_path = RepoPath::from_internal_string("to-become-empty");
let emptied_dir_file1_path = emptied_dir_path.join(&RepoPathComponent::from("file1"));
let emptied_dir_file2_path = emptied_dir_path.join(&RepoPathComponent::from("file2"));
let base1 = create_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_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_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::new(vec![base1], vec![side1, side2]));
let resolved = tree.resolve().unwrap();
let resolved_tree = resolved.as_resolved().unwrap().clone();
assert_eq!(
resolved_tree,
expected,
"actual entries: {:#?}, expected entries {:#?}",
resolved_tree.entries().collect_vec(),
expected.entries().collect_vec()
);
}
#[test]
fn test_resolve_root_becomes_empty() {
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
let store = repo.store();
let path1 = RepoPath::from_internal_string("dir1/file");
let path2 = RepoPath::from_internal_string("dir2/file");
let base1 = create_tree(repo, &[(&path1, "base1"), (&path2, "base1")]);
let side1 = create_tree(repo, &[(&path2, "base1")]);
let side2 = create_tree(repo, &[(&path1, "base1")]);
let tree = MergedTree::new(Merge::new(vec![base1], vec![side1, side2]));
let resolved = tree.resolve().unwrap();
assert_eq!(resolved.as_resolved().unwrap().id(), store.empty_tree_id());
}
#[test]
fn test_resolve_with_conflict() {
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
// The trivial conflict should be resolved but the non-trivial should not (and
// cannot)
let trivial_path = RepoPath::from_internal_string("dir1/trivial");
let conflict_path = RepoPath::from_internal_string("dir2/file_conflict");
let base1 = create_tree(repo, &[(&trivial_path, "base1"), (&conflict_path, "base1")]);
let side1 = create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "side1")]);
let side2 = create_tree(repo, &[(&trivial_path, "base1"), (&conflict_path, "side2")]);
let expected_base1 = create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "base1")]);
let expected_side1 = create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "side1")]);
let expected_side2 = create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "side2")]);
let tree = MergedTree::new(Merge::new(vec![base1], vec![side1, side2]));
let resolved_tree = tree.resolve().unwrap();
assert_eq!(
resolved_tree,
Merge::new(vec![expected_base1], vec![expected_side1, expected_side2])
)
}
#[test]
fn test_conflict_iterator() {
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
let unchanged_path = RepoPath::from_internal_string("dir/subdir/unchanged");
let trivial_path = RepoPath::from_internal_string("dir/subdir/trivial");
let trivial_hunk_path = RepoPath::from_internal_string("dir/non_trivial");
let file_conflict_path = RepoPath::from_internal_string("dir/subdir/file_conflict");
let modify_delete_path = RepoPath::from_internal_string("dir/subdir/modify_delete");
let same_add_path = RepoPath::from_internal_string("dir/subdir/same_add");
let different_add_path = RepoPath::from_internal_string("dir/subdir/different_add");
let dir_file_path = RepoPath::from_internal_string("dir/subdir/dir_file");
let added_dir_path = RepoPath::from_internal_string("dir/new_dir");
let modify_delete_dir_path = RepoPath::from_internal_string("dir/modify_delete_dir");
let base1 = create_tree(
repo,
&[
(&unchanged_path, "unchanged"),
(&trivial_path, "base"),
(&trivial_hunk_path, "line1\nline2\nline3\n"),
(&file_conflict_path, "base"),
(&modify_delete_path, "base"),
// no same_add_path
// no different_add_path
(&dir_file_path, "base"),
// no added_dir_path
(
&modify_delete_dir_path.join(&RepoPathComponent::from("base")),
"base",
),
],
);
let side1 = create_tree(
repo,
&[
(&unchanged_path, "unchanged"),
(&trivial_path, "base"),
(&file_conflict_path, "side1"),
(&trivial_hunk_path, "line1 side1\nline2\nline3\n"),
(&modify_delete_path, "modified"),
(&same_add_path, "same"),
(&different_add_path, "side1"),
(&dir_file_path, "side1"),
(
&added_dir_path.join(&RepoPathComponent::from("side1")),
"side1",
),
(
&modify_delete_dir_path.join(&RepoPathComponent::from("side1")),
"side1",
),
],
);
let side2 = create_tree(
repo,
&[
(&unchanged_path, "unchanged"),
(&trivial_path, "side2"),
(&file_conflict_path, "side2"),
(&trivial_hunk_path, "line1\nline2\nline3 side2\n"),
// no modify_delete_path
(&same_add_path, "same"),
(&different_add_path, "side2"),
(&dir_file_path.join(&RepoPathComponent::from("dir")), "new"),
(
&added_dir_path.join(&RepoPathComponent::from("side2")),
"side2",
),
// no modify_delete_dir_path
],
);
let tree = MergedTree::new(Merge::new(
vec![base1.clone()],
vec![side1.clone(), side2.clone()],
));
let conflicts = tree.conflicts().collect_vec();
let conflict_at = |path: &RepoPath| {
Merge::new(
vec![base1.path_value(path)],
vec![side1.path_value(path), side2.path_value(path)],
)
};
// We initially also get a conflict in trivial_hunk_path because we had
// forgotten to resolve conflicts
assert_eq!(
conflicts,
vec![
(trivial_hunk_path.clone(), conflict_at(&trivial_hunk_path)),
(different_add_path.clone(), conflict_at(&different_add_path)),
(dir_file_path.clone(), conflict_at(&dir_file_path)),
(file_conflict_path.clone(), conflict_at(&file_conflict_path)),
(modify_delete_path.clone(), conflict_at(&modify_delete_path)),
]
);
// After we resolve conflicts, there are only non-trivial conflicts left
let tree = MergedTree::Merge(tree.resolve().unwrap());
let conflicts = tree.conflicts().collect_vec();
assert_eq!(
conflicts,
vec![
(different_add_path.clone(), conflict_at(&different_add_path)),
(dir_file_path.clone(), conflict_at(&dir_file_path)),
(file_conflict_path.clone(), conflict_at(&file_conflict_path)),
(modify_delete_path.clone(), conflict_at(&modify_delete_path)),
]
);
let merged_legacy_tree = merge_trees(&side1, &base1, &side2).unwrap();
let legacy_conflicts = MergedTree::legacy(merged_legacy_tree)
.conflicts()
.collect_vec();
assert_eq!(legacy_conflicts, conflicts);
}
#[test]
fn test_conflict_iterator_higher_arity() {
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
let two_sided_path = RepoPath::from_internal_string("dir/2-sided");
let three_sided_path = RepoPath::from_internal_string("dir/3-sided");
let base1 = create_tree(
repo,
&[(&two_sided_path, "base1"), (&three_sided_path, "base1")],
);
let base2 = create_tree(
repo,
&[(&two_sided_path, "base2"), (&three_sided_path, "base2")],
);
let side1 = create_tree(
repo,
&[(&two_sided_path, "side1"), (&three_sided_path, "side1")],
);
let side2 = create_tree(
repo,
&[(&two_sided_path, "base1"), (&three_sided_path, "side2")],
);
let side3 = create_tree(
repo,
&[(&two_sided_path, "side3"), (&three_sided_path, "side3")],
);
let tree = MergedTree::new(Merge::new(
vec![base1.clone(), base2.clone()],
vec![side1.clone(), side2.clone(), side3.clone()],
));
let conflicts = tree.conflicts().collect_vec();
let conflict_at = |path: &RepoPath| {
Merge::new(
vec![base1.path_value(path), base2.path_value(path)],
vec![
side1.path_value(path),
side2.path_value(path),
side3.path_value(path),
],
)
};
// Both paths have the full, unsimplified conflict (3-sided)
assert_eq!(
conflicts,
vec![
(two_sided_path.clone(), conflict_at(&two_sided_path)),
(three_sided_path.clone(), conflict_at(&three_sided_path))
]
);
// Iterating over conflicts in a legacy tree yields the simplified conflict at
// each path
let merged_legacy_tree = merge_trees(&side1, &base1, &side2).unwrap();
let merged_legacy_tree = merge_trees(&merged_legacy_tree, &base2, &side3).unwrap();
let legacy_conflicts = MergedTree::legacy(merged_legacy_tree)
.conflicts()
.collect_vec();
assert_eq!(
legacy_conflicts,
vec![
(
two_sided_path.clone(),
Merge::new(
vec![base2.path_value(&two_sided_path)],
vec![
side1.path_value(&two_sided_path),
side3.path_value(&two_sided_path),
],
)
),
(three_sided_path.clone(), conflict_at(&three_sided_path))
]
);
}