Skip to content

Commit bfa4aec

Browse files
committed
implement support for resolving irreconcilable tree conflicts with 'ours' or 'ancestor'
1 parent 3228de6 commit bfa4aec

File tree

8 files changed

+424
-114
lines changed

8 files changed

+424
-114
lines changed

crate-status.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,11 @@ Check out the [performance discussion][gix-diff-performance] as well.
352352
- [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same)
353353
* [x] **tree**-diff-heuristics match Git for its test-cases
354354
- [x] a way to generate an index with stages, mostly conforming with Git.
355+
- [ ] resolve to be *ours* or the *ancestors* version of the tree.
355356
- [ ] submodule merges (*right now they count as conflicts if they differ*)
356357
- [ ] assure sparse indices are handled correctly during application - right now we refuse.
358+
- [ ] rewrite so that the whole logic can be proven to be correct - it's too insane now and probably has way
359+
more possible states than are tested, despite best attempts.
357360
* [x] **commits** - with handling of multiple merge bases by recursive merge-base merge
358361
* [x] API documentation
359362
* [ ] Examples

gix-merge/src/tree/function.rs

Lines changed: 209 additions & 105 deletions
Large diffs are not rendered by default.

gix-merge/src/tree/mod.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ enum ConflictIndexEntryPathHint {
157157
}
158158

159159
/// A utility to help define which side is what in the [`Conflict`] type.
160-
#[derive(Debug, Clone, Copy)]
160+
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
161161
enum ConflictMapping {
162162
/// The sides are as described in the field documentation, i.e. `ours` is `ours`.
163163
Original,
@@ -198,7 +198,12 @@ impl Conflict {
198198
match &self.resolution {
199199
Ok(success) => match success {
200200
Resolution::SourceLocationAffectedByRename { .. } => false,
201-
Resolution::Forced(_) => how.tree_merge == treat_as_unresolved::TreeMerge::ForcedResolution,
201+
Resolution::Forced(_) => {
202+
how.tree_merge == treat_as_unresolved::TreeMerge::ForcedResolution
203+
|| self
204+
.content_merge()
205+
.map_or(false, |merged_blob| content_merge_matches(&merged_blob))
206+
}
202207
Resolution::OursModifiedTheirsRenamedAndChangedThenRename {
203208
merged_blob,
204209
final_location,
@@ -386,7 +391,10 @@ pub struct Options {
386391
pub enum ResolveWith {
387392
/// On irreconcilable conflict, choose neither *our* nor *their* state, but keep the common *ancestor* state instead.
388393
Ancestor,
389-
/// On irreconcilable conflict, choose *our* side
394+
/// On irreconcilable conflict, choose *our* side.
395+
///
396+
/// Note that in order to get something equivalent to *theirs*, put *theirs* into the side of *ours*,
397+
/// swapping the sides essentially.
390398
Ours,
391399
}
392400

gix-merge/src/tree/utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ impl TreeNodes {
504504
cursor = &mut self.0[existing_idx];
505505
debug_assert!(
506506
cursor.is_leaf_node(),
507-
"BUG: we should really only try to remove leaf nodes"
507+
"BUG: we should really only try to remove leaf nodes: {cursor:?}"
508508
);
509509
cursor.change_idx = None;
510510
} else {
34 KB
Binary file not shown.

gix-merge/tests/fixtures/tree-baseline.sh

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ function make_conflict_index() {
3636
cp .git/index .git/"${identifier}".index
3737
}
3838

39+
function make_resolve_tree() {
40+
local resolve=${1:?Their 'ancestor' or 'ours'}
41+
local our_side=${2:-}
42+
local their_side=${3:-}
43+
44+
local filename="resolve-${our_side}-${their_side}-with-${resolve}"
45+
git write-tree > ".git/${filename}.tree"
46+
}
47+
3948
function baseline () (
4049
local dir=${1:?the directory to enter}
4150
local output_name=${2:?the basename of the output of the merge}
@@ -1074,3 +1083,122 @@ baseline rename-and-modification A-B A B
10741083
baseline symlink-modification A-B A B
10751084
baseline symlink-addition A-B A B
10761085
baseline type-change-to-symlink A-B A B
1086+
1087+
##
1088+
## Only once the tree-merges were performed can we refer to their objects
1089+
## when making tree-conflict resolution expectations. It's important
1090+
## to get these right.
1091+
##
1092+
(cd simple
1093+
rm .git/index
1094+
# 'whatever' is tree-conflict, 'greeting' is content conflict with markers
1095+
git update-index --index-info <<EOF
1096+
100644 9dc97bdc2426e68423360e3e5299280b2cf6b8ff 0 greeting
1097+
100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers
1098+
100644 5716ca5987cbf97d6bb54920bea6adde242d87e6 0 whatever
1099+
EOF
1100+
make_resolve_tree ours side1 side2
1101+
1102+
rm .git/index
1103+
git update-index --index-info <<EOF
1104+
100644 1a2664a9924754c698e323f756f9f87f3f2fb337 0 greeting
1105+
100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers
1106+
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 whatever/empty
1107+
EOF
1108+
make_resolve_tree ours side2 side1
1109+
1110+
rm .git/index
1111+
git update-index --index-info <<EOF
1112+
100644 9dc97bdc2426e68423360e3e5299280b2cf6b8ff 0 greeting
1113+
100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers
1114+
100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 whatever
1115+
EOF
1116+
make_resolve_tree ancestor side1 side2
1117+
1118+
rm .git/index
1119+
git update-index --index-info <<EOF
1120+
100644 1a2664a9924754c698e323f756f9f87f3f2fb337 0 greeting
1121+
100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers
1122+
100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 whatever
1123+
EOF
1124+
make_resolve_tree ancestor side2 side1
1125+
1126+
rm .git/index
1127+
git update-index --index-info <<EOF
1128+
100644 a4ae6e4709228b5da6001cb9d1cfa7736851e2a6 0 greeting
1129+
100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 whatever
1130+
100644 542802a799ded74fa01c47ba2f8925e284a369e2 0 Αυτά μου φαίνονται κινέζικα
1131+
EOF
1132+
make_resolve_tree ancestor tweak1 side2
1133+
1134+
rm .git/index
1135+
git update-index --index-info <<EOF
1136+
100644 a4ae6e4709228b5da6001cb9d1cfa7736851e2a6 0 greeting
1137+
100644 5716ca5987cbf97d6bb54920bea6adde242d87e6 0 whatever
1138+
100644 542802a799ded74fa01c47ba2f8925e284a369e2 0 Αυτά μου φαίνονται κινέζικα
1139+
EOF
1140+
make_resolve_tree ours tweak1 side2
1141+
1142+
rm .git/index
1143+
git update-index --index-info <<EOF
1144+
100644 blob ea28dcd7f627a2a7bbd09daa679c452180617c9f greeting
1145+
100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 whatever
1146+
100644 blob f801a62deed900f8a80ff35e3339474ad6352a93 Αυτά μου φαίνονται κινέζικα
1147+
EOF
1148+
make_resolve_tree ancestor side2 tweak1
1149+
1150+
rm .git/index
1151+
git update-index --index-info <<EOF
1152+
100644 blob ea28dcd7f627a2a7bbd09daa679c452180617c9f greeting
1153+
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 whatever/empty
1154+
100644 blob f801a62deed900f8a80ff35e3339474ad6352a93 Αυτά μου φαίνονται κινέζικα
1155+
EOF
1156+
make_resolve_tree ours side2 tweak1
1157+
)
1158+
1159+
(cd rename-add-symlink
1160+
rm .git/index
1161+
# the symlink of 'bar' from A
1162+
git update-index --index-info <<EOF
1163+
120000 blob 19102815663d23f8b75a47e7a01965dcdc96468c bar
1164+
EOF
1165+
make_resolve_tree ours A B
1166+
1167+
rm .git/index
1168+
# the merged form of 'bar' from B, not replaced by symlink
1169+
git update-index --index-info <<EOF
1170+
100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 bar
1171+
EOF
1172+
make_resolve_tree ours B A
1173+
1174+
rm .git/index
1175+
# foo is renamed to bar, type clash means neither A nor B can be added - empty tree
1176+
# It is not able to 'get foo back', it can't track that currently.
1177+
make_resolve_tree ancestor A B
1178+
make_resolve_tree ancestor B A
1179+
)
1180+
1181+
(cd rename-rename-plus-content
1182+
rm .git/index
1183+
# both sides rename 'foo' into something else.
1184+
git update-index --index-info <<EOF
1185+
100644 blob 8a1218a1024a212bb3db30becd860315f9f3ac52 foo
1186+
EOF
1187+
make_resolve_tree ancestor A B
1188+
make_resolve_tree ancestor B A
1189+
1190+
rm .git/index
1191+
# 'bar' is the name in 'A', and there is a merge with the content from 'B' (conflicting)
1192+
git update-index --index-info <<EOF
1193+
100644 blob b3b7e96453e640c018ef2d2e984cd0a3641a7d93 bar
1194+
EOF
1195+
make_resolve_tree ours A B
1196+
1197+
rm .git/index
1198+
# 'baz' is the name in 'B', and there is a merge with the content from 'A' (conflicting)
1199+
git update-index --index-info <<EOF
1200+
100644 blob b3b7e96453e640c018ef2d2e984cd0a3641a7d93 baz
1201+
EOF
1202+
make_resolve_tree ours B A
1203+
)
1204+

gix-merge/tests/merge/tree/baseline.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub enum ConflictKind {
4848

4949
/// More loosely structured information about the `Conflict`.
5050
#[derive(Debug)]
51+
#[allow(dead_code)] // used only for debugging
5152
pub struct ConflictInfo {
5253
/// All the paths involved in the informational message
5354
pub paths: Vec<String>,
@@ -371,6 +372,22 @@ pub fn show_diff_and_fail(
371372
);
372373
}
373374

375+
pub fn show_diff_trees_and_fail(
376+
case_name: &str,
377+
actual_id: ObjectId,
378+
actual: &gix_merge::tree::Outcome<'_>,
379+
expected_tree_id: gix_hash::ObjectId,
380+
additional_information: &str,
381+
odb: &gix_odb::memory::Proxy<gix_odb::Handle>,
382+
) {
383+
pretty_assertions::assert_str_eq!(
384+
visualize_tree(&actual_id, odb, None).to_string(),
385+
visualize_tree(&expected_tree_id, odb, None).to_string(),
386+
"{case_name}: merged tree mismatch\n{:#?}\n{additional_information}\n{case_name}",
387+
actual.conflicts,
388+
);
389+
}
390+
374391
pub(crate) fn apply_git_index_entries(conflicts: &[Conflict], state: &mut gix_index::State) {
375392
let len = state.entries().len();
376393
for Conflict { ours, theirs, ancestor } in conflicts {

gix-merge/tests/merge/tree/mod.rs

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ fn run_baseline() -> crate::Result {
2121
let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?;
2222
let cases = std::fs::read_to_string(root.join("baseline.cases"))?;
2323
let mut actual_cases = 0;
24-
// let new_test = Some("rename-add-symlink-A-B");
25-
let new_test = None;
24+
let mut skipped_tree_resolve_cases = 0;
25+
let new_test = Some("rename-rename-plus-content");
26+
// let new_test = None;
2627
for baseline::Expectation {
2728
root,
2829
conflict_style,
@@ -63,7 +64,7 @@ fn run_baseline() -> crate::Result {
6364
&mut blob_merge,
6465
&odb,
6566
&mut |id| id.to_hex_with_len(7).to_string(),
66-
options,
67+
options.clone(),
6768
)?
6869
.tree_merge;
6970

@@ -138,12 +139,63 @@ fn run_baseline() -> crate::Result {
138139
actual.has_unresolved_conflicts(conflicts_like_in_git),
139140
"{case_name}: If there is any kind of conflict, the index should have been changed"
140141
);
142+
143+
// The content-merge mode is not relevant for the upcoming tree-conflict resolution.
144+
if case_name.contains("diff3") {
145+
continue;
146+
}
147+
148+
for tree_resolution in [
149+
gix_merge::tree::ResolveWith::Ancestor,
150+
gix_merge::tree::ResolveWith::Ours,
151+
] {
152+
let resolution_name = match tree_resolution {
153+
gix_merge::tree::ResolveWith::Ancestor => "ancestor",
154+
gix_merge::tree::ResolveWith::Ours => "ours",
155+
};
156+
let basename = format!("resolve-{our_side_name}-{their_side_name}-with-{resolution_name}");
157+
let tree_path = root.join(".git").join(format!("{basename}.tree"));
158+
if !tree_path.exists() {
159+
skipped_tree_resolve_cases += 1;
160+
continue;
161+
};
162+
let expected_tree_id = gix_hash::ObjectId::from_hex(std::fs::read_to_string(tree_path)?.trim().as_bytes())?;
163+
options.tree_merge.tree_conflicts = Some(tree_resolution);
164+
dbg!(&case_name, &basename);
165+
let mut actual = gix_merge::commit(
166+
our_commit_id,
167+
their_commit_id,
168+
gix_merge::blob::builtin_driver::text::Labels {
169+
ancestor: None,
170+
current: Some(our_side_name.as_str().into()),
171+
other: Some(their_side_name.as_str().into()),
172+
},
173+
&mut graph,
174+
&mut diff_resource_cache,
175+
&mut blob_merge,
176+
&odb,
177+
&mut |id| id.to_hex_with_len(7).to_string(),
178+
options.clone(),
179+
)?
180+
.tree_merge;
181+
182+
let actual_id = actual.tree.write(|tree| odb.write(tree))?;
183+
if actual_id != expected_tree_id {
184+
baseline::show_diff_trees_and_fail(&case_name, actual_id, &actual, expected_tree_id, &basename, &odb);
185+
}
186+
}
141187
}
142188

143189
assert_eq!(
144190
actual_cases, 109,
145191
"BUG: update this number, and don't forget to remove a filter in the end"
146192
);
193+
assert_eq!(
194+
skipped_tree_resolve_cases,
195+
166 - 4 * 7,
196+
"this is done when no case is skipped, and we don't want to accidentally skip them.\
197+
Some don't actually have conflicts"
198+
);
147199

148200
Ok(())
149201
}
@@ -215,6 +267,4 @@ fn new_blob_merge_platform(
215267
)
216268
}
217269

218-
// TODO: make sure everything is read eventually, even if only to improve debug messages in case of failure.
219-
#[allow(dead_code)]
220270
mod baseline;

0 commit comments

Comments
 (0)