History

feat: provide a way to record and apply index changes.

These changes will then be applicable to an index that is created from the written tree editor.

Sebastian Thiel committed 1 year ago

Commit 3ee8b621 parent 5db2d2d10 files · +728 −49Browse files

Unified · Split

10 files
  1. Cargo.lock +1 −0
  2. crate-status.md +3 −2
  3. gix-merge/Cargo.toml +1 −0
  4. gix-merge/src/tree/function.rs +179 −33
  5. gix-merge/src/tree/mod.rs +191 −1
  6. gix-merge/src/tree/utils.rs +43 −4
  7. gix-merge/tests/fixtures/generated-archives/tree-baseline.tar +0 −0
  8. gix-merge/tests/fixtures/tree-baseline.sh +196 −4
  9. gix-merge/tests/merge/tree/baseline.rs +64 −4
  10. gix-merge/tests/merge/tree/mod.rs +51 −2
Cargo.lock+1−0
@@ -2114,6 +2114,7 @@ dependencies = [ · 2113 unmodified lines
21142114 "gix-filter",
21152115 "gix-fs 0.12.0",
21162116 "gix-hash 0.15.0",
2117+ "gix-index 0.36.0",
21172118 "gix-object 0.45.0",
21182119 "gix-odb",
21192120 "gix-path 0.10.12",
crate-status.md+3−2
@@ -318,6 +318,8 @@ Check out the [performance discussion][gix-diff-performance] as well. · 317 unmodified lines
318318 * [x] find blobs by similarity check
319319 * [ ] heuristics to find best candidate
320320 * [ ] find by basename to support similarity check
321+ - Not having it can lead to issues when files with the same or similar content are part of a move
322+ as files can be lost that way.
321323 * [x] directory tracking
322324 - [x] by identity
323325 - [ ] by similarity
@@ -349,8 +351,7 @@ Check out the [performance discussion][gix-diff-performance] as well. · 25 unmodified lines
349351 - [ ] various newlines-related options during the merge (see https://git-scm.com/docs/git-merge#Documentation/git-merge.txt-ignore-space-change).
350352 - [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same)
351353 * [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+ - [x] a way to generate an index with stages, mostly conforming with Git.
354355 - [ ] submodule merges (*right now they count as conflicts if they differ*)
355356 * [x] **commits** - with handling of multiple merge bases by recursive merge-base merge
356357 * [x] API documentation
gix-merge/Cargo.toml+1−0
@@ -32,6 +32,7 @@ gix-quote = { version = "^0.4.13", path = "../gix-quote" } · 31 unmodified lines
3232 gix-revision = { version = "^0.30.0", path = "../gix-revision", default-features = false, features = ["merge_base"] }
3333 gix-revwalk = { version = "^0.16.0", path = "../gix-revwalk" }
3434 gix-diff = { version = "^0.47.0", path = "../gix-diff", default-features = false, features = ["blob"] }
35+gix-index = { version = "^0.36.0", path = "../gix-index" }
3536
3637 thiserror = "2.0.0"
3738 imara-diff = { version = "0.1.7" }
gix-merge/src/tree/function.rs+179−33
@@ -3,7 +3,10 @@ use crate::tree::utils::{ · 2 unmodified lines
33 to_components, track, unique_path_in_tree, ChangeList, ChangeListRef, PossibleConflict, TrackedChange, TreeNodes,
44 };
55 use crate::tree::ConflictMapping::{Original, Swapped};
6−use crate::tree::{Conflict, ConflictMapping, ContentMerge, Error, Options, Outcome, Resolution, ResolutionFailure};
6+use crate::tree::{
7+ Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, ContentMerge, Error, Options, Outcome,
8+ Resolution, ResolutionFailure,
9+};
710 use bstr::{BString, ByteSlice};
811 use gix_diff::tree::recorder::Location;
912 use gix_diff::tree_with_rewrites::Change;
@@ -190,11 +193,14 @@ where · 180 unmodified lines
190193 }) {
191194 None => {
192195 if let Some((rewritten_location, ours_idx)) = rewritten_location {
196+ // `no_entry` to the index because that's not a conflict at all,
197+ // but somewhat advanced rename tracking.
193198 if should_fail_on_conflict(Conflict::with_resolution(
194199 Resolution::SourceLocationAffectedByRename {
195200 final_location: rewritten_location.to_owned(),
196201 },
197202 (&our_changes[*ours_idx].inner, theirs, Original, outer_side),
203+ [None, None, None],
198204 )) {
199205 break 'outer;
200206 };
@@ -254,10 +260,12 @@ where · 53 unmodified lines
254260 if let Some(ours) = ours {
255261 gix_trace::debug!("Turning a case we could probably handle into a conflict for now. theirs: {theirs:#?} ours: {ours:#?} kind: {match_kind:?}");
256262 if allow_resolution_failure
257− && should_fail_on_conflict(Conflict::without_resolution(
258− ResolutionFailure::Unknown,
259− (&ours.inner, theirs, Original, outer_side),
260− ))
263+ && should_fail_on_conflict(Conflict::unknown((
264+ &ours.inner,
265+ theirs,
266+ Original,
267+ outer_side,
268+ )))
261269 {
262270 break 'outer;
263271 };
@@ -328,7 +336,7 @@ where · 64 unmodified lines
328336 pick_our_changes(side, our_changes, their_changes),
329337 );
330338 let renamed_without_change = their_source_id == their_id;
331− let (our_id, resolution) = if renamed_without_change {
339+ let (merged_blob_id, resolution) = if renamed_without_change {
332340 (*our_id, None)
333341 } else {
334342 let (our_location, our_id, our_mode, their_location, their_id, their_mode) =
@@ -373,18 +381,23 @@ where · 38 unmodified lines
373381 location: their_rewritten_location.unwrap_or_else(|| their_location.to_owned()),
374382 relation: None,
375383 entry_mode: merged_mode,
376− id: our_id,
384+ id: merged_blob_id,
377385 };
378386 if should_fail_on_conflict(Conflict::with_resolution(
379387 Resolution::OursModifiedTheirsRenamedAndChangedThenRename {
380388 merged_mode: (merged_mode != *their_mode).then_some(merged_mode),
381389 merged_blob: resolution.map(|resolution| ContentMerge {
382390 resolution,
383− merged_blob_id: our_id,
391+ merged_blob_id,
384392 }),
385393 final_location,
386394 },
387395 (ours, theirs, side, outer_side),
396+ [
397+ index_entry(previous_entry_mode, previous_id),
398+ index_entry(our_mode, our_id),
399+ index_entry(their_mode, their_id),
400+ ],
388401 )) {
389402 break 'outer;
390403 }
@@ -401,6 +414,19 @@ where · 10 unmodified lines
401414 if should_fail_on_conflict(Conflict::without_resolution(
402415 ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch,
403416 (ours, theirs, side, outer_side),
417+ [
418+ index_entry_at_path(
419+ previous_entry_mode,
420+ previous_id,
421+ ConflictIndexEntryPathHint::RenamedOrTheirs,
422+ ),
423+ None,
424+ index_entry_at_path(
425+ their_mode,
426+ their_id,
427+ ConflictIndexEntryPathHint::RenamedOrTheirs,
428+ ),
429+ ],
404430 )) {
405431 break 'outer;
406432 }
@@ -421,7 +447,7 @@ where · 14 unmodified lines
421447 ..
422448 },
423449 ) if !involves_submodule(our_mode, their_mode)
424− && our_mode.kind() == their_mode.kind()
450+ && merge_modes(*our_mode, *their_mode).is_some()
425451 && our_id != their_id =>
426452 {
427453 let (merged_blob_id, resolution) = perform_blob_merge(
@@ -436,7 +462,11 @@ where · 8 unmodified lines
436462 (0, outer_side),
437463 &options,
438464 )?;
439− editor.upsert(toc(location), our_mode.kind(), merged_blob_id)?;
465+
466+ let merged_mode = merge_modes_prev(*our_mode, *their_mode, *previous_entry_mode)
467+ .expect("BUG: merge_modes() reports a valid mode, this one should do too");
468+
469+ editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?;
440470 if should_fail_on_conflict(Conflict::with_resolution(
441471 Resolution::OursModifiedTheirsModifiedThenBlobContentMerge {
442472 merged_blob: ContentMerge {
@@ -445,6 +475,11 @@ where · 2 unmodified lines
445475 },
446476 },
447477 (ours, theirs, Original, outer_side),
478+ [
479+ index_entry(previous_entry_mode, previous_id),
480+ index_entry(our_mode, our_id),
481+ index_entry(their_mode, their_id),
482+ ],
448483 )) {
449484 break 'outer;
450485 };
@@ -489,27 +524,28 @@ where · 38 unmodified lines
489524 },
490525 },
491526 (ours, theirs, Original, outer_side),
527+ [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)],
492528 ))
493529 } else if allow_resolution_failure {
494530 // Actually this has a preference, as symlinks are always left in place with the other side renamed.
495531 let (
496532 logical_side,
497533 label_of_side_to_be_moved,
498− (our_mode, our_id),
499− (their_mode, their_id),
534+ (our_mode, our_id, our_path_hint),
535+ (their_mode, their_id, their_path_hint),
500536 ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) {
501537 (
502538 Original,
503539 labels.other.unwrap_or_default(),
504− (*our_mode, *our_id),
505− (*their_mode, *their_id),
540+ (*our_mode, *our_id, ConflictIndexEntryPathHint::Current),
541+ (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs),
506542 )
507543 } else {
508544 (
509545 Swapped,
510546 labels.current.unwrap_or_default(),
511− (*their_mode, *their_id),
512− (*our_mode, *our_id),
547+ (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs),
548+ (*our_mode, *our_id, ConflictIndexEntryPathHint::Current),
513549 )
514550 };
515551 let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree);
@@ -525,6 +561,11 @@ where · 9 unmodified lines
525561 their_unique_location: renamed_location.clone(),
526562 },
527563 (ours, theirs, logical_side, outer_side),
564+ [
565+ None,
566+ index_entry_at_path(&our_mode, &our_id, our_path_hint),
567+ index_entry_at_path(&their_mode, &their_id, their_path_hint),
568+ ],
528569 );
529570
530571 let new_change = Change::Addition {
@@ -554,7 +595,8 @@ where · 23 unmodified lines
554595 location,
555596 entry_mode,
556597 id,
557− ..
598+ previous_entry_mode,
599+ previous_id,
558600 },
559601 Change::Deletion { .. },
560602 )
@@ -564,7 +606,8 @@ where · 3 unmodified lines
564606 location,
565607 entry_mode,
566608 id,
567− ..
609+ previous_entry_mode,
610+ previous_id,
568611 },
569612 ) if allow_resolution_failure => {
570613 let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) {
@@ -606,6 +649,11 @@ where · 35 unmodified lines
606649 renamed_unique_path_to_modified_blob: renamed_path,
607650 },
608651 (ours, theirs, side, outer_side),
652+ [
653+ index_entry(previous_entry_mode, previous_id),
654+ index_entry(entry_mode, id),
655+ None,
656+ ],
609657 ));
610658
611659 // Since we move *our* side, our tree needs to be modified.
@@ -621,6 +669,11 @@ where · 9 unmodified lines
621669 let should_break = should_fail_on_conflict(Conflict::without_resolution(
622670 ResolutionFailure::OursModifiedTheirsDeleted,
623671 (ours, theirs, side, outer_side),
672+ [
673+ index_entry(previous_entry_mode, previous_id),
674+ index_entry(entry_mode, id),
675+ None,
676+ ],
624677 ));
625678 editor.upsert(toc(location), entry_mode.kind(), *id)?;
626679 if should_break {
@@ -702,10 +755,9 @@ where · 75 unmodified lines
702755 // Pretend this is the end of the loop and keep this as conflict.
703756 // If this happens in the wild, we'd want to reproduce it.
704757 if allow_resolution_failure
705− && should_fail_on_conflict(Conflict::without_resolution(
706− ResolutionFailure::Unknown,
707− (ours, theirs, Original, outer_side),
708− ))
758+ && should_fail_on_conflict(Conflict::unknown((
759+ ours, theirs, Original, outer_side,
760+ )))
709761 {
710762 break 'outer;
711763 };
@@ -738,6 +790,23 @@ where · 26 unmodified lines
738790 }),
739791 },
740792 (ours, theirs, Original, outer_side),
793+ [
794+ index_entry_at_path(
795+ source_entry_mode,
796+ source_id,
797+ ConflictIndexEntryPathHint::Source,
798+ ),
799+ index_entry_at_path(
800+ our_mode,
801+ &merged_blob_id,
802+ ConflictIndexEntryPathHint::Current,
803+ ),
804+ index_entry_at_path(
805+ their_mode,
806+ &merged_blob_id,
807+ ConflictIndexEntryPathHint::RenamedOrTheirs,
808+ ),
809+ ],
741810 )) {
742811 break 'outer;
743812 };
@@ -767,6 +836,23 @@ where · 23 unmodified lines
767836 },
768837 },
769838 (ours, theirs, Original, outer_side),
839+ [
840+ index_entry_at_path(
841+ source_entry_mode,
842+ source_id,
843+ ConflictIndexEntryPathHint::Source,
844+ ),
845+ index_entry_at_path(
846+ our_mode,
847+ &merged_blob_id,
848+ ConflictIndexEntryPathHint::Current,
849+ ),
850+ index_entry_at_path(
851+ their_mode,
852+ &merged_blob_id,
853+ ConflictIndexEntryPathHint::RenamedOrTheirs,
854+ ),
855+ ],
770856 )) {
771857 break 'outer;
772858 };
@@ -824,6 +910,15 @@ where · 51 unmodified lines
824910 if should_fail_on_conflict(Conflict::without_resolution(
825911 ResolutionFailure::OursDeletedTheirsRenamed,
826912 (ours, theirs, side, outer_side),
913+ [
914+ None,
915+ None,
916+ index_entry_at_path(
917+ rewritten_mode,
918+ rewritten_id,
919+ ConflictIndexEntryPathHint::RenamedOrTheirs,
920+ ),
921+ ],
827922 )) {
828923 break 'outer;
829924 };
@@ -901,6 +996,7 @@ where · 71 unmodified lines
901996 },
902997 },
903998 (ours, theirs, Original, outer_side),
999+ [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)],
9041000 )) {
9051001 break 'outer;
9061002 };
@@ -916,21 +1012,21 @@ where · 9 unmodified lines
9161012 let (
9171013 logical_side,
9181014 label_of_side_to_be_moved,
919− (our_mode, our_id),
920− (their_mode, their_id),
1015+ (our_mode, our_id, our_path_hint),
1016+ (their_mode, their_id, their_path_hint),
9211017 ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) {
9221018 (
9231019 Original,
9241020 labels.other.unwrap_or_default(),
925− (*our_mode, *our_id),
926− (*their_mode, *their_id),
1021+ (*our_mode, *our_id, ConflictIndexEntryPathHint::Current),
1022+ (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs),
9271023 )
9281024 } else {
9291025 (
9301026 Swapped,
9311027 labels.current.unwrap_or_default(),
932− (*their_mode, *their_id),
933− (*our_mode, *our_id),
1028+ (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs),
1029+ (*our_mode, *our_id, ConflictIndexEntryPathHint::Current),
9341030 )
9351031 };
9361032 let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree);
@@ -946,6 +1042,11 @@ where · 9 unmodified lines
9461042 their_unique_location: renamed_location.clone(),
9471043 },
9481044 (ours, theirs, side, outer_side),
1045+ [
1046+ None,
1047+ index_entry_at_path(&our_mode, &our_id, our_path_hint),
1048+ index_entry_at_path(&their_mode, &their_id, their_path_hint),
1049+ ],
9491050 );
9501051
9511052 let new_change_with_rename = Change::Addition {
@@ -970,10 +1071,7 @@ where · 18 unmodified lines
9701071 }
9711072 _unknown => {
9721073 if allow_resolution_failure
973− && should_fail_on_conflict(Conflict::without_resolution(
974− ResolutionFailure::Unknown,
975− (ours, theirs, Original, outer_side),
976− ))
1074+ && should_fail_on_conflict(Conflict::unknown((ours, theirs, Original, outer_side)))
9771075 {
9781076 break 'outer;
9791077 };
@@ -1004,12 +1102,40 @@ fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool { · 24 unmodified lines
10041102 a.is_commit() || b.is_commit()
10051103 }
10061104
1007−/// Allows equal modes or preferes executables bits in case of blobs
1105+/// Allows equal modes or prefers executables bits in case of blobs
1106+///
1107+/// Note that this is often not correct as the previous mode of each side should be taken into account so that:
1108+///
1109+/// on | on = on
1110+/// off | off = off
1111+/// on | off || off | on = conflict
10081112 fn merge_modes(a: EntryMode, b: EntryMode) -> Option<EntryMode> {
10091113 match (a.kind(), b.kind()) {
1114+ (_, _) if a == b => Some(a),
10101115 (EntryKind::BlobExecutable, EntryKind::BlobExecutable | EntryKind::Blob)
10111116 | (EntryKind::Blob, EntryKind::BlobExecutable) => Some(EntryKind::BlobExecutable.into()),
1117+ _ => None,
1118+ }
1119+}
1120+
1121+/// Use this version if there is a single common `prev` value for both `a` and `b` to detect
1122+/// if the mode was turned on or off.
1123+fn merge_modes_prev(a: EntryMode, b: EntryMode, prev: EntryMode) -> Option<EntryMode> {
1124+ match (a.kind(), b.kind()) {
10121125 (_, _) if a == b => Some(a),
1126+ (a @ EntryKind::BlobExecutable, b @ (EntryKind::BlobExecutable | EntryKind::Blob))
1127+ | (a @ EntryKind::Blob, b @ EntryKind::BlobExecutable) => {
1128+ let prev = prev.kind();
1129+ let changed = if a == prev { b } else { a };
1130+ Some(
1131+ match (prev, changed) {
1132+ (EntryKind::Blob, EntryKind::BlobExecutable) => EntryKind::BlobExecutable,
1133+ (EntryKind::BlobExecutable, EntryKind::Blob) => EntryKind::Blob,
1134+ _ => unreachable!("upper match already assured we only deal with blobs"),
1135+ }
1136+ .into(),
1137+ )
1138+ }
10131139 _ => None,
10141140 }
10151141 }
@@ -1066,3 +1192,23 @@ fn pick_our_changes_mut<'a>( · 50 unmodified lines
10661192 Swapped => theirs,
10671193 }
10681194 }
1195+
1196+fn index_entry(mode: &gix_object::tree::EntryMode, id: &gix_hash::ObjectId) -> Option<ConflictIndexEntry> {
1197+ Some(ConflictIndexEntry {
1198+ mode: *mode,
1199+ id: *id,
1200+ path_hint: None,
1201+ })
1202+}
1203+
1204+fn index_entry_at_path(
1205+ mode: &gix_object::tree::EntryMode,
1206+ id: &gix_hash::ObjectId,
1207+ hint: ConflictIndexEntryPathHint,
1208+) -> Option<ConflictIndexEntry> {
1209+ Some(ConflictIndexEntry {
1210+ mode: *mode,
1211+ id: *id,
1212+ path_hint: Some(hint),
1213+ })
1214+}
gix-merge/src/tree/mod.rs+191−1
@@ -81,6 +81,17 @@ impl Outcome<'_> { · 80 unmodified lines
8181 pub fn has_unresolved_conflicts(&self, how: TreatAsUnresolved) -> bool {
8282 self.conflicts.iter().any(|c| c.is_unresolved(how))
8383 }
84+
85+ /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a
86+ /// conflict should be considered unresolved.
87+ /// It's important that `index` is at the state of [`Self::tree`].
88+ ///
89+ /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`.
90+ /// Also, the unconflicted stage of such entries will be removed merely by setting a flag, so the
91+ /// in-memory entry is still present.
92+ pub fn index_changed_after_applying_conflicts(&self, index: &mut gix_index::State, how: TreatAsUnresolved) -> bool {
93+ apply_index_entries(&self.conflicts, how, index)
94+ }
8495 }
8596
8697 /// A description of a conflict (i.e. merge issue without an auto-resolution) as seen during a [tree-merge](crate::tree()).
@@ -99,11 +110,45 @@ pub struct Conflict { · 12 unmodified lines
99110 pub ours: Change,
100111 /// The change representing *their* side.
101112 pub theirs: Change,
113+ /// An array to store an entry for each stage of the conflict.
114+ ///
115+ /// * `entries[0]` => Base
116+ /// * `entries[1]` => Ours
117+ /// * `entries[2]` => Theirs
118+ ///
119+ /// Note that ours and theirs might be swapped, so one should access it through [`Self::entries()`] to compensate for that.
120+ pub entries: [Option<ConflictIndexEntry>; 3],
102121 /// Determine how to interpret the `ours` and `theirs` fields. This is used to implement [`Self::changes_in_resolution()`]
103122 /// and [`Self::into_parts_by_resolution()`].
104123 map: ConflictMapping,
105124 }
106125
126+/// A conflicting entry for insertion into the index.
127+/// It will always be either on stage 1 (ancestor/base), 2 (ours) or 3 (theirs)
128+#[derive(Debug, Clone, Copy)]
129+pub struct ConflictIndexEntry {
130+ /// The kind of object at this stage.
131+ /// Note that it's possible that this is a directory, for instance if a directory was replaced with a file.
132+ pub mode: gix_object::tree::EntryMode,
133+ /// The id defining the state of the object.
134+ pub id: gix_hash::ObjectId,
135+ /// Hidden, maybe one day we can do without?
136+ path_hint: Option<ConflictIndexEntryPathHint>,
137+}
138+
139+/// A hint for [`apply_index_entries()`] to know which paths to use for an entry.
140+/// This is only used when necessary.
141+#[derive(Debug, Clone, Copy)]
142+enum ConflictIndexEntryPathHint {
143+ /// Use the previous path, i.e. rename source.
144+ Source,
145+ /// Use the current path as it is in the tree.
146+ Current,
147+ /// Use the path of the final destination, or *their* name.
148+ /// It's definitely finicky, as we don't store the actual path and instead refer to it.
149+ RenamedOrTheirs,
150+}
151+
107152 /// A utility to help define which side is what in the [`Conflict`] type.
108153 #[derive(Debug, Clone, Copy)]
109154 enum ConflictMapping {
@@ -147,7 +192,11 @@ impl Conflict { · 37 unmodified lines
147192 TreatAsUnresolved::Renames | TreatAsUnresolved::RenamesAndAutoResolvedContent => match &self.resolution {
148193 Ok(success) => match success {
149194 Resolution::SourceLocationAffectedByRename { .. } => false,
150− Resolution::OursModifiedTheirsRenamedAndChangedThenRename { .. } => true,
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),
151200 Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => {
152201 content_merge_matches(merged_blob)
153202 }
@@ -178,6 +227,14 @@ impl Conflict { · 24 unmodified lines
178227 }
179228 }
180229
230+ /// Return the index entries for insertion into the index, to match with what's returned by [`Self::changes_in_resolution()`].
231+ pub fn entries(&self) -> [Option<ConflictIndexEntry>; 3] {
232+ match self.map {
233+ ConflictMapping::Original => self.entries,
234+ ConflictMapping::Swapped => [self.entries[0], self.entries[2], self.entries[1]],
235+ }
236+ }
237+
181238 /// Return information about the content merge if it was performed.
182239 pub fn content_merge(&self) -> Option<ContentMerge> {
183240 match &self.resolution {
@@ -308,3 +365,136 @@ pub struct Options { · 124 unmodified lines
308365
309366 pub(super) mod function;
310367 mod utils;
368+pub mod apply_index_entries {
369+
370+ pub(super) mod function {
371+ use crate::tree::{Conflict, ConflictIndexEntryPathHint, Resolution, ResolutionFailure, TreatAsUnresolved};
372+ use bstr::{BStr, ByteSlice};
373+ use std::collections::{hash_map, HashMap};
374+
375+ /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a
376+ /// conflict should be considered unresolved.
377+ /// Once a stage of a path conflicts, the unconflicting stage is removed even though it might be the one
378+ /// that is currently checked out.
379+ /// This removal, however, is only done by flagging it with [gix_index::entry::Flags::REMOVE], which means
380+ /// these entries won't be written back to disk but will still be present in the index.
381+ /// It's important that `index` matches the tree that was produced as part of the merge that also
382+ /// brought about `conflicts`, or else this function will fail if it cannot find the path matching
383+ /// the conflicting entries.
384+ ///
385+ /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`.
386+ /// Errors can only occour if `index` isn't the one created from the merged tree that produced the `conflicts`.
387+ pub fn apply_index_entries(
388+ conflicts: &[Conflict],
389+ how: TreatAsUnresolved,
390+ index: &mut gix_index::State,
391+ ) -> bool {
392+ if index.is_sparse() {
393+ gix_trace::error!("Refusing to apply index entries to sparse index - it's not tested yet");
394+ return false;
395+ }
396+ let len = index.entries().len();
397+ let mut idx_by_path_stage = HashMap::<(gix_index::entry::Stage, &BStr), usize>::default();
398+ for conflict in conflicts.iter().filter(|c| c.is_unresolved(how)) {
399+ let (renamed_path, current_path): (Option<&BStr>, &BStr) = match &conflict.resolution {
400+ Ok(success) => match success {
401+ Resolution::SourceLocationAffectedByRename { final_location } => {
402+ (Some(final_location.as_bstr()), final_location.as_bstr())
403+ }
404+ Resolution::OursModifiedTheirsRenamedAndChangedThenRename { final_location, .. } => (
405+ final_location.as_ref().map(|p| p.as_bstr()),
406+ conflict.changes_in_resolution().1.location(),
407+ ),
408+ Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { .. } => {
409+ (None, conflict.ours.location())
410+ }
411+ },
412+ Err(failure) => match failure {
413+ ResolutionFailure::OursRenamedTheirsRenamedDifferently { .. } => {
414+ (Some(conflict.theirs.location()), conflict.ours.location())
415+ }
416+ ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch
417+ | ResolutionFailure::OursDeletedTheirsRenamed
418+ | ResolutionFailure::OursModifiedTheirsDeleted
419+ | ResolutionFailure::Unknown => (None, conflict.ours.location()),
420+ ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed {
421+ renamed_unique_path_to_modified_blob,
422+ } => (
423+ Some(renamed_unique_path_to_modified_blob.as_bstr()),
424+ conflict.ours.location(),
425+ ),
426+ ResolutionFailure::OursAddedTheirsAddedTypeMismatch { their_unique_location } => {
427+ (Some(their_unique_location.as_bstr()), conflict.ours.location())
428+ }
429+ },
430+ };
431+ let source_path = conflict.ours.source_location();
432+
433+ let entries_with_stage = conflict.entries().into_iter().enumerate().filter_map(|(idx, entry)| {
434+ entry.filter(|e| e.mode.is_no_tree()).map(|e| {
435+ (
436+ match idx {
437+ 0 => gix_index::entry::Stage::Base,
438+ 1 => gix_index::entry::Stage::Ours,
439+ 2 => gix_index::entry::Stage::Theirs,
440+ _ => unreachable!("fixed size array with three items"),
441+ },
442+ match e.path_hint {
443+ None => renamed_path.unwrap_or(current_path),
444+ Some(ConflictIndexEntryPathHint::Source) => source_path,
445+ Some(ConflictIndexEntryPathHint::Current) => current_path,
446+ Some(ConflictIndexEntryPathHint::RenamedOrTheirs) => {
447+ renamed_path.unwrap_or_else(|| conflict.changes_in_resolution().1.location())
448+ }
449+ },
450+ e,
451+ )
452+ })
453+ });
454+
455+ if !entries_with_stage.clone().any(|(_, path, _)| {
456+ index
457+ .entry_index_by_path_and_stage_bounded(path, gix_index::entry::Stage::Unconflicted, len)
458+ .is_some()
459+ }) {
460+ continue;
461+ }
462+
463+ for (stage, path, entry) in entries_with_stage {
464+ if let Some(pos) =
465+ index.entry_index_by_path_and_stage_bounded(path, gix_index::entry::Stage::Unconflicted, len)
466+ {
467+ index.entries_mut()[pos].flags.insert(gix_index::entry::Flags::REMOVE);
468+ };
469+ match idx_by_path_stage.entry((stage, path)) {
470+ hash_map::Entry::Occupied(map_entry) => {
471+ // This can happen due to the way the algorithm works.
472+ // The same happens in Git, but it stores the index-related data as part of its deduplicating tree.
473+ // We store each conflict we encounter, which also may duplicate their index entries, sometimes, but
474+ // with different values. The most recent value wins.
475+ // Instead of trying to deduplicate the index entries when the merge runs, we put the cost
476+ // to the tree-assembly - there is no way around it.
477+ let index_entry = &mut index.entries_mut()[*map_entry.get()];
478+ index_entry.mode = entry.mode.into();
479+ index_entry.id = entry.id;
480+ }
481+ hash_map::Entry::Vacant(map_entry) => {
482+ map_entry.insert(index.entries().len());
483+ index.dangerously_push_entry(
484+ Default::default(),
485+ entry.id,
486+ stage.into(),
487+ entry.mode.into(),
488+ path,
489+ );
490+ }
491+ };
492+ }
493+ }
494+
495+ index.sort_entries();
496+ index.entries().len() != len
497+ }
498+ }
499+}
500+pub use apply_index_entries::function::apply_index_entries;
gix-merge/src/tree/utils.rs+43−4
@@ -7,7 +7,10 @@ · 6 unmodified lines
77 //! contribute to finding a fix faster.
88 use crate::blob::builtin_driver::binary::Pick;
99 use crate::blob::ResourceKind;
10−use crate::tree::{Conflict, ConflictMapping, Error, Options, Resolution, ResolutionFailure};
10+use crate::tree::{
11+ Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, Error, Options, Resolution,
12+ ResolutionFailure,
13+};
1114 use bstr::ByteSlice;
1215 use bstr::{BStr, BString, ByteVec};
1316 use gix_diff::tree_with_rewrites::{Change, ChangeRef};
@@ -98,6 +101,14 @@ pub fn perform_blob_merge<E>( · 84 unmodified lines
98101 where
99102 E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
100103 {
104+ if our_id == their_id {
105+ // This can happen if the merge modes are different.
106+ debug_assert_ne!(
107+ our_mode, their_mode,
108+ "BUG: we must think anything has to be merged if the modes and the ids are the same"
109+ );
110+ return Ok((their_id, crate::blob::Resolution::Complete));
111+ }
101112 if matches!(our_mode.kind(), EntryKind::Link) && matches!(their_mode.kind(), EntryKind::Link) {
102113 let (pick, resolution) = crate::blob::builtin_driver::binary(options.symlink_conflicts);
103114 let (our_id, their_id) = match outer_side {
@@ -544,29 +555,57 @@ impl Conflict { · 440 unmodified lines
544555 pub(super) fn without_resolution(
545556 resolution: ResolutionFailure,
546557 changes: (&Change, &Change, ConflictMapping, ConflictMapping),
558+ entries: [Option<ConflictIndexEntry>; 3],
547559 ) -> Self {
548− Conflict::maybe_resolved(Err(resolution), changes)
560+ Conflict::maybe_resolved(Err(resolution), changes, entries)
549561 }
550562
551563 pub(super) fn with_resolution(
552564 resolution: Resolution,
553565 changes: (&Change, &Change, ConflictMapping, ConflictMapping),
566+ entries: [Option<ConflictIndexEntry>; 3],
554567 ) -> Self {
555− Conflict::maybe_resolved(Ok(resolution), changes)
568+ Conflict::maybe_resolved(Ok(resolution), changes, entries)
556569 }
557570
558− pub(super) fn maybe_resolved(
571+ fn maybe_resolved(
559572 resolution: Result<Resolution, ResolutionFailure>,
560573 (ours, theirs, map, outer_map): (&Change, &Change, ConflictMapping, ConflictMapping),
574+ entries: [Option<ConflictIndexEntry>; 3],
561575 ) -> Self {
562576 Conflict {
563577 resolution,
564578 ours: ours.clone(),
565579 theirs: theirs.clone(),
580+ entries,
566581 map: match outer_map {
567582 ConflictMapping::Original => map,
568583 ConflictMapping::Swapped => map.swapped(),
569584 },
570585 }
571586 }
587+
588+ pub(super) fn unknown(changes: (&Change, &Change, ConflictMapping, ConflictMapping)) -> Self {
589+ let (source_mode, source_id) = changes.0.source_entry_mode_and_id();
590+ let (our_mode, our_id) = changes.0.entry_mode_and_id();
591+ let (their_mode, their_id) = changes.1.entry_mode_and_id();
592+ let entries = [
593+ Some(ConflictIndexEntry {
594+ mode: source_mode,
595+ id: source_id.into(),
596+ path_hint: Some(ConflictIndexEntryPathHint::Source),
597+ }),
598+ Some(ConflictIndexEntry {
599+ mode: our_mode,
600+ id: our_id.into(),
601+ path_hint: Some(ConflictIndexEntryPathHint::Current),
602+ }),
603+ Some(ConflictIndexEntry {
604+ mode: their_mode,
605+ id: their_id.into(),
606+ path_hint: Some(ConflictIndexEntryPathHint::RenamedOrTheirs),
607+ }),
608+ ];
609+ Conflict::maybe_resolved(Err(ResolutionFailure::Unknown), changes, entries)
610+ }
572611 }
gix-merge/tests/fixtures/generated-archives/tree-baseline.tar+0−0

Binary file not shown.

gix-merge/tests/fixtures/tree-baseline.sh+196−4
@@ -31,6 +31,11 @@ function seq () { · 30 unmodified lines
3131 done
3232 }
3333
34+function make_conflict_index() {
35+ local identifier=${1:?The first argument is the name of the parent directory along with the output name}
36+ cp .git/index .git/"${identifier}".index
37+}
38+
3439 function baseline () (
3540 local dir=${1:?the directory to enter}
3641 local output_name=${2:?the basename of the output of the merge}
@@ -172,6 +177,29 @@ git init rename-add · 135 unmodified lines
172177 git commit -m "rename foo to bar"
173178 )
174179
180+git init rename-add-exe-bit-conflict
181+(cd rename-add-exe-bit-conflict
182+ touch a b
183+ chmod +x a
184+ git add --chmod=+x a
185+ git add b
186+ git commit -m "original"
187+
188+ git branch A
189+ git branch B
190+
191+ git checkout A
192+ chmod -x a
193+ git update-index --chmod=-x a
194+ git commit -m "-x a"
195+
196+ git checkout B
197+ git mv --force b a
198+ chmod +x a
199+ git update-index --chmod=+x a
200+ git commit -m "mv b a; chmod +x a"
201+)
202+
175203 git init rename-add-symlink
176204 (cd rename-add-symlink
177205 write_lines original 1 2 3 4 5 >foo
@@ -317,6 +345,34 @@ git init super-2 · 139 unmodified lines
317345 git add foo
318346 git mv foo olddir/bar
319347 git commit -m "Modify foo & rename foo -> olddir/bar"
348+
349+ rm .git/index
350+ git update-index --index-info <<EOF
351+100644 78981922613b2afb6025042ff6bd878ac1994e85 0 newdir/a
352+100644 61780798228d17af2d34fce4cfbdf35556832472 0 newdir/b
353+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 newdir/bar/file
354+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 newdir/bar~B
355+100644 f2ad6c76f0115a6ba5b00456a849810e7ec0af20 0 newdir/c
356+EOF
357+ # Git also has
358+ # 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 1 newdir/bar~B
359+ # which then looks like "deleted by us: newdir/bar-B`
360+ # Our index here doesn't manage to track the base across so many renames, but it ends up looking like
361+ # `added by them: newdir/bar~B` which to my mind is more helpful, in a situation where the index simply
362+ # cannot properly show what happened.
363+ make_conflict_index super-2-A-B
364+ make_conflict_index super-2-A-B-diff3
365+
366+ rm .git/index
367+ git update-index --index-info <<EOF
368+100644 78981922613b2afb6025042ff6bd878ac1994e85 0 newdir/a
369+100644 61780798228d17af2d34fce4cfbdf35556832472 0 newdir/b
370+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 newdir/bar/file
371+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 newdir/bar~B
372+100644 f2ad6c76f0115a6ba5b00456a849810e7ec0af20 0 newdir/c
373+EOF
374+ make_conflict_index super-2-A-B-reversed
375+ make_conflict_index super-2-A-B-diff3-reversed
320376 )
321377
322378 git init rename-within-rename
@@ -348,6 +404,10 @@ git init rename-within-rename · 25 unmodified lines
348404 git mv a/sub a/sub-renamed
349405 git mv a a-renamed
350406 git commit -am "tracked both renames, applied all modifications by merge"
407+
408+ # This means there are no conflicts actually.
409+ make_conflict_index rename-within-rename-A-B-deviates
410+ make_conflict_index rename-within-rename-A-B-deviates-reversed
351411 )
352412
353413 git init rename-within-rename-2
@@ -380,6 +440,10 @@ git init rename-within-rename-2 · 26 unmodified lines
380440 git mv a/sub a/sub-renamed
381441 git mv a a-renamed
382442 git commit -am "tracked both renames, applied all modifications by merge"
443+
444+ # This means there are no conflicts actually.
445+ make_conflict_index rename-within-rename-2-A-B-deviates
446+ make_conflict_index rename-within-rename-2-A-B-deviates-reversed
383447 )
384448
385449 git init conflicting-rename
@@ -403,6 +467,43 @@ git init conflicting-rename · 17 unmodified lines
403467 write_lines original 1 2 3 4 5 6 >a/sub/y.f
404468 git mv a a-different
405469 git commit -am "changed all content, renamed a -> a-different"
470+
471+# Git only sees the files with content changes as conflicting, and somehow misses to add the
472+# bases of the files without content changes. After all, these also have been renamed into
473+# different places which must be a conflict just as much.
474+ rm .git/index
475+ git update-index --index-info <<EOF
476+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-different/sub/y.f
477+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-different/sub/z
478+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-different/w
479+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-different/x.f
480+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-renamed/sub/y.f
481+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z
482+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/w
483+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-renamed/x.f
484+100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f
485+100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/x.f
486+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z
487+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/w
488+EOF
489+ make_conflict_index conflicting-rename-A-B
490+
491+ rm .git/index
492+ git update-index --index-info <<EOF
493+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-different/sub/y.f
494+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-different/sub/z
495+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-different/w
496+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-different/x.f
497+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-renamed/sub/y.f
498+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z
499+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/w
500+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-renamed/x.f
501+100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f
502+100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/x.f
503+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z
504+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/w
505+EOF
506+ make_conflict_index conflicting-rename-A-B-reversed
406507 )
407508
408509 git init conflicting-rename-2
@@ -426,6 +527,34 @@ git init conflicting-rename-2 · 17 unmodified lines
426527 write_lines original 1 2 3 4 5 6 >a/sub/y.f
427528 git mv a/sub a/sub-different
428529 git commit -am "changed all content, renamed a/sub -> a/sub-different"
530+
531+# Here it's the same as above, i.e. Git doesn't list files as conflicting if
532+# they didn't change, even though they have a conflicting rename.
533+ rm .git/index
534+ git update-index --index-info <<EOF
535+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/sub-different/y.f
536+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a/sub-different/z
537+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/sub-renamed/y.f
538+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a/sub-renamed/z
539+100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f
540+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z
541+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a/w
542+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a/x.f
543+EOF
544+ make_conflict_index conflicting-rename-2-A-B
545+
546+ rm .git/index
547+ git update-index --index-info <<EOF
548+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/sub-different/y.f
549+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a/sub-different/z
550+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/sub-renamed/y.f
551+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a/sub-renamed/z
552+100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f
553+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z
554+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a/w
555+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a/x.f
556+EOF
557+ make_conflict_index conflicting-rename-2-A-B-reversed
429558 )
430559
431560 git init conflicting-rename-complex
@@ -461,6 +590,27 @@ git init conflicting-rename-complex · 29 unmodified lines
461590 touch a-renamed/z a-renamed/sub/z
462591 git add .
463592 git commit -m "Close to what Git has, but different due to rename tracking (which looses 'a/w', and 'x.f' becomes y.f). But the merge is so 'erroneous' that it's beyond rescue"
593+
594+ # Since the whole state is very different, the expected index is as well, but at least it should make sense for what it is.
595+ # The main issue here is that it finds a rename of a/w to a-renamed/z which completely erases `a/z`, and this happens because it has no basename based matching
596+ # like Git during rename tracking.
597+ rm .git/index
598+ git update-index --index-info <<EOF
599+100644 8a1218a1024a212bb3db30becd860315f9f3ac52 2 a-renamed/sub/y.f
600+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z
601+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f
602+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z
603+EOF
604+ make_conflict_index conflicting-rename-complex-A-B
605+
606+ rm .git/index
607+ git update-index --index-info <<EOF
608+100644 8a1218a1024a212bb3db30becd860315f9f3ac52 3 a-renamed/sub/y.f
609+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z
610+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f
611+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z
612+EOF
613+ make_conflict_index conflicting-rename-complex-A-B-reversed
464614 )
465615
466616 git init same-rename-different-mode
@@ -491,6 +641,29 @@ git init same-rename-different-mode · 24 unmodified lines
491641 write_lines 1 2 3 4 5 6 >a/x.f
492642 git mv a a-renamed
493643 git commit -am "Git, when branches are reversed, doesn't keep the +x flag on a/w so we specify our own expectation"
644+ # Git sets +x and adds it as conflict, even though the merge is perfect, i.e. one side adds +x on top, perfectly additive.
645+ make_conflict_index same-rename-different-mode-A-B
646+ make_conflict_index same-rename-different-mode-A-B-reversed
647+)
648+
649+git init remove-executable-mode
650+(cd remove-executable-mode
651+ touch w
652+ chmod +x w
653+ git add --chmod=+x w
654+ git add . && git commit -m "original"
655+
656+ git branch A
657+ git branch B
658+
659+ git checkout A
660+ chmod -x w
661+ git update-index --chmod=-x w
662+ git commit -am "remove executable bit from w"
663+
664+ git checkout B
665+ write_lines 1 2 3 4 5 >w
666+ git commit -am "unrelated change to w"
494667 )
495668
496669 git init renamed-symlink-with-conflict
@@ -614,6 +787,23 @@ git init submodule-both-modify · 117 unmodified lines
614787 git add sub
615788 tick
616789 git commit -m b
790+
791+ # We cannot handle submodules yet and thus mark them as conflicted, always if they mismatch at least.
792+ rm .git/index
793+ git update-index --index-info <<EOF
794+160000 e835c0c403c8e494c0ca98f3d25d0b8464c18d38 1 sub
795+160000 64466ebdff775ad618d9cc993cf52840e0af528c 2 sub
796+160000 ea6eb701e03c2497915c25a851f3da8f8e362ca0 3 sub
797+EOF
798+ make_conflict_index submodule-both-modify-A-B
799+
800+ rm .git/index
801+ git update-index --index-info <<EOF
802+160000 e835c0c403c8e494c0ca98f3d25d0b8464c18d38 1 sub
803+160000 ea6eb701e03c2497915c25a851f3da8f8e362ca0 2 sub
804+160000 64466ebdff775ad618d9cc993cf52840e0af528c 3 sub
805+EOF
806+ make_conflict_index submodule-both-modify-A-B-reversed
617807 )
618808
619809 git init both-modify-union-attr
@@ -670,7 +860,7 @@ git init both-modify-file-with-binary-attr · 50 unmodified lines
670860
671861 git init big-file-merge
672862 (cd big-file-merge
673− git config --local core.bigFileThreshold 80
863+ git config --local core.bigFileThreshold 100
674864 mkdir a && write_lines original 1 2 3 4 5 >a/x.f
675865 git add . && git commit -m "original"
676866
@@ -678,8 +868,8 @@ git init big-file-merge · 1 unmodified line
678868 git branch B
679869
680870 git checkout A
681− seq 30 >a/x.f
682− git commit -am "turn normal file into big one (81 bytes)"
871+ seq 37 >a/x.f
872+ git commit -am "turn normal file into big one (102 bytes)"
683873 git branch expected
684874
685875 git checkout B
@@ -803,6 +993,9 @@ git init type-change-to-symlink · 117 unmodified lines
803993
804994
805995
996+# TODO: Git does not detect the conflict (one turns exe off, the other turns it on), and we do exactly the same.
997+baseline rename-add-exe-bit-conflict A-B A B
998+baseline remove-executable-mode A-B A B
806999 baseline simple side-1-3-without-conflict side1 side3
8071000 baseline simple fast-forward side1 main
8081001 baseline simple no-change main main
@@ -838,7 +1031,6 @@ baseline conflicting-rename-2 A-B A B · 29 unmodified lines
8381031 baseline conflicting-rename-complex A-B A B "Git has different rename tracking which is why a-renamed/w disappears - it's still close enough"
8391032
8401033 baseline same-rename-different-mode A-B A B "Git works for the A/B case, but for B/A it forgets to set the executable bit"
841−baseline same-rename-different-mode A-B-diff3 A B "Git works for the A/B case, but for B/A it forgets to set the executable bit"
8421034 baseline renamed-symlink-with-conflict A-B A B
8431035 baseline added-file-changed-content-and-mode A-B A B "We improve on executable bit handling, but loose on diff quality as we are definitely missing some tweaks"
8441036
gix-merge/tests/merge/tree/baseline.rs+64−4
@@ -6,7 +6,7 @@ use gix_object::FindExt; · 5 unmodified lines
66 use std::path::{Path, PathBuf};
77
88 /// An entry in the conflict
9−#[derive(Debug)]
9+#[derive(Debug, Eq, PartialEq)]
1010 pub struct Entry {
1111 /// The relative path in the repository
1212 pub location: String,
@@ -17,7 +17,7 @@ pub struct Entry { · 4 unmodified lines
1717 }
1818
1919 /// Keep track of all the sides of a conflict. Some might not be set to indicate removal, including the ancestor.
20−#[derive(Default, Debug)]
20+#[derive(Default, Debug, Eq, PartialEq)]
2121 pub struct Conflict {
2222 pub ancestor: Option<Entry>,
2323 pub ours: Option<Entry>,
@@ -129,7 +129,7 @@ impl Iterator for Expectations<'_> { · 105 unmodified lines
129129 let mut tokens = line.split(' ');
130130 let (
131131 Some(subdir),
132− Some(conflict_style),
132+ Some(conflict_style_name),
133133 Some(our_commit_id),
134134 Some(our_side_name),
135135 Some(their_commit_id),
@@ -160,7 +160,7 @@ impl Iterator for Expectations<'_> { · 24 unmodified lines
160160 });
161161
162162 let subdir_path = self.root.join(subdir);
163− let conflict_style = match conflict_style {
163+ let conflict_style = match conflict_style_name {
164164 "merge" => ConflictStyle::Merge,
165165 "diff3" => ConflictStyle::Diff3,
166166 unknown => unreachable!("Unknown conflict style: '{unknown}'"),
@@ -221,6 +221,10 @@ fn parse_merge_info(content: String) -> MergeInfo { · 54 unmodified lines
221221 *field = Some(entry);
222222 }
223223
224+ if conflict.any_location().is_some() && conflicts.last() != Some(&conflict) {
225+ conflicts.push(conflict);
226+ }
227+
224228 while lines.peek().is_some() {
225229 out.information
226230 .push(parse_info(&mut lines).expect("if there are lines, it should be valid info"));
@@ -285,6 +289,30 @@ fn parse_info<'a>(mut lines: impl Iterator<Item = &'a str>) -> Option<ConflictIn · 58 unmodified lines
285289 Some(ConflictInfo { paths, kind, message })
286290 }
287291
292+#[derive(Debug, PartialEq, Eq)]
293+pub struct DebugIndexEntry<'a> {
294+ path: &'a BStr,
295+ id: gix_hash::ObjectId,
296+ mode: gix_index::entry::Mode,
297+ stage: gix_index::entry::Stage,
298+}
299+
300+pub fn clear_entries(state: &gix_index::State) -> Vec<DebugIndexEntry<'_>> {
301+ state
302+ .entries()
303+ .iter()
304+ .map(|entry| {
305+ let path = entry.path(state);
306+ DebugIndexEntry {
307+ path,
308+ id: entry.id,
309+ mode: entry.mode,
310+ stage: entry.stage(),
311+ }
312+ })
313+ .collect()
314+}
315+
288316 pub fn visualize_tree(
289317 id: &gix_hash::oid,
290318 odb: &impl gix_object::Find,
@@ -342,3 +370,35 @@ pub fn show_diff_and_fail( · 51 unmodified lines
342370 expected.information
343371 );
344372 }
373+
374+pub(crate) fn apply_git_index_entries(conflicts: &[Conflict], state: &mut gix_index::State) {
375+ let len = state.entries().len();
376+ for Conflict { ours, theirs, ancestor } in conflicts {
377+ for (entry, stage) in [
378+ ancestor.as_ref().map(|e| (e, gix_index::entry::Stage::Base)),
379+ ours.as_ref().map(|e| (e, gix_index::entry::Stage::Ours)),
380+ theirs.as_ref().map(|e| (e, gix_index::entry::Stage::Theirs)),
381+ ]
382+ .into_iter()
383+ .flatten()
384+ {
385+ if let Some(pos) = state.entry_index_by_path_and_stage_bounded(
386+ entry.location.as_str().into(),
387+ gix_index::entry::Stage::Unconflicted,
388+ len,
389+ ) {
390+ state.entries_mut()[pos].flags.insert(gix_index::entry::Flags::REMOVE);
391+ }
392+
393+ state.dangerously_push_entry(
394+ Default::default(),
395+ entry.id,
396+ stage.into(),
397+ entry.mode.into(),
398+ entry.location.as_str().into(),
399+ );
400+ }
401+ }
402+ state.sort_entries();
403+ state.remove_entries(|_, _, e| e.flags.contains(gix_index::entry::Flags::REMOVE));
404+}
gix-merge/tests/merge/tree/mod.rs+51−2
@@ -1,6 +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;
45 use gix_object::Write;
56 use gix_worktree::stack::state::attributes;
67 use std::path::Path;
@@ -20,7 +21,7 @@ fn run_baseline() -> crate::Result { · 13 unmodified lines
2021 let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?;
2122 let cases = std::fs::read_to_string(root.join("baseline.cases"))?;
2223 let mut actual_cases = 0;
23− // let new_test = Some("simple-fast-forward");
24+ // let new_test = Some("rename-add-symlink-A-B");
2425 let new_test = None;
2526 for baseline::Expectation {
2627 root,
@@ -98,10 +99,58 @@ fn run_baseline() -> crate::Result { · 71 unmodified lines
9899 );
99100 }
100101 }
102+
103+ let mut actual_index = gix_index::State::from_tree(&actual_id, &odb, Default::default())?;
104+ let expected_index = {
105+ let derivative_index_path = root.join(".git").join(format!("{case_name}.index"));
106+ if derivative_index_path.exists() {
107+ gix_index::File::at(
108+ derivative_index_path,
109+ odb.store().object_hash(),
110+ true,
111+ Default::default(),
112+ )?
113+ .into()
114+ } else {
115+ let mut index = actual_index.clone();
116+ if let Some(conflicts) = &merge_info.conflicts {
117+ baseline::apply_git_index_entries(conflicts, &mut index);
118+ }
119+ index
120+ }
121+ };
122+ let conflicts_like_in_git = TreatAsUnresolved::Renames;
123+ let did_change = actual.index_changed_after_applying_conflicts(&mut actual_index, conflicts_like_in_git);
124+ actual_index.remove_entries(|_, _, e| e.flags.contains(gix_index::entry::Flags::REMOVE));
125+
126+ pretty_assertions::assert_eq!(
127+ baseline::clear_entries(&actual_index),
128+ baseline::clear_entries(&expected_index),
129+ "{case_name}: index mismatch\n{:#?}\n{:#?}",
130+ actual.conflicts,
131+ merge_info.conflicts
132+ );
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+ }
101150 }
102151
103152 assert_eq!(
104− actual_cases, 105,
153+ actual_cases, 107,
105154 "BUG: update this number, and don't forget to remove a filter in the end"
106155 );
107156

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t