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
2114 "gix-filter",2114 "gix-filter",
2115 "gix-fs 0.12.0",2115 "gix-fs 0.12.0",
2116 "gix-hash 0.15.0",2116 "gix-hash 0.15.0",
2117 "gix-index 0.36.0",
2117 "gix-object 0.45.0",2118 "gix-object 0.45.0",
2118 "gix-odb",2119 "gix-odb",
2119 "gix-path 0.10.12",2120 "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
318 * [x] find blobs by similarity check318 * [x] find blobs by similarity check
319 * [ ] heuristics to find best candidate319 * [ ] heuristics to find best candidate
320 * [ ] find by basename to support similarity check320 * [ ] 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.
321 * [x] directory tracking323 * [x] directory tracking
322 - [x] by identity324 - [x] by identity
323 - [ ] by similarity325 - [ ] by similarity
@@ -349,8 +351,7 @@ Check out the [performance discussion][gix-diff-performance] as well. · 25 unmodified lines
349 - [ ] various newlines-related options during the merge (see https://git-scm.com/docs/git-merge#Documentation/git-merge.txt-ignore-space-change).351 - [ ] various newlines-related options during the merge (see https://git-scm.com/docs/git-merge#Documentation/git-merge.txt-ignore-space-change).
350 - [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same)352 - [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same)
351* [x] **tree**-diff-heuristics match Git for its test-cases353* [x] **tree**-diff-heuristics match Git for its test-cases
352 - [ ] a way to generate an index with stages 354 - [x] a way to generate an index with stages, mostly conforming with Git.
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*)355 - [ ] submodule merges (*right now they count as conflicts if they differ*)
355* [x] **commits** - with handling of multiple merge bases by recursive merge-base merge356* [x] **commits** - with handling of multiple merge bases by recursive merge-base merge
356* [x] API documentation357* [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
32gix-revision = { version = "^0.30.0", path = "../gix-revision", default-features = false, features = ["merge_base"] }32gix-revision = { version = "^0.30.0", path = "../gix-revision", default-features = false, features = ["merge_base"] }
33gix-revwalk = { version = "^0.16.0", path = "../gix-revwalk" }33gix-revwalk = { version = "^0.16.0", path = "../gix-revwalk" }
34gix-diff = { version = "^0.47.0", path = "../gix-diff", default-features = false, features = ["blob"] }34gix-diff = { version = "^0.47.0", path = "../gix-diff", default-features = false, features = ["blob"] }
35gix-index = { version = "^0.36.0", path = "../gix-index" }
35 36
36thiserror = "2.0.0"37thiserror = "2.0.0"
37imara-diff = { version = "0.1.7" }38imara-diff = { version = "0.1.7" }
gix-merge/src/tree/function.rs+179−33
@@ -3,7 +3,10 @@ use crate::tree::utils::{ · 2 unmodified lines
3 to_components, track, unique_path_in_tree, ChangeList, ChangeListRef, PossibleConflict, TrackedChange, TreeNodes,3 to_components, track, unique_path_in_tree, ChangeList, ChangeListRef, PossibleConflict, TrackedChange, TreeNodes,
4};4};
5use crate::tree::ConflictMapping::{Original, Swapped};5use crate::tree::ConflictMapping::{Original, Swapped};
6use crate::tree::{Conflict, ConflictMapping, ContentMerge, Error, Options, Outcome, Resolution, ResolutionFailure};6use crate::tree::{
7 Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, ContentMerge, Error, Options, Outcome,
8 Resolution, ResolutionFailure,
9};
7use bstr::{BString, ByteSlice};10use bstr::{BString, ByteSlice};
8use gix_diff::tree::recorder::Location;11use gix_diff::tree::recorder::Location;
9use gix_diff::tree_with_rewrites::Change;12use gix_diff::tree_with_rewrites::Change;
@@ -190,11 +193,14 @@ where · 180 unmodified lines
190 }) {193 }) {
191 None => {194 None => {
192 if let Some((rewritten_location, ours_idx)) = rewritten_location {195 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.
193 if should_fail_on_conflict(Conflict::with_resolution(198 if should_fail_on_conflict(Conflict::with_resolution(
194 Resolution::SourceLocationAffectedByRename {199 Resolution::SourceLocationAffectedByRename {
195 final_location: rewritten_location.to_owned(),200 final_location: rewritten_location.to_owned(),
196 },201 },
197 (&our_changes[*ours_idx].inner, theirs, Original, outer_side),202 (&our_changes[*ours_idx].inner, theirs, Original, outer_side),
203 [None, None, None],
198 )) {204 )) {
199 break 'outer;205 break 'outer;
200 };206 };
@@ -254,10 +260,12 @@ where · 53 unmodified lines
254 if let Some(ours) = ours {260 if let Some(ours) = ours {
255 gix_trace::debug!("Turning a case we could probably handle into a conflict for now. theirs: {theirs:#?} ours: {ours:#?} kind: {match_kind:?}");261 gix_trace::debug!("Turning a case we could probably handle into a conflict for now. theirs: {theirs:#?} ours: {ours:#?} kind: {match_kind:?}");
256 if allow_resolution_failure262 if allow_resolution_failure
257 && should_fail_on_conflict(Conflict::without_resolution(263 && should_fail_on_conflict(Conflict::unknown((
258 ResolutionFailure::Unknown,264 &ours.inner,
259 (&ours.inner, theirs, Original, outer_side),265 theirs,
260 ))266 Original,
267 outer_side,
268 )))
261 {269 {
262 break 'outer;270 break 'outer;
263 };271 };
@@ -328,7 +336,7 @@ where · 64 unmodified lines
328 pick_our_changes(side, our_changes, their_changes),336 pick_our_changes(side, our_changes, their_changes),
329 );337 );
330 let renamed_without_change = their_source_id == their_id;338 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 {
332 (*our_id, None)340 (*our_id, None)
333 } else {341 } else {
334 let (our_location, our_id, our_mode, their_location, their_id, their_mode) =342 let (our_location, our_id, our_mode, their_location, their_id, their_mode) =
@@ -373,18 +381,23 @@ where · 38 unmodified lines
373 location: their_rewritten_location.unwrap_or_else(|| their_location.to_owned()),381 location: their_rewritten_location.unwrap_or_else(|| their_location.to_owned()),
374 relation: None,382 relation: None,
375 entry_mode: merged_mode,383 entry_mode: merged_mode,
376 id: our_id,384 id: merged_blob_id,
377 };385 };
378 if should_fail_on_conflict(Conflict::with_resolution(386 if should_fail_on_conflict(Conflict::with_resolution(
379 Resolution::OursModifiedTheirsRenamedAndChangedThenRename {387 Resolution::OursModifiedTheirsRenamedAndChangedThenRename {
380 merged_mode: (merged_mode != *their_mode).then_some(merged_mode),388 merged_mode: (merged_mode != *their_mode).then_some(merged_mode),
381 merged_blob: resolution.map(|resolution| ContentMerge {389 merged_blob: resolution.map(|resolution| ContentMerge {
382 resolution,390 resolution,
383 merged_blob_id: our_id,391 merged_blob_id,
384 }),392 }),
385 final_location,393 final_location,
386 },394 },
387 (ours, theirs, side, outer_side),395 (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 ],
388 )) {401 )) {
389 break 'outer;402 break 'outer;
390 }403 }
@@ -401,6 +414,19 @@ where · 10 unmodified lines
401 if should_fail_on_conflict(Conflict::without_resolution(414 if should_fail_on_conflict(Conflict::without_resolution(
402 ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch,415 ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch,
403 (ours, theirs, side, outer_side),416 (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 ],
404 )) {430 )) {
405 break 'outer;431 break 'outer;
406 }432 }
@@ -421,7 +447,7 @@ where · 14 unmodified lines
421 ..447 ..
422 },448 },
423 ) if !involves_submodule(our_mode, their_mode)449 ) if !involves_submodule(our_mode, their_mode)
424 && our_mode.kind() == their_mode.kind()450 && merge_modes(*our_mode, *their_mode).is_some()
425 && our_id != their_id =>451 && our_id != their_id =>
426 {452 {
427 let (merged_blob_id, resolution) = perform_blob_merge(453 let (merged_blob_id, resolution) = perform_blob_merge(
@@ -436,7 +462,11 @@ where · 8 unmodified lines
436 (0, outer_side),462 (0, outer_side),
437 &options,463 &options,
438 )?;464 )?;
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)?;
440 if should_fail_on_conflict(Conflict::with_resolution(470 if should_fail_on_conflict(Conflict::with_resolution(
441 Resolution::OursModifiedTheirsModifiedThenBlobContentMerge {471 Resolution::OursModifiedTheirsModifiedThenBlobContentMerge {
442 merged_blob: ContentMerge {472 merged_blob: ContentMerge {
@@ -445,6 +475,11 @@ where · 2 unmodified lines
445 },475 },
446 },476 },
447 (ours, theirs, Original, outer_side),477 (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 ],
448 )) {483 )) {
449 break 'outer;484 break 'outer;
450 };485 };
@@ -489,27 +524,28 @@ where · 38 unmodified lines
489 },524 },
490 },525 },
491 (ours, theirs, Original, outer_side),526 (ours, theirs, Original, outer_side),
527 [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)],
492 ))528 ))
493 } else if allow_resolution_failure {529 } else if allow_resolution_failure {
494 // Actually this has a preference, as symlinks are always left in place with the other side renamed.530 // Actually this has a preference, as symlinks are always left in place with the other side renamed.
495 let (531 let (
496 logical_side,532 logical_side,
497 label_of_side_to_be_moved,533 label_of_side_to_be_moved,
498 (our_mode, our_id),534 (our_mode, our_id, our_path_hint),
499 (their_mode, their_id),535 (their_mode, their_id, their_path_hint),
500 ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) {536 ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) {
501 (537 (
502 Original,538 Original,
503 labels.other.unwrap_or_default(),539 labels.other.unwrap_or_default(),
504 (*our_mode, *our_id),540 (*our_mode, *our_id, ConflictIndexEntryPathHint::Current),
505 (*their_mode, *their_id),541 (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs),
506 )542 )
507 } else {543 } else {
508 (544 (
509 Swapped,545 Swapped,
510 labels.current.unwrap_or_default(),546 labels.current.unwrap_or_default(),
511 (*their_mode, *their_id),547 (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs),
512 (*our_mode, *our_id),548 (*our_mode, *our_id, ConflictIndexEntryPathHint::Current),
513 )549 )
514 };550 };
515 let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree);551 let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree);
@@ -525,6 +561,11 @@ where · 9 unmodified lines
525 their_unique_location: renamed_location.clone(),561 their_unique_location: renamed_location.clone(),
526 },562 },
527 (ours, theirs, logical_side, outer_side),563 (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 ],
528 );569 );
529 570
530 let new_change = Change::Addition {571 let new_change = Change::Addition {
@@ -554,7 +595,8 @@ where · 23 unmodified lines
554 location,595 location,
555 entry_mode,596 entry_mode,
556 id,597 id,
557 ..598 previous_entry_mode,
599 previous_id,
558 },600 },
559 Change::Deletion { .. },601 Change::Deletion { .. },
560 )602 )
@@ -564,7 +606,8 @@ where · 3 unmodified lines
564 location,606 location,
565 entry_mode,607 entry_mode,
566 id,608 id,
567 ..609 previous_entry_mode,
610 previous_id,
568 },611 },
569 ) if allow_resolution_failure => {612 ) if allow_resolution_failure => {
570 let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) {613 let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) {
@@ -606,6 +649,11 @@ where · 35 unmodified lines
606 renamed_unique_path_to_modified_blob: renamed_path,649 renamed_unique_path_to_modified_blob: renamed_path,
607 },650 },
608 (ours, theirs, side, outer_side),651 (ours, theirs, side, outer_side),
652 [
653 index_entry(previous_entry_mode, previous_id),
654 index_entry(entry_mode, id),
655 None,
656 ],
609 ));657 ));
610 658
611 // Since we move *our* side, our tree needs to be modified.659 // Since we move *our* side, our tree needs to be modified.
@@ -621,6 +669,11 @@ where · 9 unmodified lines
621 let should_break = should_fail_on_conflict(Conflict::without_resolution(669 let should_break = should_fail_on_conflict(Conflict::without_resolution(
622 ResolutionFailure::OursModifiedTheirsDeleted,670 ResolutionFailure::OursModifiedTheirsDeleted,
623 (ours, theirs, side, outer_side),671 (ours, theirs, side, outer_side),
672 [
673 index_entry(previous_entry_mode, previous_id),
674 index_entry(entry_mode, id),
675 None,
676 ],
624 ));677 ));
625 editor.upsert(toc(location), entry_mode.kind(), *id)?;678 editor.upsert(toc(location), entry_mode.kind(), *id)?;
626 if should_break {679 if should_break {
@@ -702,10 +755,9 @@ where · 75 unmodified lines
702 // Pretend this is the end of the loop and keep this as conflict.755 // Pretend this is the end of the loop and keep this as conflict.
703 // If this happens in the wild, we'd want to reproduce it.756 // If this happens in the wild, we'd want to reproduce it.
704 if allow_resolution_failure757 if allow_resolution_failure
705 && should_fail_on_conflict(Conflict::without_resolution(758 && should_fail_on_conflict(Conflict::unknown((
706 ResolutionFailure::Unknown,759 ours, theirs, Original, outer_side,
707 (ours, theirs, Original, outer_side),760 )))
708 ))
709 {761 {
710 break 'outer;762 break 'outer;
711 };763 };
@@ -738,6 +790,23 @@ where · 26 unmodified lines
738 }),790 }),
739 },791 },
740 (ours, theirs, Original, outer_side),792 (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 ],
741 )) {810 )) {
742 break 'outer;811 break 'outer;
743 };812 };
@@ -767,6 +836,23 @@ where · 23 unmodified lines
767 },836 },
768 },837 },
769 (ours, theirs, Original, outer_side),838 (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 ],
770 )) {856 )) {
771 break 'outer;857 break 'outer;
772 };858 };
@@ -824,6 +910,15 @@ where · 51 unmodified lines
824 if should_fail_on_conflict(Conflict::without_resolution(910 if should_fail_on_conflict(Conflict::without_resolution(
825 ResolutionFailure::OursDeletedTheirsRenamed,911 ResolutionFailure::OursDeletedTheirsRenamed,
826 (ours, theirs, side, outer_side),912 (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 ],
827 )) {922 )) {
828 break 'outer;923 break 'outer;
829 };924 };
@@ -901,6 +996,7 @@ where · 71 unmodified lines
901 },996 },
902 },997 },
903 (ours, theirs, Original, outer_side),998 (ours, theirs, Original, outer_side),
999 [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)],
904 )) {1000 )) {
905 break 'outer;1001 break 'outer;
906 };1002 };
@@ -916,21 +1012,21 @@ where · 9 unmodified lines
916 let (1012 let (
917 logical_side,1013 logical_side,
918 label_of_side_to_be_moved,1014 label_of_side_to_be_moved,
919 (our_mode, our_id),1015 (our_mode, our_id, our_path_hint),
920 (their_mode, their_id),1016 (their_mode, their_id, their_path_hint),
921 ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) {1017 ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) {
922 (1018 (
923 Original,1019 Original,
924 labels.other.unwrap_or_default(),1020 labels.other.unwrap_or_default(),
925 (*our_mode, *our_id),1021 (*our_mode, *our_id, ConflictIndexEntryPathHint::Current),
926 (*their_mode, *their_id),1022 (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs),
927 )1023 )
928 } else {1024 } else {
929 (1025 (
930 Swapped,1026 Swapped,
931 labels.current.unwrap_or_default(),1027 labels.current.unwrap_or_default(),
932 (*their_mode, *their_id),1028 (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs),
933 (*our_mode, *our_id),1029 (*our_mode, *our_id, ConflictIndexEntryPathHint::Current),
934 )1030 )
935 };1031 };
936 let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree);1032 let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree);
@@ -946,6 +1042,11 @@ where · 9 unmodified lines
946 their_unique_location: renamed_location.clone(),1042 their_unique_location: renamed_location.clone(),
947 },1043 },
948 (ours, theirs, side, outer_side),1044 (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 ],
949 );1050 );
950 1051
951 let new_change_with_rename = Change::Addition {1052 let new_change_with_rename = Change::Addition {
@@ -970,10 +1071,7 @@ where · 18 unmodified lines
970 }1071 }
971 _unknown => {1072 _unknown => {
972 if allow_resolution_failure1073 if allow_resolution_failure
973 && should_fail_on_conflict(Conflict::without_resolution(1074 && should_fail_on_conflict(Conflict::unknown((ours, theirs, Original, outer_side)))
974 ResolutionFailure::Unknown,
975 (ours, theirs, Original, outer_side),
976 ))
977 {1075 {
978 break 'outer;1076 break 'outer;
979 };1077 };
@@ -1004,12 +1102,40 @@ fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool { · 24 unmodified lines
1004 a.is_commit() || b.is_commit()1102 a.is_commit() || b.is_commit()
1005}1103}
1006 1104
1007/// Allows equal modes or preferes executables bits in case of blobs1105/// 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
1008fn merge_modes(a: EntryMode, b: EntryMode) -> Option<EntryMode> {1112fn merge_modes(a: EntryMode, b: EntryMode) -> Option<EntryMode> {
1009 match (a.kind(), b.kind()) {1113 match (a.kind(), b.kind()) {
1114 (_, _) if a == b => Some(a),
1010 (EntryKind::BlobExecutable, EntryKind::BlobExecutable | EntryKind::Blob)1115 (EntryKind::BlobExecutable, EntryKind::BlobExecutable | EntryKind::Blob)
1011 | (EntryKind::Blob, EntryKind::BlobExecutable) => Some(EntryKind::BlobExecutable.into()),1116 | (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.
1123fn merge_modes_prev(a: EntryMode, b: EntryMode, prev: EntryMode) -> Option<EntryMode> {
1124 match (a.kind(), b.kind()) {
1012 (_, _) if a == b => Some(a),1125 (_, _) 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 }
1013 _ => None,1139 _ => None,
1014 }1140 }
1015}1141}
@@ -1066,3 +1192,23 @@ fn pick_our_changes_mut<'a>( · 50 unmodified lines
1066 Swapped => theirs,1192 Swapped => theirs,
1067 }1193 }
1068}1194}
1195
1196fn 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
1204fn 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
81 pub fn has_unresolved_conflicts(&self, how: TreatAsUnresolved) -> bool {81 pub fn has_unresolved_conflicts(&self, how: TreatAsUnresolved) -> bool {
82 self.conflicts.iter().any(|c| c.is_unresolved(how))82 self.conflicts.iter().any(|c| c.is_unresolved(how))
83 }83 }
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 }
84}95}
85 96
86/// A description of a conflict (i.e. merge issue without an auto-resolution) as seen during a [tree-merge](crate::tree()).97/// 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
99 pub ours: Change,110 pub ours: Change,
100 /// The change representing *their* side.111 /// The change representing *their* side.
101 pub theirs: Change,112 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],
102 /// Determine how to interpret the `ours` and `theirs` fields. This is used to implement [`Self::changes_in_resolution()`]121 /// Determine how to interpret the `ours` and `theirs` fields. This is used to implement [`Self::changes_in_resolution()`]
103 /// and [`Self::into_parts_by_resolution()`].122 /// and [`Self::into_parts_by_resolution()`].
104 map: ConflictMapping,123 map: ConflictMapping,
105}124}
106 125
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)]
129pub 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)]
142enum 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
107/// A utility to help define which side is what in the [`Conflict`] type.152/// A utility to help define which side is what in the [`Conflict`] type.
108#[derive(Debug, Clone, Copy)]153#[derive(Debug, Clone, Copy)]
109enum ConflictMapping {154enum ConflictMapping {
@@ -147,7 +192,11 @@ impl Conflict { · 37 unmodified lines
147 TreatAsUnresolved::Renames | TreatAsUnresolved::RenamesAndAutoResolvedContent => match &self.resolution {192 TreatAsUnresolved::Renames | TreatAsUnresolved::RenamesAndAutoResolvedContent => match &self.resolution {
148 Ok(success) => match success {193 Ok(success) => match success {
149 Resolution::SourceLocationAffectedByRename { .. } => false,194 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),
151 Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => {200 Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => {
152 content_merge_matches(merged_blob)201 content_merge_matches(merged_blob)
153 }202 }
@@ -178,6 +227,14 @@ impl Conflict { · 24 unmodified lines
178 }227 }
179 }228 }
180 229
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
181 /// Return information about the content merge if it was performed.238 /// Return information about the content merge if it was performed.
182 pub fn content_merge(&self) -> Option<ContentMerge> {239 pub fn content_merge(&self) -> Option<ContentMerge> {
183 match &self.resolution {240 match &self.resolution {
@@ -308,3 +365,136 @@ pub struct Options { · 124 unmodified lines
308 365
309pub(super) mod function;366pub(super) mod function;
310mod utils;367mod utils;
368pub 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}
500pub use apply_index_entries::function::apply_index_entries;
gix-merge/src/tree/utils.rs+43−4
@@ -7,7 +7,10 @@ · 6 unmodified lines
7//! contribute to finding a fix faster.7//! contribute to finding a fix faster.
8use crate::blob::builtin_driver::binary::Pick;8use crate::blob::builtin_driver::binary::Pick;
9use crate::blob::ResourceKind;9use crate::blob::ResourceKind;
10use crate::tree::{Conflict, ConflictMapping, Error, Options, Resolution, ResolutionFailure};10use crate::tree::{
11 Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, Error, Options, Resolution,
12 ResolutionFailure,
13};
11use bstr::ByteSlice;14use bstr::ByteSlice;
12use bstr::{BStr, BString, ByteVec};15use bstr::{BStr, BString, ByteVec};
13use gix_diff::tree_with_rewrites::{Change, ChangeRef};16use gix_diff::tree_with_rewrites::{Change, ChangeRef};
@@ -98,6 +101,14 @@ pub fn perform_blob_merge<E>( · 84 unmodified lines
98where101where
99 E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,102 E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
100{103{
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 }
101 if matches!(our_mode.kind(), EntryKind::Link) && matches!(their_mode.kind(), EntryKind::Link) {112 if matches!(our_mode.kind(), EntryKind::Link) && matches!(their_mode.kind(), EntryKind::Link) {
102 let (pick, resolution) = crate::blob::builtin_driver::binary(options.symlink_conflicts);113 let (pick, resolution) = crate::blob::builtin_driver::binary(options.symlink_conflicts);
103 let (our_id, their_id) = match outer_side {114 let (our_id, their_id) = match outer_side {
@@ -544,29 +555,57 @@ impl Conflict { · 440 unmodified lines
544 pub(super) fn without_resolution(555 pub(super) fn without_resolution(
545 resolution: ResolutionFailure,556 resolution: ResolutionFailure,
546 changes: (&Change, &Change, ConflictMapping, ConflictMapping),557 changes: (&Change, &Change, ConflictMapping, ConflictMapping),
558 entries: [Option<ConflictIndexEntry>; 3],
547 ) -> Self {559 ) -> Self {
548 Conflict::maybe_resolved(Err(resolution), changes)560 Conflict::maybe_resolved(Err(resolution), changes, entries)
549 }561 }
550 562
551 pub(super) fn with_resolution(563 pub(super) fn with_resolution(
552 resolution: Resolution,564 resolution: Resolution,
553 changes: (&Change, &Change, ConflictMapping, ConflictMapping),565 changes: (&Change, &Change, ConflictMapping, ConflictMapping),
566 entries: [Option<ConflictIndexEntry>; 3],
554 ) -> Self {567 ) -> Self {
555 Conflict::maybe_resolved(Ok(resolution), changes)568 Conflict::maybe_resolved(Ok(resolution), changes, entries)
556 }569 }
557 570
558 pub(super) fn maybe_resolved(571 fn maybe_resolved(
559 resolution: Result<Resolution, ResolutionFailure>,572 resolution: Result<Resolution, ResolutionFailure>,
560 (ours, theirs, map, outer_map): (&Change, &Change, ConflictMapping, ConflictMapping),573 (ours, theirs, map, outer_map): (&Change, &Change, ConflictMapping, ConflictMapping),
574 entries: [Option<ConflictIndexEntry>; 3],
561 ) -> Self {575 ) -> Self {
562 Conflict {576 Conflict {
563 resolution,577 resolution,
564 ours: ours.clone(),578 ours: ours.clone(),
565 theirs: theirs.clone(),579 theirs: theirs.clone(),
580 entries,
566 map: match outer_map {581 map: match outer_map {
567 ConflictMapping::Original => map,582 ConflictMapping::Original => map,
568 ConflictMapping::Swapped => map.swapped(),583 ConflictMapping::Swapped => map.swapped(),
569 },584 },
570 }585 }
571 }586 }
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 }
572}611}
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
31 done31 done
32}32}
33 33
34function 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
34function baseline () (39function baseline () (
35 local dir=${1:?the directory to enter}40 local dir=${1:?the directory to enter}
36 local output_name=${2:?the basename of the output of the merge}41 local output_name=${2:?the basename of the output of the merge}
@@ -172,6 +177,29 @@ git init rename-add · 135 unmodified lines
172 git commit -m "rename foo to bar"177 git commit -m "rename foo to bar"
173)178)
174 179
180git 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
175git init rename-add-symlink203git init rename-add-symlink
176(cd rename-add-symlink204(cd rename-add-symlink
177 write_lines original 1 2 3 4 5 >foo205 write_lines original 1 2 3 4 5 >foo
@@ -317,6 +345,34 @@ git init super-2 · 139 unmodified lines
317 git add foo345 git add foo
318 git mv foo olddir/bar346 git mv foo olddir/bar
319 git commit -m "Modify foo & rename foo -> olddir/bar"347 git commit -m "Modify foo & rename foo -> olddir/bar"
348
349 rm .git/index
350 git update-index --index-info <<EOF
351100644 78981922613b2afb6025042ff6bd878ac1994e85 0 newdir/a
352100644 61780798228d17af2d34fce4cfbdf35556832472 0 newdir/b
353100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 newdir/bar/file
354100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 newdir/bar~B
355100644 f2ad6c76f0115a6ba5b00456a849810e7ec0af20 0 newdir/c
356EOF
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
368100644 78981922613b2afb6025042ff6bd878ac1994e85 0 newdir/a
369100644 61780798228d17af2d34fce4cfbdf35556832472 0 newdir/b
370100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 newdir/bar/file
371100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 newdir/bar~B
372100644 f2ad6c76f0115a6ba5b00456a849810e7ec0af20 0 newdir/c
373EOF
374 make_conflict_index super-2-A-B-reversed
375 make_conflict_index super-2-A-B-diff3-reversed
320)376)
321 377
322git init rename-within-rename378git init rename-within-rename
@@ -348,6 +404,10 @@ git init rename-within-rename · 25 unmodified lines
348 git mv a/sub a/sub-renamed404 git mv a/sub a/sub-renamed
349 git mv a a-renamed405 git mv a a-renamed
350 git commit -am "tracked both renames, applied all modifications by merge"406 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
351)411)
352 412
353git init rename-within-rename-2413git init rename-within-rename-2
@@ -380,6 +440,10 @@ git init rename-within-rename-2 · 26 unmodified lines
380 git mv a/sub a/sub-renamed440 git mv a/sub a/sub-renamed
381 git mv a a-renamed441 git mv a a-renamed
382 git commit -am "tracked both renames, applied all modifications by merge"442 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
383)447)
384 448
385git init conflicting-rename449git init conflicting-rename
@@ -403,6 +467,43 @@ git init conflicting-rename · 17 unmodified lines
403 write_lines original 1 2 3 4 5 6 >a/sub/y.f467 write_lines original 1 2 3 4 5 6 >a/sub/y.f
404 git mv a a-different468 git mv a a-different
405 git commit -am "changed all content, renamed a -> a-different"469 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
476100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-different/sub/y.f
477100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-different/sub/z
478100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-different/w
479100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-different/x.f
480100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-renamed/sub/y.f
481100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z
482100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/w
483100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-renamed/x.f
484100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f
485100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/x.f
486100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z
487100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/w
488EOF
489 make_conflict_index conflicting-rename-A-B
490
491 rm .git/index
492 git update-index --index-info <<EOF
493100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-different/sub/y.f
494100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-different/sub/z
495100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-different/w
496100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-different/x.f
497100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-renamed/sub/y.f
498100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z
499100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/w
500100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-renamed/x.f
501100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f
502100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/x.f
503100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z
504100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/w
505EOF
506 make_conflict_index conflicting-rename-A-B-reversed
406)507)
407 508
408git init conflicting-rename-2509git init conflicting-rename-2
@@ -426,6 +527,34 @@ git init conflicting-rename-2 · 17 unmodified lines
426 write_lines original 1 2 3 4 5 6 >a/sub/y.f527 write_lines original 1 2 3 4 5 6 >a/sub/y.f
427 git mv a/sub a/sub-different528 git mv a/sub a/sub-different
428 git commit -am "changed all content, renamed a/sub -> a/sub-different"529 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
535100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/sub-different/y.f
536100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a/sub-different/z
537100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/sub-renamed/y.f
538100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a/sub-renamed/z
539100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f
540100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z
541100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a/w
542100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a/x.f
543EOF
544 make_conflict_index conflicting-rename-2-A-B
545
546 rm .git/index
547 git update-index --index-info <<EOF
548100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/sub-different/y.f
549100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a/sub-different/z
550100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/sub-renamed/y.f
551100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a/sub-renamed/z
552100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f
553100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z
554100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a/w
555100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a/x.f
556EOF
557 make_conflict_index conflicting-rename-2-A-B-reversed
429)558)
430 559
431git init conflicting-rename-complex560git init conflicting-rename-complex
@@ -461,6 +590,27 @@ git init conflicting-rename-complex · 29 unmodified lines
461 touch a-renamed/z a-renamed/sub/z590 touch a-renamed/z a-renamed/sub/z
462 git add .591 git add .
463 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"592 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
599100644 8a1218a1024a212bb3db30becd860315f9f3ac52 2 a-renamed/sub/y.f
600100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z
601100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f
602100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z
603EOF
604 make_conflict_index conflicting-rename-complex-A-B
605
606 rm .git/index
607 git update-index --index-info <<EOF
608100644 8a1218a1024a212bb3db30becd860315f9f3ac52 3 a-renamed/sub/y.f
609100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z
610100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f
611100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z
612EOF
613 make_conflict_index conflicting-rename-complex-A-B-reversed
464)614)
465 615
466git init same-rename-different-mode616git init same-rename-different-mode
@@ -491,6 +641,29 @@ git init same-rename-different-mode · 24 unmodified lines
491 write_lines 1 2 3 4 5 6 >a/x.f641 write_lines 1 2 3 4 5 6 >a/x.f
492 git mv a a-renamed642 git mv a a-renamed
493 git commit -am "Git, when branches are reversed, doesn't keep the +x flag on a/w so we specify our own expectation"643 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
649git 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"
494)667)
495 668
496git init renamed-symlink-with-conflict669git init renamed-symlink-with-conflict
@@ -614,6 +787,23 @@ git init submodule-both-modify · 117 unmodified lines
614 git add sub787 git add sub
615 tick788 tick
616 git commit -m b789 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
794160000 e835c0c403c8e494c0ca98f3d25d0b8464c18d38 1 sub
795160000 64466ebdff775ad618d9cc993cf52840e0af528c 2 sub
796160000 ea6eb701e03c2497915c25a851f3da8f8e362ca0 3 sub
797EOF
798 make_conflict_index submodule-both-modify-A-B
799
800 rm .git/index
801 git update-index --index-info <<EOF
802160000 e835c0c403c8e494c0ca98f3d25d0b8464c18d38 1 sub
803160000 ea6eb701e03c2497915c25a851f3da8f8e362ca0 2 sub
804160000 64466ebdff775ad618d9cc993cf52840e0af528c 3 sub
805EOF
806 make_conflict_index submodule-both-modify-A-B-reversed
617)807)
618 808
619git init both-modify-union-attr809git init both-modify-union-attr
@@ -670,7 +860,7 @@ git init both-modify-file-with-binary-attr · 50 unmodified lines
670 860
671git init big-file-merge861git init big-file-merge
672(cd big-file-merge862(cd big-file-merge
673 git config --local core.bigFileThreshold 80863 git config --local core.bigFileThreshold 100
674 mkdir a && write_lines original 1 2 3 4 5 >a/x.f864 mkdir a && write_lines original 1 2 3 4 5 >a/x.f
675 git add . && git commit -m "original"865 git add . && git commit -m "original"
676 866
@@ -678,8 +868,8 @@ git init big-file-merge · 1 unmodified line
678 git branch B868 git branch B
679 869
680 git checkout A870 git checkout A
681 seq 30 >a/x.f871 seq 37 >a/x.f
682 git commit -am "turn normal file into big one (81 bytes)"872 git commit -am "turn normal file into big one (102 bytes)"
683 git branch expected873 git branch expected
684 874
685 git checkout B875 git checkout B
@@ -803,6 +993,9 @@ git init type-change-to-symlink · 117 unmodified lines
803 993
804 994
805 995
996# TODO: Git does not detect the conflict (one turns exe off, the other turns it on), and we do exactly the same.
997baseline rename-add-exe-bit-conflict A-B A B
998baseline remove-executable-mode A-B A B
806baseline simple side-1-3-without-conflict side1 side3999baseline simple side-1-3-without-conflict side1 side3
807baseline simple fast-forward side1 main1000baseline simple fast-forward side1 main
808baseline simple no-change main main1001baseline simple no-change main main
@@ -838,7 +1031,6 @@ baseline conflicting-rename-2 A-B A B · 29 unmodified lines
838baseline conflicting-rename-complex A-B A B "Git has different rename tracking which is why a-renamed/w disappears - it's still close enough"1031baseline conflicting-rename-complex A-B A B "Git has different rename tracking which is why a-renamed/w disappears - it's still close enough"
839 1032
840baseline 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"1033baseline 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"
841baseline 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"
842baseline renamed-symlink-with-conflict A-B A B1034baseline renamed-symlink-with-conflict A-B A B
843baseline 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"1035baseline 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"
844 1036
gix-merge/tests/merge/tree/baseline.rs+64−4
@@ -6,7 +6,7 @@ use gix_object::FindExt; · 5 unmodified lines
6use std::path::{Path, PathBuf};6use std::path::{Path, PathBuf};
7 7
8/// An entry in the conflict8/// An entry in the conflict
9#[derive(Debug)]9#[derive(Debug, Eq, PartialEq)]
10pub struct Entry {10pub struct Entry {
11 /// The relative path in the repository11 /// The relative path in the repository
12 pub location: String,12 pub location: String,
@@ -17,7 +17,7 @@ pub struct Entry { · 4 unmodified lines
17}17}
18 18
19/// Keep track of all the sides of a conflict. Some might not be set to indicate removal, including the ancestor.19/// 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)]
21pub struct Conflict {21pub struct Conflict {
22 pub ancestor: Option<Entry>,22 pub ancestor: Option<Entry>,
23 pub ours: Option<Entry>,23 pub ours: Option<Entry>,
@@ -129,7 +129,7 @@ impl Iterator for Expectations<'_> { · 105 unmodified lines
129 let mut tokens = line.split(' ');129 let mut tokens = line.split(' ');
130 let (130 let (
131 Some(subdir),131 Some(subdir),
132 Some(conflict_style),132 Some(conflict_style_name),
133 Some(our_commit_id),133 Some(our_commit_id),
134 Some(our_side_name),134 Some(our_side_name),
135 Some(their_commit_id),135 Some(their_commit_id),
@@ -160,7 +160,7 @@ impl Iterator for Expectations<'_> { · 24 unmodified lines
160 });160 });
161 161
162 let subdir_path = self.root.join(subdir);162 let subdir_path = self.root.join(subdir);
163 let conflict_style = match conflict_style {163 let conflict_style = match conflict_style_name {
164 "merge" => ConflictStyle::Merge,164 "merge" => ConflictStyle::Merge,
165 "diff3" => ConflictStyle::Diff3,165 "diff3" => ConflictStyle::Diff3,
166 unknown => unreachable!("Unknown conflict style: '{unknown}'"),166 unknown => unreachable!("Unknown conflict style: '{unknown}'"),
@@ -221,6 +221,10 @@ fn parse_merge_info(content: String) -> MergeInfo { · 54 unmodified lines
221 *field = Some(entry);221 *field = Some(entry);
222 }222 }
223 223
224 if conflict.any_location().is_some() && conflicts.last() != Some(&conflict) {
225 conflicts.push(conflict);
226 }
227
224 while lines.peek().is_some() {228 while lines.peek().is_some() {
225 out.information229 out.information
226 .push(parse_info(&mut lines).expect("if there are lines, it should be valid info"));230 .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
285 Some(ConflictInfo { paths, kind, message })289 Some(ConflictInfo { paths, kind, message })
286}290}
287 291
292#[derive(Debug, PartialEq, Eq)]
293pub 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
300pub 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
288pub fn visualize_tree(316pub fn visualize_tree(
289 id: &gix_hash::oid,317 id: &gix_hash::oid,
290 odb: &impl gix_object::Find,318 odb: &impl gix_object::Find,
@@ -342,3 +370,35 @@ pub fn show_diff_and_fail( · 51 unmodified lines
342 expected.information370 expected.information
343 );371 );
344}372}
373
374pub(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 @@
1use crate::tree::baseline::Deviation;1use crate::tree::baseline::Deviation;
2use gix_diff::Rewrites;2use gix_diff::Rewrites;
3use gix_merge::commit::Options;3use gix_merge::commit::Options;
4use gix_merge::tree::TreatAsUnresolved;
4use gix_object::Write;5use gix_object::Write;
5use gix_worktree::stack::state::attributes;6use gix_worktree::stack::state::attributes;
6use std::path::Path;7use std::path::Path;
@@ -20,7 +21,7 @@ fn run_baseline() -> crate::Result { · 13 unmodified lines
20 let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?;21 let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?;
21 let cases = std::fs::read_to_string(root.join("baseline.cases"))?;22 let cases = std::fs::read_to_string(root.join("baseline.cases"))?;
22 let mut actual_cases = 0;23 let mut actual_cases = 0;
23 // let new_test = Some("simple-fast-forward");24 // let new_test = Some("rename-add-symlink-A-B");
24 let new_test = None;25 let new_test = None;
25 for baseline::Expectation {26 for baseline::Expectation {
26 root,27 root,
@@ -98,10 +99,58 @@ fn run_baseline() -> crate::Result { · 71 unmodified lines
98 );99 );
99 }100 }
100 }101 }
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 }
101 }150 }
102 151
103 assert_eq!(152 assert_eq!(
104 actual_cases, 105,153 actual_cases, 107,
105 "BUG: update this number, and don't forget to remove a filter in the end"154 "BUG: update this number, and don't forget to remove a filter in the end"
106 );155 );
107 156

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