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 | "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", |
| @@ -318,6 +318,8 @@ Check out the [performance discussion][gix-diff-performance] as well. · 317 unmodified lines | |||
| 318 | * [x] find blobs by similarity check | 318 | * [x] find blobs by similarity check |
| 319 | * [ ] heuristics to find best candidate | 319 | * [ ] heuristics to find best candidate |
| 320 | * [ ] find by basename to support similarity check | 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 | * [x] directory tracking | 323 | * [x] directory tracking |
| 322 | - [x] by identity | 324 | - [x] by identity |
| 323 | - [ ] by similarity | 325 | - [ ] 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-cases | 353 | * [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 merge | 356 | * [x] **commits** - with handling of multiple merge bases by recursive merge-base merge |
| 356 | * [x] API documentation | 357 | * [x] API documentation |
| @@ -32,6 +32,7 @@ gix-quote = { version = "^0.4.13", path = "../gix-quote" } · 31 unmodified lines | |||
| 32 | gix-revision = { version = "^0.30.0", path = "../gix-revision", default-features = false, features = ["merge_base"] } | 32 | gix-revision = { version = "^0.30.0", path = "../gix-revision", default-features = false, features = ["merge_base"] } |
| 33 | gix-revwalk = { version = "^0.16.0", path = "../gix-revwalk" } | 33 | gix-revwalk = { version = "^0.16.0", path = "../gix-revwalk" } |
| 34 | gix-diff = { version = "^0.47.0", path = "../gix-diff", default-features = false, features = ["blob"] } | 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 | thiserror = "2.0.0" | 37 | thiserror = "2.0.0" |
| 37 | imara-diff = { version = "0.1.7" } | 38 | imara-diff = { version = "0.1.7" } |
| @@ -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 | }; |
| 5 | use crate::tree::ConflictMapping::{Original, Swapped}; | 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 | use bstr::{BString, ByteSlice}; | 10 | use bstr::{BString, ByteSlice}; |
| 8 | use gix_diff::tree::recorder::Location; | 11 | use gix_diff::tree::recorder::Location; |
| 9 | use gix_diff::tree_with_rewrites::Change; | 12 | use 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_failure | 262 | 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_failure | 757 | 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_failure | 1073 | 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 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 | fn merge_modes(a: EntryMode, b: EntryMode) -> Option<EntryMode> { | 1112 | fn 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. | ||
| 1123 | fn 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 | |||
| 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 | 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)] | ||
| 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 | /// 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)] |
| 109 | enum ConflictMapping { | 154 | enum 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 | ||
| 309 | pub(super) mod function; | 366 | pub(super) mod function; |
| 310 | mod utils; | 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 | //! contribute to finding a fix faster. | 7 | //! contribute to finding a fix faster. |
| 8 | use crate::blob::builtin_driver::binary::Pick; | 8 | use crate::blob::builtin_driver::binary::Pick; |
| 9 | use crate::blob::ResourceKind; | 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 | use bstr::ByteSlice; | 14 | use bstr::ByteSlice; |
| 12 | use bstr::{BStr, BString, ByteVec}; | 15 | use bstr::{BStr, BString, ByteVec}; |
| 13 | use gix_diff::tree_with_rewrites::{Change, ChangeRef}; | 16 | use gix_diff::tree_with_rewrites::{Change, ChangeRef}; |
| @@ -98,6 +101,14 @@ pub fn perform_blob_merge<E>( · 84 unmodified lines | |||
| 98 | where | 101 | where |
| 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 | } |
Binary file not shown.
| @@ -31,6 +31,11 @@ function seq () { · 30 unmodified lines | |||
| 31 | done | 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 | function baseline () ( | 39 | function 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 | ||
| 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 | git init rename-add-symlink | 203 | git init rename-add-symlink |
| 176 | (cd rename-add-symlink | 204 | (cd rename-add-symlink |
| 177 | write_lines original 1 2 3 4 5 >foo | 205 | write_lines original 1 2 3 4 5 >foo |
| @@ -317,6 +345,34 @@ git init super-2 · 139 unmodified lines | |||
| 317 | git add foo | 345 | git add foo |
| 318 | git mv foo olddir/bar | 346 | 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 | ||
| 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 | git init rename-within-rename | 378 | git init rename-within-rename |
| @@ -348,6 +404,10 @@ git init rename-within-rename · 25 unmodified lines | |||
| 348 | git mv a/sub a/sub-renamed | 404 | git mv a/sub a/sub-renamed |
| 349 | git mv a a-renamed | 405 | 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 | ||
| 353 | git init rename-within-rename-2 | 413 | git 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-renamed | 440 | git mv a/sub a/sub-renamed |
| 381 | git mv a a-renamed | 441 | 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 | ||
| 385 | git init conflicting-rename | 449 | git 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.f | 467 | write_lines original 1 2 3 4 5 6 >a/sub/y.f |
| 404 | git mv a a-different | 468 | 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 | ||
| 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 | git init conflicting-rename-2 | 509 | git 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.f | 527 | write_lines original 1 2 3 4 5 6 >a/sub/y.f |
| 427 | git mv a/sub a/sub-different | 528 | 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 | ||
| 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 | git init conflicting-rename-complex | 560 | git init conflicting-rename-complex |
| @@ -461,6 +590,27 @@ git init conflicting-rename-complex · 29 unmodified lines | |||
| 461 | touch a-renamed/z a-renamed/sub/z | 590 | 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 | ||
| 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 | git init same-rename-different-mode | 616 | git 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.f | 641 | write_lines 1 2 3 4 5 6 >a/x.f |
| 492 | git mv a a-renamed | 642 | 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 | |||
| 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 | git init renamed-symlink-with-conflict | 669 | git init renamed-symlink-with-conflict |
| @@ -614,6 +787,23 @@ git init submodule-both-modify · 117 unmodified lines | |||
| 614 | git add sub | 787 | git add sub |
| 615 | tick | 788 | tick |
| 616 | git commit -m b | 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 | git init both-modify-union-attr | 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 | git init big-file-merge | 861 | git init big-file-merge |
| 672 | (cd big-file-merge | 862 | (cd big-file-merge |
| 673 | git config --local core.bigFileThreshold 80 | 863 | git config --local core.bigFileThreshold 100 |
| 674 | mkdir a && write_lines original 1 2 3 4 5 >a/x.f | 864 | 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 B | 868 | git branch B |
| 679 | 869 | ||
| 680 | git checkout A | 870 | git checkout A |
| 681 | seq 30 >a/x.f | 871 | 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 expected | 873 | git branch expected |
| 684 | 874 | ||
| 685 | git checkout B | 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 | baseline simple side-1-3-without-conflict side1 side3 | 999 | baseline simple side-1-3-without-conflict side1 side3 |
| 807 | baseline simple fast-forward side1 main | 1000 | baseline simple fast-forward side1 main |
| 808 | baseline simple no-change main main | 1001 | baseline simple no-change main main |
| @@ -838,7 +1031,6 @@ baseline conflicting-rename-2 A-B A B · 29 unmodified lines | |||
| 838 | 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" | 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 | 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" | 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 | baseline renamed-symlink-with-conflict A-B A B | 1034 | baseline renamed-symlink-with-conflict A-B A B |
| 843 | 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" | 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 | use std::path::{Path, PathBuf}; | 6 | use std::path::{Path, PathBuf}; |
| 7 | 7 | ||
| 8 | /// An entry in the conflict | 8 | /// An entry in the conflict |
| 9 | #[derive(Debug)] | 9 | #[derive(Debug, Eq, PartialEq)] |
| 10 | pub struct Entry { | 10 | pub struct Entry { |
| 11 | /// The relative path in the repository | 11 | /// 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)] |
| 21 | pub struct Conflict { | 21 | pub 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.information | 229 | 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)] | ||
| 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 | pub fn visualize_tree( | 316 | pub 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.information | 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 | use crate::tree::baseline::Deviation; | 1 | use crate::tree::baseline::Deviation; |
| 2 | use gix_diff::Rewrites; | 2 | use gix_diff::Rewrites; |
| 3 | use gix_merge::commit::Options; | 3 | use gix_merge::commit::Options; |
| 4 | use gix_merge::tree::TreatAsUnresolved; | ||
| 4 | use gix_object::Write; | 5 | use gix_object::Write; |
| 5 | use gix_worktree::stack::state::attributes; | 6 | use gix_worktree::stack::state::attributes; |
| 6 | use std::path::Path; | 7 | use 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 | ||