forked from mirrors/jj
tree: simplify conflict before resolving at hunk level
I ran into a bug the other day where `jj status` said there was a conflict in a file but there were no conflict markers in the working copy. The commit was created when I squashed a conflict resolution into the commit's parent. The rebased child commit then ended up in this state. I.e., it looked something like this before squashing: ``` C (no conflict) | | B conflict |/ A conflict ``` The conflict in B was different from the conflict in A. When I squashed in C, jj would try to resolve the conflicts by first creating a 7-way conflict (3 from A, 3 from B, 1 from C). Because of the exact content-level changes, the 7-way conflict couldn't be automatically resolved by `files::merge()` (the way it currently works anyway). However, after simplifying the conflict, it could be resolved. Because `MergedTree::merge()` does another round of conflict simplification of the result at the end of the function, it was the simplifed version that actually got stored in the commit. So when inspecting the conflict later (e.g. in the working copy, as I did), it could be automatically resolved. I think there are at least two ways to solve this. One is to call `merge_trees()` again after calling `tree.simplify()` in `MergedTree::merge()`. However, I think it would only matter in the case of content-level conflicts. Therefore, it seems better to make the content-level resolution solve this case to start with. I've done that by simplifying the conflict before passing it into `files::merge()`. We could even do the fix in `files::merge()`, but doing it before calling it has the advantage that we can avoid reading some unchanged content from the backend.
This commit is contained in:
parent
afdc3fc8b5
commit
51b5d168ae
3 changed files with 150 additions and 1 deletions
|
@ -392,7 +392,15 @@ impl MergedTree {
|
|||
// 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()))
|
||||
let tree = tree.simplify();
|
||||
// If debug assertions are enabled, check that the merge was idempotent. In
|
||||
// particular, that this last simplification doesn't enable further automatic
|
||||
// resolutions
|
||||
if cfg!(debug_assertions) {
|
||||
let re_merged = merge_trees(&tree).unwrap();
|
||||
debug_assert_eq!(re_merged, tree);
|
||||
}
|
||||
Ok(MergedTree::Merge(tree))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -433,6 +433,13 @@ pub fn try_resolve_file_conflict(
|
|||
executable,
|
||||
}));
|
||||
}
|
||||
|
||||
// Simplify the conflict for two reasons:
|
||||
// 1. Avoid reading unchanged file contents
|
||||
// 2. The simplified conflict can sometimes be resolved when the unsimplfied one
|
||||
// cannot
|
||||
let file_id_conflict = file_id_conflict.simplify();
|
||||
|
||||
let contents: Merge<Vec<u8>> =
|
||||
file_id_conflict.try_map(|&file_id| -> Result<Vec<u8>, TreeMergeError> {
|
||||
let mut content = vec![];
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
use itertools::Itertools;
|
||||
use jj_lib::backend::{FileId, MergedTreeId, TreeValue};
|
||||
use jj_lib::files::MergeResult;
|
||||
use jj_lib::matchers::{EverythingMatcher, FilesMatcher};
|
||||
use jj_lib::merge::{Merge, MergeBuilder};
|
||||
use jj_lib::merged_tree::{MergedTree, MergedTreeBuilder, MergedTreeValue};
|
||||
|
@ -1182,3 +1183,136 @@ fn test_merge_simplify_result() {
|
|||
let merged = side1_merged.merge(&base1_merged, &side2_merged).unwrap();
|
||||
assert_eq!(merged, expected_merged);
|
||||
}
|
||||
|
||||
/// Test that we simplify content-level conflicts before passing them to
|
||||
/// files::merge().
|
||||
///
|
||||
/// This is what happens when you squash a conflict resolution into a conflict
|
||||
/// and it gets propagated to a child where the conflict is different.
|
||||
#[test]
|
||||
fn test_merge_simplify_file_conflict() {
|
||||
let test_repo = TestRepo::init();
|
||||
let repo = &test_repo.repo;
|
||||
|
||||
let conflict_path = RepoPath::from_internal_string("CHANGELOG.md");
|
||||
let other_path = RepoPath::from_internal_string("other");
|
||||
|
||||
let prefix = r#"### New features
|
||||
|
||||
* The `ancestors()` revset function now takes an optional `depth` argument
|
||||
to limit the depth of the ancestor set. For example, use `jj log -r
|
||||
'ancestors(@, 5)` to view the last 5 commits.
|
||||
|
||||
* Support for the Watchman filesystem monitor is now bundled by default. Set
|
||||
`core.fsmonitor = "watchman"` in your repo to enable.
|
||||
"#;
|
||||
let suffix = r#"
|
||||
### Fixed bugs
|
||||
"#;
|
||||
let parent_base_text = format!(r#"{prefix}{suffix}"#);
|
||||
let parent_left_text = format!(
|
||||
r#"{prefix}
|
||||
* `jj op log` now supports `--no-graph`.
|
||||
{suffix}"#
|
||||
);
|
||||
let parent_right_text = format!(
|
||||
r#"{prefix}
|
||||
* You can now configure the set of immutable commits via
|
||||
`revsets.immutable-heads`. For example, set it to `"main"` to prevent
|
||||
rewriting commits on the `main` branch.
|
||||
{suffix}"#
|
||||
);
|
||||
let child1_right_text = format!(
|
||||
r#"{prefix}
|
||||
* You can now configure the set of immutable commits via
|
||||
`revsets.immutable-heads`. For example, set it to `"main"` to prevent
|
||||
rewriting commits on the `main` branch. The new `immutable()` revset resolves
|
||||
to these immutable commits.
|
||||
{suffix}"#
|
||||
);
|
||||
let child2_text = format!(
|
||||
r#"{prefix}
|
||||
* You can now configure the set of immutable commits via
|
||||
`revsets.immutable-heads`. For example, set it to `"main"` to prevent
|
||||
rewriting commits on the `main` branch.
|
||||
|
||||
* `jj op log` now supports `--no-graph`.
|
||||
{suffix}"#
|
||||
);
|
||||
let expected_text = format!(
|
||||
r#"{prefix}
|
||||
* You can now configure the set of immutable commits via
|
||||
`revsets.immutable-heads`. For example, set it to `"main"` to prevent
|
||||
rewriting commits on the `main` branch. The new `immutable()` revset resolves
|
||||
to these immutable commits.
|
||||
|
||||
* `jj op log` now supports `--no-graph`.
|
||||
{suffix}"#
|
||||
);
|
||||
|
||||
// conflict in parent commit
|
||||
let parent_base = create_single_tree(repo, &[(&conflict_path, &parent_base_text)]);
|
||||
let parent_left = create_single_tree(repo, &[(&conflict_path, &parent_left_text)]);
|
||||
let parent_right = create_single_tree(repo, &[(&conflict_path, &parent_right_text)]);
|
||||
let parent_merged = MergedTree::new(Merge::new(
|
||||
vec![parent_base],
|
||||
vec![parent_left, parent_right],
|
||||
));
|
||||
|
||||
// different conflict in child
|
||||
let child1_base = create_single_tree(
|
||||
repo,
|
||||
&[(&other_path, "child1"), (&conflict_path, &parent_base_text)],
|
||||
);
|
||||
let child1_left = create_single_tree(
|
||||
repo,
|
||||
&[(&other_path, "child1"), (&conflict_path, &parent_left_text)],
|
||||
);
|
||||
let child1_right = create_single_tree(
|
||||
repo,
|
||||
&[
|
||||
(&other_path, "child1"),
|
||||
(&conflict_path, &child1_right_text),
|
||||
],
|
||||
);
|
||||
let child1_merged = MergedTree::new(Merge::new(
|
||||
vec![child1_base],
|
||||
vec![child1_left, child1_right],
|
||||
));
|
||||
|
||||
// resolved state
|
||||
let child2 = create_single_tree(repo, &[(&conflict_path, &child2_text)]);
|
||||
let child2_merged = MergedTree::resolved(child2.clone());
|
||||
|
||||
// expected result
|
||||
let expected = create_single_tree(
|
||||
repo,
|
||||
&[(&other_path, "child1"), (&conflict_path, &expected_text)],
|
||||
);
|
||||
let expected_merged = MergedTree::resolved(expected);
|
||||
|
||||
let merged = child1_merged.merge(&parent_merged, &child2_merged).unwrap();
|
||||
assert_eq!(merged, expected_merged);
|
||||
|
||||
// Also test the setup by checking that the unsimplified content conflict cannot
|
||||
// be resolved. If we later change files::merge() so this no longer fails, it
|
||||
// probably means that we can delete this whole test (the Merge::simplify() call
|
||||
// in try_resolve_file_conflict() is just an optimization then).
|
||||
let text_merge = Merge::new(
|
||||
vec![Merge::new(
|
||||
vec![parent_base_text.as_bytes()],
|
||||
vec![parent_left_text.as_bytes(), parent_right_text.as_bytes()],
|
||||
)],
|
||||
vec![
|
||||
Merge::new(
|
||||
vec![parent_base_text.as_bytes()],
|
||||
vec![parent_left_text.as_bytes(), child1_right_text.as_bytes()],
|
||||
),
|
||||
Merge::resolved(child2_text.as_bytes()),
|
||||
],
|
||||
);
|
||||
assert!(matches!(
|
||||
jj_lib::files::merge(text_merge.flatten()),
|
||||
MergeResult::Conflict(_)
|
||||
));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue