forked from mirrors/jj
0108673087
This moves the logic for handling the root commit when writing commits from `CommitBuilder` into the individual backends. It always bothered me a bit that the `commit::Commit` wrapper had a different idea of the number of parents than the wrapped `backend::Commit` had. With this change, the `LocalBackend` will now write the root commit in the list of parents if it's there in the argument to `write_commit()`. Note that root commit itself won't be written. The main argument for not writing it is that we can then keep the fake all-zeros hash for it. One argument for writing it, if we were to do so, is that it would make the set of written objects consistent, so any future processing of them (such as GC) doesn't have to know to ignore the root commit in the list of parents. We still treat the two backends the same, so the user won't be allowed to create merges including the root commit even when using the `LocalBackend`.
636 lines
23 KiB
Rust
636 lines
23 KiB
Rust
// Copyright 2020 Google LLC
|
|
//
|
|
// 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 assert_matches::assert_matches;
|
|
use itertools::Itertools;
|
|
use jujutsu_lib::backend::{ConflictPart, TreeValue};
|
|
use jujutsu_lib::commit_builder::CommitBuilder;
|
|
use jujutsu_lib::repo_path::{RepoPath, RepoPathComponent};
|
|
use jujutsu_lib::rewrite::rebase_commit;
|
|
use jujutsu_lib::testutils::TestRepo;
|
|
use jujutsu_lib::tree::Tree;
|
|
use jujutsu_lib::{testutils, tree};
|
|
use test_case::test_case;
|
|
|
|
#[test_case(false ; "local backend")]
|
|
#[test_case(true ; "git backend")]
|
|
fn test_same_type(use_git: bool) {
|
|
// Tests all possible cases where the entry type is unchanged, specifically
|
|
// using only normal files in all trees (no symlinks, no trees, etc.).
|
|
|
|
let test_repo = TestRepo::init(use_git);
|
|
let repo = &test_repo.repo;
|
|
let store = repo.store();
|
|
|
|
// The file name encodes the state in the base and in each side ("_" means
|
|
// missing)
|
|
let files = vec![
|
|
"__a", // side 2 added
|
|
"_a_", // side 1 added
|
|
"_aa", // both sides added, same content
|
|
"_ab", // both sides added, different content
|
|
"a__", // both sides removed
|
|
"a_a", // side 1 removed
|
|
"a_b", // side 1 removed, side 2 modified
|
|
"aa_", // side 2 removed
|
|
"aaa", // no changes
|
|
"aab", // side 2 modified
|
|
"ab_", // side 1 modified, side 2 removed
|
|
"aba", // side 1 modified
|
|
"abb", // both sides modified, same content
|
|
"abc", // both sides modified, different content
|
|
];
|
|
|
|
let write_tree = |index: usize| -> Tree {
|
|
let mut tree_builder = store.tree_builder(store.empty_tree_id().clone());
|
|
for path in &files {
|
|
let contents = &path[index..index + 1];
|
|
if contents != "_" {
|
|
testutils::write_normal_file(
|
|
&mut tree_builder,
|
|
&RepoPath::from_internal_string(path),
|
|
contents,
|
|
);
|
|
}
|
|
}
|
|
let tree_id = tree_builder.write_tree();
|
|
store.get_tree(&RepoPath::root(), &tree_id).unwrap()
|
|
};
|
|
|
|
let base_tree = write_tree(0);
|
|
let side1_tree = write_tree(1);
|
|
let side2_tree = write_tree(2);
|
|
|
|
// Create the merged tree
|
|
let merged_tree_id = tree::merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap();
|
|
let merged_tree = store.get_tree(&RepoPath::root(), &merged_tree_id).unwrap();
|
|
|
|
// Check that we have exactly the paths we expect in the merged tree
|
|
let names = merged_tree
|
|
.entries_non_recursive()
|
|
.map(|entry| entry.name().as_str())
|
|
.collect_vec();
|
|
assert_eq!(
|
|
names,
|
|
vec!["__a", "_a_", "_aa", "_ab", "a_b", "aaa", "aab", "ab_", "aba", "abb", "abc",]
|
|
);
|
|
|
|
// Check that the simple, non-conflicting cases were resolved correctly
|
|
assert_eq!(
|
|
merged_tree.value(&RepoPathComponent::from("__a")),
|
|
side2_tree.value(&RepoPathComponent::from("__a"))
|
|
);
|
|
assert_eq!(
|
|
merged_tree.value(&RepoPathComponent::from("_a_")),
|
|
side1_tree.value(&RepoPathComponent::from("_a_"))
|
|
);
|
|
assert_eq!(
|
|
merged_tree.value(&RepoPathComponent::from("_aa")),
|
|
side1_tree.value(&RepoPathComponent::from("_aa"))
|
|
);
|
|
assert_eq!(
|
|
merged_tree.value(&RepoPathComponent::from("aaa")),
|
|
side1_tree.value(&RepoPathComponent::from("aaa"))
|
|
);
|
|
assert_eq!(
|
|
merged_tree.value(&RepoPathComponent::from("aab")),
|
|
side2_tree.value(&RepoPathComponent::from("aab"))
|
|
);
|
|
assert_eq!(
|
|
merged_tree.value(&RepoPathComponent::from("aba")),
|
|
side1_tree.value(&RepoPathComponent::from("aba"))
|
|
);
|
|
assert_eq!(
|
|
merged_tree.value(&RepoPathComponent::from("abb")),
|
|
side1_tree.value(&RepoPathComponent::from("abb"))
|
|
);
|
|
|
|
// Check the conflicting cases
|
|
let component = RepoPathComponent::from("_ab");
|
|
match merged_tree.value(&component).unwrap() {
|
|
TreeValue::Conflict(id) => {
|
|
let conflict = store
|
|
.read_conflict(&RepoPath::from_internal_string("_ab"), id)
|
|
.unwrap();
|
|
assert_eq!(
|
|
conflict.adds,
|
|
vec![
|
|
ConflictPart {
|
|
value: side1_tree.value(&component).cloned().unwrap()
|
|
},
|
|
ConflictPart {
|
|
value: side2_tree.value(&component).cloned().unwrap()
|
|
}
|
|
]
|
|
);
|
|
assert!(conflict.removes.is_empty());
|
|
}
|
|
_ => panic!("unexpected value"),
|
|
};
|
|
let component = RepoPathComponent::from("a_b");
|
|
match merged_tree.value(&component).unwrap() {
|
|
TreeValue::Conflict(id) => {
|
|
let conflict = store
|
|
.read_conflict(&RepoPath::from_internal_string("a_b"), id)
|
|
.unwrap();
|
|
assert_eq!(
|
|
conflict.removes,
|
|
vec![ConflictPart {
|
|
value: base_tree.value(&component).cloned().unwrap()
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
conflict.adds,
|
|
vec![ConflictPart {
|
|
value: side2_tree.value(&component).cloned().unwrap()
|
|
}]
|
|
);
|
|
}
|
|
_ => panic!("unexpected value"),
|
|
};
|
|
let component = RepoPathComponent::from("ab_");
|
|
match merged_tree.value(&component).unwrap() {
|
|
TreeValue::Conflict(id) => {
|
|
let conflict = store
|
|
.read_conflict(&RepoPath::from_internal_string("ab_"), id)
|
|
.unwrap();
|
|
assert_eq!(
|
|
conflict.removes,
|
|
vec![ConflictPart {
|
|
value: base_tree.value(&component).cloned().unwrap()
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
conflict.adds,
|
|
vec![ConflictPart {
|
|
value: side1_tree.value(&component).cloned().unwrap()
|
|
}]
|
|
);
|
|
}
|
|
_ => panic!("unexpected value"),
|
|
};
|
|
let component = RepoPathComponent::from("abc");
|
|
match merged_tree.value(&component).unwrap() {
|
|
TreeValue::Conflict(id) => {
|
|
let conflict = store
|
|
.read_conflict(&RepoPath::from_internal_string("abc"), id)
|
|
.unwrap();
|
|
assert_eq!(
|
|
conflict.removes,
|
|
vec![ConflictPart {
|
|
value: base_tree.value(&component).cloned().unwrap()
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
conflict.adds,
|
|
vec![
|
|
ConflictPart {
|
|
value: side1_tree.value(&component).cloned().unwrap()
|
|
},
|
|
ConflictPart {
|
|
value: side2_tree.value(&component).cloned().unwrap()
|
|
}
|
|
]
|
|
);
|
|
}
|
|
_ => panic!("unexpected value"),
|
|
};
|
|
}
|
|
|
|
#[test_case(false ; "local backend")]
|
|
#[test_case(true ; "git backend")]
|
|
fn test_subtrees(use_git: bool) {
|
|
// Tests that subtrees are merged.
|
|
|
|
let test_repo = TestRepo::init(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 base_tree = write_tree(vec!["f1", "d1/f1", "d1/d1/f1", "d1/d1/d1/f1"]);
|
|
let side1_tree = write_tree(vec![
|
|
"f1",
|
|
"f2",
|
|
"d1/f1",
|
|
"d1/f2",
|
|
"d1/d1/f1",
|
|
"d1/d1/d1/f1",
|
|
]);
|
|
let side2_tree = write_tree(vec![
|
|
"f1",
|
|
"d1/f1",
|
|
"d1/d1/f1",
|
|
"d1/d1/d1/f1",
|
|
"d1/d1/d1/f2",
|
|
]);
|
|
|
|
let merged_tree_id = tree::merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap();
|
|
let merged_tree = store.get_tree(&RepoPath::root(), &merged_tree_id).unwrap();
|
|
let entries = merged_tree.entries().collect_vec();
|
|
|
|
let expected_tree = write_tree(vec![
|
|
"f1",
|
|
"f2",
|
|
"d1/f1",
|
|
"d1/f2",
|
|
"d1/d1/f1",
|
|
"d1/d1/d1/f1",
|
|
"d1/d1/d1/f2",
|
|
]);
|
|
let expected_entries = expected_tree.entries().collect_vec();
|
|
assert_eq!(entries, expected_entries);
|
|
}
|
|
|
|
#[test_case(false ; "local backend")]
|
|
#[test_case(true ; "git backend")]
|
|
fn test_subtree_becomes_empty(use_git: bool) {
|
|
// Tests that subtrees that become empty are removed from the parent tree.
|
|
|
|
let test_repo = TestRepo::init(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 base_tree = write_tree(vec!["f1", "d1/f1", "d1/d1/d1/f1", "d1/d1/d1/f2"]);
|
|
let side1_tree = write_tree(vec!["f1", "d1/f1", "d1/d1/d1/f1"]);
|
|
let side2_tree = write_tree(vec!["d1/d1/d1/f2"]);
|
|
|
|
let merged_tree_id = tree::merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap();
|
|
let merged_tree = store.get_tree(&RepoPath::root(), &merged_tree_id).unwrap();
|
|
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 test_repo = TestRepo::init(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(true ; "git backend")]
|
|
fn test_types(use_git: bool) {
|
|
// Tests conflicts between different types. This is mostly to test that the
|
|
// conflicts survive the roundtrip to the store.
|
|
|
|
let test_repo = TestRepo::init(use_git);
|
|
let repo = &test_repo.repo;
|
|
let store = repo.store();
|
|
|
|
let mut base_tree_builder = store.tree_builder(store.empty_tree_id().clone());
|
|
let mut side1_tree_builder = store.tree_builder(store.empty_tree_id().clone());
|
|
let mut side2_tree_builder = store.tree_builder(store.empty_tree_id().clone());
|
|
testutils::write_normal_file(
|
|
&mut base_tree_builder,
|
|
&RepoPath::from_internal_string("normal_executable_symlink"),
|
|
"contents",
|
|
);
|
|
testutils::write_executable_file(
|
|
&mut side1_tree_builder,
|
|
&RepoPath::from_internal_string("normal_executable_symlink"),
|
|
"contents",
|
|
);
|
|
testutils::write_symlink(
|
|
&mut side2_tree_builder,
|
|
&RepoPath::from_internal_string("normal_executable_symlink"),
|
|
"contents",
|
|
);
|
|
let tree_id = store.empty_tree_id().clone();
|
|
base_tree_builder.set(
|
|
RepoPath::from_internal_string("tree_normal_symlink"),
|
|
TreeValue::Tree(tree_id),
|
|
);
|
|
testutils::write_normal_file(
|
|
&mut side1_tree_builder,
|
|
&RepoPath::from_internal_string("tree_normal_symlink"),
|
|
"contents",
|
|
);
|
|
testutils::write_symlink(
|
|
&mut side2_tree_builder,
|
|
&RepoPath::from_internal_string("tree_normal_symlink"),
|
|
"contents",
|
|
);
|
|
let base_tree_id = base_tree_builder.write_tree();
|
|
let base_tree = store.get_tree(&RepoPath::root(), &base_tree_id).unwrap();
|
|
let side1_tree_id = side1_tree_builder.write_tree();
|
|
let side1_tree = store.get_tree(&RepoPath::root(), &side1_tree_id).unwrap();
|
|
let side2_tree_id = side2_tree_builder.write_tree();
|
|
let side2_tree = store.get_tree(&RepoPath::root(), &side2_tree_id).unwrap();
|
|
|
|
// Created the merged tree
|
|
let merged_tree_id = tree::merge_trees(&side1_tree, &base_tree, &side2_tree).unwrap();
|
|
let merged_tree = store.get_tree(&RepoPath::root(), &merged_tree_id).unwrap();
|
|
|
|
// Check the conflicting cases
|
|
let component = RepoPathComponent::from("normal_executable_symlink");
|
|
match merged_tree.value(&component).unwrap() {
|
|
TreeValue::Conflict(id) => {
|
|
let conflict = store
|
|
.read_conflict(
|
|
&RepoPath::from_internal_string("normal_executable_symlink"),
|
|
id,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
conflict.removes,
|
|
vec![ConflictPart {
|
|
value: base_tree.value(&component).cloned().unwrap()
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
conflict.adds,
|
|
vec![
|
|
ConflictPart {
|
|
value: side1_tree.value(&component).cloned().unwrap()
|
|
},
|
|
ConflictPart {
|
|
value: side2_tree.value(&component).cloned().unwrap()
|
|
},
|
|
]
|
|
);
|
|
}
|
|
_ => panic!("unexpected value"),
|
|
};
|
|
let component = RepoPathComponent::from("tree_normal_symlink");
|
|
match merged_tree.value(&component).unwrap() {
|
|
TreeValue::Conflict(id) => {
|
|
let conflict = store
|
|
.read_conflict(&RepoPath::from_internal_string("tree_normal_symlink"), id)
|
|
.unwrap();
|
|
assert_eq!(
|
|
conflict.removes,
|
|
vec![ConflictPart {
|
|
value: base_tree.value(&component).cloned().unwrap()
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
conflict.adds,
|
|
vec![
|
|
ConflictPart {
|
|
value: side1_tree.value(&component).cloned().unwrap()
|
|
},
|
|
ConflictPart {
|
|
value: side2_tree.value(&component).cloned().unwrap()
|
|
},
|
|
]
|
|
);
|
|
}
|
|
_ => panic!("unexpected value"),
|
|
};
|
|
}
|
|
|
|
#[test_case(false ; "local backend")]
|
|
#[test_case(true ; "git backend")]
|
|
fn test_simplify_conflict(use_git: bool) {
|
|
let test_repo = TestRepo::init(use_git);
|
|
let repo = &test_repo.repo;
|
|
let store = repo.store();
|
|
|
|
let component = RepoPathComponent::from("file");
|
|
let path = RepoPath::from_internal_string("file");
|
|
let write_tree =
|
|
|contents: &str| -> Tree { testutils::create_tree(repo, &[(&path, contents)]) };
|
|
|
|
let base_tree = write_tree("base contents");
|
|
let branch_tree = write_tree("branch contents");
|
|
let upstream1_tree = write_tree("upstream1 contents");
|
|
let upstream2_tree = write_tree("upstream2 contents");
|
|
|
|
let merge_trees = |base: &Tree, side1: &Tree, side2: &Tree| -> Tree {
|
|
let tree_id = tree::merge_trees(side1, base, side2).unwrap();
|
|
store.get_tree(&RepoPath::root(), &tree_id).unwrap()
|
|
};
|
|
|
|
// Rebase the branch tree to the first upstream tree
|
|
let rebased1_tree = merge_trees(&base_tree, &branch_tree, &upstream1_tree);
|
|
// Make sure we have a conflict (testing the test setup)
|
|
match rebased1_tree.value(&component).unwrap() {
|
|
TreeValue::Conflict(_) => {
|
|
// expected
|
|
}
|
|
_ => panic!("unexpected value"),
|
|
};
|
|
|
|
// Rebase the rebased tree back to the base. The conflict should be gone. Try
|
|
// both directions.
|
|
let rebased_back_tree = merge_trees(&upstream1_tree, &rebased1_tree, &base_tree);
|
|
assert_eq!(
|
|
rebased_back_tree.value(&component),
|
|
branch_tree.value(&component)
|
|
);
|
|
let rebased_back_tree = merge_trees(&upstream1_tree, &base_tree, &rebased1_tree);
|
|
assert_eq!(
|
|
rebased_back_tree.value(&component),
|
|
branch_tree.value(&component)
|
|
);
|
|
|
|
// Rebase the rebased tree further upstream. The conflict should be simplified
|
|
// to not mention the contents from the first rebase.
|
|
let further_rebased_tree = merge_trees(&upstream1_tree, &rebased1_tree, &upstream2_tree);
|
|
match further_rebased_tree.value(&component).unwrap() {
|
|
TreeValue::Conflict(id) => {
|
|
let conflict = store
|
|
.read_conflict(&RepoPath::from_components(vec![component.clone()]), id)
|
|
.unwrap();
|
|
assert_eq!(
|
|
conflict.removes,
|
|
vec![ConflictPart {
|
|
value: base_tree.value(&component).cloned().unwrap()
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
conflict.adds,
|
|
vec![
|
|
ConflictPart {
|
|
value: branch_tree.value(&component).cloned().unwrap()
|
|
},
|
|
ConflictPart {
|
|
value: upstream2_tree.value(&component).cloned().unwrap()
|
|
},
|
|
]
|
|
);
|
|
}
|
|
_ => panic!("unexpected value"),
|
|
};
|
|
let further_rebased_tree = merge_trees(&upstream1_tree, &upstream2_tree, &rebased1_tree);
|
|
match further_rebased_tree.value(&component).unwrap() {
|
|
TreeValue::Conflict(id) => {
|
|
let conflict = store.read_conflict(&path, id).unwrap();
|
|
assert_eq!(
|
|
conflict.removes,
|
|
vec![ConflictPart {
|
|
value: base_tree.value(&component).cloned().unwrap()
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
conflict.adds,
|
|
vec![
|
|
ConflictPart {
|
|
value: upstream2_tree.value(&component).cloned().unwrap()
|
|
},
|
|
ConflictPart {
|
|
value: branch_tree.value(&component).cloned().unwrap()
|
|
},
|
|
]
|
|
);
|
|
}
|
|
_ => panic!("unexpected value"),
|
|
};
|
|
}
|
|
|
|
#[test_case(false ; "local backend")]
|
|
#[test_case(true ; "git backend")]
|
|
fn test_simplify_conflict_after_resolving_parent(use_git: bool) {
|
|
let settings = testutils::user_settings();
|
|
let test_repo = TestRepo::init(use_git);
|
|
let repo = &test_repo.repo;
|
|
|
|
// Set up a repo like this:
|
|
// D
|
|
// | C
|
|
// | B
|
|
// |/
|
|
// A
|
|
//
|
|
// Commit A has a file with 3 lines. B and D make conflicting changes to the
|
|
// first line. C changes the third line. We then rebase B and C onto D,
|
|
// which creates a conflict. We resolve the conflict in the first line and
|
|
// rebase C2 (the rebased C) onto the resolved conflict. C3 should not have
|
|
// a conflict since it changed an unrelated line.
|
|
let path = RepoPath::from_internal_string("dir/file");
|
|
let mut tx = repo.start_transaction("test");
|
|
let tree_a = testutils::create_tree(repo, &[(&path, "abc\ndef\nghi\n")]);
|
|
let commit_a = CommitBuilder::for_new_commit(
|
|
&settings,
|
|
vec![repo.store().root_commit_id().clone()],
|
|
tree_a.id().clone(),
|
|
)
|
|
.write_to_repo(tx.mut_repo());
|
|
let tree_b = testutils::create_tree(repo, &[(&path, "Abc\ndef\nghi\n")]);
|
|
let commit_b =
|
|
CommitBuilder::for_new_commit(&settings, vec![commit_a.id().clone()], tree_b.id().clone())
|
|
.write_to_repo(tx.mut_repo());
|
|
let tree_c = testutils::create_tree(repo, &[(&path, "Abc\ndef\nGhi\n")]);
|
|
let commit_c =
|
|
CommitBuilder::for_new_commit(&settings, vec![commit_b.id().clone()], tree_c.id().clone())
|
|
.write_to_repo(tx.mut_repo());
|
|
let tree_d = testutils::create_tree(repo, &[(&path, "abC\ndef\nghi\n")]);
|
|
let commit_d =
|
|
CommitBuilder::for_new_commit(&settings, vec![commit_a.id().clone()], tree_d.id().clone())
|
|
.write_to_repo(tx.mut_repo());
|
|
|
|
let commit_b2 = rebase_commit(&settings, tx.mut_repo(), &commit_b, &[commit_d]);
|
|
let commit_c2 = rebase_commit(&settings, tx.mut_repo(), &commit_c, &[commit_b2.clone()]);
|
|
|
|
// Test the setup: Both B and C should have conflicts.
|
|
assert_matches!(
|
|
commit_b2.tree().path_value(&path),
|
|
Some(TreeValue::Conflict(_))
|
|
);
|
|
assert_matches!(
|
|
commit_c2.tree().path_value(&path),
|
|
Some(TreeValue::Conflict(_))
|
|
);
|
|
|
|
// Create the resolved B and rebase C on top.
|
|
let tree_b3 = testutils::create_tree(repo, &[(&path, "AbC\ndef\nghi\n")]);
|
|
let commit_b3 = CommitBuilder::for_rewrite_from(&settings, &commit_b2)
|
|
.set_tree(tree_b3.id().clone())
|
|
.write_to_repo(tx.mut_repo());
|
|
let commit_c3 = rebase_commit(&settings, tx.mut_repo(), &commit_c2, &[commit_b3]);
|
|
tx.mut_repo().rebase_descendants(&settings).unwrap();
|
|
let repo = tx.commit();
|
|
|
|
// The conflict should now be resolved.
|
|
let resolved_value = commit_c3.tree().path_value(&path);
|
|
match resolved_value {
|
|
Some(TreeValue::Normal {
|
|
id,
|
|
executable: false,
|
|
}) => {
|
|
assert_eq!(
|
|
testutils::read_file(repo.store(), &path, &id),
|
|
b"AbC\ndef\nGhi\n"
|
|
);
|
|
}
|
|
other => {
|
|
panic!("unexpected value: {:#?}", other);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Add tests for simplification of multi-way conflicts. Both the content
|
|
// and the executable bit need testing.
|