trees: when merging trees and one is missing, treat it as empty

When a directory is missing in one merge input (base or one side), we
would consider that a merge conflict. This patch changes that so we
instead merge trees by treating the missing tree as empty.
This commit is contained in:
Martin von Zweigbergk 2022-04-17 20:51:24 -07:00 committed by Martin von Zweigbergk
parent 5e729eced7
commit 762c8984c6
3 changed files with 75 additions and 12 deletions

View File

@ -48,6 +48,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
but the contents was the same used to lead to a crash. That has now been but the contents was the same used to lead to a crash. That has now been
fixed. fixed.
* If one side of a merge modified a directory and the other side deleted it, it
used to be considered a conflict. The same was true if both sides added a
directory with different files in. They are now merged as if the missing
directory had been empty.
* `jj untrack` now requires at least one path (allowing no arguments was a UX * `jj untrack` now requires at least one path (allowing no arguments was a UX
bug). bug).

View File

@ -552,6 +552,19 @@ pub fn merge_trees(
store.write_tree(dir, &new_tree) store.write_tree(dir, &new_tree)
} }
/// Returns `Some(TreeId)` if this is a directory or missing. If it's missing,
/// we treat it as an empty tree.
fn maybe_tree_id<'id>(
value: Option<&'id TreeValue>,
empty_tree_id: &'id TreeId,
) -> Option<&'id TreeId> {
match value {
Some(TreeValue::Tree(id)) => Some(id),
None => Some(empty_tree_id),
_ => None,
}
}
fn merge_tree_value( fn merge_tree_value(
store: &Arc<Store>, store: &Arc<Store>,
dir: &RepoPath, dir: &RepoPath,
@ -566,19 +579,18 @@ fn merge_tree_value(
// * leave other conflicts (e.g. file/dir conflicts, remove/modify conflicts) // * leave other conflicts (e.g. file/dir conflicts, remove/modify conflicts)
// unresolved // unresolved
Ok(match (maybe_base, maybe_side1, maybe_side2) { let empty_tree_id = store.empty_tree_id();
( let base_tree_id = maybe_tree_id(maybe_base, empty_tree_id);
Some(TreeValue::Tree(base)), let side1_tree_id = maybe_tree_id(maybe_side1, empty_tree_id);
Some(TreeValue::Tree(side1)), let side2_tree_id = maybe_tree_id(maybe_side2, empty_tree_id);
Some(TreeValue::Tree(side2)), Ok(match (base_tree_id, side1_tree_id, side2_tree_id) {
) => { (Some(base_id), Some(side1_id), Some(side2_id)) => {
let subdir = dir.join(basename); let subdir = dir.join(basename);
let merged_tree_id = merge_trees( let base_tree = store.get_tree(&subdir, base_id)?;
&store.get_tree(&subdir, side1).unwrap(), let side1_tree = store.get_tree(&subdir, side1_id)?;
&store.get_tree(&subdir, base).unwrap(), let side2_tree = store.get_tree(&subdir, side2_id)?;
&store.get_tree(&subdir, side2).unwrap(), let merged_tree_id = merge_trees(&side1_tree, &base_tree, &side2_tree)?;
)?; if merged_tree_id == *empty_tree_id {
if &merged_tree_id == store.empty_tree_id() {
None None
} else { } else {
Some(TreeValue::Tree(merged_tree_id)) Some(TreeValue::Tree(merged_tree_id))

View File

@ -297,6 +297,52 @@ fn test_subtree_becomes_empty(use_git: bool) {
assert_eq!(merged_tree.id(), store.empty_tree_id()); assert_eq!(merged_tree.id(), store.empty_tree_id());
} }
#[test_case(false ; "local backend")]
#[test_case(true ; "git backend")]
fn test_subtree_one_missing(use_git: bool) {
// Tests that merging trees where one side is missing is resolved as if the
// missing side was empty.
let settings = testutils::user_settings();
let test_repo = testutils::init_repo(&settings, use_git);
let repo = &test_repo.repo;
let store = repo.store();
let write_tree = |paths: Vec<&str>| -> Tree {
let mut tree_builder = store.tree_builder(store.empty_tree_id().clone());
for path in paths {
testutils::write_normal_file(
&mut tree_builder,
&RepoPath::from_internal_string(path),
&format!("contents of {:?}", path),
);
}
let tree_id = tree_builder.write_tree();
store.get_tree(&RepoPath::root(), &tree_id).unwrap()
};
let tree1 = write_tree(vec![]);
let tree2 = write_tree(vec!["d1/f1"]);
let tree3 = write_tree(vec!["d1/f1", "d1/f2"]);
// The two sides add different trees
let merged_tree_id = tree::merge_trees(&tree2, &tree1, &tree3).unwrap();
let merged_tree = store.get_tree(&RepoPath::root(), &merged_tree_id).unwrap();
let expected_entries = write_tree(vec!["d1/f1", "d1/f2"]).entries().collect_vec();
assert_eq!(merged_tree.entries().collect_vec(), expected_entries);
// Same tree other way
let merged_tree_id = tree::merge_trees(&tree3, &tree1, &tree2).unwrap();
assert_eq!(merged_tree_id, *merged_tree.id());
// One side removes, the other side modifies
let merged_tree_id = tree::merge_trees(&tree1, &tree2, &tree3).unwrap();
let merged_tree = store.get_tree(&RepoPath::root(), &merged_tree_id).unwrap();
let expected_entries = write_tree(vec!["d1/f2"]).entries().collect_vec();
assert_eq!(merged_tree.entries().collect_vec(), expected_entries);
// Same tree other way
let merged_tree_id = tree::merge_trees(&tree3, &tree2, &tree1).unwrap();
assert_eq!(merged_tree_id, *merged_tree.id());
}
#[test_case(false ; "local backend")] #[test_case(false ; "local backend")]
#[test_case(true ; "git backend")] #[test_case(true ; "git backend")]
fn test_types(use_git: bool) { fn test_types(use_git: bool) {