These changes will then be applicable to an index that is created from the written tree editor.
Unified · Split
| @@ -2114,6 +2114,7 @@ dependencies = [ · 2113 unmodified lines | ||
| 2114 | 2114 | "gix-filter", |
| 2115 | 2115 | "gix-fs 0.12.0", |
| 2116 | 2116 | "gix-hash 0.15.0", |
| 2117 | + "gix-index 0.36.0", | |
| 2117 | 2118 | "gix-object 0.45.0", |
| 2118 | 2119 | "gix-odb", |
| 2119 | 2120 | "gix-path 0.10.12", |
| @@ -318,6 +318,8 @@ Check out the [performance discussion][gix-diff-performance] as well. · 317 unmodified lines | ||
| 318 | 318 | * [x] find blobs by similarity check |
| 319 | 319 | * [ ] heuristics to find best candidate |
| 320 | 320 | * [ ] 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 | 323 | * [x] directory tracking |
| 322 | 324 | - [x] by identity |
| 323 | 325 | - [ ] by similarity |
| @@ -349,8 +351,7 @@ Check out the [performance discussion][gix-diff-performance] as well. · 25 unmodified lines | ||
| 349 | 351 | - [ ] various newlines-related options during the merge (see https://git-scm.com/docs/git-merge#Documentation/git-merge.txt-ignore-space-change). |
| 350 | 352 | - [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same) |
| 351 | 353 | * [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. | |
| 354 | 355 | - [ ] submodule merges (*right now they count as conflicts if they differ*) |
| 355 | 356 | * [x] **commits** - with handling of multiple merge bases by recursive merge-base merge |
| 356 | 357 | * [x] API documentation |
| @@ -32,6 +32,7 @@ gix-quote = { version = "^0.4.13", path = "../gix-quote" } · 31 unmodified lines | ||
| 32 | 32 | gix-revision = { version = "^0.30.0", path = "../gix-revision", default-features = false, features = ["merge_base"] } |
| 33 | 33 | gix-revwalk = { version = "^0.16.0", path = "../gix-revwalk" } |
| 34 | 34 | gix-diff = { version = "^0.47.0", path = "../gix-diff", default-features = false, features = ["blob"] } |
| 35 | +gix-index = { version = "^0.36.0", path = "../gix-index" } | |
| 35 | 36 | |
| 36 | 37 | thiserror = "2.0.0" |
| 37 | 38 | imara-diff = { version = "0.1.7" } |
| @@ -3,7 +3,10 @@ use crate::tree::utils::{ · 2 unmodified lines | ||
| 3 | 3 | to_components, track, unique_path_in_tree, ChangeList, ChangeListRef, PossibleConflict, TrackedChange, TreeNodes, |
| 4 | 4 | }; |
| 5 | 5 | 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 | +}; | |
| 7 | 10 | use bstr::{BString, ByteSlice}; |
| 8 | 11 | use gix_diff::tree::recorder::Location; |
| 9 | 12 | use gix_diff::tree_with_rewrites::Change; |
| @@ -190,11 +193,14 @@ where · 180 unmodified lines | ||
| 190 | 193 | }) { |
| 191 | 194 | None => { |
| 192 | 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 | 198 | if should_fail_on_conflict(Conflict::with_resolution( |
| 194 | 199 | Resolution::SourceLocationAffectedByRename { |
| 195 | 200 | final_location: rewritten_location.to_owned(), |
| 196 | 201 | }, |
| 197 | 202 | (&our_changes[*ours_idx].inner, theirs, Original, outer_side), |
| 203 | + [None, None, None], | |
| 198 | 204 | )) { |
| 199 | 205 | break 'outer; |
| 200 | 206 | }; |
| @@ -254,10 +260,12 @@ where · 53 unmodified lines | ||
| 254 | 260 | if let Some(ours) = ours { |
| 255 | 261 | gix_trace::debug!("Turning a case we could probably handle into a conflict for now. theirs: {theirs:#?} ours: {ours:#?} kind: {match_kind:?}"); |
| 256 | 262 | 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 | + ))) | |
| 261 | 269 | { |
| 262 | 270 | break 'outer; |
| 263 | 271 | }; |
| @@ -328,7 +336,7 @@ where · 64 unmodified lines | ||
| 328 | 336 | pick_our_changes(side, our_changes, their_changes), |
| 329 | 337 | ); |
| 330 | 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 | 340 | (*our_id, None) |
| 333 | 341 | } else { |
| 334 | 342 | let (our_location, our_id, our_mode, their_location, their_id, their_mode) = |
| @@ -373,18 +381,23 @@ where · 38 unmodified lines | ||
| 373 | 381 | location: their_rewritten_location.unwrap_or_else(|| their_location.to_owned()), |
| 374 | 382 | relation: None, |
| 375 | 383 | entry_mode: merged_mode, |
| 376 | − id: our_id, | |
| 384 | + id: merged_blob_id, | |
| 377 | 385 | }; |
| 378 | 386 | if should_fail_on_conflict(Conflict::with_resolution( |
| 379 | 387 | Resolution::OursModifiedTheirsRenamedAndChangedThenRename { |
| 380 | 388 | merged_mode: (merged_mode != *their_mode).then_some(merged_mode), |
| 381 | 389 | merged_blob: resolution.map(|resolution| ContentMerge { |
| 382 | 390 | resolution, |
| 383 | − merged_blob_id: our_id, | |
| 391 | + merged_blob_id, | |
| 384 | 392 | }), |
| 385 | 393 | final_location, |
| 386 | 394 | }, |
| 387 | 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 | 402 | break 'outer; |
| 390 | 403 | } |
| @@ -401,6 +414,19 @@ where · 10 unmodified lines | ||
| 401 | 414 | if should_fail_on_conflict(Conflict::without_resolution( |
| 402 | 415 | ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch, |
| 403 | 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 | 431 | break 'outer; |
| 406 | 432 | } |
| @@ -421,7 +447,7 @@ where · 14 unmodified lines | ||
| 421 | 447 | .. |
| 422 | 448 | }, |
| 423 | 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 | 451 | && our_id != their_id => |
| 426 | 452 | { |
| 427 | 453 | let (merged_blob_id, resolution) = perform_blob_merge( |
| @@ -436,7 +462,11 @@ where · 8 unmodified lines | ||
| 436 | 462 | (0, outer_side), |
| 437 | 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 | 470 | if should_fail_on_conflict(Conflict::with_resolution( |
| 441 | 471 | Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { |
| 442 | 472 | merged_blob: ContentMerge { |
| @@ -445,6 +475,11 @@ where · 2 unmodified lines | ||
| 445 | 475 | }, |
| 446 | 476 | }, |
| 447 | 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 | 484 | break 'outer; |
| 450 | 485 | }; |
| @@ -489,27 +524,28 @@ where · 38 unmodified lines | ||
| 489 | 524 | }, |
| 490 | 525 | }, |
| 491 | 526 | (ours, theirs, Original, outer_side), |
| 527 | + [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], | |
| 492 | 528 | )) |
| 493 | 529 | } else if allow_resolution_failure { |
| 494 | 530 | // Actually this has a preference, as symlinks are always left in place with the other side renamed. |
| 495 | 531 | let ( |
| 496 | 532 | logical_side, |
| 497 | 533 | 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), | |
| 500 | 536 | ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) { |
| 501 | 537 | ( |
| 502 | 538 | Original, |
| 503 | 539 | 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), | |
| 506 | 542 | ) |
| 507 | 543 | } else { |
| 508 | 544 | ( |
| 509 | 545 | Swapped, |
| 510 | 546 | 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), | |
| 513 | 549 | ) |
| 514 | 550 | }; |
| 515 | 551 | let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree); |
| @@ -525,6 +561,11 @@ where · 9 unmodified lines | ||
| 525 | 561 | their_unique_location: renamed_location.clone(), |
| 526 | 562 | }, |
| 527 | 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 | 571 | let new_change = Change::Addition { |
| @@ -554,7 +595,8 @@ where · 23 unmodified lines | ||
| 554 | 595 | location, |
| 555 | 596 | entry_mode, |
| 556 | 597 | id, |
| 557 | − .. | |
| 598 | + previous_entry_mode, | |
| 599 | + previous_id, | |
| 558 | 600 | }, |
| 559 | 601 | Change::Deletion { .. }, |
| 560 | 602 | ) |
| @@ -564,7 +606,8 @@ where · 3 unmodified lines | ||
| 564 | 606 | location, |
| 565 | 607 | entry_mode, |
| 566 | 608 | id, |
| 567 | − .. | |
| 609 | + previous_entry_mode, | |
| 610 | + previous_id, | |
| 568 | 611 | }, |
| 569 | 612 | ) if allow_resolution_failure => { |
| 570 | 613 | let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) { |
| @@ -606,6 +649,11 @@ where · 35 unmodified lines | ||
| 606 | 649 | renamed_unique_path_to_modified_blob: renamed_path, |
| 607 | 650 | }, |
| 608 | 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 | 659 | // Since we move *our* side, our tree needs to be modified. |
| @@ -621,6 +669,11 @@ where · 9 unmodified lines | ||
| 621 | 669 | let should_break = should_fail_on_conflict(Conflict::without_resolution( |
| 622 | 670 | ResolutionFailure::OursModifiedTheirsDeleted, |
| 623 | 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 | 678 | editor.upsert(toc(location), entry_mode.kind(), *id)?; |
| 626 | 679 | if should_break { |
| @@ -702,10 +755,9 @@ where · 75 unmodified lines | ||
| 702 | 755 | // Pretend this is the end of the loop and keep this as conflict. |
| 703 | 756 | // If this happens in the wild, we'd want to reproduce it. |
| 704 | 757 | 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 | + ))) | |
| 709 | 761 | { |
| 710 | 762 | break 'outer; |
| 711 | 763 | }; |
| @@ -738,6 +790,23 @@ where · 26 unmodified lines | ||
| 738 | 790 | }), |
| 739 | 791 | }, |
| 740 | 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 | 811 | break 'outer; |
| 743 | 812 | }; |
| @@ -767,6 +836,23 @@ where · 23 unmodified lines | ||
| 767 | 836 | }, |
| 768 | 837 | }, |
| 769 | 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 | 857 | break 'outer; |
| 772 | 858 | }; |
| @@ -824,6 +910,15 @@ where · 51 unmodified lines | ||
| 824 | 910 | if should_fail_on_conflict(Conflict::without_resolution( |
| 825 | 911 | ResolutionFailure::OursDeletedTheirsRenamed, |
| 826 | 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 | 923 | break 'outer; |
| 829 | 924 | }; |
| @@ -901,6 +996,7 @@ where · 71 unmodified lines | ||
| 901 | 996 | }, |
| 902 | 997 | }, |
| 903 | 998 | (ours, theirs, Original, outer_side), |
| 999 | + [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], | |
| 904 | 1000 | )) { |
| 905 | 1001 | break 'outer; |
| 906 | 1002 | }; |
| @@ -916,21 +1012,21 @@ where · 9 unmodified lines | ||
| 916 | 1012 | let ( |
| 917 | 1013 | logical_side, |
| 918 | 1014 | 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), | |
| 921 | 1017 | ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) { |
| 922 | 1018 | ( |
| 923 | 1019 | Original, |
| 924 | 1020 | 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), | |
| 927 | 1023 | ) |
| 928 | 1024 | } else { |
| 929 | 1025 | ( |
| 930 | 1026 | Swapped, |
| 931 | 1027 | 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), | |
| 934 | 1030 | ) |
| 935 | 1031 | }; |
| 936 | 1032 | let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree); |
| @@ -946,6 +1042,11 @@ where · 9 unmodified lines | ||
| 946 | 1042 | their_unique_location: renamed_location.clone(), |
| 947 | 1043 | }, |
| 948 | 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 | 1052 | let new_change_with_rename = Change::Addition { |
| @@ -970,10 +1071,7 @@ where · 18 unmodified lines | ||
| 970 | 1071 | } |
| 971 | 1072 | _unknown => { |
| 972 | 1073 | 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))) | |
| 977 | 1075 | { |
| 978 | 1076 | break 'outer; |
| 979 | 1077 | }; |
| @@ -1004,12 +1102,40 @@ fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool { · 24 unmodified lines | ||
| 1004 | 1102 | a.is_commit() || b.is_commit() |
| 1005 | 1103 | } |
| 1006 | 1104 | |
| 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 | |
| 1008 | 1112 | fn merge_modes(a: EntryMode, b: EntryMode) -> Option<EntryMode> { |
| 1009 | 1113 | match (a.kind(), b.kind()) { |
| 1114 | + (_, _) if a == b => Some(a), | |
| 1010 | 1115 | (EntryKind::BlobExecutable, EntryKind::BlobExecutable | EntryKind::Blob) |
| 1011 | 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. | |
| 1123 | +fn merge_modes_prev(a: EntryMode, b: EntryMode, prev: EntryMode) -> Option<EntryMode> { | |
| 1124 | + match (a.kind(), b.kind()) { | |
| 1012 | 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 | 1139 | _ => None, |
| 1014 | 1140 | } |
| 1015 | 1141 | } |
| @@ -1066,3 +1192,23 @@ fn pick_our_changes_mut<'a>( · 50 unmodified lines | ||
| 1066 | 1192 | Swapped => theirs, |
| 1067 | 1193 | } |
| 1068 | 1194 | } |
| 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 | +} | |
| @@ -81,6 +81,17 @@ impl Outcome<'_> { · 80 unmodified lines | ||
| 81 | 81 | pub fn has_unresolved_conflicts(&self, how: TreatAsUnresolved) -> bool { |
| 82 | 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 | 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 | 110 | pub ours: Change, |
| 100 | 111 | /// The change representing *their* side. |
| 101 | 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 | 121 | /// Determine how to interpret the `ours` and `theirs` fields. This is used to implement [`Self::changes_in_resolution()`] |
| 103 | 122 | /// and [`Self::into_parts_by_resolution()`]. |
| 104 | 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)] | |
| 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 | + | |
| 107 | 152 | /// A utility to help define which side is what in the [`Conflict`] type. |
| 108 | 153 | #[derive(Debug, Clone, Copy)] |
| 109 | 154 | enum ConflictMapping { |
| @@ -147,7 +192,11 @@ impl Conflict { · 37 unmodified lines | ||
| 147 | 192 | TreatAsUnresolved::Renames | TreatAsUnresolved::RenamesAndAutoResolvedContent => match &self.resolution { |
| 148 | 193 | Ok(success) => match success { |
| 149 | 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 | 200 | Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => { |
| 152 | 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 | 238 | /// Return information about the content merge if it was performed. |
| 182 | 239 | pub fn content_merge(&self) -> Option<ContentMerge> { |
| 183 | 240 | match &self.resolution { |
| @@ -308,3 +365,136 @@ pub struct Options { · 124 unmodified lines | ||
| 308 | 365 | |
| 309 | 366 | pub(super) mod function; |
| 310 | 367 | 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; | |
| @@ -7,7 +7,10 @@ · 6 unmodified lines | ||
| 7 | 7 | //! contribute to finding a fix faster. |
| 8 | 8 | use crate::blob::builtin_driver::binary::Pick; |
| 9 | 9 | 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 | +}; | |
| 11 | 14 | use bstr::ByteSlice; |
| 12 | 15 | use bstr::{BStr, BString, ByteVec}; |
| 13 | 16 | use gix_diff::tree_with_rewrites::{Change, ChangeRef}; |
| @@ -98,6 +101,14 @@ pub fn perform_blob_merge<E>( · 84 unmodified lines | ||
| 98 | 101 | where |
| 99 | 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 | 112 | if matches!(our_mode.kind(), EntryKind::Link) && matches!(their_mode.kind(), EntryKind::Link) { |
| 102 | 113 | let (pick, resolution) = crate::blob::builtin_driver::binary(options.symlink_conflicts); |
| 103 | 114 | let (our_id, their_id) = match outer_side { |
| @@ -544,29 +555,57 @@ impl Conflict { · 440 unmodified lines | ||
| 544 | 555 | pub(super) fn without_resolution( |
| 545 | 556 | resolution: ResolutionFailure, |
| 546 | 557 | changes: (&Change, &Change, ConflictMapping, ConflictMapping), |
| 558 | + entries: [Option<ConflictIndexEntry>; 3], | |
| 547 | 559 | ) -> Self { |
| 548 | − Conflict::maybe_resolved(Err(resolution), changes) | |
| 560 | + Conflict::maybe_resolved(Err(resolution), changes, entries) | |
| 549 | 561 | } |
| 550 | 562 | |
| 551 | 563 | pub(super) fn with_resolution( |
| 552 | 564 | resolution: Resolution, |
| 553 | 565 | changes: (&Change, &Change, ConflictMapping, ConflictMapping), |
| 566 | + entries: [Option<ConflictIndexEntry>; 3], | |
| 554 | 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 | 572 | resolution: Result<Resolution, ResolutionFailure>, |
| 560 | 573 | (ours, theirs, map, outer_map): (&Change, &Change, ConflictMapping, ConflictMapping), |
| 574 | + entries: [Option<ConflictIndexEntry>; 3], | |
| 561 | 575 | ) -> Self { |
| 562 | 576 | Conflict { |
| 563 | 577 | resolution, |
| 564 | 578 | ours: ours.clone(), |
| 565 | 579 | theirs: theirs.clone(), |
| 580 | + entries, | |
| 566 | 581 | map: match outer_map { |
| 567 | 582 | ConflictMapping::Original => map, |
| 568 | 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 | } |
Binary file not shown.
| @@ -31,6 +31,11 @@ function seq () { · 30 unmodified lines | ||
| 31 | 31 | done |
| 32 | 32 | } |
| 33 | 33 | |
| 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 | + | |
| 34 | 39 | function baseline () ( |
| 35 | 40 | local dir=${1:?the directory to enter} |
| 36 | 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 | 177 | git commit -m "rename foo to bar" |
| 173 | 178 | ) |
| 174 | 179 | |
| 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 | + | |
| 175 | 203 | git init rename-add-symlink |
| 176 | 204 | (cd rename-add-symlink |
| 177 | 205 | write_lines original 1 2 3 4 5 >foo |
| @@ -317,6 +345,34 @@ git init super-2 · 139 unmodified lines | ||
| 317 | 345 | git add foo |
| 318 | 346 | git mv foo olddir/bar |
| 319 | 347 | 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 | |
| 320 | 376 | ) |
| 321 | 377 | |
| 322 | 378 | git init rename-within-rename |
| @@ -348,6 +404,10 @@ git init rename-within-rename · 25 unmodified lines | ||
| 348 | 404 | git mv a/sub a/sub-renamed |
| 349 | 405 | git mv a a-renamed |
| 350 | 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 | |
| 353 | 413 | git init rename-within-rename-2 |
| @@ -380,6 +440,10 @@ git init rename-within-rename-2 · 26 unmodified lines | ||
| 380 | 440 | git mv a/sub a/sub-renamed |
| 381 | 441 | git mv a a-renamed |
| 382 | 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 | |
| 385 | 449 | git init conflicting-rename |
| @@ -403,6 +467,43 @@ git init conflicting-rename · 17 unmodified lines | ||
| 403 | 467 | write_lines original 1 2 3 4 5 6 >a/sub/y.f |
| 404 | 468 | git mv a a-different |
| 405 | 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 | |
| 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 | |
| 406 | 507 | ) |
| 407 | 508 | |
| 408 | 509 | git init conflicting-rename-2 |
| @@ -426,6 +527,34 @@ git init conflicting-rename-2 · 17 unmodified lines | ||
| 426 | 527 | write_lines original 1 2 3 4 5 6 >a/sub/y.f |
| 427 | 528 | git mv a/sub a/sub-different |
| 428 | 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 | |
| 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 | |
| 429 | 558 | ) |
| 430 | 559 | |
| 431 | 560 | git init conflicting-rename-complex |
| @@ -461,6 +590,27 @@ git init conflicting-rename-complex · 29 unmodified lines | ||
| 461 | 590 | touch a-renamed/z a-renamed/sub/z |
| 462 | 591 | git add . |
| 463 | 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 | |
| 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 | |
| 464 | 614 | ) |
| 465 | 615 | |
| 466 | 616 | git init same-rename-different-mode |
| @@ -491,6 +641,29 @@ git init same-rename-different-mode · 24 unmodified lines | ||
| 491 | 641 | write_lines 1 2 3 4 5 6 >a/x.f |
| 492 | 642 | git mv a a-renamed |
| 493 | 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 | + | |
| 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" | |
| 494 | 667 | ) |
| 495 | 668 | |
| 496 | 669 | git init renamed-symlink-with-conflict |
| @@ -614,6 +787,23 @@ git init submodule-both-modify · 117 unmodified lines | ||
| 614 | 787 | git add sub |
| 615 | 788 | tick |
| 616 | 789 | 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 | |
| 617 | 807 | ) |
| 618 | 808 | |
| 619 | 809 | git init both-modify-union-attr |
| @@ -670,7 +860,7 @@ git init both-modify-file-with-binary-attr · 50 unmodified lines | ||
| 670 | 860 | |
| 671 | 861 | git init big-file-merge |
| 672 | 862 | (cd big-file-merge |
| 673 | − git config --local core.bigFileThreshold 80 | |
| 863 | + git config --local core.bigFileThreshold 100 | |
| 674 | 864 | mkdir a && write_lines original 1 2 3 4 5 >a/x.f |
| 675 | 865 | git add . && git commit -m "original" |
| 676 | 866 | |
| @@ -678,8 +868,8 @@ git init big-file-merge · 1 unmodified line | ||
| 678 | 868 | git branch B |
| 679 | 869 | |
| 680 | 870 | 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)" | |
| 683 | 873 | git branch expected |
| 684 | 874 | |
| 685 | 875 | 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. | |
| 997 | +baseline rename-add-exe-bit-conflict A-B A B | |
| 998 | +baseline remove-executable-mode A-B A B | |
| 806 | 999 | baseline simple side-1-3-without-conflict side1 side3 |
| 807 | 1000 | baseline simple fast-forward side1 main |
| 808 | 1001 | baseline simple no-change main main |
| @@ -838,7 +1031,6 @@ baseline conflicting-rename-2 A-B A B · 29 unmodified lines | ||
| 838 | 1031 | 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" |
| 839 | 1032 | |
| 840 | 1033 | 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" | |
| 842 | 1034 | baseline renamed-symlink-with-conflict A-B A B |
| 843 | 1035 | 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" |
| 844 | 1036 | |
| @@ -6,7 +6,7 @@ use gix_object::FindExt; · 5 unmodified lines | ||
| 6 | 6 | use std::path::{Path, PathBuf}; |
| 7 | 7 | |
| 8 | 8 | /// An entry in the conflict |
| 9 | −#[derive(Debug)] | |
| 9 | +#[derive(Debug, Eq, PartialEq)] | |
| 10 | 10 | pub struct Entry { |
| 11 | 11 | /// The relative path in the repository |
| 12 | 12 | pub location: String, |
| @@ -17,7 +17,7 @@ pub struct Entry { · 4 unmodified lines | ||
| 17 | 17 | } |
| 18 | 18 | |
| 19 | 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)] | |
| 21 | 21 | pub struct Conflict { |
| 22 | 22 | pub ancestor: Option<Entry>, |
| 23 | 23 | pub ours: Option<Entry>, |
| @@ -129,7 +129,7 @@ impl Iterator for Expectations<'_> { · 105 unmodified lines | ||
| 129 | 129 | let mut tokens = line.split(' '); |
| 130 | 130 | let ( |
| 131 | 131 | Some(subdir), |
| 132 | − Some(conflict_style), | |
| 132 | + Some(conflict_style_name), | |
| 133 | 133 | Some(our_commit_id), |
| 134 | 134 | Some(our_side_name), |
| 135 | 135 | Some(their_commit_id), |
| @@ -160,7 +160,7 @@ impl Iterator for Expectations<'_> { · 24 unmodified lines | ||
| 160 | 160 | }); |
| 161 | 161 | |
| 162 | 162 | let subdir_path = self.root.join(subdir); |
| 163 | − let conflict_style = match conflict_style { | |
| 163 | + let conflict_style = match conflict_style_name { | |
| 164 | 164 | "merge" => ConflictStyle::Merge, |
| 165 | 165 | "diff3" => ConflictStyle::Diff3, |
| 166 | 166 | unknown => unreachable!("Unknown conflict style: '{unknown}'"), |
| @@ -221,6 +221,10 @@ fn parse_merge_info(content: String) -> MergeInfo { · 54 unmodified lines | ||
| 221 | 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 | 228 | while lines.peek().is_some() { |
| 225 | 229 | out.information |
| 226 | 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 | 289 | Some(ConflictInfo { paths, kind, message }) |
| 286 | 290 | } |
| 287 | 291 | |
| 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 | + | |
| 288 | 316 | pub fn visualize_tree( |
| 289 | 317 | id: &gix_hash::oid, |
| 290 | 318 | odb: &impl gix_object::Find, |
| @@ -342,3 +370,35 @@ pub fn show_diff_and_fail( · 51 unmodified lines | ||
| 342 | 370 | expected.information |
| 343 | 371 | ); |
| 344 | 372 | } |
| 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 | +} | |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | use crate::tree::baseline::Deviation; |
| 2 | 2 | use gix_diff::Rewrites; |
| 3 | 3 | use gix_merge::commit::Options; |
| 4 | +use gix_merge::tree::TreatAsUnresolved; | |
| 4 | 5 | use gix_object::Write; |
| 5 | 6 | use gix_worktree::stack::state::attributes; |
| 6 | 7 | use std::path::Path; |
| @@ -20,7 +21,7 @@ fn run_baseline() -> crate::Result { · 13 unmodified lines | ||
| 20 | 21 | let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?; |
| 21 | 22 | let cases = std::fs::read_to_string(root.join("baseline.cases"))?; |
| 22 | 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 | 25 | let new_test = None; |
| 25 | 26 | for baseline::Expectation { |
| 26 | 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 | 152 | assert_eq!( |
| 104 | − actual_cases, 105, | |
| 153 | + actual_cases, 107, | |
| 105 | 154 | "BUG: update this number, and don't forget to remove a filter in the end" |
| 106 | 155 | ); |
| 107 | 156 | |