mirror of
https://github.com/martinvonz/jj.git
synced 2025-02-06 03:22:59 +00:00
merged_tree: add a function for merging 3 MergedTree
s
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:
parent
1674a421ec
commit
873a6f0674
2 changed files with 177 additions and 4 deletions
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue