Skip to content

Commit 496ef35

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

File tree

8 files changed

+1268
-202
lines changed

8 files changed

+1268
-202
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: 365 additions & 153 deletions
Large diffs are not rendered by default.

gix-merge/src/tree/mod.rs

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ pub struct TreatAsUnresolved {
5757

5858
///
5959
pub mod treat_as_unresolved {
60+
use crate::tree::TreatAsUnresolved;
61+
6062
/// Which kind of content merges should be considered unresolved?
6163
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
6264
pub enum ContentMerge {
@@ -80,6 +82,34 @@ pub mod treat_as_unresolved {
8082
/// with a [resolution strategy](super::ResolveWith).
8183
ForcedResolution,
8284
}
85+
86+
/// Instantiation/Presets
87+
impl TreatAsUnresolved {
88+
/// Return an instance with the highest sensitivity to what should be considered unresolved as it
89+
/// includes entries which have been resolved using a [merge strategy](super::ResolveWith).
90+
pub fn forced_resolution() -> Self {
91+
Self {
92+
content_merge: ContentMerge::ForcedResolution,
93+
tree_merge: TreeMerge::ForcedResolution,
94+
}
95+
}
96+
97+
/// Return an instance that considers unresolved any conflict that Git would also consider unresolved.
98+
/// This is the same as the `default()` implementation.
99+
pub fn git() -> Self {
100+
Self::default()
101+
}
102+
103+
/// Only undecidable tree merges and conflict markers are considered unresolved.
104+
/// This also means that renamed entries to make space for a conflicting one is considered acceptable,
105+
/// making this preset the most lenient.
106+
pub fn undecidable() -> Self {
107+
Self {
108+
content_merge: ContentMerge::Markers,
109+
tree_merge: TreeMerge::Undecidable,
110+
}
111+
}
112+
}
83113
}
84114

85115
impl Outcome<'_> {
@@ -157,7 +187,7 @@ enum ConflictIndexEntryPathHint {
157187
}
158188

159189
/// A utility to help define which side is what in the [`Conflict`] type.
160-
#[derive(Debug, Clone, Copy)]
190+
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
161191
enum ConflictMapping {
162192
/// The sides are as described in the field documentation, i.e. `ours` is `ours`.
163193
Original,
@@ -175,13 +205,19 @@ impl ConflictMapping {
175205
ConflictMapping::Swapped => ConflictMapping::Original,
176206
}
177207
}
208+
fn to_global(self, global: ConflictMapping) -> ConflictMapping {
209+
match global {
210+
ConflictMapping::Original => self,
211+
ConflictMapping::Swapped => self.swapped(),
212+
}
213+
}
178214
}
179215

180216
impl Conflict {
181217
/// Return `true` if this instance is considered unresolved based on the criterion specified by `how`.
182218
pub fn is_unresolved(&self, how: TreatAsUnresolved) -> bool {
183219
use crate::blob;
184-
let content_merge_matches = |info: &ContentMerge| match how.content_merge {
220+
let content_merge_unresolved = |info: &ContentMerge| match how.content_merge {
185221
treat_as_unresolved::ContentMerge::Markers => matches!(info.resolution, blob::Resolution::Conflict),
186222
treat_as_unresolved::ContentMerge::ForcedResolution => {
187223
matches!(
@@ -192,20 +228,28 @@ impl Conflict {
192228
};
193229
match how.tree_merge {
194230
treat_as_unresolved::TreeMerge::Undecidable => {
195-
self.resolution.is_err() || self.content_merge().map_or(false, |info| content_merge_matches(&info))
231+
self.resolution.is_err()
232+
|| self
233+
.content_merge()
234+
.map_or(false, |info| content_merge_unresolved(&info))
196235
}
197236
treat_as_unresolved::TreeMerge::EvasiveRenames | treat_as_unresolved::TreeMerge::ForcedResolution => {
198237
match &self.resolution {
199238
Ok(success) => match success {
200239
Resolution::SourceLocationAffectedByRename { .. } => false,
201-
Resolution::Forced(_) => how.tree_merge == treat_as_unresolved::TreeMerge::ForcedResolution,
240+
Resolution::Forced(_) => {
241+
how.tree_merge == treat_as_unresolved::TreeMerge::ForcedResolution
242+
|| self
243+
.content_merge()
244+
.map_or(false, |merged_blob| content_merge_unresolved(&merged_blob))
245+
}
202246
Resolution::OursModifiedTheirsRenamedAndChangedThenRename {
203247
merged_blob,
204248
final_location,
205249
..
206-
} => final_location.is_some() || merged_blob.as_ref().map_or(false, content_merge_matches),
250+
} => final_location.is_some() || merged_blob.as_ref().map_or(false, content_merge_unresolved),
207251
Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => {
208-
content_merge_matches(merged_blob)
252+
content_merge_unresolved(merged_blob)
209253
}
210254
},
211255
Err(_failure) => true,
@@ -249,6 +293,7 @@ impl Conflict {
249293
match failure {
250294
ResolutionFailure::OursRenamedTheirsRenamedDifferently { merged_blob } => *merged_blob,
251295
ResolutionFailure::Unknown
296+
| ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { .. }
252297
| ResolutionFailure::OursModifiedTheirsDeleted
253298
| ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch
254299
| ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed {
@@ -321,6 +366,17 @@ pub enum ResolutionFailure {
321366
/// The path at which `ours` can be found in the tree - it's in the same directory that it was in before.
322367
renamed_unique_path_to_modified_blob: BString,
323368
},
369+
/// *ours* is a directory, but *theirs* is a non-directory (i.e. file), which wants to be in its place, even though
370+
/// *ours* has a modification in that subtree.
371+
/// Rename *theirs* to retain that modification.
372+
///
373+
/// Important: there is no actual modification on *ours* side, so *ours* is filled in with *theirs* as the data structure
374+
/// cannot represent this case.
375+
// TODO: Can we have a better data-structure? This would be for a rewrite though.
376+
OursDirectoryTheirsNonDirectoryTheirsRenamed {
377+
/// The non-conflicting path of *their* non-tree entry.
378+
renamed_unique_path_of_theirs: BString,
379+
},
324380
/// *ours* was added (or renamed into place) with a different mode than theirs, e.g. blob and symlink, and we kept
325381
/// the symlink in its original location, renaming the other side to `their_unique_location`.
326382
OursAddedTheirsAddedTypeMismatch {
@@ -376,8 +432,10 @@ pub struct Options {
376432
/// If `None`, tree irreconcilable tree conflicts will result in [resolution failures](ResolutionFailure).
377433
/// Otherwise, one can choose a side. Note that it's still possible to determine that auto-resolution happened
378434
/// despite this choice, which allows carry forward the conflicting information, possibly for later resolution.
379-
/// If `Some(…)`, irreconcilable conflicts are reconciled by making a choice. This mlso means that [`Conflict::entries()`]
380-
/// won't be set as the conflict was officially resolved.
435+
/// If `Some(…)`, irreconcilable conflicts are reconciled by making a choice.
436+
/// Note that [`Conflict::entries()`] will still be set, to not degenerate information, even though they then represent
437+
/// the entries what would fit the index if no forced resolution was performed.
438+
/// It's up to the caller to handle that information mindfully.
381439
pub tree_conflicts: Option<ResolveWith>,
382440
}
383441

@@ -386,7 +444,10 @@ pub struct Options {
386444
pub enum ResolveWith {
387445
/// On irreconcilable conflict, choose neither *our* nor *their* state, but keep the common *ancestor* state instead.
388446
Ancestor,
389-
/// On irreconcilable conflict, choose *our* side
447+
/// On irreconcilable conflict, choose *our* side.
448+
///
449+
/// Note that in order to get something equivalent to *theirs*, put *theirs* into the side of *ours*,
450+
/// swapping the sides essentially.
390451
Ours,
391452
}
392453

@@ -439,6 +500,9 @@ pub mod apply_index_entries {
439500
}
440501
},
441502
Err(failure) => match failure {
503+
ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed {
504+
renamed_unique_path_of_theirs,
505+
} => (Some(renamed_unique_path_of_theirs.as_bstr()), conflict.ours.location()),
442506
ResolutionFailure::OursRenamedTheirsRenamedDifferently { .. } => {
443507
(Some(conflict.theirs.location()), conflict.ours.location())
444508
}

gix-merge/src/tree/utils.rs

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use std::collections::HashMap;
2323
/// over a directory rewrite in *our* tree. If so, rewrite it so that we get the path
2424
/// it would have had if it had been renamed along with *our* directory.
2525
pub fn possibly_rewritten_location(
26-
check_tree: &mut TreeNodes,
26+
check_tree: &TreeNodes,
2727
their_location: &BStr,
2828
our_changes: &ChangeListRef,
2929
) -> Option<BString> {
@@ -60,7 +60,7 @@ pub fn rewrite_location_with_renamed_directory(their_location: &BStr, passed_cha
6060
pub fn unique_path_in_tree(
6161
file_path: &BStr,
6262
editor: &tree::Editor<'_>,
63-
tree: &mut TreeNodes,
63+
tree: &TreeNodes,
6464
side_name: &BStr,
6565
) -> Result<BString, Error> {
6666
let mut buf = file_path.to_owned();
@@ -400,11 +400,11 @@ impl TreeNodes {
400400
Some(index) => {
401401
cursor = &mut self.0[index];
402402
if is_last && !cursor.is_leaf_node() {
403-
assert_eq!(
404-
cursor.change_idx, None,
405-
"BUG: each node should only see a single change when tracking initially: {path} {change_idx}"
406-
);
407-
cursor.change_idx = Some(change_idx);
403+
// NOTE: we might encounter the same path multiple times in rare conditions.
404+
// At least we avoid overwriting existing intermediate changes, for good measure.
405+
if cursor.change_idx.is_none() {
406+
cursor.change_idx = Some(change_idx);
407+
}
408408
}
409409
}
410410
}
@@ -414,12 +414,12 @@ impl TreeNodes {
414414

415415
/// Search the tree with `our` changes for `theirs` by [`source_location()`](Change::source_location())).
416416
/// If there is an entry but both are the same, or if there is no entry, return `None`.
417-
pub fn check_conflict(&mut self, theirs_location: &BStr) -> Option<PossibleConflict> {
417+
pub fn check_conflict(&self, theirs_location: &BStr) -> Option<PossibleConflict> {
418418
if self.0.len() == 1 {
419419
return None;
420420
}
421421
let components = to_components(theirs_location);
422-
let mut cursor = &mut self.0[0];
422+
let mut cursor = &self.0[0];
423423
let mut cursor_idx = 0;
424424
let mut intermediate_change = None;
425425
for component in components {
@@ -436,7 +436,7 @@ impl TreeNodes {
436436
} else {
437437
// a change somewhere else, i.e. `a/c` and we know `a/b` only.
438438
intermediate_change.and_then(|(change, cursor_idx)| {
439-
let cursor = &mut self.0[cursor_idx];
439+
let cursor = &self.0[cursor_idx];
440440
// If this is a destination location of a rename, then the `their_location`
441441
// is already at the right spot, and we can just ignore it.
442442
if matches!(cursor.location, ChangeLocation::CurrentLocation) {
@@ -450,7 +450,7 @@ impl TreeNodes {
450450
}
451451
Some(child_idx) => {
452452
cursor_idx = child_idx;
453-
cursor = &mut self.0[cursor_idx];
453+
cursor = &self.0[cursor_idx];
454454
}
455455
}
456456
}
@@ -496,15 +496,15 @@ impl TreeNodes {
496496
let mut cursor = &mut self.0[0];
497497
while let Some(component) = components.next() {
498498
match cursor.children.get(component).copied() {
499-
None => assert!(!must_exist, "didn't find '{location}' for removal"),
499+
None => debug_assert!(!must_exist, "didn't find '{location}' for removal"),
500500
Some(existing_idx) => {
501501
let is_last = components.peek().is_none();
502502
if is_last {
503503
cursor.children.remove(component);
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 {
@@ -578,10 +578,7 @@ impl Conflict {
578578
ours: ours.clone(),
579579
theirs: theirs.clone(),
580580
entries,
581-
map: match outer_map {
582-
ConflictMapping::Original => map,
583-
ConflictMapping::Swapped => map.swapped(),
584-
},
581+
map: map.to_global(outer_map),
585582
}
586583
}
587584

Binary file not shown.

0 commit comments

Comments
 (0)