Skip to content

Commit b01badb

Browse files
committed
Add more of our own tests to trigger the remaining code
This also allows for deviations in case the Git result is worse.
1 parent 5707ff6 commit b01badb

File tree

10 files changed

+1468
-711
lines changed

10 files changed

+1468
-711
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crate-status.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,15 +338,20 @@ Check out the [performance discussion][gix-diff-performance] as well.
338338

339339
### gix-merge
340340

341-
* [x] three-way merge analysis of **blobs** with choice of how to resolve conflicts
341+
* [x] three-way content-merge analysis of **blobs** with choice of how to resolve conflicts
342+
- [x] respect git attributes and drivers.
342343
- [ ] choose how to resolve conflicts on the data-structure
344+
- [ ] more efficient handling of paths with `merge=binary` attributes (do not load them into memory)
343345
- [x] produce a new blob based on data-structure containing possible resolutions
344346
- [x] `merge` style
345347
- [x] `diff3` style
346348
- [x] `zdiff` style
347349
- [ ] various newlines-related options during the merge (see https://git-scm.com/docs/git-merge#Documentation/git-merge.txt-ignore-space-change).
348350
- [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same)
349-
* [ ] diff-heuristics match Git perfectly
351+
* [x] **tree**-diff-heuristics match Git for its test-cases
352+
- [ ] a way to generate an index with stages
353+
- *currently the data it provides won't generate index entries, and possibly can't be used for it yet*
354+
- [ ] submodule merges (*right now they count as conflicts if they differ*)
350355
* [x] API documentation
351356
* [ ] Examples
352357

gix-merge/src/tree/function.rs

Lines changed: 409 additions & 183 deletions
Large diffs are not rendered by default.

gix-merge/src/tree/mod.rs

Lines changed: 72 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -43,31 +43,25 @@ pub struct Outcome<'a> {
4343
pub failed_on_first_unresolved_conflict: bool,
4444
}
4545

46+
/// Determine what should be considered an unresolved conflict.
47+
///
48+
/// Note that no matter which variant, [conflicts](Conflict) with [resolution failure](`ResolutionFailure`)
49+
/// will always be unresolved.
50+
///
51+
/// However, those whose resolution was
52+
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
53+
pub enum UnresolvedConflict {
54+
/// Only consider content merges with conflict markers as unresolved.
55+
ConflictMarkers,
56+
/// Whenever there was any rename, or conflict markers, it is unresolved.
57+
Renames,
58+
}
59+
4660
impl Outcome<'_> {
4761
/// Return `true` if there is any conflict that would still need to be resolved as they would yield undesirable trees.
48-
/// Note that this interpretation of conflicts and their resolution, see
49-
/// [`has_unresolved_conflicts_strict`](Self::has_unresolved_conflicts_strict).
50-
pub fn has_unresolved_conflicts(&self) -> bool {
51-
self.conflicts.iter().any(|c| {
52-
c.resolution.is_err()
53-
|| c.content_merge().map_or(false, |info| {
54-
matches!(info.resolution, crate::blob::Resolution::Conflict)
55-
})
56-
})
57-
}
58-
59-
/// Return `true` only if there was any (even resolved) conflict in the tree structure, or if there are still conflict markers.
60-
pub fn has_unresolved_conflicts_strict(&self) -> bool {
61-
self.conflicts.iter().any(|c| match &c.resolution {
62-
Ok(success) => match success {
63-
Resolution::OursAddedTheirsAddedTypeMismatch { .. }
64-
| Resolution::OursModifiedTheirsRenamedAndChangedThenRename { .. } => true,
65-
Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => {
66-
matches!(merged_blob.resolution, crate::blob::Resolution::Conflict)
67-
}
68-
},
69-
Err(_failure) => true,
70-
})
62+
/// This is based on `how` to determine what should be considered unresolved.
63+
pub fn has_unresolved_conflicts(&self, how: UnresolvedConflict) -> bool {
64+
function::is_unresolved(&self.conflicts, how)
7165
}
7266
}
7367

