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.
This commit is contained in:
Martin von Zweigbergk 2023-06-12 06:34:35 -07:00 committed by Martin von Zweigbergk
parent 4f30417ffd
commit 828d528361
4 changed files with 261 additions and 3 deletions

View file

@ -14,17 +14,20 @@
#![allow(missing_docs)]
use std::borrow::Borrow;
use std::hash::Hash;
use std::io::Write;
use std::sync::Arc;
use itertools::Itertools;
use crate::backend::{BackendResult, FileId, ObjectId, TreeValue};
use crate::backend::{BackendError, BackendResult, FileId, ObjectId, TreeId, TreeValue};
use crate::diff::{find_line_ranges, Diff, DiffHunk};
use crate::files::{ContentHunk, MergeResult};
use crate::merge::trivial_merge;
use crate::repo_path::RepoPath;
use crate::store::Store;
use crate::tree::Tree;
use crate::{backend, files};
const CONFLICT_START_LINE: &[u8] = b"<<<<<<<\n";
@ -366,6 +369,44 @@ impl Conflict<Option<FileId>> {
}
}
impl<T> Conflict<Option<T>>
where
T: Borrow<TreeValue>,
{
/// If every non-`None` term of a `Conflict<Option<TreeValue>>`
/// is a `TreeValue::Tree`, this converts it to
/// a `Conflict<Tree>`, with empty trees instead of
/// any `None` terms. Otherwise, returns `None`.
pub fn to_tree_conflict(
&self,
store: &Arc<Store>,
dir: &RepoPath,
) -> Result<Option<Conflict<Tree>>, BackendError> {
let tree_id_conflict = self.maybe_map(|term| match term {
None => Some(None),
Some(value) => {
if let TreeValue::Tree(id) = value.borrow() {
Some(Some(id))
} else {
None
}
}
});
if let Some(tree_id_conflict) = tree_id_conflict {
let get_tree = |id: &Option<&TreeId>| -> Result<Tree, BackendError> {
if let Some(id) = id {
store.get_tree(dir, id)
} else {
Ok(Tree::null(store.clone(), dir.clone()))
}
};
Ok(Some(tree_id_conflict.try_map(get_tree)?))
} else {
Ok(None)
}
}
}
fn describe_conflict_term(value: &TreeValue) -> String {
match value {
TreeValue::File {

View file

@ -15,14 +15,16 @@
//! A lazily merged view of a set of trees.
use std::cmp::max;
use std::sync::Arc;
use itertools::Itertools;
use crate::backend;
use crate::backend::TreeValue;
use crate::conflicts::Conflict;
use crate::repo_path::{RepoPath, RepoPathComponent};
use crate::store::Store;
use crate::tree::Tree;
use crate::tree::{try_resolve_file_conflict, Tree, TreeMergeError};
use crate::tree_builder::TreeBuilder;
/// Presents a view of a merged set of trees.
@ -154,4 +156,104 @@ impl MergedTree {
}
}
}
/// Tries to resolve any conflicts, resolving any conflicts that can be
/// automatically resolved and leaving the rest unresolved. The returned
/// conflict will either be resolved or have the same number of sides as
/// the input.
pub fn resolve(&self) -> Result<Conflict<Tree>, TreeMergeError> {
match self {
MergedTree::Legacy(tree) => Ok(Conflict::resolved(tree.clone())),
MergedTree::Merge(conflict) => merge_trees(conflict),
}
}
}
fn merge_trees(conflict: &Conflict<Tree>) -> Result<Conflict<Tree>, TreeMergeError> {
if let Some(tree) = conflict.resolve_trivial() {
return Ok(Conflict::resolved(tree.clone()));
}
let base_names = itertools::chain(conflict.removes(), conflict.adds())
.map(|tree| tree.data().names())
.kmerge()
.dedup();
let base_tree = &conflict.adds()[0];
let store = base_tree.store();
let dir = base_tree.dir();
// Keep resolved entries in `new_tree` and conflicted entries in `conflicts` to
// start with. Then we'll create the full trees later, and only if there are
// any conflicts.
let mut new_tree = backend::Tree::default();
let mut conflicts = vec![];
for basename in base_names {
let path_conflict = conflict.map(|tree| tree.value(basename).cloned());
let path_conflict = merge_tree_values(store, dir, path_conflict)?;
if let Some(value) = path_conflict.as_resolved() {
new_tree.set_or_remove(basename, value.clone());
} else {
conflicts.push((basename, path_conflict));
};
}
if conflicts.is_empty() {
let new_tree_id = store.write_tree(dir, new_tree)?;
Ok(Conflict::resolved(new_tree_id))
} else {
// For each side of the conflict, overwrite the entries in `new_tree` with the
// values from `conflicts`. Entries that are not in `conflicts` will remain
// unchanged and will be reused for each side.
let mut tree_removes = vec![];
for i in 0..conflict.removes().len() {
for (basename, path_conflict) in &conflicts {
new_tree.set_or_remove(basename, path_conflict.removes()[i].clone());
}
let tree = store.write_tree(dir, new_tree.clone())?;
tree_removes.push(tree);
}
let mut tree_adds = vec![];
for i in 0..conflict.adds().len() {
for (basename, path_conflict) in &conflicts {
new_tree.set_or_remove(basename, path_conflict.adds()[i].clone());
}
let tree = store.write_tree(dir, new_tree.clone())?;
tree_adds.push(tree);
}
Ok(Conflict::new(tree_removes, tree_adds))
}
}
/// Tries to resolve a conflict between tree values. Returns
/// Ok(Conflict::resolved(Some(value))) if the conflict was resolved, and
/// Ok(Conflict::resolved(None)) 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,
conflict: Conflict<Option<TreeValue>>,
) -> Result<Conflict<Option<TreeValue>>, TreeMergeError> {
if let Some(resolved) = conflict.resolve_trivial() {
return Ok(Conflict::resolved(resolved.clone()));
}
if let Some(tree_conflict) = conflict.to_tree_conflict(store, path)? {
// If all sides are trees or missing, merge the trees recursively, treating
// missing trees as empty.
let merged_tree = merge_trees(&tree_conflict)?;
if merged_tree.as_resolved().map(|tree| tree.id()) == Some(store.empty_tree_id()) {
Ok(Conflict::resolved(None))
} else {
Ok(merged_tree.map(|tree| Some(TreeValue::Tree(tree.id().clone()))))
}
} else {
// Try to resolve file conflicts by merging the file contents. Treats missing
// files as empty.
if let Some(resolved) = try_resolve_file_conflict(store, path, &conflict)? {
Ok(Conflict::resolved(Some(resolved)))
} else {
// Failed to merge the files, or the paths are not files
Ok(conflict)
}
}
}

View file

@ -581,7 +581,7 @@ fn merge_tree_value(
})
}
fn try_resolve_file_conflict(
pub fn try_resolve_file_conflict(
store: &Store,
filename: &RepoPath,
conflict: &Conflict<Option<TreeValue>>,

View file

@ -12,6 +12,7 @@
// 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};
@ -178,3 +179,117 @@ fn test_from_legacy_tree() {
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])
)
}