ok/jj
1
0
Fork 0
forked from mirrors/jj
jj/lib/tests/test_merged_tree.rs
Martin von Zweigbergk 828d528361 merged_tree: add a function for resolving conflicts
This adds a function for resolving conflicts that can be automatically
resolved, i.e. like our current `merge_trees()` function. However, the
new function is written to merge an arbitrary number of trees and, in
case of unresolvable conflicts, to produce a `Conflict<TreeId>` as
result instead of writing path-level conflicts to the backend. Like
`merge_trees()`, it still leaves conflicts unresolved at the file
level if any hunks conflict, and it resolves paths that can be
trivially resolved even if there are other paths that do conflict.
2023-07-19 22:04:16 -07:00

296 lines
11 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::conflicts::Conflict;
use jj_lib::merged_tree::{MergedTree, MergedTreeValue};
use jj_lib::repo::Repo;
use jj_lib::repo_path::{RepoPath, RepoPathComponent, RepoPathJoin};
use testutils::{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 = Conflict::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 = Conflict::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 = Conflict::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 = Conflict::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(Conflict::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(Conflict::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(Conflict::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(Conflict::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_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 = testutils::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 = testutils::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 = testutils::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 = testutils::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(Conflict::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 = testutils::create_tree(repo, &[(&path1, "base1"), (&path2, "base1")]);
let side1 = testutils::create_tree(repo, &[(&path2, "base1")]);
let side2 = testutils::create_tree(repo, &[(&path1, "base1")]);
let tree = MergedTree::new(Conflict::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 =
testutils::create_tree(repo, &[(&trivial_path, "base1"), (&conflict_path, "base1")]);
let side1 =
testutils::create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "side1")]);
let side2 =
testutils::create_tree(repo, &[(&trivial_path, "base1"), (&conflict_path, "side2")]);
let expected_base1 =
testutils::create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "base1")]);
let expected_side1 =
testutils::create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "side1")]);
let expected_side2 =
testutils::create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "side2")]);
let tree = MergedTree::new(Conflict::new(vec![base1], vec![side1, side2]));
let resolved_tree = tree.resolve().unwrap();
assert_eq!(
resolved_tree,
Conflict::new(vec![expected_base1], vec![expected_side1, expected_side2])
)
}