@@ -135,14 +129,19 @@ impl Conflict {
135129
pub fn content_merge(&self) -> Option<ContentMerge> {
136130
match &self.resolution {
137131
Ok(success) => match success {
138-
Resolution::OursAddedTheirsAddedTypeMismatch { .. } => None,
132+
Resolution::SourceLocationAffectedByRename { .. } => None,
139133
Resolution::OursModifiedTheirsRenamedAndChangedThenRename { merged_blob, .. } => *merged_blob,
140134
Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => Some(*merged_blob),
141135
},
142136
Err(failure) => match failure {
143-
ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed {
144-
renamed_path_to_modified_blob: _,
137+
ResolutionFailure::OursRenamedTheirsRenamedDifferently { merged_blob } => *merged_blob,
138+
ResolutionFailure::Unknown
139+
| ResolutionFailure::OursModifiedTheirsDeleted
140+
| ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch
141+
| ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed {
142+
renamed_unique_path_to_modified_blob: _,
145143
}
144+
| ResolutionFailure::OursAddedTheirsAddedTypeMismatch { .. }
146145
| ResolutionFailure::OursDeletedTheirsRenamed => None,
147146
},
148147
}
@@ -154,11 +153,20 @@ impl Conflict {
154153
/// Note that all resolutions are side-agnostic, so *ours* could also have been *theirs* and vice versa.
155154
#[derive(Debug, Clone)]
156155
pub enum Resolution {
156+
/// *ours* had a renamed directory and *theirs* made a change in the now renamed directory.
157+
/// We moved that change into its location.
158+
SourceLocationAffectedByRename {
159+
/// The repository-relative path to the location that the change ended up in after
160+
/// being affected by a renamed directory.
161+
final_location: BString,
162+
},
157163
/// *ours* was a modified blob and *theirs* renamed that blob.
158164
/// We moved the changed blob from *ours* to its new location, and merged it successfully.
159165
/// If this is a `copy`, the source of the copy was set to be the changed blob as well so both match.
160166
OursModifiedTheirsRenamedAndChangedThenRename {
161-
/// If not `None`, the content of the involved blob had to be merged.
167+
/// If one side added the executable bit, we always add it in the merged result.
168+
merged_mode: Option<gix_object::tree::EntryMode>,
169+
/// If `Some(…)`, the content of the involved blob had to be merged.
162170
merged_blob: Option<ContentMerge>,
163171
/// The repository relative path to the location the blob finally ended up in.
164172
/// It's `Some()` only if *they* rewrote the blob into a directory which *we* renamed on *our* side.
@@ -172,11 +180,38 @@ pub enum Resolution {
172180
/// The outcome of the content merge.
173181
merged_blob: ContentMerge,
174182
},
183+
}
184+
185+
/// Describes of a conflict involving *our* change and *their* failed to be resolved.
186+
#[derive(Debug, Clone)]
187+
pub enum ResolutionFailure {
188+
/// *ours* was renamed, but *theirs* was renamed differently. Both versions will be present in the tree,
189+
OursRenamedTheirsRenamedDifferently {
190+
/// If `Some(…)`, the content of the involved blob had to be merged.
191+
merged_blob: Option<ContentMerge>,
192+
},
193+
/// *ours* was modified, but *theirs* was turned into a directory, so *ours* was renamed to a non-conflicting path.
194+
OursModifiedTheirsDirectoryThenOursRenamed {
195+
/// The path at which `ours` can be found in the tree - it's in the same directory that it was in before.
196+
renamed_unique_path_to_modified_blob: BString,
197+
},
175198
/// *ours* was added (or renamed into place) with a different mode than theirs, e.g. blob and symlink, and we kept
199+
/// the symlink in its original location, renaming the other side to `their_unique_location`.
176200
OursAddedTheirsAddedTypeMismatch {
177-
/// The location at which *their* state was placed to resolve the name and type clash.
178-
their_final_location: BString,
201+
/// The location at which *their* state was placed to resolve the name and type clash, named to indicate
202+
/// where the entry is coming from.
203+
their_unique_location: BString,
179204
},
205+
/// *ours* was modified, and they renamed the same file, but there is also a non-mergable type-change.
206+
/// Here we keep both versions of the file.
207+
OursModifiedTheirsRenamedTypeMismatch,
208+
/// *ours* was deleted, but *theirs* was renamed.
209+
OursDeletedTheirsRenamed,
210+
/// *ours* was modified and *theirs* was deleted. We keep the modified one and ignore the deletion.
211+
OursModifiedTheirsDeleted,
212+
/// *ours* and *theirs* are in an untested state so it can't be handled yet, and is considered a conflict
213+
/// without adding our *or* their side to the resulting tree.
214+
Unknown,
180215
}
181216

182217
/// Information about a blob content merge for use in a [`Resolution`].
@@ -190,18 +225,6 @@ pub struct ContentMerge {
190225
pub resolution: crate::blob::Resolution,
191226
}
192227

193-
/// Describes of a conflict involving *our* change and *their* failed to be resolved.
194-
#[derive(Debug, Clone)]
195-
pub enum ResolutionFailure {
196-
/// *ours* was modified, but *theirs* was turned into a directory, so *ours* was renamed to a non-conflicting path.
197-
OursModifiedTheirsDirectoryThenOursRenamed {
198-
/// The path at which `ours` can be found in the tree - it's in the same directory that it was in before.
199-
renamed_path_to_modified_blob: BString,
200-
},
201-
/// *ours* was deleted, but *theirs* was renamed.
202-
OursDeletedTheirsRenamed,
203-
}
204-
205228
/// A way to configure [`tree()`](crate::tree()).
206229
#[derive(Default, Debug, Clone)]
207230
pub struct Options {
@@ -211,14 +234,19 @@ pub struct Options {
211234
pub blob_merge: crate::blob::platform::merge::Options,
212235
/// The context to use when invoking merge-drivers.
213236
pub blob_merge_command_ctx: gix_command::Context,
214-
/// If `true`, the first conflict will cause the entire
215-
pub fail_on_conflict: bool,
237+
/// If `Some(what-is-unresolved)`, the first unresolved conflict will cause the entire merge to stop.
238+
/// This is useful to see if there is any conflict, without performing the whole operation.
239+
// TODO: Maybe remove this if the cost of figuring out conflicts is so low - after all, the data structures
240+
// and initial diff is the expensive thing right now, which are always done upfront.
241+
// However, this could change once we know do everything during the traversal, which probably doesn't work
242+
// without caching stuff and is too complicated to actually do.
243+
pub fail_on_conflict: Option<UnresolvedConflict>,
216244
/// If greater than 0, each level indicates another merge-of-merge. This can be greater than
217245
/// 0 when merging merge-bases, which are merged like a pyramid.
218246
/// This value also affects the size of merge-conflict markers, to allow differentiating
219247
/// merge conflicts on each level.
220248
pub call_depth: u8,
221-
// TODO(Perf) add a flag to allow parallelizing the tree-diff itself.
249+
// TODO(Perf) add a flag to allow parallelizing the tree-diff itself once there was some profiling.
222250
}
223251

224252
pub(super) mod function;

0 commit comments

Comments
 (0)