Skip to content

Commit 1c3ba81

Browse files
committed
feat!: replace tree::Options::allow_lossy_resolution with *::tree_conflicts.
That way it's possible to steer how to resolve tree-related conflicts while making it possible to detect that a conflict happened.
1 parent a57192c commit 1c3ba81

File tree

9 files changed

+133
-87
lines changed

9 files changed

+133
-87
lines changed

gix-merge/src/blob/builtin_driver/text/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,11 @@ pub enum ConflictStyle {
5757
/// That way it becomes clearer where the content of conflicts are originating from.
5858
#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
5959
pub struct Labels<'a> {
60+
/// The label for the common *ancestor*.
6061
pub ancestor: Option<&'a BStr>,
62+
/// The label for the *current* (or *our*) side.
6163
pub current: Option<&'a BStr>,
64+
/// The label for the *other* (or *their*) side.
6265
pub other: Option<&'a BStr>,
6366
}
6467

gix-merge/src/commit/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ pub struct Outcome<'a> {
5555

5656
pub(super) mod function;
5757

58+
///
5859
pub mod virtual_merge_base;
5960
pub use virtual_merge_base::function::virtual_merge_base;

gix-merge/src/commit/virtual_merge_base.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub enum Error {
3030
pub(super) mod function {
3131
use super::Error;
3232
use crate::blob::builtin_driver;
33-
use crate::tree::TreatAsUnresolved;
33+
use crate::tree::{treat_as_unresolved, TreatAsUnresolved};
3434
use gix_object::FindExt;
3535

3636
/// Create a single virtual merge-base by merging `first_commit`, `second_commit` and `others` into one.
@@ -55,7 +55,7 @@ pub(super) mod function {
5555
let mut merged_commit_id = first_commit;
5656
others.push(second_commit);
5757

58-
options.allow_lossy_resolution = true;
58+
options.tree_conflicts = Some(crate::tree::ResolveWith::Ancestor);
5959
options.blob_merge.is_virtual_ancestor = true;
6060
options.blob_merge.text.conflict = builtin_driver::text::Conflict::ResolveWithOurs;
6161
let favor_ancestor = Some(builtin_driver::binary::ResolveWith::Ancestor);
@@ -86,10 +86,10 @@ pub(super) mod function {
8686
},
8787
)?;
8888
// This shouldn't happen, but if for some buggy reason it does, we rather bail.
89-
if out
90-
.tree_merge
91-
.has_unresolved_conflicts(TreatAsUnresolved::ConflictMarkers)
92-
{
89+
if out.tree_merge.has_unresolved_conflicts(TreatAsUnresolved {
90+
content_merge: treat_as_unresolved::ContentMerge::Markers,
91+
tree_merge: treat_as_unresolved::TreeMerge::Undecidable,
92+
}) {
9393
return Err(Error::VirtualMergeBaseConflict.into());
9494
}
9595
let merged_tree_id = out

gix-merge/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! * [tree-merges](mod@tree) look at trees and merge them structurally, triggering blob-merges as needed.
55
//! * [commit-merges](mod@commit) are like tree merges, but compute or create the merge-base on the fly.
66
#![deny(rust_2018_idioms)]
7+
#![deny(missing_docs)]
78
#![forbid(unsafe_code)]
89

910
///

gix-merge/src/tree/function.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ where
7171
let _span = gix_trace::coarse!("gix_merge::tree", ?base_tree, ?our_tree, ?their_tree, ?labels);
7272
let (mut base_buf, mut side_buf) = (Vec::new(), Vec::new());
7373
let ancestor_tree = objects.find_tree(base_tree, &mut base_buf)?;
74-
let allow_resolution_failure = !options.allow_lossy_resolution;
74+
// TODO: properly handle 'Ours' as well.
75+
let allow_resolution_failure = !matches!(options.tree_conflicts, Some(crate::tree::ResolveWith::Ancestor));
7576

7677
let mut editor = tree::Editor::new(ancestor_tree.to_owned(), objects, base_tree.kind());
7778
let ancestor_tree = gix_object::TreeRefIter::from_bytes(&base_buf);

gix-merge/src/tree/mod.rs

Lines changed: 87 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -39,40 +39,47 @@ pub struct Outcome<'a> {
3939
/// Use [`has_unresolved_conflicts()`](Outcome::has_unresolved_conflicts()) to see if any action is needed
4040
/// before using [`tree`](Outcome::tree).
4141
pub conflicts: Vec<Conflict>,
42-
/// `true` if `conflicts` contains only a single *unresolved* conflict in the last slot, but possibly more resolved ones.
42+
/// `true` if `conflicts` contains only a single [*unresolved* conflict](ResolutionFailure) in the last slot, but
43+
/// possibly more [resolved ones](Resolution) before that.
4344
/// This also makes this outcome a very partial merge that cannot be completed.
4445
/// Only set if [`fail_on_conflict`](Options::fail_on_conflict) is `true`.
4546
pub failed_on_first_unresolved_conflict: bool,
4647
}
4748

4849
/// Determine what should be considered an unresolved conflict.
50+
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
51+
pub struct TreatAsUnresolved {
52+
/// Determine which content merges should be considered unresolved.
53+
pub content_merge: treat_as_unresolved::ContentMerge,
54+
/// Determine which tree merges should be considered unresolved.
55+
pub tree_merge: treat_as_unresolved::TreeMerge,
56+
}
57+
4958
///
50-
/// Note that no matter which variant, [conflicts](Conflict) with
51-
/// [resolution failure](`ResolutionFailure`) will always be unresolved.
52-
///
53-
/// Also, when one side was modified but the other side renamed it, this will not
54-
/// be considered a conflict, even if a non-conflicting merge happened.
55-
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
56-
pub enum TreatAsUnresolved {
57-
/// Only consider content merges with conflict markers as unresolved.
58-
///
59-
/// Auto-resolved tree conflicts will *not* be considered unresolved.
60-
ConflictMarkers,
61-
/// Consider content merges with conflict markers as unresolved, and content
62-
/// merges where conflicts where auto-resolved in any way, like choosing
63-
/// *ours*, *theirs* or by their *union*.
64-
///
65-
/// Auto-resolved tree conflicts will *not* be considered unresolved.
66-
ConflictMarkersAndAutoResolved,
67-
/// Whenever there were conflicting renames, or conflict markers, it is unresolved.
68-
/// Note that auto-resolved content merges will *not* be considered unresolved.
69-
///
70-
/// Also note that files that were changed in one and renamed in another will
71-
/// be moved into place, which will be considered resolved.
72-
Renames,
73-
/// Similar to [`Self::Renames`], but auto-resolved content-merges will
74-
/// also be considered unresolved.
75-
RenamesAndAutoResolvedContent,
59+
pub mod treat_as_unresolved {
60+
/// Which kind of content merges should be considered unresolved?
61+
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
62+
pub enum ContentMerge {
63+
/// Content merges that still show conflict markers.
64+
#[default]
65+
Markers,
66+
/// Content merges who would have conflicted if it wasn't for a
67+
/// [resolution strategy](crate::blob::builtin_driver::text::Conflict::ResolveWithOurs).
68+
ForcedResolution,
69+
}
70+
71+
/// Which kind of tree merges should be considered unresolved?
72+
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
73+
pub enum TreeMerge {
74+
/// All failed renames.
75+
Undecidable,
76+
/// All failed renames, and the ones where a tree item was renamed to avoid a clash.
77+
#[default]
78+
EvasiveRenames,
79+
/// All of `EvasiveRenames`, and tree merges that would have conflicted but which were resolved
80+
/// with a [resolution strategy](super::ResolveWith).
81+
ForcedResolution,
82+
}
7683
}
7784

7885
impl Outcome<'_> {
@@ -174,35 +181,36 @@ impl Conflict {
174181
/// Return `true` if this instance is considered unresolved based on the criterion specified by `how`.
175182
pub fn is_unresolved(&self, how: TreatAsUnresolved) -> bool {
176183
use crate::blob;
177-
let content_merge_matches = |info: &ContentMerge| match how {
178-
TreatAsUnresolved::ConflictMarkers | TreatAsUnresolved::Renames => {
179-
matches!(info.resolution, blob::Resolution::Conflict)
180-
}
181-
TreatAsUnresolved::RenamesAndAutoResolvedContent | TreatAsUnresolved::ConflictMarkersAndAutoResolved => {
184+
let content_merge_matches = |info: &ContentMerge| match how.content_merge {
185+
treat_as_unresolved::ContentMerge::Markers => matches!(info.resolution, blob::Resolution::Conflict),
186+
treat_as_unresolved::ContentMerge::ForcedResolution => {
182187
matches!(
183188
info.resolution,
184189
blob::Resolution::Conflict | blob::Resolution::CompleteWithAutoResolvedConflict
185190
)
186191
}
187192
};
188-
match how {
189-
TreatAsUnresolved::ConflictMarkers | TreatAsUnresolved::ConflictMarkersAndAutoResolved => {
193+
match how.tree_merge {
194+
treat_as_unresolved::TreeMerge::Undecidable => {
190195
self.resolution.is_err() || self.content_merge().map_or(false, |info| content_merge_matches(&info))
191196
}
192-
TreatAsUnresolved::Renames | TreatAsUnresolved::RenamesAndAutoResolvedContent => match &self.resolution {
193-
Ok(success) => match success {
194-
Resolution::SourceLocationAffectedByRename { .. } => false,
195-
Resolution::OursModifiedTheirsRenamedAndChangedThenRename {
196-
merged_blob,
197-
final_location,
198-
..
199-
} => final_location.is_some() || merged_blob.as_ref().map_or(false, content_merge_matches),
200-
Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => {
201-
content_merge_matches(merged_blob)
202-
}
203-
},
204-
Err(_failure) => true,
205-
},
197+
treat_as_unresolved::TreeMerge::EvasiveRenames | treat_as_unresolved::TreeMerge::ForcedResolution => {
198+
match &self.resolution {
199+
Ok(success) => match success {
200+
Resolution::SourceLocationAffectedByRename { .. } => false,
201+
Resolution::Forced(_) => how.tree_merge == treat_as_unresolved::TreeMerge::ForcedResolution,
202+
Resolution::OursModifiedTheirsRenamedAndChangedThenRename {
203+
merged_blob,
204+
final_location,
205+
..
206+
} => final_location.is_some() || merged_blob.as_ref().map_or(false, content_merge_matches),
207+
Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => {
208+
content_merge_matches(merged_blob)
209+
}
210+
},
211+
Err(_failure) => true,
212+
}
213+
}
206214
}
207215
}
208216

@@ -237,13 +245,8 @@ impl Conflict {
237245

238246
/// Return information about the content merge if it was performed.
239247
pub fn content_merge(&self) -> Option<ContentMerge> {
240-
match &self.resolution {
241-
Ok(success) => match success {
242-
Resolution::SourceLocationAffectedByRename { .. } => None,
243-
Resolution::OursModifiedTheirsRenamedAndChangedThenRename { merged_blob, .. } => *merged_blob,
244-
Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => Some(*merged_blob),
245-
},
246-
Err(failure) => match failure {
248+
fn failure_merged_blob(failure: &ResolutionFailure) -> Option<ContentMerge> {
249+
match failure {
247250
ResolutionFailure::OursRenamedTheirsRenamedDifferently { merged_blob } => *merged_blob,
248251
ResolutionFailure::Unknown
249252
| ResolutionFailure::OursModifiedTheirsDeleted
@@ -253,7 +256,16 @@ impl Conflict {
253256
}
254257
| ResolutionFailure::OursAddedTheirsAddedTypeMismatch { .. }
255258
| ResolutionFailure::OursDeletedTheirsRenamed => None,
259+
}
260+
}
261+
match &self.resolution {
262+
Ok(success) => match success {
263+
Resolution::Forced(failure) => failure_merged_blob(failure),
264+
Resolution::SourceLocationAffectedByRename { .. } => None,
265+
Resolution::OursModifiedTheirsRenamedAndChangedThenRename { merged_blob, .. } => *merged_blob,
266+
Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => Some(*merged_blob),
256267
},
268+
Err(failure) => failure_merged_blob(failure),
257269
}
258270
}
259271
}
@@ -291,6 +303,9 @@ pub enum Resolution {
291303
/// The outcome of the content merge.
292304
merged_blob: ContentMerge,
293305
},
306+
/// This is a resolution failure was forcefully turned into a usable resolution, i.e. [making a choice](ResolveWith)
307+
/// is turned into a valid resolution.
308+
Forced(ResolutionFailure),
294309
}
295310

296311
/// Describes of a conflict involving *our* change and *their* failed to be resolved.
@@ -358,13 +373,26 @@ pub struct Options {
358373
/// If `None`, when symlinks clash *ours* will be chosen and a conflict will occur.
359374
/// Otherwise, the same logic applies as for the merge of binary resources.
360375
pub symlink_conflicts: Option<crate::blob::builtin_driver::binary::ResolveWith>,
361-
/// If `true`, instead of issuing a conflict with [`ResolutionFailure`], do nothing and keep the base/ancestor
362-
/// version. This is useful when one wants to avoid any kind of merge-conflict to have *some*, *lossy* resolution.
363-
pub allow_lossy_resolution: bool,
376+
/// If `None`, tree irreconcilable tree conflicts will result in [resolution failures](ResolutionFailure).
377+
/// Otherwise, one can choose a side. Note that it's still possible to determine that auto-resolution happened
378+
/// 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.
381+
pub tree_conflicts: Option<ResolveWith>,
382+
}
383+
384+
/// Decide how to resolve tree-related conflicts, but only those that have [no way of being correct](ResolutionFailure).
385+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
386+
pub enum ResolveWith {
387+
/// On irreconcilable conflict, choose neither *our* nor *their* state, but keep the common *ancestor* state instead.
388+
Ancestor,
389+
/// On irreconcilable conflict, choose *our* side
390+
Ours,
364391
}
365392

366393
pub(super) mod function;
367394
mod utils;
395+
///
368396
pub mod apply_index_entries {
369397

370398
pub(super) mod function {
@@ -398,6 +426,7 @@ pub mod apply_index_entries {
398426
for conflict in conflicts.iter().filter(|c| c.is_unresolved(how)) {
399427
let (renamed_path, current_path): (Option<&BStr>, &BStr) = match &conflict.resolution {
400428
Ok(success) => match success {
429+
Resolution::Forced(_) => continue,
401430
Resolution::SourceLocationAffectedByRename { final_location } => {
402431
(Some(final_location.as_bstr()), final_location.as_bstr())
403432
}
Binary file not shown.

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,26 @@ git init rename-add-symlink
222222
git commit -m "rename foo to bar"
223223
)
224224

225+
git init rename-add-same-symlink
226+
(cd rename-add-same-symlink
227+
touch target
228+
ln -s target link
229+
git add .
230+
git commit -m "original"
231+
232+
git branch A
233+
git branch B
234+
235+
git checkout A
236+
git mv link link-new
237+
git commit -m "rename link to link-new"
238+
239+
git checkout B
240+
ln -s target link-new
241+
git add link-new
242+
git commit -m "create link-new"
243+
)
244+
225245
git init rename-rename-plus-content
226246
(cd rename-rename-plus-content
227247
write_lines 1 2 3 4 5 >foo
@@ -993,7 +1013,7 @@ git init type-change-to-symlink
9931013

9941014

9951015

996-
# TODO: Git does not detect the conflict (one turns exe off, the other turns it on), and we do exactly the same.
1016+
baseline rename-add-same-symlink A-B A B
9971017
baseline rename-add-exe-bit-conflict A-B A B
9981018
baseline remove-executable-mode A-B A B
9991019
baseline simple side-1-3-without-conflict side1 side3

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

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::tree::baseline::Deviation;
22
use gix_diff::Rewrites;
33
use gix_merge::commit::Options;
4-
use gix_merge::tree::TreatAsUnresolved;
4+
use gix_merge::tree::{treat_as_unresolved, TreatAsUnresolved};
55
use gix_object::Write;
66
use gix_worktree::stack::state::attributes;
77
use std::path::Path;
@@ -119,7 +119,10 @@ fn run_baseline() -> crate::Result {
119119
index
120120
}
121121
};
122-
let conflicts_like_in_git = TreatAsUnresolved::Renames;
122+
let conflicts_like_in_git = TreatAsUnresolved {
123+
content_merge: treat_as_unresolved::ContentMerge::Markers,
124+
tree_merge: treat_as_unresolved::TreeMerge::EvasiveRenames,
125+
};
123126
let did_change = actual.index_changed_after_applying_conflicts(&mut actual_index, conflicts_like_in_git);
124127
actual_index.remove_entries(|_, _, e| e.flags.contains(gix_index::entry::Flags::REMOVE));
125128

@@ -130,27 +133,15 @@ fn run_baseline() -> crate::Result {
130133
actual.conflicts,
131134
merge_info.conflicts
132135
);
133-
// if case_name.starts_with("submodule-both-modify-A-B") {
134-
if false {
135-
assert!(
136-
!did_change,
137-
"{case_name}: We can't handle submodules, so there is no index change"
138-
);
139-
assert!(
140-
actual.has_unresolved_conflicts(conflicts_like_in_git),
141-
"{case_name}: submodules currently result in an unresolved (unknown) conflict"
142-
);
143-
} else {
144-
assert_eq!(
145-
did_change,
146-
actual.has_unresolved_conflicts(conflicts_like_in_git),
147-
"{case_name}: If there is any kind of conflict, the index should have been changed"
148-
);
149-
}
136+
assert_eq!(
137+
did_change,
138+
actual.has_unresolved_conflicts(conflicts_like_in_git),
139+
"{case_name}: If there is any kind of conflict, the index should have been changed"
140+
);
150141
}
151142

152143
assert_eq!(
153-
actual_cases, 107,
144+
actual_cases, 109,
154145
"BUG: update this number, and don't forget to remove a filter in the end"
155146
);
156147

@@ -163,6 +154,7 @@ fn basic_merge_options() -> Options {
163154
use_first_merge_base: false,
164155
tree_merge: gix_merge::tree::Options {
165156
symlink_conflicts: None,
157+
tree_conflicts: None,
166158
rewrites: Some(Rewrites {
167159
copies: None,
168160
percentage: Some(0.5),
@@ -172,7 +164,6 @@ fn basic_merge_options() -> Options {
172164
blob_merge_command_ctx: Default::default(),
173165
fail_on_conflict: None,
174166
marker_size_multiplier: 0,
175-
allow_lossy_resolution: false,
176167
},
177168
}
178169
}

0 commit comments

Comments
 (0)