merged_tree: add a function for merging 3 MergedTrees

With the already existing `MergedTree::resolve()` and all the recent
refactorings into `Merge<T>`, it's now very easy to add support for
3-way merging of `MergedTree` instances.
This commit is contained in:
Martin von Zweigbergk 2023-06-29 06:22:22 -07:00 committed by Martin von Zweigbergk
parent 1674a421ec
commit 873a6f0674
2 changed files with 177 additions and 4 deletions

View file

@ -22,7 +22,6 @@ use std::{iter, vec};
use itertools::Itertools;
use crate::backend;
use crate::backend::{BackendError, BackendResult, ConflictId, MergedTreeId, TreeId, TreeValue};
use crate::matchers::{EverythingMatcher, Matcher};
use crate::merge::{Merge, MergeBuilder};
@ -30,9 +29,10 @@ use crate::repo_path::{RepoPath, RepoPathComponent, RepoPathJoin};
use crate::store::Store;
use crate::tree::{try_resolve_file_conflict, Tree, TreeMergeError};
use crate::tree_builder::TreeBuilder;
use crate::{backend, tree};
/// Presents a view of a merged set of trees.
#[derive(Clone, Debug)]
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum MergedTree {
/// A single tree, possibly with path-level conflicts.
Legacy(Tree),
@ -321,6 +321,41 @@ impl MergedTree {
) -> TreeDiffIterator<'matcher> {
TreeDiffIterator::new(self.clone(), other.clone(), matcher)
}
/// Merges this tree with `other`, using `base` as base.
pub fn merge(
&self,
base: &MergedTree,
other: &MergedTree,
) -> Result<MergedTree, TreeMergeError> {
if let (MergedTree::Legacy(this), MergedTree::Legacy(base), MergedTree::Legacy(other)) =
(self, base, other)
{
let merged_tree = tree::merge_trees(this, base, other)?;
Ok(MergedTree::legacy(merged_tree))
} else {
// Convert legacy trees to merged trees and unwrap to `Merge<Tree>`
let to_merge = |tree: &MergedTree| -> Merge<Tree> {
match tree {
MergedTree::Legacy(tree) => {
let MergedTree::Merge(tree) = MergedTree::from_legacy_tree(tree.clone())
else {
unreachable!();
};
tree
}
MergedTree::Merge(conflict) => conflict.clone(),
}
};
let nested = Merge::new(vec![to_merge(base)], vec![to_merge(self), to_merge(other)]);
let tree = merge_trees(&nested.flatten().simplify())?;
// If the result can be resolved, then `merge_trees()` above would have returned
// a resolved merge. However, that function will always preserve the arity of
// conflicts it cannot resolve. So we simplify the conflict again
// here to possibly reduce a complex conflict to a simpler one.
Ok(MergedTree::Merge(tree.simplify()))
}
}
}
fn all_tree_conflict_names(trees: &Merge<Tree>) -> impl Iterator<Item = &RepoPathComponent> {
@ -385,8 +420,8 @@ fn merge_trees(merge: &Merge<Tree>) -> Result<Merge<Tree>, TreeMergeError> {
/// Tries to resolve a conflict between tree values. Returns
/// Ok(Merge::normal(value)) if the conflict was resolved, and
/// Ok(Merge::absent()) if the path should be removed. Returns the conflict
/// unmodified if it cannot be resolved automatically.
/// Ok(Merge::absent()) if the path should be removed. Returns the
/// conflict unmodified if it cannot be resolved automatically.
fn merge_tree_values(
store: &Arc<Store>,
path: &RepoPath,

View file

@ -1028,3 +1028,141 @@ fn test_diff_dir_file() {
];
assert_eq!(actual_diff, expected_diff);
}
/// Merge 3 resolved trees that can be resolved
#[test]
fn test_merge_simple() {
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
let path1 = RepoPath::from_internal_string("dir1/file");
let path2 = RepoPath::from_internal_string("dir2/file");
let base1 = testutils::create_tree(repo, &[(&path1, "base"), (&path2, "base")]);
let side1 = testutils::create_tree(repo, &[(&path1, "side1"), (&path2, "base")]);
let side2 = testutils::create_tree(repo, &[(&path1, "base"), (&path2, "side2")]);
let expected = testutils::create_tree(repo, &[(&path1, "side1"), (&path2, "side2")]);
let base1_merged = MergedTree::new(Merge::resolved(base1));
let side1_merged = MergedTree::new(Merge::resolved(side1));
let side2_merged = MergedTree::new(Merge::resolved(side2));
let expected_merged = MergedTree::new(Merge::resolved(expected));
let merged = side1_merged.merge(&base1_merged, &side2_merged).unwrap();
assert_eq!(merged, expected_merged);
}
/// Merge 3 resolved trees that can be partially resolved
#[test]
fn test_merge_partial_resolution() {
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
// path1 can be resolved, path2 cannot
let path1 = RepoPath::from_internal_string("dir1/file");
let path2 = RepoPath::from_internal_string("dir2/file");
let base1 = testutils::create_tree(repo, &[(&path1, "base"), (&path2, "base")]);
let side1 = testutils::create_tree(repo, &[(&path1, "side1"), (&path2, "side1")]);
let side2 = testutils::create_tree(repo, &[(&path1, "base"), (&path2, "side2")]);
let expected_base1 = testutils::create_tree(repo, &[(&path1, "side1"), (&path2, "base")]);
let expected_side1 = testutils::create_tree(repo, &[(&path1, "side1"), (&path2, "side1")]);
let expected_side2 = testutils::create_tree(repo, &[(&path1, "side1"), (&path2, "side2")]);
let base1_merged = MergedTree::new(Merge::resolved(base1));
let side1_merged = MergedTree::new(Merge::resolved(side1));
let side2_merged = MergedTree::new(Merge::resolved(side2));
let expected_merged = MergedTree::new(Merge::new(
vec![expected_base1],
vec![expected_side1, expected_side2],
));
let merged = side1_merged.merge(&base1_merged, &side2_merged).unwrap();
assert_eq!(merged, expected_merged);
}
/// Merge 3 resolved trees, including one empty legacy tree
#[test]
fn test_merge_with_empty_legacy_tree() {
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
let path1 = RepoPath::from_internal_string("dir1/file");
let path2 = RepoPath::from_internal_string("dir2/file");
let base1 = repo
.store()
.get_tree(&RepoPath::root(), repo.store().empty_tree_id())
.unwrap();
let side1 = testutils::create_tree(repo, &[(&path1, "side1")]);
let side2 = testutils::create_tree(repo, &[(&path2, "side2")]);
let expected = testutils::create_tree(repo, &[(&path1, "side1"), (&path2, "side2")]);
let base1_merged = MergedTree::legacy(base1);
let side1_merged = MergedTree::new(Merge::resolved(side1));
let side2_merged = MergedTree::new(Merge::resolved(side2));
let expected_merged = MergedTree::new(Merge::resolved(expected));
let merged = side1_merged.merge(&base1_merged, &side2_merged).unwrap();
assert_eq!(merged, expected_merged);
}
/// Merge 3 trees where each one is a 3-way conflict and the result is arrived
/// at by only simplifying the conflict (no need to recurse)
#[test]
fn test_merge_simplify_only() {
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
let path = RepoPath::from_internal_string("dir1/file");
let tree1 = testutils::create_tree(repo, &[(&path, "1")]);
let tree2 = testutils::create_tree(repo, &[(&path, "2")]);
let tree3 = testutils::create_tree(repo, &[(&path, "3")]);
let tree4 = testutils::create_tree(repo, &[(&path, "4")]);
let tree5 = testutils::create_tree(repo, &[(&path, "5")]);
let expected = tree5.clone();
let base1_merged = MergedTree::new(Merge::new(
vec![tree1.clone()],
vec![tree2.clone(), tree3.clone()],
));
let side1_merged = MergedTree::new(Merge::new(
vec![tree1.clone()],
vec![tree4.clone(), tree2.clone()],
));
let side2_merged = MergedTree::new(Merge::new(
vec![tree4.clone()],
vec![tree5.clone(), tree3.clone()],
));
let expected_merged = MergedTree::new(Merge::resolved(expected));
let merged = side1_merged.merge(&base1_merged, &side2_merged).unwrap();
assert_eq!(merged, expected_merged);
}
/// Merge 3 trees with 3+1+1 terms (i.e. a 5-way conflict) such that resolving
/// the conflict between the trees leads to two trees being the same, so the
/// result is a 3-way conflict.
#[test]
fn test_merge_simplify_result() {
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
// The conflict in path1 cannot be resolved, but the conflict in path2 can.
let path1 = RepoPath::from_internal_string("dir1/file");
let path2 = RepoPath::from_internal_string("dir2/file");
let tree1 = testutils::create_tree(repo, &[(&path1, "1"), (&path2, "1")]);
let tree2 = testutils::create_tree(repo, &[(&path1, "2"), (&path2, "2")]);
let tree3 = testutils::create_tree(repo, &[(&path1, "3"), (&path2, "3")]);
let tree4 = testutils::create_tree(repo, &[(&path1, "4"), (&path2, "2")]);
let tree5 = testutils::create_tree(repo, &[(&path1, "4"), (&path2, "1")]);
let expected_base1 = testutils::create_tree(repo, &[(&path1, "1"), (&path2, "3")]);
let expected_side1 = testutils::create_tree(repo, &[(&path1, "2"), (&path2, "3")]);
let expected_side2 = testutils::create_tree(repo, &[(&path1, "3"), (&path2, "3")]);
let side1_merged = MergedTree::new(Merge::new(
vec![tree1.clone()],
vec![tree2.clone(), tree3.clone()],
));
let base1_merged = MergedTree::new(Merge::resolved(tree4.clone()));
let side2_merged = MergedTree::new(Merge::resolved(tree5.clone()));
let expected_merged = MergedTree::new(Merge::new(
vec![expected_base1],
vec![expected_side1, expected_side2],
));
let merged = side1_merged.merge(&base1_merged, &side2_merged).unwrap();
assert_eq!(merged, expected_merged);
}