tree merge with tree-related auto-resolution
Unified · Split
| @@ -2792,6 +2792,7 @@ dependencies = [ · 2791 unmodified lines | ||
| 2792 | 2792 | "gix-features 0.39.1", |
| 2793 | 2793 | "gix-path 0.10.13", |
| 2794 | 2794 | "gix-testtools", |
| 2795 | + "percent-encoding", | |
| 2795 | 2796 | "serde", |
| 2796 | 2797 | "thiserror 2.0.3", |
| 2797 | 2798 | "url", |
| @@ -352,8 +352,11 @@ Check out the [performance discussion][gix-diff-performance] as well. · 351 unmodified lines | ||
| 352 | 352 | - [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same) |
| 353 | 353 | * [x] **tree**-diff-heuristics match Git for its test-cases |
| 354 | 354 | - [x] a way to generate an index with stages, mostly conforming with Git. |
| 355 | + - [ ] resolve to be *ours* or the *ancestors* version of the tree. | |
| 355 | 356 | - [ ] submodule merges (*right now they count as conflicts if they differ*) |
| 356 | 357 | - [ ] assure sparse indices are handled correctly during application - right now we refuse. |
| 358 | + - [ ] rewrite so that the whole logic can be proven to be correct - it's too insane now and probably has way | |
| 359 | + more possible states than are tested, despite best attempts. | |
| 357 | 360 | * [x] **commits** - with handling of multiple merge bases by recursive merge-base merge |
| 358 | 361 | * [x] API documentation |
| 359 | 362 | * [ ] Examples |
| @@ -139,7 +139,12 @@ pub fn update( · 138 unmodified lines | ||
| 139 | 139 | }); |
| 140 | 140 | |
| 141 | 141 | let rewrites = { |
| 142 | − let mut r = gix::diff::new_rewrites(&repo.config_snapshot(), true)?.unwrap_or_default(); | |
| 142 | + // These are either configured, or we set them to the default. There is no turning them off. | |
| 143 | + let (r, was_configured) = gix::diff::new_rewrites(&repo.config_snapshot(), true)?; | |
| 144 | + if was_configured && r.is_none() { | |
| 145 | + gix::trace::warn!("Rename tracking is disabled by configuration, but we enable it using the default"); | |
| 146 | + } | |
| 147 | + let mut r = r.unwrap_or_default(); | |
| 143 | 148 | r.copies = Some(gix::diff::rewrites::Copies { |
| 144 | 149 | source: if find_copies_harder { |
| 145 | 150 | CopySource::FromSetOfModifiedFilesAndAllSources |
| @@ -17,6 +17,7 @@ pub fn commit( · 16 unmodified lines | ||
| 17 | 17 | Options { |
| 18 | 18 | format, |
| 19 | 19 | file_favor, |
| 20 | + tree_favor, | |
| 20 | 21 | in_memory, |
| 21 | 22 | debug, |
| 22 | 23 | }: Options, |
| @@ -31,7 +32,10 @@ pub fn commit( · 8 unmodified lines | ||
| 31 | 32 | let (ours_ref, ours_id) = refname_and_commit(&repo, ours)?; |
| 32 | 33 | let (theirs_ref, theirs_id) = refname_and_commit(&repo, theirs)?; |
| 33 | 34 | |
| 34 | − let options = repo.tree_merge_options()?.with_file_favor(file_favor); | |
| 35 | + let options = repo | |
| 36 | + .tree_merge_options()? | |
| 37 | + .with_file_favor(file_favor) | |
| 38 | + .with_tree_favor(tree_favor); | |
| 35 | 39 | let ours_id_str = ours_id.to_string(); |
| 36 | 40 | let theirs_id_str = theirs_id.to_string(); |
| 37 | 41 | let labels = gix::merge::blob::builtin_driver::text::Labels { |
| @@ -49,7 +53,7 @@ pub fn commit( · 11 unmodified lines | ||
| 49 | 53 | .merge_commits(ours_id, theirs_id, labels, options.into())? |
| 50 | 54 | .tree_merge; |
| 51 | 55 | let has_conflicts = res.conflicts.is_empty(); |
| 52 | − let has_unresolved_conflicts = res.has_unresolved_conflicts(TreatAsUnresolved::Renames); | |
| 56 | + let has_unresolved_conflicts = res.has_unresolved_conflicts(TreatAsUnresolved::default()); | |
| 53 | 57 | { |
| 54 | 58 | let _span = gix::trace::detail!("Writing merged tree"); |
| 55 | 59 | let mut written = 0; |
| @@ -3,6 +3,7 @@ use crate::OutputFormat; · 2 unmodified lines | ||
| 3 | 3 | pub struct Options { |
| 4 | 4 | pub format: OutputFormat, |
| 5 | 5 | pub file_favor: Option<gix::merge::tree::FileFavor>, |
| 6 | + pub tree_favor: Option<gix::merge::tree::TreeFavor>, | |
| 6 | 7 | pub in_memory: bool, |
| 7 | 8 | pub debug: bool, |
| 8 | 9 | } |
| @@ -29,6 +30,7 @@ pub(super) mod function { · 20 unmodified lines | ||
| 29 | 30 | Options { |
| 30 | 31 | format, |
| 31 | 32 | file_favor, |
| 33 | + tree_favor, | |
| 32 | 34 | in_memory, |
| 33 | 35 | debug, |
| 34 | 36 | }: Options, |
| @@ -44,7 +46,10 @@ pub(super) mod function { · 9 unmodified lines | ||
| 44 | 46 | let (ours_ref, ours_id) = refname_and_tree(&repo, ours)?; |
| 45 | 47 | let (theirs_ref, theirs_id) = refname_and_tree(&repo, theirs)?; |
| 46 | 48 | |
| 47 | − let options = repo.tree_merge_options()?.with_file_favor(file_favor); | |
| 49 | + let options = repo | |
| 50 | + .tree_merge_options()? | |
| 51 | + .with_file_favor(file_favor) | |
| 52 | + .with_tree_favor(tree_favor); | |
| 48 | 53 | let base_id_str = base_id.to_string(); |
| 49 | 54 | let ours_id_str = ours_id.to_string(); |
| 50 | 55 | let theirs_id_str = theirs_id.to_string(); |
| @@ -64,7 +69,7 @@ pub(super) mod function { · 13 unmodified lines | ||
| 64 | 69 | }; |
| 65 | 70 | let res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?; |
| 66 | 71 | let has_conflicts = res.conflicts.is_empty(); |
| 67 | − let has_unresolved_conflicts = res.has_unresolved_conflicts(TreatAsUnresolved::Renames); | |
| 72 | + let has_unresolved_conflicts = res.has_unresolved_conflicts(TreatAsUnresolved::default()); | |
| 68 | 73 | { |
| 69 | 74 | let _span = gix::trace::detail!("Writing merged tree"); |
| 70 | 75 | let mut written = 0; |
| @@ -81,6 +81,7 @@ pub fn show( · 80 unmodified lines | ||
| 81 | 81 | copies: None, |
| 82 | 82 | percentage: Some(percentage), |
| 83 | 83 | limit: 0, |
| 84 | + track_empty: false, | |
| 84 | 85 | }); |
| 85 | 86 | if opts.rewrites.is_some() { |
| 86 | 87 | if let Some(opts) = opts.dirwalk_options.as_mut() { |
| @@ -37,6 +37,11 @@ pub struct Rewrites { · 36 unmodified lines | ||
| 37 | 37 | /// If the limit would not be enough to test the entire set of combinations, the algorithm will trade in precision and not |
| 38 | 38 | /// run the fuzzy version of identity tests at all. That way results are never partial. |
| 39 | 39 | pub limit: usize, |
| 40 | + | |
| 41 | + /// If `true`, empty blobs will be tracked. If `false`, they do not participate in rename tracking. | |
| 42 | + /// | |
| 43 | + /// Leaving this off usually leads to better results as empty files don't have a unique-enough identity. | |
| 44 | + pub track_empty: bool, | |
| 40 | 45 | } |
| 41 | 46 | |
| 42 | 47 | /// Contains a [Tracker](rewrites::Tracker) to detect rewrites. |
| @@ -70,6 +70,7 @@ impl Default for Rewrites { · 69 unmodified lines | ||
| 70 | 70 | copies: None, |
| 71 | 71 | percentage: Some(0.5), |
| 72 | 72 | limit: 1000, |
| 73 | + track_empty: false, | |
| 73 | 74 | } |
| 74 | 75 | } |
| 75 | 76 | } |
| @@ -286,12 +286,10 @@ impl<T: Change> Tracker<T> { · 285 unmodified lines | ||
| 286 | 286 | CopySource::FromSetOfModifiedFiles => {} |
| 287 | 287 | CopySource::FromSetOfModifiedFilesAndAllSources => { |
| 288 | 288 | push_source_tree(&mut |change, location| { |
| 289 | − assert!( | |
| 290 | − self.try_push_change(change, location).is_none(), | |
| 291 | − "we must accept every change" | |
| 292 | − ); | |
| 293 | − // make sure these aren't viable to be emitted anymore. | |
| 294 | − self.items.last_mut().expect("just pushed").emitted = true; | |
| 289 | + if self.try_push_change(change, location).is_none() { | |
| 290 | + // make sure these aren't viable to be emitted anymore. | |
| 291 | + self.items.last_mut().expect("just pushed").emitted = true; | |
| 292 | + } | |
| 295 | 293 | }) |
| 296 | 294 | .map_err(|err| emit::Error::GetItemsForExhaustiveCopyDetection(Box::new(err)))?; |
| 297 | 295 | self.items.sort_by(by_id_and_location); |
| @@ -341,6 +339,10 @@ impl<T: Change> Tracker<T> { · 43 unmodified lines | ||
| 341 | 339 | ) -> Result<(), emit::Error> { |
| 342 | 340 | // we try to cheaply reduce the set of possibilities first, before possibly looking more exhaustively. |
| 343 | 341 | let needs_second_pass = !needs_exact_match(percentage); |
| 342 | + | |
| 343 | + // https://github.com/git/git/blob/cc01bad4a9f566cf4453c7edd6b433851b0835e2/diffcore-rename.c#L350-L369 | |
| 344 | + // We would need a hashmap to be OK to not use the limit here, otherwise the performance is too bad. | |
| 345 | + // This also means we don't find all renames if we hit the rename limit. | |
| 344 | 346 | if self.match_pairs(cb, None /* by identity */, kind, out, diff_cache, objects, filter)? == Action::Cancel { |
| 345 | 347 | return Ok(()); |
| 346 | 348 | } |
| @@ -384,10 +386,35 @@ impl<T: Change> Tracker<T> { · 37 unmodified lines | ||
| 384 | 386 | filter: Option<fn(&T) -> bool>, |
| 385 | 387 | ) -> Result<Action, emit::Error> { |
| 386 | 388 | let mut dest_ofs = 0; |
| 389 | + let mut num_checks = 0; | |
| 390 | + let max_checks = { | |
| 391 | + let limit = self.rewrites.limit.saturating_pow(2); | |
| 392 | + // There can be trees with a lot of entries and pathological search behaviour, as they can be repeated | |
| 393 | + // and then have a lot of similar hashes. This also means we have to search a lot of candidates which | |
| 394 | + // can be too slow despite best attempts. So play it save and detect such cases 'roughly' by amount of items. | |
| 395 | + if self.items.len() < 100_000 { | |
| 396 | + 0 | |
| 397 | + } else { | |
| 398 | + limit | |
| 399 | + } | |
| 400 | + }; | |
| 401 | + | |
| 387 | 402 | while let Some((mut dest_idx, dest)) = self.items[dest_ofs..].iter().enumerate().find_map(|(idx, item)| { |
| 388 | 403 | (!item.emitted |
| 389 | 404 | && matches!(item.change.kind(), ChangeKind::Addition) |
| 390 | − && filter.map_or(true, |f| f(&item.change))) | |
| 405 | + && filter.map_or_else( | |
| 406 | + || { | |
| 407 | + self.rewrites.track_empty | |
| 408 | + // We always want to keep track of entries that are involved of a directory rename. | |
| 409 | + // Note that this may still match them up arbitrarily if empty, but empty is empty. | |
| 410 | + || matches!(item.change.relation(), Some(Relation::ChildOfParent(_))) | |
| 411 | + || { | |
| 412 | + let id = item.change.id(); | |
| 413 | + id != gix_hash::ObjectId::empty_blob(id.kind()) | |
| 414 | + } | |
| 415 | + }, | |
| 416 | + |f| f(&item.change), | |
| 417 | + )) | |
| 391 | 418 | .then_some((idx, item)) |
| 392 | 419 | }) { |
| 393 | 420 | dest_idx += dest_ofs; |
| @@ -403,6 +430,7 @@ impl<T: Change> Tracker<T> { · 9 unmodified lines | ||
| 403 | 430 | objects, |
| 404 | 431 | diff_cache, |
| 405 | 432 | &self.path_backing, |
| 433 | + &mut num_checks, | |
| 406 | 434 | )? |
| 407 | 435 | .map(|(src_idx, src, diff)| { |
| 408 | 436 | let (id, entry_mode) = src.change.id_and_entry_mode(); |
| @@ -420,6 +448,12 @@ impl<T: Change> Tracker<T> { · 11 unmodified lines | ||
| 420 | 448 | src_idx, |
| 421 | 449 | ) |
| 422 | 450 | }); |
| 451 | + if max_checks != 0 && num_checks > max_checks { | |
| 452 | + gix_trace::warn!( | |
| 453 | + "Cancelled rename matching as there were too many iterations ({num_checks} > {max_checks})" | |
| 454 | + ); | |
| 455 | + return Ok(Action::Cancel); | |
| 456 | + } | |
| 423 | 457 | let Some((src, src_idx)) = src else { |
| 424 | 458 | continue; |
| 425 | 459 | }; |
| @@ -631,6 +665,7 @@ fn find_match<'a, T: Change>( · 205 unmodified lines | ||
| 631 | 665 | objects: &impl gix_object::FindObjectOrHeader, |
| 632 | 666 | diff_cache: &mut crate::blob::Platform, |
| 633 | 667 | path_backing: &[u8], |
| 668 | + num_checks: &mut usize, | |
| 634 | 669 | ) -> Result<Option<SourceTuple<'a, T>>, emit::Error> { |
| 635 | 670 | let (item_id, item_mode) = item.change.id_and_entry_mode(); |
| 636 | 671 | if needs_exact_match(percentage) || item_mode.is_link() { |
| @@ -651,6 +686,7 @@ fn find_match<'a, T: Change>( · 14 unmodified lines | ||
| 651 | 686 | } |
| 652 | 687 | let res = items[range.clone()].iter().enumerate().find_map(|(mut src_idx, src)| { |
| 653 | 688 | src_idx += range.start; |
| 689 | + *num_checks += 1; | |
| 654 | 690 | (src_idx != item_idx && src.is_source_for_destination_of(kind, item_mode)).then_some((src_idx, src, None)) |
| 655 | 691 | }); |
| 656 | 692 | if let Some(src) = res { |
| @@ -685,6 +721,7 @@ fn find_match<'a, T: Change>( · 28 unmodified lines | ||
| 685 | 721 | )?; |
| 686 | 722 | let prep = diff_cache.prepare_diff()?; |
| 687 | 723 | stats.num_similarity_checks += 1; |
| 724 | + *num_checks += 1; | |
| 688 | 725 | match prep.operation { |
| 689 | 726 | Operation::InternalDiff { algorithm } => { |
| 690 | 727 | let tokens = |
| @@ -27,6 +27,7 @@ fn rename_by_id() -> crate::Result { · 26 unmodified lines | ||
| 27 | 27 | copies: None, |
| 28 | 28 | percentage: None, |
| 29 | 29 | limit, |
| 30 | + track_empty: false, | |
| 30 | 31 | }; |
| 31 | 32 | let mut track = util::new_tracker(rewrites); |
| 32 | 33 | assert!( |
| @@ -80,6 +81,7 @@ fn copy_by_similarity_reports_limit_if_encountered() -> crate::Result { · 47 unmodified lines | ||
| 80 | 81 | }), |
| 81 | 82 | percentage: None, |
| 82 | 83 | limit: 1, |
| 84 | + track_empty: false, | |
| 83 | 85 | }; |
| 84 | 86 | let mut track = util::new_tracker(rewrites); |
| 85 | 87 | let odb = util::add_retained_blobs( |
| @@ -132,6 +134,7 @@ fn copy_by_id() -> crate::Result { · 46 unmodified lines | ||
| 132 | 134 | }), |
| 133 | 135 | percentage: None, |
| 134 | 136 | limit, |
| 137 | + track_empty: false, | |
| 135 | 138 | }; |
| 136 | 139 | let mut track = util::new_tracker(rewrites); |
| 137 | 140 | let odb = util::add_retained_blobs( |
| @@ -206,6 +209,7 @@ fn copy_by_id_search_in_all_sources() -> crate::Result { · 68 unmodified lines | ||
| 206 | 209 | }), |
| 207 | 210 | percentage: None, |
| 208 | 211 | limit, |
| 212 | + track_empty: false, | |
| 209 | 213 | }; |
| 210 | 214 | let mut track = util::new_tracker(rewrites); |
| 211 | 215 | let odb = util::add_retained_blobs( |
| @@ -284,6 +288,7 @@ fn copy_by_50_percent_similarity() -> crate::Result { · 72 unmodified lines | ||
| 284 | 288 | }), |
| 285 | 289 | percentage: None, |
| 286 | 290 | limit: 0, |
| 291 | + track_empty: false, | |
| 287 | 292 | }; |
| 288 | 293 | let mut track = util::new_tracker(rewrites); |
| 289 | 294 | let odb = util::add_retained_blobs( |
| @@ -363,6 +368,7 @@ fn copy_by_id_in_additions_only() -> crate::Result { · 73 unmodified lines | ||
| 363 | 368 | }), |
| 364 | 369 | percentage: None, |
| 365 | 370 | limit: 0, |
| 371 | + track_empty: false, | |
| 366 | 372 | }; |
| 367 | 373 | let mut track = util::new_tracker(rewrites); |
| 368 | 374 | let odb = util::add_retained_blobs( |
| @@ -413,6 +419,7 @@ fn rename_by_similarity_reports_limit_if_encountered() -> crate::Result { · 44 unmodified lines | ||
| 413 | 419 | copies: None, |
| 414 | 420 | percentage: Some(0.5), |
| 415 | 421 | limit: 1, |
| 422 | + track_empty: false, | |
| 416 | 423 | }; |
| 417 | 424 | let mut track = util::new_tracker(rewrites); |
| 418 | 425 | let odb = util::add_retained_blobs( |
| @@ -458,6 +465,7 @@ fn rename_by_50_percent_similarity() -> crate::Result { · 39 unmodified lines | ||
| 458 | 465 | copies: None, |
| 459 | 466 | percentage: Some(0.5), |
| 460 | 467 | limit: 0, |
| 468 | + track_empty: false, | |
| 461 | 469 | }; |
| 462 | 470 | let mut track = util::new_tracker(rewrites); |
| 463 | 471 | let odb = util::add_retained_blobs( |
| @@ -547,6 +555,7 @@ fn directory_renames_by_id_can_fail_gracefully() -> crate::Result { · 83 unmodified lines | ||
| 547 | 555 | copies: None, |
| 548 | 556 | percentage: Some(0.5), |
| 549 | 557 | limit: 0, |
| 558 | + track_empty: false, | |
| 550 | 559 | }; |
| 551 | 560 | let mut track = util::new_tracker(rename_by_similarity); |
| 552 | 561 | let tree_dst_id = 1; |
| @@ -638,6 +647,7 @@ fn simple_directory_rename_by_id() -> crate::Result { · 85 unmodified lines | ||
| 638 | 647 | copies: None, |
| 639 | 648 | percentage: None, |
| 640 | 649 | limit: 0, |
| 650 | + track_empty: false, | |
| 641 | 651 | }; |
| 642 | 652 | let mut track = util::new_tracker(renames_by_identity); |
| 643 | 653 | let tree_dst_id = 1; |
| @@ -192,12 +192,13 @@ fn changes_against_modified_tree_with_filename_tracking() -> crate::Result { · 191 unmodified lines | ||
| 192 | 192 | |
| 193 | 193 | #[test] |
| 194 | 194 | fn renames_by_identity() -> crate::Result { |
| 195 | − for (from, to, expected, assert_msg) in [ | |
| 195 | + for (from, to, expected, assert_msg, track_empty) in [ | |
| 196 | 196 | ( |
| 197 | 197 | "c3-modification", |
| 198 | 198 | "r1-identity", |
| 199 | 199 | vec![BStr::new("a"), "dir/a-moved".into()], |
| 200 | 200 | "one rename and nothing else", |
| 201 | + false, | |
| 201 | 202 | ), |
| 202 | 203 | ( |
| 203 | 204 | "c4 - add identical files", |
| @@ -211,24 +212,35 @@ fn renames_by_identity() -> crate::Result { · 7 unmodified lines | ||
| 211 | 212 | "z".into(), |
| 212 | 213 | ], |
| 213 | 214 | "multiple possible sources decide by ordering everything lexicographically", |
| 215 | + true, | |
| 216 | + ), | |
| 217 | + ( | |
| 218 | + "c4 - add identical files", | |
| 219 | + "r2-ambiguous", | |
| 220 | + vec![], | |
| 221 | + "nothing is tracked with `track_empty = false`", | |
| 222 | + false, | |
| 214 | 223 | ), |
| 215 | 224 | ( |
| 216 | 225 | "c5 - add links", |
| 217 | 226 | "r4-symlinks", |
| 218 | 227 | vec!["link-1".into(), "renamed-link-1".into()], |
| 219 | 228 | "symlinks are only tracked by identity", |
| 229 | + false, | |
| 220 | 230 | ), |
| 221 | 231 | ( |
| 222 | 232 | "r1-identity", |
| 223 | 233 | "c4 - add identical files", |
| 224 | 234 | vec![], |
| 225 | 235 | "not having any renames is OK as well", |
| 236 | + false, | |
| 226 | 237 | ), |
| 227 | 238 | ( |
| 228 | 239 | "tc1-identity", |
| 229 | 240 | "tc1-identity", |
| 230 | 241 | vec![], |
| 231 | 242 | "copy tracking is off by default", |
| 243 | + false, | |
| 232 | 244 | ), |
| 233 | 245 | ] { |
| 234 | 246 | for percentage in [None, Some(0.5)] { |
| @@ -239,6 +251,7 @@ fn renames_by_identity() -> crate::Result { · 4 unmodified lines | ||
| 239 | 251 | location: Some(Location::Path), |
| 240 | 252 | rewrites: Some(Rewrites { |
| 241 | 253 | percentage, |
| 254 | + track_empty, | |
| 242 | 255 | ..Default::default() |
| 243 | 256 | }), |
| 244 | 257 | }, |
| @@ -704,6 +717,7 @@ fn copies_in_entire_tree_by_similarity_with_limit() -> crate::Result { · 459 unmodified lines | ||
| 704 | 717 | ..Default::default() |
| 705 | 718 | }), |
| 706 | 719 | limit: 2, // similarity checks can't be made that way |
| 720 | + track_empty: false, | |
| 707 | 721 | ..Default::default() |
| 708 | 722 | }), |
| 709 | 723 | }, |
| @@ -833,6 +847,7 @@ fn realistic_renames_by_identity() -> crate::Result { · 123 unmodified lines | ||
| 833 | 847 | rewrites: Some(Rewrites { |
| 834 | 848 | copies: Some(Copies::default()), |
| 835 | 849 | limit: 1, |
| 850 | + track_empty: true, | |
| 836 | 851 | ..Default::default() |
| 837 | 852 | }), |
| 838 | 853 | }, |
| @@ -1324,6 +1339,7 @@ fn realistic_renames_by_identity_3() -> crate::Result { · 485 unmodified lines | ||
| 1324 | 1339 | rewrites: Some(Rewrites { |
| 1325 | 1340 | copies: Some(Copies::default()), |
| 1326 | 1341 | limit: 1, |
| 1342 | + track_empty: true, | |
| 1327 | 1343 | ..Default::default() |
| 1328 | 1344 | }), |
| 1329 | 1345 | }, |
| @@ -1402,6 +1418,7 @@ fn realistic_renames_2() -> crate::Result { · 72 unmodified lines | ||
| 1402 | 1418 | rewrites: Some(Rewrites { |
| 1403 | 1419 | copies: Some(Copies::default()), |
| 1404 | 1420 | limit: 1, |
| 1421 | + track_empty: false, | |
| 1405 | 1422 | ..Default::default() |
| 1406 | 1423 | }), |
| 1407 | 1424 | }, |
| @@ -1665,6 +1682,7 @@ fn realistic_renames_3_without_identity() -> crate::Result { · 257 unmodified lines | ||
| 1665 | 1682 | copies: None, |
| 1666 | 1683 | percentage: None, |
| 1667 | 1684 | limit: 0, |
| 1685 | + track_empty: false, | |
| 1668 | 1686 | }), |
| 1669 | 1687 | }, |
| 1670 | 1688 | )?; |
| @@ -25,8 +25,10 @@ pub(super) mod function { · 24 unmodified lines | ||
| 25 | 25 | use crate::blob::Resolution; |
| 26 | 26 | |
| 27 | 27 | /// As this algorithm doesn't look at the actual data, it returns a choice solely based on logic. |
| 28 | + /// This also means that the caller has to assure this only gets called if the input *doesn't* match. | |
| 28 | 29 | /// |
| 29 | − /// It always results in a conflict with `current` being picked unless `on_conflict` is not `None`. | |
| 30 | + /// It always results in a conflict with `current` being picked unless `on_conflict` is not `None`, | |
| 31 | + /// which is when we always return [`Resolution::CompleteWithAutoResolvedConflict`]. | |
| 30 | 32 | pub fn merge(on_conflict: Option<ResolveWith>) -> (Pick, Resolution) { |
| 31 | 33 | match on_conflict { |
| 32 | 34 | None => (Pick::Ours, Resolution::Conflict), |
| @@ -36,7 +38,7 @@ pub(super) mod function { · 3 unmodified lines | ||
| 36 | 38 | ResolveWith::Theirs => Pick::Theirs, |
| 37 | 39 | ResolveWith::Ancestor => Pick::Ancestor, |
| 38 | 40 | }, |
| 39 | − Resolution::Complete, | |
| 41 | + Resolution::CompleteWithAutoResolvedConflict, | |
| 40 | 42 | ), |
| 41 | 43 | } |
| 42 | 44 | } |
| @@ -57,8 +57,11 @@ pub enum ConflictStyle { · 56 unmodified lines | ||
| 57 | 57 | /// That way it becomes clearer where the content of conflicts are originating from. |
| 58 | 58 | #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] |
| 59 | 59 | pub struct Labels<'a> { |
| 60 | + /// The label for the common *ancestor*. | |
| 60 | 61 | pub ancestor: Option<&'a BStr>, |
| 62 | + /// The label for the *current* (or *our*) side. | |
| 61 | 63 | pub current: Option<&'a BStr>, |
| 64 | + /// The label for the *other* (or *their*) side. | |
| 62 | 65 | pub other: Option<&'a BStr>, |
| 63 | 66 | } |
| 64 | 67 | |
| @@ -16,8 +16,9 @@ pub fn hunks_differ_in_diff3( · 15 unmodified lines | ||
| 16 | 16 | return true; |
| 17 | 17 | } |
| 18 | 18 | |
| 19 | − let tokens_for_hunk = | |
| 20 | − |hunk: &Hunk| -> &[imara_diff::intern::Token] { tokens_for_side(hunk.side, input, current_tokens) }; | |
| 19 | + let tokens_for_hunk = |hunk: &Hunk| -> &[imara_diff::intern::Token] { | |
| 20 | + &tokens_for_side(hunk.side, input, current_tokens)[hunk.after.start as usize..hunk.after.end as usize] | |
| 21 | + }; | |
| 21 | 22 | |
| 22 | 23 | a.iter() |
| 23 | 24 | .flat_map(tokens_for_hunk) |
| @@ -329,13 +329,21 @@ pub(super) mod inner { · 328 unmodified lines | ||
| 329 | 329 | (Pick::Buffer, resolution) |
| 330 | 330 | } |
| 331 | 331 | BuiltinDriver::Binary => { |
| 332 | − let (pick, resolution) = builtin_driver::binary(self.options.resolve_binary_with); | |
| 333 | − let pick = match pick { | |
| 334 | − builtin_driver::binary::Pick::Ours => Pick::Ours, | |
| 335 | − builtin_driver::binary::Pick::Theirs => Pick::Theirs, | |
| 336 | − builtin_driver::binary::Pick::Ancestor => Pick::Ancestor, | |
| 337 | − }; | |
| 338 | − (pick, resolution) | |
| 332 | + // easier to reason about the 'split' compared to merging both conditions | |
| 333 | + #[allow(clippy::if_same_then_else)] | |
| 334 | + if !(self.current.id.is_null() || self.other.id.is_null()) && self.current.id == self.other.id { | |
| 335 | + (Pick::Ours, Resolution::Complete) | |
| 336 | + } else if (self.current.id.is_null() || self.other.id.is_null()) && ours == theirs { | |
| 337 | + (Pick::Ours, Resolution::Complete) | |
| 338 | + } else { | |
| 339 | + let (pick, resolution) = builtin_driver::binary(self.options.resolve_binary_with); | |
| 340 | + let pick = match pick { | |
| 341 | + builtin_driver::binary::Pick::Ours => Pick::Ours, | |
| 342 | + builtin_driver::binary::Pick::Theirs => Pick::Theirs, | |
| 343 | + builtin_driver::binary::Pick::Ancestor => Pick::Ancestor, | |
| 344 | + }; | |
| 345 | + (pick, resolution) | |
| 346 | + } | |
| 339 | 347 | } |
| 340 | 348 | BuiltinDriver::Union => { |
| 341 | 349 | let resolution = builtin_driver::text( |
| @@ -55,5 +55,6 @@ pub struct Outcome<'a> { · 54 unmodified lines | ||
| 55 | 55 | |
| 56 | 56 | pub(super) mod function; |
| 57 | 57 | |
| 58 | +/// | |
| 58 | 59 | pub mod virtual_merge_base; |
| 59 | 60 | pub use virtual_merge_base::function::virtual_merge_base; |
| @@ -30,7 +30,7 @@ pub enum Error { · 29 unmodified lines | ||
| 30 | 30 | pub(super) mod function { |
| 31 | 31 | use super::Error; |
| 32 | 32 | use crate::blob::builtin_driver; |
| 33 | − use crate::tree::TreatAsUnresolved; | |
| 33 | + use crate::tree::{treat_as_unresolved, TreatAsUnresolved}; | |
| 34 | 34 | use gix_object::FindExt; |
| 35 | 35 | |
| 36 | 36 | /// Create a single virtual merge-base by merging `first_commit`, `second_commit` and `others` into one. |
| @@ -55,7 +55,7 @@ pub(super) mod function { · 18 unmodified lines | ||
| 55 | 55 | let mut merged_commit_id = first_commit; |
| 56 | 56 | others.push(second_commit); |
| 57 | 57 | |
| 58 | − options.allow_lossy_resolution = true; | |
| 58 | + options.tree_conflicts = Some(crate::tree::ResolveWith::Ancestor); | |
| 59 | 59 | options.blob_merge.is_virtual_ancestor = true; |
| 60 | 60 | options.blob_merge.text.conflict = builtin_driver::text::Conflict::ResolveWithOurs; |
| 61 | 61 | let favor_ancestor = Some(builtin_driver::binary::ResolveWith::Ancestor); |
| @@ -86,10 +86,10 @@ pub(super) mod function { · 24 unmodified lines | ||
| 86 | 86 | }, |
| 87 | 87 | )?; |
| 88 | 88 | // This shouldn't happen, but if for some buggy reason it does, we rather bail. |
| 89 | − if out | |
| 90 | − .tree_merge | |
| 91 | − .has_unresolved_conflicts(TreatAsUnresolved::ConflictMarkers) | |
| 92 | − { | |
| 89 | + if out.tree_merge.has_unresolved_conflicts(TreatAsUnresolved { | |
| 90 | + content_merge: treat_as_unresolved::ContentMerge::Markers, | |
| 91 | + tree_merge: treat_as_unresolved::TreeMerge::Undecidable, | |
| 92 | + }) { | |
| 93 | 93 | return Err(Error::VirtualMergeBaseConflict.into()); |
| 94 | 94 | } |
| 95 | 95 | let merged_tree_id = out |
| @@ -4,6 +4,7 @@ · 3 unmodified lines | ||
| 4 | 4 | //! * [tree-merges](mod@tree) look at trees and merge them structurally, triggering blob-merges as needed. |
| 5 | 5 | //! * [commit-merges](mod@commit) are like tree merges, but compute or create the merge-base on the fly. |
| 6 | 6 | #![deny(rust_2018_idioms)] |
| 7 | +#![deny(missing_docs)] | |
| 7 | 8 | #![forbid(unsafe_code)] |
| 8 | 9 | |
| 9 | 10 | /// |
| @@ -5,7 +5,7 @@ use crate::tree::utils::{ · 4 unmodified lines | ||
| 5 | 5 | use crate::tree::ConflictMapping::{Original, Swapped}; |
| 6 | 6 | use crate::tree::{ |
| 7 | 7 | Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, ContentMerge, Error, Options, Outcome, |
| 8 | − Resolution, ResolutionFailure, | |
| 8 | + Resolution, ResolutionFailure, ResolveWith, | |
| 9 | 9 | }; |
| 10 | 10 | use bstr::{BString, ByteSlice}; |
| 11 | 11 | use gix_diff::tree::recorder::Location; |
| @@ -13,6 +13,7 @@ use gix_diff::tree_with_rewrites::Change; · 1 unmodified line | ||
| 13 | 13 | use gix_hash::ObjectId; |
| 14 | 14 | use gix_object::tree::{EntryKind, EntryMode}; |
| 15 | 15 | use gix_object::{tree, FindExt}; |
| 16 | +use std::borrow::Cow; | |
| 16 | 17 | use std::convert::Infallible; |
| 17 | 18 | |
| 18 | 19 | /// Perform a merge between `our_tree` and `their_tree`, using `base_tree` as merge-base. |
| @@ -71,10 +72,9 @@ where · 52 unmodified lines | ||
| 71 | 72 | let _span = gix_trace::coarse!("gix_merge::tree", ?base_tree, ?our_tree, ?their_tree, ?labels); |
| 72 | 73 | let (mut base_buf, mut side_buf) = (Vec::new(), Vec::new()); |
| 73 | 74 | let ancestor_tree = objects.find_tree(base_tree, &mut base_buf)?; |
| 74 | − let allow_resolution_failure = !options.allow_lossy_resolution; | |
| 75 | − | |
| 76 | 75 | let mut editor = tree::Editor::new(ancestor_tree.to_owned(), objects, base_tree.kind()); |
| 77 | 76 | let ancestor_tree = gix_object::TreeRefIter::from_bytes(&base_buf); |
| 77 | + let tree_conflicts = options.tree_conflicts; | |
| 78 | 78 | |
| 79 | 79 | let mut our_changes = Vec::new(); |
| 80 | 80 | if ours_needs_diff { |
| @@ -128,7 +128,12 @@ where · 47 unmodified lines | ||
| 128 | 128 | |
| 129 | 129 | let mut conflicts = Vec::new(); |
| 130 | 130 | let mut failed_on_first_conflict = false; |
| 131 | − let mut should_fail_on_conflict = |conflict: Conflict| -> bool { | |
| 131 | + let mut should_fail_on_conflict = |mut conflict: Conflict| -> bool { | |
| 132 | + if tree_conflicts.is_some() { | |
| 133 | + if let Err(failure) = conflict.resolution { | |
| 134 | + conflict.resolution = Ok(Resolution::Forced(failure)); | |
| 135 | + } | |
| 136 | + } | |
| 132 | 137 | if let Some(how) = options.fail_on_conflict { |
| 133 | 138 | if conflict.resolution.is_err() || conflict.is_unresolved(how) { |
| 134 | 139 | failed_on_first_conflict = true; |
| @@ -238,7 +243,7 @@ where · 103 unmodified lines | ||
| 238 | 243 | PossibleConflict::TreeToNonTree { change_idx: Some(idx) } |
| 239 | 244 | if matches!( |
| 240 | 245 | our_changes[idx].inner, |
| 241 | − Change::Deletion { .. } | Change::Addition { .. } | |
| 246 | + Change::Deletion { .. } | Change::Addition { .. } | Change::Rewrite { .. } | |
| 242 | 247 | ) => |
| 243 | 248 | { |
| 244 | 249 | (Some(idx), Some(MatchKind::EraseTree)) |
| @@ -255,34 +260,76 @@ where · 10 unmodified lines | ||
| 255 | 260 | PossibleConflict::Match { change_idx } |
| 256 | 261 | | PossibleConflict::PassedRewrittenDirectory { change_idx } => Some(change_idx), |
| 257 | 262 | } |
| 258 | − .map(|idx| &our_changes[idx]); | |
| 263 | + .map(|idx| &mut our_changes[idx]); | |
| 259 | 264 | |
| 260 | 265 | if let Some(ours) = ours { |
| 261 | 266 | gix_trace::debug!("Turning a case we could probably handle into a conflict for now. theirs: {theirs:#?} ours: {ours:#?} kind: {match_kind:?}"); |
| 262 | − if allow_resolution_failure | |
| 263 | − && should_fail_on_conflict(Conflict::unknown(( | |
| 264 | − &ours.inner, | |
| 265 | − theirs, | |
| 266 | − Original, | |
| 267 | − outer_side, | |
| 268 | − ))) | |
| 269 | − { | |
| 267 | + let conflict = Conflict::unknown((&ours.inner, theirs, Original, outer_side)); | |
| 268 | + if let Some(ResolveWith::Ours) = tree_conflicts { | |
| 269 | + apply_our_resolution(&ours.inner, theirs, outer_side, &mut editor)?; | |
| 270 | + *match outer_side { | |
| 271 | + Original => &mut ours.was_written, | |
| 272 | + Swapped => &mut their_changes[theirs_idx].was_written, | |
| 273 | + } = true; | |
| 274 | + } | |
| 275 | + if should_fail_on_conflict(conflict) { | |
| 270 | 276 | break 'outer; |
| 271 | 277 | }; |
| 272 | − continue; | |
| 278 | + } else if matches!(candidate, PossibleConflict::TreeToNonTree { .. }) { | |
| 279 | + let (mode, id) = theirs.entry_mode_and_id(); | |
| 280 | + let location = theirs.location(); | |
| 281 | + let renamed_location = unique_path_in_tree( | |
| 282 | + location.as_bstr(), | |
| 283 | + &editor, | |
| 284 | + their_tree, | |
| 285 | + labels.other.unwrap_or_default(), | |
| 286 | + )?; | |
| 287 | + match tree_conflicts { | |
| 288 | + None => { | |
| 289 | + editor.upsert(toc(&renamed_location), mode.kind(), id.to_owned())?; | |
| 290 | + } | |
| 291 | + Some(ResolveWith::Ours) => { | |
| 292 | + if outer_side.is_swapped() { | |
| 293 | + editor.upsert(to_components(location), mode.kind(), id.to_owned())?; | |
| 294 | + } | |
| 295 | + } | |
| 296 | + Some(ResolveWith::Ancestor) => { | |
| 297 | + // we found no matching node of 'ours', so nothing to apply here. | |
| 298 | + } | |
| 299 | + } | |
| 300 | + | |
| 301 | + let conflict = Conflict::without_resolution( | |
| 302 | + ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { | |
| 303 | + renamed_unique_path_of_theirs: renamed_location, | |
| 304 | + }, | |
| 305 | + (theirs, theirs, Original, outer_side), | |
| 306 | + [ | |
| 307 | + None, | |
| 308 | + None, | |
| 309 | + index_entry_at_path( | |
| 310 | + &mode.kind().into(), | |
| 311 | + &id.to_owned(), | |
| 312 | + ConflictIndexEntryPathHint::RenamedOrTheirs, | |
| 313 | + ), | |
| 314 | + ], | |
| 315 | + ); | |
| 316 | + their_changes[theirs_idx].was_written = true; | |
| 317 | + if should_fail_on_conflict(conflict) { | |
| 318 | + break 'outer; | |
| 319 | + }; | |
| 320 | + } else if matches!(candidate, PossibleConflict::NonTreeToTree { .. }) { | |
| 321 | + // We are writing on top of what was a file, a conflict we probably already saw and dealt with. | |
| 322 | + let location = theirs.location(); | |
| 323 | + let (mode, id) = theirs.entry_mode_and_id(); | |
| 324 | + editor.upsert(to_components(location), mode.kind(), id.to_owned())?; | |
| 325 | + their_changes[theirs_idx].was_written = true; | |
| 273 | 326 | } else { |
| 274 | 327 | gix_trace::debug!("Couldn't figure out how to handle {match_kind:?} theirs: {theirs:#?} candidate: {candidate:#?}"); |
| 275 | − continue; | |
| 276 | 328 | } |
| 329 | + continue; | |
| 277 | 330 | }; |
| 278 | 331 | |
| 279 | 332 | let ours = &our_changes[ours_idx].inner; |
| 280 | − debug_assert!( | |
| 281 | − match_kind.is_none() | |
| 282 | − || (ours.location() == theirs.location() | |
| 283 | − || ours.source_location() == theirs.source_location()), | |
| 284 | − "BUG: right now it's not known to be possible to match changes from different paths: {match_kind:?} {candidate:?}" | |
| 285 | − ); | |
| 286 | 333 | match (ours, theirs) { |
| 287 | 334 | ( |
| 288 | 335 | Change::Modification { |
| @@ -326,7 +373,7 @@ where · 37 unmodified lines | ||
| 326 | 373 | Swapped |
| 327 | 374 | }; |
| 328 | 375 | if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { |
| 329 | − assert_eq!( | |
| 376 | + debug_assert_eq!( | |
| 330 | 377 | previous_id, their_source_id, |
| 331 | 378 | "both refer to the same base, so should always match" |
| 332 | 379 | ); |
| @@ -407,9 +454,23 @@ where · 74 unmodified lines | ||
| 407 | 454 | (new_change, None), |
| 408 | 455 | pick_our_changes_mut(side, their_changes, our_changes), |
| 409 | 456 | ); |
| 410 | − } else if allow_resolution_failure { | |
| 411 | − editor.upsert(toc(our_location), our_mode.kind(), *our_id)?; | |
| 412 | − editor.upsert(toc(their_location), their_mode.kind(), *their_id)?; | |
| 457 | + } else { | |
| 458 | + match tree_conflicts { | |
| 459 | + None => { | |
| 460 | + // keep both states - 'our_location' is the previous location as well. | |
| 461 | + editor.upsert(toc(our_location), our_mode.kind(), *our_id)?; | |
| 462 | + editor.upsert(toc(their_location), their_mode.kind(), *their_id)?; | |
| 463 | + } | |
| 464 | + Some(ResolveWith::Ours) => { | |
| 465 | + editor.remove(toc(source_location))?; | |
| 466 | + if side.to_global(outer_side).is_swapped() { | |
| 467 | + editor.upsert(toc(their_location), their_mode.kind(), *their_id)?; | |
| 468 | + } else { | |
| 469 | + editor.upsert(toc(our_location), our_mode.kind(), *our_id)?; | |
| 470 | + } | |
| 471 | + } | |
| 472 | + Some(ResolveWith::Ancestor) => {} | |
| 473 | + } | |
| 413 | 474 | |
| 414 | 475 | if should_fail_on_conflict(Conflict::without_resolution( |
| 415 | 476 | ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch, |
| @@ -516,7 +577,7 @@ where · 100 unmodified lines | ||
| 516 | 577 | &options, |
| 517 | 578 | )?; |
| 518 | 579 | editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; |
| 519 | − Some(Conflict::with_resolution( | |
| 580 | + Conflict::with_resolution( | |
| 520 | 581 | Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { |
| 521 | 582 | merged_blob: ContentMerge { |
| 522 | 583 | resolution, |
| @@ -525,8 +586,8 @@ where · 2 unmodified lines | ||
| 525 | 586 | }, |
| 526 | 587 | (ours, theirs, Original, outer_side), |
| 527 | 588 | [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], |
| 528 | − )) | |
| 529 | − } else if allow_resolution_failure { | |
| 589 | + ) | |
| 590 | + } else { | |
| 530 | 591 | // Actually this has a preference, as symlinks are always left in place with the other side renamed. |
| 531 | 592 | let ( |
| 532 | 593 | logical_side, |
| @@ -555,8 +616,7 @@ where · 22 unmodified lines | ||
| 555 | 616 | tree_with_rename, |
| 556 | 617 | label_of_side_to_be_moved, |
| 557 | 618 | )?; |
| 558 | − editor.upsert(toc(location), our_mode.kind(), our_id)?; | |
| 559 | − let conflict = Conflict::without_resolution( | |
| 619 | + let mut conflict = Conflict::without_resolution( | |
| 560 | 620 | ResolutionFailure::OursAddedTheirsAddedTypeMismatch { |
| 561 | 621 | their_unique_location: renamed_location.clone(), |
| 562 | 622 | }, |
| @@ -567,27 +627,45 @@ where · 4 unmodified lines | ||
| 567 | 627 | index_entry_at_path(&their_mode, &their_id, their_path_hint), |
| 568 | 628 | ], |
| 569 | 629 | ); |
| 570 | − | |
| 571 | − let new_change = Change::Addition { | |
| 572 | − location: renamed_location, | |
| 573 | − entry_mode: their_mode, | |
| 574 | − id: their_id, | |
| 575 | − relation: None, | |
| 576 | − }; | |
| 577 | − tree_with_rename.remove_existing_leaf(location.as_bstr()); | |
| 578 | − push_deferred( | |
| 579 | − (new_change, None), | |
| 580 | − pick_our_changes_mut(logical_side, their_changes, our_changes), | |
| 581 | − ); | |
| 582 | − Some(conflict) | |
| 583 | − } else { | |
| 584 | − None | |
| 630 | + match tree_conflicts { | |
| 631 | + None => { | |
| 632 | + let new_change = Change::Addition { | |
| 633 | + location: renamed_location, | |
| 634 | + entry_mode: their_mode, | |
| 635 | + id: their_id, | |
| 636 | + relation: None, | |
| 637 | + }; | |
| 638 | + editor.upsert(toc(location), our_mode.kind(), our_id)?; | |
| 639 | + tree_with_rename.remove_existing_leaf(location.as_bstr()); | |
| 640 | + push_deferred( | |
| 641 | + (new_change, None), | |
| 642 | + pick_our_changes_mut(logical_side, their_changes, our_changes), | |
| 643 | + ); | |
| 644 | + } | |
| 645 | + Some(resolve) => { | |
| 646 | + conflict.entries = Default::default(); | |
| 647 | + match resolve { | |
| 648 | + ResolveWith::Ours => match outer_side { | |
| 649 | + Original => { | |
| 650 | + editor.upsert(toc(location), our_mode.kind(), our_id)?; | |
| 651 | + } | |
| 652 | + Swapped => { | |
| 653 | + editor.upsert(toc(location), their_mode.kind(), their_id)?; | |
| 654 | + } | |
| 655 | + }, | |
| 656 | + ResolveWith::Ancestor => { | |
| 657 | + // Do nothing - this discards both sides. | |
| 658 | + // Note that one of these adds might be the result of a rename, which | |
| 659 | + // means we effectively loose the original and can't get it back as that information is degenerated. | |
| 660 | + } | |
| 661 | + } | |
| 662 | + } | |
| 663 | + } | |
| 664 | + conflict | |
| 585 | 665 | }; |
| 586 | 666 | |
| 587 | − if let Some(conflict) = conflict { | |
| 588 | − if should_fail_on_conflict(conflict) { | |
| 589 | − break 'outer; | |
| 590 | − }; | |
| 667 | + if should_fail_on_conflict(conflict) { | |
| 668 | + break 'outer; | |
| 591 | 669 | } |
| 592 | 670 | } |
| 593 | 671 | ( |
| @@ -609,7 +687,7 @@ where · 15 unmodified lines | ||
| 609 | 687 | previous_entry_mode, |
| 610 | 688 | previous_id, |
| 611 | 689 | }, |
| 612 | − ) if allow_resolution_failure => { | |
| 690 | + ) => { | |
| 613 | 691 | let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) { |
| 614 | 692 | (labels.current.unwrap_or_default(), Original) |
| 615 | 693 | } else { |
| @@ -622,62 +700,135 @@ where · 6 unmodified lines | ||
| 622 | 700 | }; |
| 623 | 701 | change_on_right |
| 624 | 702 | .map(|change| { |
| 625 | − change.inner.entry_mode().is_tree() && change.inner.location() == location | |
| 703 | + change.inner.entry_mode().is_tree() | |
| 704 | + && change.inner.location() == location | |
| 705 | + && matches!(change.inner, Change::Addition { .. }) | |
| 626 | 706 | }) |
| 627 | 707 | .unwrap_or_default() |
| 628 | 708 | }; |
| 629 | 709 | |
| 630 | − if deletion_prefaces_addition_of_directory { | |
| 631 | − let our_tree = pick_our_tree(side, our_tree, their_tree); | |
| 632 | − let renamed_path = unique_path_in_tree( | |
| 633 | − location.as_bstr(), | |
| 634 | − &editor, | |
| 635 | − our_tree, | |
| 636 | − label_of_side_to_be_moved, | |
| 637 | − )?; | |
| 638 | − editor.remove(toc(location))?; | |
| 639 | − our_tree.remove_existing_leaf(location.as_bstr()); | |
| 710 | + let should_break = if deletion_prefaces_addition_of_directory { | |
| 711 | + let entries = [ | |
| 712 | + index_entry(previous_entry_mode, previous_id), | |
| 713 | + index_entry(entry_mode, id), | |
| 714 | + None, | |
| 715 | + ]; | |
| 716 | + match tree_conflicts { | |
| 717 | + None => { | |
| 718 | + let our_tree = pick_our_tree(side, our_tree, their_tree); | |
| 719 | + let renamed_path = unique_path_in_tree( | |
| 720 | + location.as_bstr(), | |
| 721 | + &editor, | |
| 722 | + our_tree, | |
| 723 | + label_of_side_to_be_moved, | |
| 724 | + )?; | |
| 725 | + editor.remove(toc(location))?; | |
| 726 | + our_tree.remove_existing_leaf(location.as_bstr()); | |
| 640 | 727 | |
| 641 | − let new_change = Change::Addition { | |
| 642 | − location: renamed_path.clone(), | |
| 643 | − relation: None, | |
| 644 | − entry_mode: *entry_mode, | |
| 645 | − id: *id, | |
| 646 | − }; | |
| 647 | − let should_break = should_fail_on_conflict(Conflict::without_resolution( | |
| 648 | − ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { | |
| 649 | − renamed_unique_path_to_modified_blob: renamed_path, | |
| 650 | − }, | |
| 651 | − (ours, theirs, side, outer_side), | |
| 652 | − [ | |
| 653 | − index_entry(previous_entry_mode, previous_id), | |
| 654 | − index_entry(entry_mode, id), | |
| 655 | − None, | |
| 656 | − ], | |
| 657 | − )); | |
| 728 | + let new_change = Change::Addition { | |
| 729 | + location: renamed_path.clone(), | |
| 730 | + relation: None, | |
| 731 | + entry_mode: *entry_mode, | |
| 732 | + id: *id, | |
| 733 | + }; | |
| 734 | + let should_break = should_fail_on_conflict(Conflict::without_resolution( | |
| 735 | + ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { | |
| 736 | + renamed_unique_path_to_modified_blob: renamed_path, | |
| 737 | + }, | |
| 738 | + (ours, theirs, side, outer_side), | |
| 739 | + entries, | |
| 740 | + )); | |
| 658 | 741 | |
| 659 | − // Since we move *our* side, our tree needs to be modified. | |
| 660 | − push_deferred( | |
| 661 | − (new_change, None), | |
| 662 | − pick_our_changes_mut(side, our_changes, their_changes), | |
| 663 | − ); | |
| 742 | + // Since we move *our* side, our tree needs to be modified. | |
| 743 | + push_deferred( | |
| 744 | + (new_change, None), | |
| 745 | + pick_our_changes_mut(side, our_changes, their_changes), | |
| 746 | + ); | |
| 747 | + should_break | |
| 748 | + } | |
| 749 | + Some(ResolveWith::Ours) => { | |
| 750 | + match side.to_global(outer_side) { | |
| 751 | + Original => { | |
| 752 | + // ours is modification | |
| 753 | + editor.upsert(toc(location), entry_mode.kind(), *id)?; | |
| 754 | + } | |
| 755 | + Swapped => { | |
| 756 | + // ours is deletion | |
| 757 | + editor.remove(toc(location))?; | |
| 758 | + } | |
| 759 | + } | |
| 760 | + should_fail_on_conflict(Conflict::without_resolution( | |
| 761 | + ResolutionFailure::OursModifiedTheirsDeleted, | |
| 762 | + (ours, theirs, side, outer_side), | |
| 763 | + entries, | |
| 764 | + )) | |
| 765 | + } | |
| 766 | + Some(ResolveWith::Ancestor) => { | |
| 767 | + should_fail_on_conflict(Conflict::without_resolution( | |
| 768 | + ResolutionFailure::OursModifiedTheirsDeleted, | |
| 769 | + (ours, theirs, side, outer_side), | |
| 770 | + entries, | |
| 771 | + )) | |
| 772 | + } | |
| 773 | + } | |
| 774 | + } else { | |
| 775 | + let entries = [ | |
| 776 | + index_entry(previous_entry_mode, previous_id), | |
| 777 | + index_entry(entry_mode, id), | |
| 778 | + None, | |
| 779 | + ]; | |
| 780 | + match tree_conflicts { | |
| 781 | + None => { | |
| 782 | + editor.upsert(toc(location), entry_mode.kind(), *id)?; | |
| 783 | + } | |
| 784 | + Some(ResolveWith::Ours) => { | |
| 785 | + let ours = match outer_side { | |
| 786 | + Original => ours, | |
| 787 | + Swapped => theirs, | |
| 788 | + }; | |
| 664 | 789 | |
| 665 | − if should_break { | |
| 666 | − break 'outer; | |
| 790 | + match ours { | |
| 791 | + Change::Modification { .. } => { | |
| 792 | + editor.upsert(toc(location), entry_mode.kind(), *id)?; | |
| 793 | + } | |
| 794 | + Change::Deletion { .. } => { | |
| 795 | + editor.remove(toc(location))?; | |
| 796 | + } | |
| 797 | + _ => unreachable!("parent-match assures this"), | |
| 798 | + }; | |
| 799 | + } | |
| 800 | + Some(ResolveWith::Ancestor) => {} | |
| 667 | 801 | }; |
| 668 | − } else { | |
| 669 | − let should_break = should_fail_on_conflict(Conflict::without_resolution( | |
| 802 | + should_fail_on_conflict(Conflict::without_resolution( | |
| 670 | 803 | ResolutionFailure::OursModifiedTheirsDeleted, |
| 671 | 804 | (ours, theirs, side, outer_side), |
| 672 | − [ | |
| 673 | − index_entry(previous_entry_mode, previous_id), | |
| 674 | − index_entry(entry_mode, id), | |
| 675 | − None, | |
| 676 | − ], | |
| 677 | − )); | |
| 678 | − editor.upsert(toc(location), entry_mode.kind(), *id)?; | |
| 679 | − if should_break { | |
| 680 | − break 'outer; | |
| 805 | + entries, | |
| 806 | + )) | |
| 807 | + }; | |
| 808 | + if should_break { | |
| 809 | + break 'outer; | |
| 810 | + }; | |
| 811 | + } | |
| 812 | + ( | |
| 813 | + Change::Modification { .. }, | |
| 814 | + Change::Addition { | |
| 815 | + location, | |
| 816 | + entry_mode, | |
| 817 | + id, | |
| 818 | + .. | |
| 819 | + }, | |
| 820 | + ) if ours.location() != theirs.location() => { | |
| 821 | + match tree_conflicts { | |
| 822 | + None => { | |
| 823 | + unreachable!("modification/deletion pair should prevent modification/addition from happening") | |
| 824 | + } | |
| 825 | + Some(ResolveWith::Ancestor) => {} | |
| 826 | + Some(ResolveWith::Ours) => { | |
| 827 | + if outer_side.is_swapped() { | |
| 828 | + editor.upsert(toc(location), entry_mode.kind(), *id)?; | |
| 829 | + } | |
| 830 | + // we have already taken care of the 'root' of this - | |
| 831 | + // everything that follows can safely be ignored | |
| 681 | 832 | } |
| 682 | 833 | } |
| 683 | 834 | } |
| @@ -720,112 +871,89 @@ where · 36 unmodified lines | ||
| 720 | 871 | let merged_mode = |
| 721 | 872 | merge_modes(*our_mode, *their_mode).expect("this case was assured earlier"); |
| 722 | 873 | |
| 723 | − editor.remove(toc(source_location))?; | |
| 724 | − our_tree.remove_existing_leaf(source_location.as_bstr()); | |
| 725 | − their_tree.remove_existing_leaf(source_location.as_bstr()); | |
| 874 | + if matches!(tree_conflicts, None | Some(ResolveWith::Ours)) { | |
| 875 | + editor.remove(toc(source_location))?; | |
| 876 | + our_tree.remove_existing_leaf(source_location.as_bstr()); | |
| 877 | + their_tree.remove_existing_leaf(source_location.as_bstr()); | |
| 878 | + } | |
| 726 | 879 | |
| 727 | − let their_rewritten_location = | |
| 728 | − possibly_rewritten_location(our_tree, their_location.as_bstr(), our_changes); | |
| 729 | − let our_rewritten_location = | |
| 730 | − possibly_rewritten_location(their_tree, our_location.as_bstr(), their_changes); | |
| 731 | − let (our_addition, their_addition) = | |
| 732 | − match (our_rewritten_location, their_rewritten_location) { | |
| 733 | − (None, Some(location)) => ( | |
| 734 | − None, | |
| 735 | − Some(Change::Addition { | |
| 736 | − location, | |
| 880 | + let their_location = | |
| 881 | + possibly_rewritten_location(our_tree, their_location.as_bstr(), our_changes) | |
| 882 | + .map_or(Cow::Borrowed(their_location.as_bstr()), Cow::Owned); | |
| 883 | + let our_location = | |
| 884 | + possibly_rewritten_location(their_tree, our_location.as_bstr(), their_changes) | |
| 885 | + .map_or(Cow::Borrowed(our_location.as_bstr()), Cow::Owned); | |
| 886 | + let (our_addition, their_addition) = if our_location == their_location { | |
| 887 | + ( | |
| 888 | + None, | |
| 889 | + Some(Change::Addition { | |
| 890 | + location: our_location.into_owned(), | |
| 891 | + relation: None, | |
| 892 | + entry_mode: merged_mode, | |
| 893 | + id: merged_blob_id, | |
| 894 | + }), | |
| 895 | + ) | |
| 896 | + } else { | |
| 897 | + if should_fail_on_conflict(Conflict::without_resolution( | |
| 898 | + ResolutionFailure::OursRenamedTheirsRenamedDifferently { | |
| 899 | + merged_blob: resolution.take().map(|resolution| ContentMerge { | |
| 900 | + resolution, | |
| 901 | + merged_blob_id, | |
| 902 | + }), | |
| 903 | + }, | |
| 904 | + (ours, theirs, Original, outer_side), | |
| 905 | + [ | |
| 906 | + index_entry_at_path( | |
| 907 | + source_entry_mode, | |
| 908 | + source_id, | |
| 909 | + ConflictIndexEntryPathHint::Source, | |
| 910 | + ), | |
| 911 | + index_entry_at_path( | |
| 912 | + our_mode, | |
| 913 | + &merged_blob_id, | |
| 914 | + ConflictIndexEntryPathHint::Current, | |
| 915 | + ), | |
| 916 | + index_entry_at_path( | |
| 917 | + their_mode, | |
| 918 | + &merged_blob_id, | |
| 919 | + ConflictIndexEntryPathHint::RenamedOrTheirs, | |
| 920 | + ), | |
| 921 | + ], | |
| 922 | + )) { | |
| 923 | + break 'outer; | |
| 924 | + }; | |
| 925 | + match tree_conflicts { | |
| 926 | + None => { | |
| 927 | + let our_addition = Change::Addition { | |
| 928 | + location: our_location.into_owned(), | |
| 737 | 929 | relation: None, |
| 738 | 930 | entry_mode: merged_mode, |
| 739 | 931 | id: merged_blob_id, |
| 740 | − }), | |
| 741 | − ), | |
| 742 | − (Some(location), None) => ( | |
| 743 | − None, | |
| 744 | − Some(Change::Addition { | |
| 745 | − location, | |
| 932 | + }; | |
| 933 | + let their_addition = Change::Addition { | |
| 934 | + location: their_location.into_owned(), | |
| 746 | 935 | relation: None, |
| 747 | 936 | entry_mode: merged_mode, |
| 748 | 937 | id: merged_blob_id, |
| 749 | − }), | |
| 750 | − ), | |
| 751 | − (Some(_ours), Some(_theirs)) => { | |
| 752 | − gix_trace::debug!( | |
| 753 | − "Found two rewritten locations, '{_ours}' and '{_theirs}'" | |
| 754 | − ); | |
| 755 | − // Pretend this is the end of the loop and keep this as conflict. | |
| 756 | − // If this happens in the wild, we'd want to reproduce it. | |
| 757 | − if allow_resolution_failure | |
| 758 | − && should_fail_on_conflict(Conflict::unknown(( | |
| 759 | − ours, theirs, Original, outer_side, | |
| 760 | − ))) | |
| 761 | − { | |
| 762 | − break 'outer; | |
| 763 | 938 | }; |
| 764 | − their_changes[theirs_idx].was_written = true; | |
| 765 | − our_changes[ours_idx].was_written = true; | |
| 766 | − continue; | |
| 939 | + (Some(our_addition), Some(their_addition)) | |
| 767 | 940 | } |
| 768 | − (None, None) => { | |
| 769 | − if our_location == their_location { | |
| 770 | − ( | |
| 771 | − None, | |
| 772 | − Some(Change::Addition { | |
| 773 | − location: our_location.to_owned(), | |
| 774 | − relation: None, | |
| 775 | − entry_mode: merged_mode, | |
| 776 | − id: merged_blob_id, | |
| 777 | − }), | |
| 778 | − ) | |
| 779 | − } else { | |
| 780 | − if !allow_resolution_failure { | |
| 781 | − their_changes[theirs_idx].was_written = true; | |
| 782 | − our_changes[ours_idx].was_written = true; | |
| 783 | − continue; | |
| 941 | + Some(ResolveWith::Ancestor) => (None, None), | |
| 942 | + Some(ResolveWith::Ours) => { | |
| 943 | + let our_addition = Change::Addition { | |
| 944 | + location: match outer_side { | |
| 945 | + Original => our_location, | |
| 946 | + Swapped => their_location, | |
| 784 | 947 | } |
| 785 | − if should_fail_on_conflict(Conflict::without_resolution( | |
| 786 | − ResolutionFailure::OursRenamedTheirsRenamedDifferently { | |
| 787 | − merged_blob: resolution.take().map(|resolution| ContentMerge { | |
| 788 | − resolution, | |
| 789 | − merged_blob_id, | |
| 790 | − }), | |
| 791 | − }, | |
| 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 | − ], | |
| 810 | − )) { | |
| 811 | − break 'outer; | |
| 812 | − }; | |
| 813 | − let our_addition = Change::Addition { | |
| 814 | − location: our_location.to_owned(), | |
| 815 | − relation: None, | |
| 816 | − entry_mode: merged_mode, | |
| 817 | − id: merged_blob_id, | |
| 818 | − }; | |
| 819 | − let their_addition = Change::Addition { | |
| 820 | − location: their_location.to_owned(), | |
| 821 | − relation: None, | |
| 822 | − entry_mode: merged_mode, | |
| 823 | − id: merged_blob_id, | |
| 824 | − }; | |
| 825 | − (Some(our_addition), Some(their_addition)) | |
| 826 | − } | |
| 948 | + .into_owned(), | |
| 949 | + relation: None, | |
| 950 | + entry_mode: merged_mode, | |
| 951 | + id: merged_blob_id, | |
| 952 | + }; | |
| 953 | + (Some(our_addition), None) | |
| 827 | 954 | } |
| 828 | − }; | |
| 955 | + } | |
| 956 | + }; | |
| 829 | 957 | |
| 830 | 958 | if let Some(resolution) = resolution { |
| 831 | 959 | if should_fail_on_conflict(Conflict::with_resolution( |
| @@ -883,16 +1011,21 @@ where · 51 unmodified lines | ||
| 883 | 1011 | .. |
| 884 | 1012 | }, |
| 885 | 1013 | Change::Deletion { .. }, |
| 886 | − ) if !rewritten_mode.is_commit() && allow_resolution_failure => { | |
| 1014 | + ) if !rewritten_mode.is_commit() => { | |
| 887 | 1015 | let side = if matches!(ours, Change::Deletion { .. }) { |
| 888 | 1016 | Original |
| 889 | 1017 | } else { |
| 890 | 1018 | Swapped |
| 891 | 1019 | }; |
| 892 | 1020 | |
| 893 | − editor.remove(toc(source_location))?; | |
| 894 | − pick_our_tree(side, our_tree, their_tree) | |
| 895 | − .remove_existing_leaf(source_location.as_bstr()); | |
| 1021 | + match tree_conflicts { | |
| 1022 | + None | Some(ResolveWith::Ours) => { | |
| 1023 | + editor.remove(toc(source_location))?; | |
| 1024 | + pick_our_tree(side, our_tree, their_tree) | |
| 1025 | + .remove_existing_leaf(source_location.as_bstr()); | |
| 1026 | + } | |
| 1027 | + Some(ResolveWith::Ancestor) => {} | |
| 1028 | + } | |
| 896 | 1029 | |
| 897 | 1030 | let their_rewritten_location = possibly_rewritten_location( |
| 898 | 1031 | pick_our_tree(side, our_tree, their_tree), |
| @@ -923,10 +1056,15 @@ where · 24 unmodified lines | ||
| 923 | 1056 | break 'outer; |
| 924 | 1057 | }; |
| 925 | 1058 | |
| 926 | − push_deferred( | |
| 927 | − (our_addition, None), | |
| 928 | − pick_our_changes_mut(side, their_changes, our_changes), | |
| 929 | − ); | |
| 1059 | + let ours_is_rewrite = side.is_swapped(); | |
| 1060 | + if tree_conflicts.is_none() | |
| 1061 | + || (matches!(tree_conflicts, Some(ResolveWith::Ours)) && ours_is_rewrite) | |
| 1062 | + { | |
| 1063 | + push_deferred( | |
| 1064 | + (our_addition, None), | |
| 1065 | + pick_our_changes_mut(side, their_changes, our_changes), | |
| 1066 | + ); | |
| 1067 | + } | |
| 930 | 1068 | } |
| 931 | 1069 | ( |
| 932 | 1070 | Change::Rewrite { |
| @@ -941,6 +1079,7 @@ where · 8 unmodified lines | ||
| 941 | 1079 | Change::Addition { |
| 942 | 1080 | id: their_id, |
| 943 | 1081 | entry_mode: their_mode, |
| 1082 | + location: add_location, | |
| 944 | 1083 | .. |
| 945 | 1084 | }, |
| 946 | 1085 | ) |
| @@ -948,6 +1087,7 @@ where · 1 unmodified line | ||
| 948 | 1087 | Change::Addition { |
| 949 | 1088 | id: their_id, |
| 950 | 1089 | entry_mode: their_mode, |
| 1090 | + location: add_location, | |
| 951 | 1091 | .. |
| 952 | 1092 | }, |
| 953 | 1093 | Change::Rewrite { |
| @@ -1005,9 +1145,17 @@ where · 51 unmodified lines | ||
| 1005 | 1145 | // Because this constellation can only be found by the lookup tree, there is |
| 1006 | 1146 | // no need to put it as addition, we know it's not going to intersect on the other side. |
| 1007 | 1147 | editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; |
| 1008 | − } else if allow_resolution_failure { | |
| 1009 | − editor.remove(toc(source_location))?; | |
| 1010 | − pick_our_tree(side, our_tree, their_tree).remove_leaf(source_location.as_bstr()); | |
| 1148 | + } else { | |
| 1149 | + // We always remove the source from the tree - it might be re-added later. | |
| 1150 | + let ours_is_rename = | |
| 1151 | + tree_conflicts == Some(ResolveWith::Ours) && side == outer_side; | |
| 1152 | + let remove_rename_source = | |
| 1153 | + tree_conflicts.is_none() || ours_is_rename || add_location != source_location; | |
| 1154 | + if remove_rename_source { | |
| 1155 | + editor.remove(toc(source_location))?; | |
| 1156 | + pick_our_tree(side, our_tree, their_tree) | |
| 1157 | + .remove_leaf(source_location.as_bstr()); | |
| 1158 | + } | |
| 1011 | 1159 | |
| 1012 | 1160 | let ( |
| 1013 | 1161 | logical_side, |
| @@ -1036,7 +1184,13 @@ where · 22 unmodified lines | ||
| 1036 | 1184 | tree_with_rename, |
| 1037 | 1185 | label_of_side_to_be_moved, |
| 1038 | 1186 | )?; |
| 1039 | − editor.upsert(toc(location), our_mode.kind(), our_id)?; | |
| 1187 | + | |
| 1188 | + let upsert_rename_destination = tree_conflicts.is_none() || ours_is_rename; | |
| 1189 | + if upsert_rename_destination { | |
| 1190 | + editor.upsert(toc(location), our_mode.kind(), our_id)?; | |
| 1191 | + tree_with_rename.remove_existing_leaf(location.as_bstr()); | |
| 1192 | + } | |
| 1193 | + | |
| 1040 | 1194 | let conflict = Conflict::without_resolution( |
| 1041 | 1195 | ResolutionFailure::OursAddedTheirsAddedTypeMismatch { |
| 1042 | 1196 | their_unique_location: renamed_location.clone(), |
| @@ -1049,20 +1203,21 @@ where · 6 unmodified lines | ||
| 1049 | 1203 | ], |
| 1050 | 1204 | ); |
| 1051 | 1205 | |
| 1052 | − let new_change_with_rename = Change::Addition { | |
| 1053 | − location: renamed_location, | |
| 1054 | − entry_mode: their_mode, | |
| 1055 | − id: their_id, | |
| 1056 | − relation: None, | |
| 1057 | − }; | |
| 1058 | − tree_with_rename.remove_existing_leaf(location.as_bstr()); | |
| 1059 | − push_deferred( | |
| 1060 | − ( | |
| 1061 | − new_change_with_rename, | |
| 1062 | − Some(pick_idx(logical_side, theirs_idx, ours_idx)), | |
| 1063 | − ), | |
| 1064 | − pick_our_changes_mut(logical_side, their_changes, our_changes), | |
| 1065 | − ); | |
| 1206 | + if tree_conflicts.is_none() { | |
| 1207 | + let new_change_with_rename = Change::Addition { | |
| 1208 | + location: renamed_location, | |
| 1209 | + entry_mode: their_mode, | |
| 1210 | + id: their_id, | |
| 1211 | + relation: None, | |
| 1212 | + }; | |
| 1213 | + push_deferred( | |
| 1214 | + ( | |
| 1215 | + new_change_with_rename, | |
| 1216 | + Some(pick_idx(logical_side, theirs_idx, ours_idx)), | |
| 1217 | + ), | |
| 1218 | + pick_our_changes_mut(logical_side, their_changes, our_changes), | |
| 1219 | + ); | |
| 1220 | + } | |
| 1066 | 1221 | |
| 1067 | 1222 | if should_fail_on_conflict(conflict) { |
| 1068 | 1223 | break 'outer; |
| @@ -1070,9 +1225,16 @@ where · 1 unmodified line | ||
| 1070 | 1225 | } |
| 1071 | 1226 | } |
| 1072 | 1227 | _unknown => { |
| 1073 | − if allow_resolution_failure | |
| 1074 | − && should_fail_on_conflict(Conflict::unknown((ours, theirs, Original, outer_side))) | |
| 1075 | − { | |
| 1228 | + debug_assert!( | |
| 1229 | + match_kind.is_none() | |
| 1230 | + || (ours.location() == theirs.location() | |
| 1231 | + || ours.source_location() == theirs.source_location()), | |
| 1232 | + "BUG: right now it's not known to be possible to match changes from different paths: {match_kind:?} {candidate:?}" | |
| 1233 | + ); | |
| 1234 | + if let Some(ResolveWith::Ours) = tree_conflicts { | |
| 1235 | + apply_our_resolution(ours, theirs, outer_side, &mut editor)?; | |
| 1236 | + } | |
| 1237 | + if should_fail_on_conflict(Conflict::unknown((ours, theirs, Original, outer_side))) { | |
| 1076 | 1238 | break 'outer; |
| 1077 | 1239 | }; |
| 1078 | 1240 | } |
| @@ -1098,6 +1260,19 @@ where · 19 unmodified lines | ||
| 1098 | 1260 | }) |
| 1099 | 1261 | } |
| 1100 | 1262 | |
| 1263 | +fn apply_our_resolution( | |
| 1264 | + local_ours: &Change, | |
| 1265 | + local_theirs: &Change, | |
| 1266 | + outer_side: ConflictMapping, | |
| 1267 | + editor: &mut gix_object::tree::Editor<'_>, | |
| 1268 | +) -> Result<(), Error> { | |
| 1269 | + let ours = match outer_side { | |
| 1270 | + Original => local_ours, | |
| 1271 | + Swapped => local_theirs, | |
| 1272 | + }; | |
| 1273 | + Ok(apply_change(editor, ours, None)?) | |
| 1274 | +} | |
| 1275 | + | |
| 1101 | 1276 | fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool { |
| 1102 | 1277 | a.is_commit() || b.is_commit() |
| 1103 | 1278 | } |
| @@ -39,40 +39,77 @@ pub struct Outcome<'a> { · 38 unmodified lines | ||
| 39 | 39 | /// Use [`has_unresolved_conflicts()`](Outcome::has_unresolved_conflicts()) to see if any action is needed |
| 40 | 40 | /// before using [`tree`](Outcome::tree). |
| 41 | 41 | pub conflicts: Vec<Conflict>, |
| 42 | − /// `true` if `conflicts` contains only a single *unresolved* conflict in the last slot, but possibly more resolved ones. | |
| 42 | + /// `true` if `conflicts` contains only a single [*unresolved* conflict](ResolutionFailure) in the last slot, but | |
| 43 | + /// possibly more [resolved ones](Resolution) before that. | |
| 43 | 44 | /// This also makes this outcome a very partial merge that cannot be completed. |
| 44 | 45 | /// Only set if [`fail_on_conflict`](Options::fail_on_conflict) is `true`. |
| 45 | 46 | pub failed_on_first_unresolved_conflict: bool, |
| 46 | 47 | } |
| 47 | 48 | |
| 48 | 49 | /// Determine what should be considered an unresolved conflict. |
| 50 | +#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] | |
| 51 | +pub struct TreatAsUnresolved { | |
| 52 | + /// Determine which content merges should be considered unresolved. | |
| 53 | + pub content_merge: treat_as_unresolved::ContentMerge, | |
| 54 | + /// Determine which tree merges should be considered unresolved. | |
| 55 | + pub tree_merge: treat_as_unresolved::TreeMerge, | |
| 56 | +} | |
| 57 | + | |
| 49 | 58 | /// |
| 50 | −/// Note that no matter which variant, [conflicts](Conflict) with | |
| 51 | −/// [resolution failure](`ResolutionFailure`) will always be unresolved. | |
| 52 | −/// | |
| 53 | −/// Also, when one side was modified but the other side renamed it, this will not | |
| 54 | −/// be considered a conflict, even if a non-conflicting merge happened. | |
| 55 | −#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] | |
| 56 | −pub enum TreatAsUnresolved { | |
| 57 | − /// Only consider content merges with conflict markers as unresolved. | |
| 58 | − /// | |
| 59 | − /// Auto-resolved tree conflicts will *not* be considered unresolved. | |
| 60 | − ConflictMarkers, | |
| 61 | − /// Consider content merges with conflict markers as unresolved, and content | |
| 62 | − /// merges where conflicts where auto-resolved in any way, like choosing | |
| 63 | − /// *ours*, *theirs* or by their *union*. | |
| 64 | − /// | |
| 65 | − /// Auto-resolved tree conflicts will *not* be considered unresolved. | |
| 66 | − ConflictMarkersAndAutoResolved, | |
| 67 | − /// Whenever there were conflicting renames, or conflict markers, it is unresolved. | |
| 68 | − /// Note that auto-resolved content merges will *not* be considered unresolved. | |
| 69 | − /// | |
| 70 | − /// Also note that files that were changed in one and renamed in another will | |
| 71 | − /// be moved into place, which will be considered resolved. | |
| 72 | − Renames, | |
| 73 | − /// Similar to [`Self::Renames`], but auto-resolved content-merges will | |
| 74 | − /// also be considered unresolved. | |
| 75 | − RenamesAndAutoResolvedContent, | |
| 59 | +pub mod treat_as_unresolved { | |
| 60 | + use crate::tree::TreatAsUnresolved; | |
| 61 | + | |
| 62 | + /// Which kind of content merges should be considered unresolved? | |
| 63 | + #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] | |
| 64 | + pub enum ContentMerge { | |
| 65 | + /// Content merges that still show conflict markers. | |
| 66 | + #[default] | |
| 67 | + Markers, | |
| 68 | + /// Content merges who would have conflicted if it wasn't for a | |
| 69 | + /// [resolution strategy](crate::blob::builtin_driver::text::Conflict::ResolveWithOurs). | |
| 70 | + ForcedResolution, | |
| 71 | + } | |
| 72 | + | |
| 73 | + /// Which kind of tree merges should be considered unresolved? | |
| 74 | + #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] | |
| 75 | + pub enum TreeMerge { | |
| 76 | + /// All failed renames. | |
| 77 | + Undecidable, | |
| 78 | + /// All failed renames, and the ones where a tree item was renamed to avoid a clash. | |
| 79 | + #[default] | |
| 80 | + EvasiveRenames, | |
| 81 | + /// All of `EvasiveRenames`, and tree merges that would have conflicted but which were resolved | |
| 82 | + /// with a [resolution strategy](super::ResolveWith). | |
| 83 | + ForcedResolution, | |
| 84 | + } | |
| 85 | + | |
| 86 | + /// Instantiation/Presets | |
| 87 | + impl TreatAsUnresolved { | |
| 88 | + /// Return an instance with the highest sensitivity to what should be considered unresolved as it | |
| 89 | + /// includes entries which have been resolved using a [merge strategy](super::ResolveWith). | |
| 90 | + pub fn forced_resolution() -> Self { | |
| 91 | + Self { | |
| 92 | + content_merge: ContentMerge::ForcedResolution, | |
| 93 | + tree_merge: TreeMerge::ForcedResolution, | |
| 94 | + } | |
| 95 | + } | |
| 96 | + | |
| 97 | + /// Return an instance that considers unresolved any conflict that Git would also consider unresolved. | |
| 98 | + /// This is the same as the `default()` implementation. | |
| 99 | + pub fn git() -> Self { | |
| 100 | + Self::default() | |
| 101 | + } | |
| 102 | + | |
| 103 | + /// Only undecidable tree merges and conflict markers are considered unresolved. | |
| 104 | + /// This also means that renamed entries to make space for a conflicting one is considered acceptable, | |
| 105 | + /// making this preset the most lenient. | |
| 106 | + pub fn undecidable() -> Self { | |
| 107 | + Self { | |
| 108 | + content_merge: ContentMerge::Markers, | |
| 109 | + tree_merge: TreeMerge::Undecidable, | |
| 110 | + } | |
| 111 | + } | |
| 112 | + } | |
| 76 | 113 | } |
| 77 | 114 | |
| 78 | 115 | impl Outcome<'_> { |
| @@ -84,13 +121,18 @@ impl Outcome<'_> { · 5 unmodified lines | ||
| 84 | 121 | |
| 85 | 122 | /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a |
| 86 | 123 | /// conflict should be considered unresolved. |
| 124 | + /// `removal_mode` decides how unconflicted entries should be removed if they are superseded by | |
| 125 | + /// their conflicted counterparts. | |
| 87 | 126 | /// It's important that `index` is at the state of [`Self::tree`]. |
| 88 | 127 | /// |
| 89 | 128 | /// 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) | |
| 129 | + pub fn index_changed_after_applying_conflicts( | |
| 130 | + &self, | |
| 131 | + index: &mut gix_index::State, | |
| 132 | + how: TreatAsUnresolved, | |
| 133 | + removal_mode: apply_index_entries::RemovalMode, | |
| 134 | + ) -> bool { | |
| 135 | + apply_index_entries(&self.conflicts, how, index, removal_mode) | |
| 94 | 136 | } |
| 95 | 137 | } |
| 96 | 138 | |
| @@ -150,7 +192,7 @@ enum ConflictIndexEntryPathHint { · 53 unmodified lines | ||
| 150 | 192 | } |
| 151 | 193 | |
| 152 | 194 | /// A utility to help define which side is what in the [`Conflict`] type. |
| 153 | −#[derive(Debug, Clone, Copy)] | |
| 195 | +#[derive(Debug, Clone, Copy, Eq, PartialEq)] | |
| 154 | 196 | enum ConflictMapping { |
| 155 | 197 | /// The sides are as described in the field documentation, i.e. `ours` is `ours`. |
| 156 | 198 | Original, |
| @@ -168,41 +210,56 @@ impl ConflictMapping { · 11 unmodified lines | ||
| 168 | 210 | ConflictMapping::Swapped => ConflictMapping::Original, |
| 169 | 211 | } |
| 170 | 212 | } |
| 213 | + fn to_global(self, global: ConflictMapping) -> ConflictMapping { | |
| 214 | + match global { | |
| 215 | + ConflictMapping::Original => self, | |
| 216 | + ConflictMapping::Swapped => self.swapped(), | |
| 217 | + } | |
| 218 | + } | |
| 171 | 219 | } |
| 172 | 220 | |
| 173 | 221 | impl Conflict { |
| 174 | 222 | /// Return `true` if this instance is considered unresolved based on the criterion specified by `how`. |
| 175 | 223 | pub fn is_unresolved(&self, how: TreatAsUnresolved) -> bool { |
| 176 | 224 | use crate::blob; |
| 177 | − let content_merge_matches = |info: &ContentMerge| match how { | |
| 178 | − TreatAsUnresolved::ConflictMarkers | TreatAsUnresolved::Renames => { | |
| 179 | − matches!(info.resolution, blob::Resolution::Conflict) | |
| 180 | − } | |
| 181 | − TreatAsUnresolved::RenamesAndAutoResolvedContent | TreatAsUnresolved::ConflictMarkersAndAutoResolved => { | |
| 225 | + let content_merge_unresolved = |info: &ContentMerge| match how.content_merge { | |
| 226 | + treat_as_unresolved::ContentMerge::Markers => matches!(info.resolution, blob::Resolution::Conflict), | |
| 227 | + treat_as_unresolved::ContentMerge::ForcedResolution => { | |
| 182 | 228 | matches!( |
| 183 | 229 | info.resolution, |
| 184 | 230 | blob::Resolution::Conflict | blob::Resolution::CompleteWithAutoResolvedConflict |
| 185 | 231 | ) |
| 186 | 232 | } |
| 187 | 233 | }; |
| 188 | − match how { | |
| 189 | − TreatAsUnresolved::ConflictMarkers | TreatAsUnresolved::ConflictMarkersAndAutoResolved => { | |
| 190 | − self.resolution.is_err() || self.content_merge().map_or(false, |info| content_merge_matches(&info)) | |
| 234 | + match how.tree_merge { | |
| 235 | + treat_as_unresolved::TreeMerge::Undecidable => { | |
| 236 | + self.resolution.is_err() | |
| 237 | + || self | |
| 238 | + .content_merge() | |
| 239 | + .map_or(false, |info| content_merge_unresolved(&info)) | |
| 240 | + } | |
| 241 | + treat_as_unresolved::TreeMerge::EvasiveRenames | treat_as_unresolved::TreeMerge::ForcedResolution => { | |
| 242 | + match &self.resolution { | |
| 243 | + Ok(success) => match success { | |
| 244 | + Resolution::SourceLocationAffectedByRename { .. } => false, | |
| 245 | + Resolution::Forced(_) => { | |
| 246 | + how.tree_merge == treat_as_unresolved::TreeMerge::ForcedResolution | |
| 247 | + || self | |
| 248 | + .content_merge() | |
| 249 | + .map_or(false, |merged_blob| content_merge_unresolved(&merged_blob)) | |
| 250 | + } | |
| 251 | + Resolution::OursModifiedTheirsRenamedAndChangedThenRename { | |
| 252 | + merged_blob, | |
| 253 | + final_location, | |
| 254 | + .. | |
| 255 | + } => final_location.is_some() || merged_blob.as_ref().map_or(false, content_merge_unresolved), | |
| 256 | + Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => { | |
| 257 | + content_merge_unresolved(merged_blob) | |
| 258 | + } | |
| 259 | + }, | |
| 260 | + Err(_failure) => true, | |
| 261 | + } | |
| 191 | 262 | } |
| 192 | − TreatAsUnresolved::Renames | TreatAsUnresolved::RenamesAndAutoResolvedContent => match &self.resolution { | |
| 193 | − Ok(success) => match success { | |
| 194 | − Resolution::SourceLocationAffectedByRename { .. } => false, | |
| 195 | − Resolution::OursModifiedTheirsRenamedAndChangedThenRename { | |
| 196 | − merged_blob, | |
| 197 | − final_location, | |
| 198 | − .. | |
| 199 | − } => final_location.is_some() || merged_blob.as_ref().map_or(false, content_merge_matches), | |
| 200 | − Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => { | |
| 201 | − content_merge_matches(merged_blob) | |
| 202 | − } | |
| 203 | − }, | |
| 204 | − Err(_failure) => true, | |
| 205 | − }, | |
| 206 | 263 | } |
| 207 | 264 | } |
| 208 | 265 | |
| @@ -237,15 +294,11 @@ impl Conflict { · 28 unmodified lines | ||
| 237 | 294 | |
| 238 | 295 | /// Return information about the content merge if it was performed. |
| 239 | 296 | pub fn content_merge(&self) -> Option<ContentMerge> { |
| 240 | − match &self.resolution { | |
| 241 | − Ok(success) => match success { | |
| 242 | − Resolution::SourceLocationAffectedByRename { .. } => None, | |
| 243 | − Resolution::OursModifiedTheirsRenamedAndChangedThenRename { merged_blob, .. } => *merged_blob, | |
| 244 | − Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => Some(*merged_blob), | |
| 245 | − }, | |
| 246 | − Err(failure) => match failure { | |
| 297 | + fn failure_merged_blob(failure: &ResolutionFailure) -> Option<ContentMerge> { | |
| 298 | + match failure { | |
| 247 | 299 | ResolutionFailure::OursRenamedTheirsRenamedDifferently { merged_blob } => *merged_blob, |
| 248 | 300 | ResolutionFailure::Unknown |
| 301 | + | ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { .. } | |
| 249 | 302 | | ResolutionFailure::OursModifiedTheirsDeleted |
| 250 | 303 | | ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch |
| 251 | 304 | | ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { |
| @@ -253,7 +306,16 @@ impl Conflict { · 1 unmodified line | ||
| 253 | 306 | } |
| 254 | 307 | | ResolutionFailure::OursAddedTheirsAddedTypeMismatch { .. } |
| 255 | 308 | | ResolutionFailure::OursDeletedTheirsRenamed => None, |
| 309 | + } | |
| 310 | + } | |
| 311 | + match &self.resolution { | |
| 312 | + Ok(success) => match success { | |
| 313 | + Resolution::Forced(failure) => failure_merged_blob(failure), | |
| 314 | + Resolution::SourceLocationAffectedByRename { .. } => None, | |
| 315 | + Resolution::OursModifiedTheirsRenamedAndChangedThenRename { merged_blob, .. } => *merged_blob, | |
| 316 | + Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => Some(*merged_blob), | |
| 256 | 317 | }, |
| 318 | + Err(failure) => failure_merged_blob(failure), | |
| 257 | 319 | } |
| 258 | 320 | } |
| 259 | 321 | } |
| @@ -291,6 +353,9 @@ pub enum Resolution { · 31 unmodified lines | ||
| 291 | 353 | /// The outcome of the content merge. |
| 292 | 354 | merged_blob: ContentMerge, |
| 293 | 355 | }, |
| 356 | + /// This is a resolution failure was forcefully turned into a usable resolution, i.e. [making a choice](ResolveWith) | |
| 357 | + /// is turned into a valid resolution. | |
| 358 | + Forced(ResolutionFailure), | |
| 294 | 359 | } |
| 295 | 360 | |
| 296 | 361 | /// Describes of a conflict involving *our* change and *their* failed to be resolved. |
| @@ -306,6 +371,17 @@ pub enum ResolutionFailure { · 9 unmodified lines | ||
| 306 | 371 | /// The path at which `ours` can be found in the tree - it's in the same directory that it was in before. |
| 307 | 372 | renamed_unique_path_to_modified_blob: BString, |
| 308 | 373 | }, |
| 374 | + /// *ours* is a directory, but *theirs* is a non-directory (i.e. file), which wants to be in its place, even though | |
| 375 | + /// *ours* has a modification in that subtree. | |
| 376 | + /// Rename *theirs* to retain that modification. | |
| 377 | + /// | |
| 378 | + /// Important: there is no actual modification on *ours* side, so *ours* is filled in with *theirs* as the data structure | |
| 379 | + /// cannot represent this case. | |
| 380 | + // TODO: Can we have a better data-structure? This would be for a rewrite though. | |
| 381 | + OursDirectoryTheirsNonDirectoryTheirsRenamed { | |
| 382 | + /// The non-conflicting path of *their* non-tree entry. | |
| 383 | + renamed_unique_path_of_theirs: BString, | |
| 384 | + }, | |
| 309 | 385 | /// *ours* was added (or renamed into place) with a different mode than theirs, e.g. blob and symlink, and we kept |
| 310 | 386 | /// the symlink in its original location, renaming the other side to `their_unique_location`. |
| 311 | 387 | OursAddedTheirsAddedTypeMismatch { |
| @@ -340,6 +416,8 @@ pub struct ContentMerge { · 28 unmodified lines | ||
| 340 | 416 | #[derive(Default, Debug, Clone)] |
| 341 | 417 | pub struct Options { |
| 342 | 418 | /// If *not* `None`, rename tracking will be performed when determining the changes of each side of the merge. |
| 419 | + /// | |
| 420 | + /// Note that [empty blobs](Rewrites::track_empty) should not be tracked for best results. | |
| 343 | 421 | pub rewrites: Option<Rewrites>, |
| 344 | 422 | /// Decide how blob-merges should be done. This relates to if conflicts can be resolved or not. |
| 345 | 423 | pub blob_merge: crate::blob::platform::merge::Options, |
| @@ -358,16 +436,51 @@ pub struct Options { · 12 unmodified lines | ||
| 358 | 436 | /// If `None`, when symlinks clash *ours* will be chosen and a conflict will occur. |
| 359 | 437 | /// Otherwise, the same logic applies as for the merge of binary resources. |
| 360 | 438 | pub symlink_conflicts: Option<crate::blob::builtin_driver::binary::ResolveWith>, |
| 361 | − /// If `true`, instead of issuing a conflict with [`ResolutionFailure`], do nothing and keep the base/ancestor | |
| 362 | − /// version. This is useful when one wants to avoid any kind of merge-conflict to have *some*, *lossy* resolution. | |
| 363 | − pub allow_lossy_resolution: bool, | |
| 439 | + /// If `None`, tree irreconcilable tree conflicts will result in [resolution failures](ResolutionFailure). | |
| 440 | + /// Otherwise, one can choose a side. Note that it's still possible to determine that auto-resolution happened | |
| 441 | + /// despite this choice, which allows carry forward the conflicting information, possibly for later resolution. | |
| 442 | + /// If `Some(…)`, irreconcilable conflicts are reconciled by making a choice. | |
| 443 | + /// Note that [`Conflict::entries()`] will still be set, to not degenerate information, even though they then represent | |
| 444 | + /// the entries what would fit the index if no forced resolution was performed. | |
| 445 | + /// It's up to the caller to handle that information mindfully. | |
| 446 | + pub tree_conflicts: Option<ResolveWith>, | |
| 447 | +} | |
| 448 | + | |
| 449 | +/// Decide how to resolve tree-related conflicts, but only those that have [no way of being correct](ResolutionFailure). | |
| 450 | +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] | |
| 451 | +pub enum ResolveWith { | |
| 452 | + /// On irreconcilable conflict, choose neither *our* nor *their* state, but keep the common *ancestor* state instead. | |
| 453 | + Ancestor, | |
| 454 | + /// On irreconcilable conflict, choose *our* side. | |
| 455 | + /// | |
| 456 | + /// Note that in order to get something equivalent to *theirs*, put *theirs* into the side of *ours*, | |
| 457 | + /// swapping the sides essentially. | |
| 458 | + Ours, | |
| 364 | 459 | } |
| 365 | 460 | |
| 366 | 461 | pub(super) mod function; |
| 367 | 462 | mod utils; |
| 463 | +/// | |
| 368 | 464 | pub mod apply_index_entries { |
| 369 | 465 | |
| 466 | + /// Determines how we deal with the removal of unconflicted entries if these are superseded by their conflicted counterparts, | |
| 467 | + /// i.e. stage 1, 2 and 3. | |
| 468 | + #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] | |
| 469 | + pub enum RemovalMode { | |
| 470 | + /// Add the [`gix_index::entry::Flags::REMOVE`] flag to entries that are to be removed. | |
| 471 | + /// | |
| 472 | + /// **Note** that this also means that unconflicted and conflicted stages will be visible in the same index. | |
| 473 | + /// When written, entries marked for removal will automatically be ignored. However, this also means that | |
| 474 | + /// one must not use the in-memory index or take specific care of entries that are marked for removal. | |
| 475 | + Mark, | |
| 476 | + /// Entries marked for removal (even those that were already marked) will be removed from memory at the end. | |
| 477 | + /// | |
| 478 | + /// This is an expensive step that leaves a consistent index, ready for use. | |
| 479 | + Prune, | |
| 480 | + } | |
| 481 | + | |
| 370 | 482 | pub(super) mod function { |
| 483 | + use crate::tree::apply_index_entries::RemovalMode; | |
| 371 | 484 | use crate::tree::{Conflict, ConflictIndexEntryPathHint, Resolution, ResolutionFailure, TreatAsUnresolved}; |
| 372 | 485 | use bstr::{BStr, ByteSlice}; |
| 373 | 486 | use std::collections::{hash_map, HashMap}; |
| @@ -376,8 +489,9 @@ pub mod apply_index_entries { · 2 unmodified lines | ||
| 376 | 489 | /// conflict should be considered unresolved. |
| 377 | 490 | /// Once a stage of a path conflicts, the unconflicting stage is removed even though it might be the one |
| 378 | 491 | /// 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. | |
| 492 | + /// This removal is only done by flagging it with [gix_index::entry::Flags::REMOVE], which means | |
| 493 | + /// these entries won't be written back to disk but will still be present in the index if `removal_mode` | |
| 494 | + /// is [`RemovalMode::Mark`]. For proper removal, choose [`RemovalMode::Prune`]. | |
| 381 | 495 | /// It's important that `index` matches the tree that was produced as part of the merge that also |
| 382 | 496 | /// brought about `conflicts`, or else this function will fail if it cannot find the path matching |
| 383 | 497 | /// the conflicting entries. |
| @@ -388,6 +502,7 @@ pub mod apply_index_entries { · 4 unmodified lines | ||
| 388 | 502 | conflicts: &[Conflict], |
| 389 | 503 | how: TreatAsUnresolved, |
| 390 | 504 | index: &mut gix_index::State, |
| 505 | + removal_mode: RemovalMode, | |
| 391 | 506 | ) -> bool { |
| 392 | 507 | if index.is_sparse() { |
| 393 | 508 | gix_trace::error!("Refusing to apply index entries to sparse index - it's not tested yet"); |
| @@ -398,6 +513,7 @@ pub mod apply_index_entries { · 4 unmodified lines | ||
| 398 | 513 | for conflict in conflicts.iter().filter(|c| c.is_unresolved(how)) { |
| 399 | 514 | let (renamed_path, current_path): (Option<&BStr>, &BStr) = match &conflict.resolution { |
| 400 | 515 | Ok(success) => match success { |
| 516 | + Resolution::Forced(_) => continue, | |
| 401 | 517 | Resolution::SourceLocationAffectedByRename { final_location } => { |
| 402 | 518 | (Some(final_location.as_bstr()), final_location.as_bstr()) |
| 403 | 519 | } |
| @@ -410,6 +526,9 @@ pub mod apply_index_entries { · 6 unmodified lines | ||
| 410 | 526 | } |
| 411 | 527 | }, |
| 412 | 528 | Err(failure) => match failure { |
| 529 | + ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { | |
| 530 | + renamed_unique_path_of_theirs, | |
| 531 | + } => (Some(renamed_unique_path_of_theirs.as_bstr()), conflict.ours.location()), | |
| 413 | 532 | ResolutionFailure::OursRenamedTheirsRenamedDifferently { .. } => { |
| 414 | 533 | (Some(conflict.theirs.location()), conflict.ours.location()) |
| 415 | 534 | } |
| @@ -492,8 +611,15 @@ pub mod apply_index_entries { · 76 unmodified lines | ||
| 492 | 611 | } |
| 493 | 612 | } |
| 494 | 613 | |
| 614 | + let res = index.entries().len() != len; | |
| 615 | + match removal_mode { | |
| 616 | + RemovalMode::Mark => {} | |
| 617 | + RemovalMode::Prune => { | |
| 618 | + index.remove_entries(|_, _, e| e.flags.contains(gix_index::entry::Flags::REMOVE)); | |
| 619 | + } | |
| 620 | + } | |
| 495 | 621 | index.sort_entries(); |
| 496 | − index.entries().len() != len | |
| 622 | + res | |
| 497 | 623 | } |
| 498 | 624 | } |
| 499 | 625 | } |
| @@ -23,7 +23,7 @@ use std::collections::HashMap; · 22 unmodified lines | ||
| 23 | 23 | /// over a directory rewrite in *our* tree. If so, rewrite it so that we get the path |
| 24 | 24 | /// it would have had if it had been renamed along with *our* directory. |
| 25 | 25 | pub fn possibly_rewritten_location( |
| 26 | − check_tree: &mut TreeNodes, | |
| 26 | + check_tree: &TreeNodes, | |
| 27 | 27 | their_location: &BStr, |
| 28 | 28 | our_changes: &ChangeListRef, |
| 29 | 29 | ) -> Option<BString> { |
| @@ -60,7 +60,7 @@ pub fn rewrite_location_with_renamed_directory(their_location: &BStr, passed_cha · 30 unmodified lines | ||
| 60 | 60 | pub fn unique_path_in_tree( |
| 61 | 61 | file_path: &BStr, |
| 62 | 62 | editor: &tree::Editor<'_>, |
| 63 | − tree: &mut TreeNodes, | |
| 63 | + tree: &TreeNodes, | |
| 64 | 64 | side_name: &BStr, |
| 65 | 65 | ) -> Result<BString, Error> { |
| 66 | 66 | let mut buf = file_path.to_owned(); |
| @@ -400,11 +400,11 @@ impl TreeNodes { · 333 unmodified lines | ||
| 400 | 400 | Some(index) => { |
| 401 | 401 | cursor = &mut self.0[index]; |
| 402 | 402 | if is_last && !cursor.is_leaf_node() { |
| 403 | − assert_eq!( | |
| 404 | − cursor.change_idx, None, | |
| 405 | − "BUG: each node should only see a single change when tracking initially: {path} {change_idx}" | |
| 406 | − ); | |
| 407 | − cursor.change_idx = Some(change_idx); | |
| 403 | + // NOTE: we might encounter the same path multiple times in rare conditions. | |
| 404 | + // At least we avoid overwriting existing intermediate changes, for good measure. | |
| 405 | + if cursor.change_idx.is_none() { | |
| 406 | + cursor.change_idx = Some(change_idx); | |
| 407 | + } | |
| 408 | 408 | } |
| 409 | 409 | } |
| 410 | 410 | } |
| @@ -414,12 +414,12 @@ impl TreeNodes { · 3 unmodified lines | ||
| 414 | 414 | |
| 415 | 415 | /// Search the tree with `our` changes for `theirs` by [`source_location()`](Change::source_location())). |
| 416 | 416 | /// If there is an entry but both are the same, or if there is no entry, return `None`. |
| 417 | − pub fn check_conflict(&mut self, theirs_location: &BStr) -> Option<PossibleConflict> { | |
| 417 | + pub fn check_conflict(&self, theirs_location: &BStr) -> Option<PossibleConflict> { | |
| 418 | 418 | if self.0.len() == 1 { |
| 419 | 419 | return None; |
| 420 | 420 | } |
| 421 | 421 | let components = to_components(theirs_location); |
| 422 | − let mut cursor = &mut self.0[0]; | |
| 422 | + let mut cursor = &self.0[0]; | |
| 423 | 423 | let mut cursor_idx = 0; |
| 424 | 424 | let mut intermediate_change = None; |
| 425 | 425 | for component in components { |
| @@ -436,7 +436,7 @@ impl TreeNodes { · 10 unmodified lines | ||
| 436 | 436 | } else { |
| 437 | 437 | // a change somewhere else, i.e. `a/c` and we know `a/b` only. |
| 438 | 438 | intermediate_change.and_then(|(change, cursor_idx)| { |
| 439 | − let cursor = &mut self.0[cursor_idx]; | |
| 439 | + let cursor = &self.0[cursor_idx]; | |
| 440 | 440 | // If this is a destination location of a rename, then the `their_location` |
| 441 | 441 | // is already at the right spot, and we can just ignore it. |
| 442 | 442 | if matches!(cursor.location, ChangeLocation::CurrentLocation) { |
| @@ -450,7 +450,7 @@ impl TreeNodes { · 7 unmodified lines | ||
| 450 | 450 | } |
| 451 | 451 | Some(child_idx) => { |
| 452 | 452 | cursor_idx = child_idx; |
| 453 | − cursor = &mut self.0[cursor_idx]; | |
| 453 | + cursor = &self.0[cursor_idx]; | |
| 454 | 454 | } |
| 455 | 455 | } |
| 456 | 456 | } |
| @@ -496,7 +496,7 @@ impl TreeNodes { · 39 unmodified lines | ||
| 496 | 496 | let mut cursor = &mut self.0[0]; |
| 497 | 497 | while let Some(component) = components.next() { |
| 498 | 498 | match cursor.children.get(component).copied() { |
| 499 | − None => assert!(!must_exist, "didn't find '{location}' for removal"), | |
| 499 | + None => debug_assert!(!must_exist, "didn't find '{location}' for removal"), | |
| 500 | 500 | Some(existing_idx) => { |
| 501 | 501 | let is_last = components.peek().is_none(); |
| 502 | 502 | if is_last { |
| @@ -504,7 +504,7 @@ impl TreeNodes { · 1 unmodified line | ||
| 504 | 504 | cursor = &mut self.0[existing_idx]; |
| 505 | 505 | debug_assert!( |
| 506 | 506 | cursor.is_leaf_node(), |
| 507 | − "BUG: we should really only try to remove leaf nodes" | |
| 507 | + "BUG: we should really only try to remove leaf nodes: {cursor:?}" | |
| 508 | 508 | ); |
| 509 | 509 | cursor.change_idx = None; |
| 510 | 510 | } else { |
| @@ -578,10 +578,7 @@ impl Conflict { · 67 unmodified lines | ||
| 578 | 578 | ours: ours.clone(), |
| 579 | 579 | theirs: theirs.clone(), |
| 580 | 580 | entries, |
| 581 | − map: match outer_map { | |
| 582 | − ConflictMapping::Original => map, | |
| 583 | − ConflictMapping::Swapped => map.swapped(), | |
| 584 | − }, | |
| 581 | + map: map.to_global(outer_map), | |
| 585 | 582 | } |
| 586 | 583 | } |
| 587 | 584 | |
Binary file not shown.
| @@ -36,6 +36,15 @@ function make_conflict_index() { · 35 unmodified lines | ||
| 36 | 36 | cp .git/index .git/"${identifier}".index |
| 37 | 37 | } |
| 38 | 38 | |
| 39 | +function make_resolve_tree() { | |
| 40 | + local resolve=${1:?Their 'ancestor' or 'ours'} | |
| 41 | + local our_side=${2:-} | |
| 42 | + local their_side=${3:-} | |
| 43 | + | |
| 44 | + local filename="resolve-${our_side}-${their_side}-with-${resolve}" | |
| 45 | + git write-tree > ".git/${filename}.tree" | |
| 46 | +} | |
| 47 | + | |
| 39 | 48 | function baseline () ( |
| 40 | 49 | local dir=${1:?the directory to enter} |
| 41 | 50 | local output_name=${2:?the basename of the output of the merge} |
| @@ -58,8 +67,13 @@ function baseline () ( · 16 unmodified lines | ||
| 58 | 67 | our_commit_id="$(git rev-parse "$our_committish")" |
| 59 | 68 | their_commit_id="$(git rev-parse "$their_committish")" |
| 60 | 69 | local maybe_expected_tree="$(git rev-parse expected^{tree})" |
| 70 | + local maybe_expected_reversed_tree="$(git rev-parse expected-reversed^{tree})" | |
| 71 | + if [ "$maybe_expected_reversed_tree" == "expected-reversed^{tree}" ]; then | |
| 72 | + maybe_expected_reversed_tree="$(git rev-parse expected^{tree} || :)" | |
| 73 | + fi | |
| 61 | 74 | if [ -z "$opt_deviation_message" ]; then |
| 62 | 75 | maybe_expected_tree="expected^{tree}" |
| 76 | + maybe_expected_reversed_tree="expected^{tree}" | |
| 63 | 77 | fi |
| 64 | 78 | |
| 65 | 79 | local merge_info="${output_name}.merge-info" |
| @@ -69,10 +83,108 @@ function baseline () ( · 3 unmodified lines | ||
| 69 | 83 | if [[ "$one_side" != "no-reverse" ]]; then |
| 70 | 84 | local merge_info="${output_name}-reversed.merge-info" |
| 71 | 85 | git -c merge.conflictStyle=$conflict_style merge-tree -z --write-tree --allow-unrelated-histories "$their_committish" "$our_committish" > "$merge_info" || : |
| 72 | − echo "$dir" "$conflict_style" "$their_commit_id" "$their_committish" "$our_commit_id" "$our_committish" "$merge_info" "$maybe_expected_tree" "$opt_deviation_message" >> ../baseline.cases | |
| 86 | + echo "$dir" "$conflict_style" "$their_commit_id" "$their_committish" "$our_commit_id" "$our_committish" "$merge_info" "$maybe_expected_reversed_tree" "$opt_deviation_message" >> ../baseline.cases | |
| 73 | 87 | fi |
| 74 | 88 | ) |
| 75 | 89 | |
| 90 | + | |
| 91 | +git init non-tree-to-tree | |
| 92 | +(cd non-tree-to-tree | |
| 93 | + write_lines original 1 2 3 4 5 >a | |
| 94 | + git add a && git commit -m "init" | |
| 95 | + | |
| 96 | + git branch A | |
| 97 | + git branch B | |
| 98 | + | |
| 99 | + git checkout A | |
| 100 | + write_lines 1 2 3 4 5 6 >a | |
| 101 | + git commit -am "'A' changes 'a'" | |
| 102 | + | |
| 103 | + git checkout B | |
| 104 | + rm a | |
| 105 | + mkdir -p a/sub | |
| 106 | + touch a/sub/b a/sub/c a/d a/e | |
| 107 | + git add a && git commit -m "mv 'a' to 'a/sub/b', populate 'a/' with empty files" | |
| 108 | +) | |
| 109 | + | |
| 110 | +git init tree-to-non-tree | |
| 111 | +(cd tree-to-non-tree | |
| 112 | + mkdir -p a/sub | |
| 113 | + write_lines original 1 2 3 4 5 >a/sub/b | |
| 114 | + touch a/sub/c a/d a/e | |
| 115 | + git add a && git commit -m "init" | |
| 116 | + | |
| 117 | + git branch A | |
| 118 | + git branch B | |
| 119 | + | |
| 120 | + git checkout A | |
| 121 | + write_lines 1 2 3 4 5 6 >a/sub/b | |
| 122 | + git commit -am "'A' changes 'a/sub/b'" | |
| 123 | + | |
| 124 | + git checkout B | |
| 125 | + rm -Rf a | |
| 126 | + echo "new file" > a | |
| 127 | + git add a && git commit -m "rm -Rf a/ && add non-empty 'a'" | |
| 128 | +) | |
| 129 | + | |
| 130 | +git init non-tree-to-tree-with-rename | |
| 131 | +(cd non-tree-to-tree-with-rename | |
| 132 | + write_lines original 1 2 3 4 5 >a | |
| 133 | + git add a && git commit -m "init" | |
| 134 | + | |
| 135 | + git branch A | |
| 136 | + git branch B | |
| 137 | + | |
| 138 | + git checkout A | |
| 139 | + write_lines 1 2 3 4 5 6 >a | |
| 140 | + git commit -am "'A' changes 'a'" | |
| 141 | + | |
| 142 | + git checkout B | |
| 143 | + mv a tmp | |
| 144 | + mkdir -p a/sub | |
| 145 | + mv tmp a/sub/b | |
| 146 | + touch a/sub/c a/d a/e | |
| 147 | + git add a && git commit -m "mv 'a' to 'a/sub/b', populate 'a/' with empty files" | |
| 148 | +) | |
| 149 | + | |
| 150 | +git init tree-to-non-tree-with-rename | |
| 151 | +(cd tree-to-non-tree-with-rename | |
| 152 | + mkdir -p a/sub | |
| 153 | + write_lines original 1 2 3 4 5 >a/sub/b | |
| 154 | + touch a/sub/c a/d a/e | |
| 155 | + git add a && git commit -m "init" | |
| 156 | + | |
| 157 | + git branch A | |
| 158 | + git branch B | |
| 159 | + | |
| 160 | + git checkout A | |
| 161 | + write_lines 1 2 3 4 5 6 >a/sub/b | |
| 162 | + git commit -am "'A' changes 'a/sub/b'" | |
| 163 | + | |
| 164 | + git checkout B | |
| 165 | + rm -Rf a | |
| 166 | + touch a | |
| 167 | + git add a && git commit -m "rm -Rf a/ && add empty 'a' (which is like a rename from an empty deleted file)" | |
| 168 | + # And because it's so thrown off, it gets a completely different result if reversed. | |
| 169 | + git branch expected-reversed | |
| 170 | + | |
| 171 | + rm .git/index | |
| 172 | + git update-index --index-info <<EOF | |
| 173 | +100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/b | |
| 174 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/sub/b | |
| 175 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a~B | |
| 176 | +EOF | |
| 177 | + make_conflict_index tree-to-non-tree-with-rename-A-B | |
| 178 | + | |
| 179 | + rm .git/index | |
| 180 | + git update-index --index-info <<EOF | |
| 181 | +100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/b | |
| 182 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/sub/b | |
| 183 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a~B | |
| 184 | +EOF | |
| 185 | + make_conflict_index tree-to-non-tree-with-rename-A-B-reversed | |
| 186 | +) | |
| 187 | + | |
| 76 | 188 | git init simple |
| 77 | 189 | (cd simple |
| 78 | 190 | rm -Rf .git/hooks |
| @@ -222,6 +334,26 @@ git init rename-add-symlink · 143 unmodified lines | ||
| 222 | 334 | git commit -m "rename foo to bar" |
| 223 | 335 | ) |
| 224 | 336 | |
| 337 | +git init rename-add-same-symlink | |
| 338 | +(cd rename-add-same-symlink | |
| 339 | + touch target | |
| 340 | + ln -s target link | |
| 341 | + git add . | |
| 342 | + git commit -m "original" | |
| 343 | + | |
| 344 | + git branch A | |
| 345 | + git branch B | |
| 346 | + | |
| 347 | + git checkout A | |
| 348 | + git mv link link-new | |
| 349 | + git commit -m "rename link to link-new" | |
| 350 | + | |
| 351 | + git checkout B | |
| 352 | + ln -s target link-new | |
| 353 | + git add link-new | |
| 354 | + git commit -m "create link-new" | |
| 355 | +) | |
| 356 | + | |
| 225 | 357 | git init rename-rename-plus-content |
| 226 | 358 | (cd rename-rename-plus-content |
| 227 | 359 | write_lines 1 2 3 4 5 >foo |
| @@ -401,12 +533,40 @@ git init rename-within-rename · 173 unmodified lines | ||
| 401 | 533 | git checkout expected |
| 402 | 534 | write_lines 1 2 3 4 5 6 >a/x.f |
| 403 | 535 | write_lines 1 2 3 4 5 6 >a/sub/y.f |
| 404 | − git mv a/sub a/sub-renamed | |
| 536 | + cp -Rv a/sub a/sub-renamed | |
| 537 | + git add . | |
| 405 | 538 | git mv a a-renamed |
| 406 | − git commit -am "tracked both renames, applied all modifications by merge" | |
| 539 | + git commit -am "we also have duplication just like Git, but we are consistent independently of the side, hence the expectation" | |
| 407 | 540 | |
| 408 | − # This means there are no conflicts actually. | |
| 541 | + # We have duplication just like Git, but our index is definitely more complex. This one seems more plausible. | |
| 542 | + # The problem is that renames can't be indicated correctly in the index. | |
| 543 | + rm .git/index | |
| 544 | + git update-index --index-info <<EOF | |
| 545 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-renamed/sub/y.f | |
| 546 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z | |
| 547 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/sub-renamed/y.f | |
| 548 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/sub-renamed/z | |
| 549 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w | |
| 550 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f | |
| 551 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/sub-renamed/y.f | |
| 552 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a/sub-renamed/z | |
| 553 | +100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f | |
| 554 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z | |
| 555 | +EOF | |
| 409 | 556 | make_conflict_index rename-within-rename-A-B-deviates |
| 557 | + rm .git/index | |
| 558 | + git update-index --index-info <<EOF | |
| 559 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-renamed/sub/y.f | |
| 560 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z | |
| 561 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/sub-renamed/y.f | |
| 562 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/sub-renamed/z | |
| 563 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w | |
| 564 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f | |
| 565 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/sub-renamed/y.f | |
| 566 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a/sub-renamed/z | |
| 567 | +100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f | |
| 568 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z | |
| 569 | +EOF | |
| 410 | 570 | make_conflict_index rename-within-rename-A-B-deviates-reversed |
| 411 | 571 | ) |
| 412 | 572 | |
| @@ -586,10 +746,12 @@ git init conflicting-rename-complex · 173 unmodified lines | ||
| 586 | 746 | rm -Rf ./a |
| 587 | 747 | mkdir -p a-renamed/sub |
| 588 | 748 | write_lines 1 2 3 4 5 >a-renamed/sub/y.f |
| 749 | + write_lines 1 2 3 4 5 6 >a-renamed/x.f | |
| 589 | 750 | write_lines 1 2 3 4 5 6 >a-renamed/y.f |
| 590 | − touch a-renamed/z a-renamed/sub/z | |
| 751 | + touch a-renamed/z a-renamed/w a-renamed/sub/z | |
| 591 | 752 | git add . |
| 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" | |
| 753 | + git commit -m "Close to what Git has, but different due to rename tracking. This is why content ends up in a different place, which is the only difference." | |
| 754 | + | |
| 593 | 755 | |
| 594 | 756 | # 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 | 757 | # 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 |
| @@ -599,7 +761,11 @@ git init conflicting-rename-complex · 3 unmodified lines | ||
| 599 | 761 | 100644 8a1218a1024a212bb3db30becd860315f9f3ac52 2 a-renamed/sub/y.f |
| 600 | 762 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z |
| 601 | 763 | 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f |
| 764 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-renamed/x.f | |
| 765 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/w | |
| 602 | 766 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z |
| 767 | +100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/x.f | |
| 768 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/y.f | |
| 603 | 769 | EOF |
| 604 | 770 | make_conflict_index conflicting-rename-complex-A-B |
| 605 | 771 | |
| @@ -608,7 +774,11 @@ EOF · 2 unmodified lines | ||
| 608 | 774 | 100644 8a1218a1024a212bb3db30becd860315f9f3ac52 3 a-renamed/sub/y.f |
| 609 | 775 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z |
| 610 | 776 | 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f |
| 777 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-renamed/x.f | |
| 778 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/w | |
| 611 | 779 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z |
| 780 | +100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/x.f | |
| 781 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/y.f | |
| 612 | 782 | EOF |
| 613 | 783 | make_conflict_index conflicting-rename-complex-A-B-reversed |
| 614 | 784 | ) |
| @@ -628,7 +798,7 @@ git init same-rename-different-mode · 13 unmodified lines | ||
| 628 | 798 | chmod +x a/x.f a/w |
| 629 | 799 | git update-index --chmod=+x a/x.f a/w |
| 630 | 800 | git mv a a-renamed |
| 631 | − git commit -am "changed all content, add +x, renamed a -> a-renamed" | |
| 801 | + git commit -am "changed a/xf, add +x everywhere, renamed a -> a-renamed" | |
| 632 | 802 | |
| 633 | 803 | git checkout B |
| 634 | 804 | write_lines original 1 2 3 4 5 6 >a/x.f |
| @@ -993,7 +1163,11 @@ git init type-change-to-symlink · 358 unmodified lines | ||
| 993 | 1163 | |
| 994 | 1164 | |
| 995 | 1165 | |
| 996 | −# TODO: Git does not detect the conflict (one turns exe off, the other turns it on), and we do exactly the same. | |
| 1166 | +baseline non-tree-to-tree A-B A B | |
| 1167 | +baseline tree-to-non-tree A-B A B | |
| 1168 | +baseline tree-to-non-tree-with-rename A-B A B | |
| 1169 | +baseline non-tree-to-tree-with-rename A-B A B | |
| 1170 | +baseline rename-add-same-symlink A-B A B | |
| 997 | 1171 | baseline rename-add-exe-bit-conflict A-B A B |
| 998 | 1172 | baseline remove-executable-mode A-B A B |
| 999 | 1173 | baseline simple side-1-3-without-conflict side1 side3 |
| @@ -1024,11 +1198,11 @@ baseline super-1 A-B-diff3 A B · 24 unmodified lines | ||
| 1024 | 1198 | baseline super-2 A-B A B |
| 1025 | 1199 | baseline super-2 A-B-diff3 A B |
| 1026 | 1200 | |
| 1027 | −baseline rename-within-rename A-B-deviates A B "Git doesn't detect the rename-nesting so there is duplication - we achieve the optimal result" | |
| 1201 | +baseline rename-within-rename A-B-deviates A B "Git doesn't detect the rename-nesting, and we do neith, and we do neither" | |
| 1028 | 1202 | baseline rename-within-rename-2 A-B-deviates A B "TBD: Right, something is different documentation was forgotten :/" |
| 1029 | 1203 | baseline conflicting-rename A-B A B |
| 1030 | 1204 | baseline conflicting-rename-2 A-B A B |
| 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" | |
| 1205 | +baseline conflicting-rename-complex A-B A B "Git has different rename tracking - overall result it's still close enough" | |
| 1032 | 1206 | |
| 1033 | 1207 | 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" |
| 1034 | 1208 | baseline renamed-symlink-with-conflict A-B A B |
| @@ -1054,3 +1228,622 @@ baseline rename-and-modification A-B A B · 19 unmodified lines | ||
| 1054 | 1228 | baseline symlink-modification A-B A B |
| 1055 | 1229 | baseline symlink-addition A-B A B |
| 1056 | 1230 | baseline type-change-to-symlink A-B A B |
| 1231 | + | |
| 1232 | +## | |
| 1233 | +## Only once the tree-merges were performed can we refer to their objects | |
| 1234 | +## when making tree-conflict resolution expectations. It's important | |
| 1235 | +## to get these right. | |
| 1236 | +## | |
| 1237 | +(cd simple | |
| 1238 | + rm .git/index | |
| 1239 | + # 'whatever' is tree-conflict, 'greeting' is content conflict with markers | |
| 1240 | + git update-index --index-info <<EOF | |
| 1241 | +100644 45b983be36b73c0788dc9cbcb76cbb80fc7bb057 0 greeting | |
| 1242 | +100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers | |
| 1243 | +100644 5716ca5987cbf97d6bb54920bea6adde242d87e6 0 whatever | |
| 1244 | +EOF | |
| 1245 | + make_resolve_tree ours side1 side2 | |
| 1246 | + | |
| 1247 | + rm .git/index | |
| 1248 | + git update-index --index-info <<EOF | |
| 1249 | +100644 092bfb9bdf74dd8cfd22e812151281ee9aa6f01a 0 greeting | |
| 1250 | +100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers | |
| 1251 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 whatever/empty | |
| 1252 | +EOF | |
| 1253 | + make_resolve_tree ours side2 side1 | |
| 1254 | + | |
| 1255 | + rm .git/index | |
| 1256 | + git update-index --index-info <<EOF | |
| 1257 | +100644 9dc97bdc2426e68423360e3e5299280b2cf6b8ff 0 greeting | |
| 1258 | +100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers | |
| 1259 | +100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 whatever | |
| 1260 | +EOF | |
| 1261 | + make_resolve_tree ancestor side1 side2 | |
| 1262 | + | |
| 1263 | + rm .git/index | |
| 1264 | + git update-index --index-info <<EOF | |
| 1265 | +100644 1a2664a9924754c698e323f756f9f87f3f2fb337 0 greeting | |
| 1266 | +100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers | |
| 1267 | +100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 whatever | |
| 1268 | +EOF | |
| 1269 | + make_resolve_tree ancestor side2 side1 | |
| 1270 | + | |
| 1271 | + rm .git/index | |
| 1272 | + git update-index --index-info <<EOF | |
| 1273 | +100644 a4ae6e4709228b5da6001cb9d1cfa7736851e2a6 0 greeting | |
| 1274 | +100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 whatever | |
| 1275 | +100644 542802a799ded74fa01c47ba2f8925e284a369e2 0 Αυτά μου φαίνονται κινέζικα | |
| 1276 | +EOF | |
| 1277 | + make_resolve_tree ancestor tweak1 side2 | |
| 1278 | + | |
| 1279 | + rm .git/index | |
| 1280 | + git update-index --index-info <<EOF | |
| 1281 | +100644 45b983be36b73c0788dc9cbcb76cbb80fc7bb057 0 greeting | |
| 1282 | +100644 5716ca5987cbf97d6bb54920bea6adde242d87e6 0 whatever | |
| 1283 | +100644 65bc6a1e238f4bf05b28fd05240636e2cfb657e0 0 Αυτά μου φαίνονται κινέζικα | |
| 1284 | +EOF | |
| 1285 | + make_resolve_tree ours tweak1 side2 | |
| 1286 | + | |
| 1287 | + rm .git/index | |
| 1288 | + git update-index --index-info <<EOF | |
| 1289 | +100644 blob ea28dcd7f627a2a7bbd09daa679c452180617c9f greeting | |
| 1290 | +100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 whatever | |
| 1291 | +100644 blob f801a62deed900f8a80ff35e3339474ad6352a93 Αυτά μου φαίνονται κινέζικα | |
| 1292 | +EOF | |
| 1293 | + make_resolve_tree ancestor side2 tweak1 | |
| 1294 | + | |
| 1295 | + rm .git/index | |
| 1296 | + git update-index --index-info <<EOF | |
| 1297 | +100644 blob 092bfb9bdf74dd8cfd22e812151281ee9aa6f01a greeting | |
| 1298 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 whatever/empty | |
| 1299 | +100644 blob 09c277aa66897c58157f57a374eacc63a407dcab Αυτά μου φαίνονται κινέζικα | |
| 1300 | +EOF | |
| 1301 | + make_resolve_tree ours side2 tweak1 | |
| 1302 | +) | |
| 1303 | + | |
| 1304 | +(cd rename-add-symlink | |
| 1305 | + rm .git/index | |
| 1306 | + # the symlink of 'bar' from A | |
| 1307 | + git update-index --index-info <<EOF | |
| 1308 | +120000 blob 19102815663d23f8b75a47e7a01965dcdc96468c bar | |
| 1309 | +EOF | |
| 1310 | + make_resolve_tree ours A B | |
| 1311 | + | |
| 1312 | + rm .git/index | |
| 1313 | + # the merged form of 'bar' from B, not replaced by symlink | |
| 1314 | + git update-index --index-info <<EOF | |
| 1315 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 bar | |
| 1316 | +EOF | |
| 1317 | + make_resolve_tree ours B A | |
| 1318 | + | |
| 1319 | + rm .git/index | |
| 1320 | + # foo is renamed to bar, type clash means neither A nor B can be added - empty tree | |
| 1321 | + # It is not able to 'get foo back', it can't track that currently. | |
| 1322 | + make_resolve_tree ancestor A B | |
| 1323 | + make_resolve_tree ancestor B A | |
| 1324 | +) | |
| 1325 | + | |
| 1326 | +(cd rename-rename-plus-content | |
| 1327 | + rm .git/index | |
| 1328 | + # both sides rename 'foo' into something else. | |
| 1329 | + git update-index --index-info <<EOF | |
| 1330 | +100644 blob 8a1218a1024a212bb3db30becd860315f9f3ac52 foo | |
| 1331 | +EOF | |
| 1332 | + make_resolve_tree ancestor A B | |
| 1333 | + make_resolve_tree ancestor B A | |
| 1334 | + | |
| 1335 | + rm .git/index | |
| 1336 | + # 'bar' is the name in 'A', and there is a merge with the content from 'B' | |
| 1337 | + # which we auto-resolve. | |
| 1338 | + git update-index --index-info <<EOF | |
| 1339 | +100644 blob d0549c3d3c96a464289f3b820b7d96aedc58924b bar | |
| 1340 | +EOF | |
| 1341 | + make_resolve_tree ours A B | |
| 1342 | + | |
| 1343 | + rm .git/index | |
| 1344 | + # 'baz' is the name in 'B', and there is a merge with the content from 'A' | |
| 1345 | + # which we auto-resolve. | |
| 1346 | + git update-index --index-info <<EOF | |
| 1347 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 baz | |
| 1348 | +EOF | |
| 1349 | + make_resolve_tree ours B A | |
| 1350 | +) | |
| 1351 | + | |
| 1352 | +(cd rename-add-delete | |
| 1353 | + rm .git/index | |
| 1354 | + # 'foo' is deleted, 'bar' is added in 'A', but renamed to 'bar' in 'B'. | |
| 1355 | + # Do nothing, *should* keep 'foo', and it's re-added later, which copies it. | |
| 1356 | + # But instead we keep foo the first time, but then it 'turns' and we process a remaining | |
| 1357 | + # addition that once again sees the rename from the other side, which is not a conflict | |
| 1358 | + # and thus removes 'foo' after all, and merges 'bar'. | |
| 1359 | + # It's not super-correct, but it's only an issue for virtual merge bases, which are kind of | |
| 1360 | + # hidden anyway. | |
| 1361 | + git update-index --index-info <<EOF | |
| 1362 | +100644 blob 0cde534c2fca6c92c07b3e7a696665e844b9b933 bar | |
| 1363 | +EOF | |
| 1364 | + make_resolve_tree ancestor A B | |
| 1365 | + # B A isn't tested, as it is not an Err() conflict. | |
| 1366 | + | |
| 1367 | + # This case ends up being exactly the same as the ancestor, but with the merge-conflict | |
| 1368 | + # auto-resolved to 'ours'. | |
| 1369 | + # This time, it's expected to not have 'foo', but 'ours' in the clashing pair is a deletion. The rename side | |
| 1370 | + # is dropped, but what's left is the rename/add pair once the algorithm turns around/flips. | |
| 1371 | + rm .git/index | |
| 1372 | + git update-index --index-info <<EOF | |
| 1373 | +100644 blob f286e5cdd97ac6895438ea4548638bb98ac9bd6b bar | |
| 1374 | +EOF | |
| 1375 | + make_resolve_tree ours A B | |
| 1376 | +) | |
| 1377 | + | |
| 1378 | +(cd rename-rename-delete-delete | |
| 1379 | + rm .git/index | |
| 1380 | + # 'A' deletes 'bar' and 'B' turns 'bar' into 'baz'. 'A' renames 'foo' into 'bar', and | |
| 1381 | + # 'B' deletes 'foo'. 'ancestor' resolves to avoid any edits, leaving the state from 'main'. | |
| 1382 | + git update-index --index-info <<EOF | |
| 1383 | +100644 blob 5716ca5987cbf97d6bb54920bea6adde242d87e6 bar | |
| 1384 | +100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 foo | |
| 1385 | +EOF | |
| 1386 | + make_resolve_tree ancestor A B | |
| 1387 | + # this works in reverse as well (this time). | |
| 1388 | + make_resolve_tree ancestor B A | |
| 1389 | + | |
| 1390 | + rm .git/index | |
| 1391 | + # As 'ours' is a deletion of 'foo', it goes through, but we also acknowledge 'theirs' | |
| 1392 | + # as it gives better results, so end up with `baz`. | |
| 1393 | + git update-index --index-info <<EOF | |
| 1394 | +100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 baz | |
| 1395 | +EOF | |
| 1396 | + make_resolve_tree ours A B | |
| 1397 | + | |
| 1398 | + rm .git/index | |
| 1399 | + # Here we end up in exactly the same spot as if we'd do a normal merge, | |
| 1400 | + # which ends `baz` in a conflict. However, with content-merges set to 'ours' | |
| 1401 | + # it ends up like it should, giving a good result. | |
| 1402 | + git update-index --index-info <<EOF | |
| 1403 | + 100644 blob 5716ca5987cbf97d6bb54920bea6adde242d87e6 baz | |
| 1404 | +EOF | |
| 1405 | + make_resolve_tree ours B A | |
| 1406 | +) | |
| 1407 | + | |
| 1408 | +(cd super-1 | |
| 1409 | + # Each of the ancestor files are renamed in a conflicting way, and here | |
| 1410 | + # with ancestor choice, nothing happens, making this equivalent to `main` | |
| 1411 | + git checkout main | |
| 1412 | + make_resolve_tree ancestor A B | |
| 1413 | + make_resolve_tree ancestor B A | |
| 1414 | + | |
| 1415 | + rm .git/index | |
| 1416 | + # We do indeed perform the renames like this, and the content is merges as well as possible, | |
| 1417 | + # (here) configured to content-merge with 'ours' as well where needed. | |
| 1418 | + git update-index --index-info <<EOF | |
| 1419 | +100644 blob 4b5599c7c2ed4390417d9699bec86144a386873d four | |
| 1420 | +100644 blob 64012489f118cb4011c8902b4a635f70dcb0c0ca six | |
| 1421 | +100644 blob e33f5e94470d3b5fa0220ff6a9cabb78a3f72fa3 two | |
| 1422 | +EOF | |
| 1423 | + make_resolve_tree ours A B | |
| 1424 | + | |
| 1425 | + rm .git/index | |
| 1426 | + # The same, but from the other side. | |
| 1427 | + git update-index --index-info <<EOF | |
| 1428 | + 100644 blob 64012489f118cb4011c8902b4a635f70dcb0c0ca four | |
| 1429 | + 100644 blob e33f5e94470d3b5fa0220ff6a9cabb78a3f72fa3 six | |
| 1430 | + 100644 blob 4178ea6795c4c3e07b4e17e6a04aa49584b07ecd two | |
| 1431 | +EOF | |
| 1432 | + make_resolve_tree ours B A | |
| 1433 | +) | |
| 1434 | + | |
| 1435 | +(cd super-2 | |
| 1436 | + rm .git/index | |
| 1437 | + # 'B' changes foo, and moves it into 'olddir/bar', but `A' deleted 'foo', and adds 'newdir/bar/file' | |
| 1438 | + # after renaming 'olddir' to 'newdir'. | |
| 1439 | + # As `B` only has a single change that gets dropped when it clashes with the deletion of 'foo', | |
| 1440 | + # all other changes of 'A' can just be applied without any conflict whatsoever. | |
| 1441 | + git update-index --index-info <<EOF | |
| 1442 | +100644 blob 8a1218a1024a212bb3db30becd860315f9f3ac52 foo | |
| 1443 | +100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 newdir/a | |
| 1444 | +100644 blob 61780798228d17af2d34fce4cfbdf35556832472 newdir/b | |
| 1445 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 newdir/bar/file | |
| 1446 | +100644 blob f2ad6c76f0115a6ba5b00456a849810e7ec0af20 newdir/c | |
| 1447 | +EOF | |
| 1448 | + make_resolve_tree ancestor A B | |
| 1449 | + make_resolve_tree ancestor B A | |
| 1450 | + | |
| 1451 | + rm .git/index | |
| 1452 | + # Similar to the ancestor version, but now we choose 'ours', so the rename of 'foo' gets | |
| 1453 | + # dropped and it just gets deleted. Everything else is then 'A'. | |
| 1454 | + git update-index --index-info <<EOF | |
| 1455 | +100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 newdir/a | |
| 1456 | +100644 blob 61780798228d17af2d34fce4cfbdf35556832472 newdir/b | |
| 1457 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 newdir/bar/file | |
| 1458 | +100644 blob f2ad6c76f0115a6ba5b00456a849810e7ec0af20 newdir/c | |
| 1459 | +EOF | |
| 1460 | + make_resolve_tree ours A B | |
| 1461 | + | |
| 1462 | + rm .git/index | |
| 1463 | + # 'B' changes 'foo' and moves it to 'olddir/bar', which gets tracked to be | |
| 1464 | + # 'newdir/bar' and is taken verbatim. The clash that it finds it | |
| 1465 | + # resolves in 'B's favor, leaving only 'newdir/bar'. | |
| 1466 | + git update-index --index-info <<EOF | |
| 1467 | +100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 newdir/a | |
| 1468 | +100644 blob 61780798228d17af2d34fce4cfbdf35556832472 newdir/b | |
| 1469 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 newdir/bar | |
| 1470 | +100644 blob f2ad6c76f0115a6ba5b00456a849810e7ec0af20 newdir/c | |
| 1471 | +EOF | |
| 1472 | + make_resolve_tree ours B A | |
| 1473 | +) | |
| 1474 | + | |
| 1475 | +(cd conflicting-rename | |
| 1476 | + rm .git/index | |
| 1477 | + # 'A' renames 'a' to 'a-renamed', 'B' renames 'a' to 'a-different'. | |
| 1478 | + # All these conflicts are dropped in favor of keeping the 'ancestor' *location*. | |
| 1479 | + git update-index --index-info <<EOF | |
| 1480 | +100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/y.f | |
| 1481 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/z | |
| 1482 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w | |
| 1483 | +100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/x.f | |
| 1484 | +EOF | |
| 1485 | + make_resolve_tree ancestor A B | |
| 1486 | + make_resolve_tree ancestor B A | |
| 1487 | + | |
| 1488 | + rm .git/index | |
| 1489 | + # Much like the ancestor version, except that it applied the 'A' rename, | |
| 1490 | + # along with its *merged* content. | |
| 1491 | + git update-index --index-info <<EOF | |
| 1492 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-renamed/sub/y.f | |
| 1493 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/sub/z | |
| 1494 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/w | |
| 1495 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-renamed/x.f | |
| 1496 | +EOF | |
| 1497 | + make_resolve_tree ours A B | |
| 1498 | + rm .git/index | |
| 1499 | + # Just like 'A' above, but with the 'B' rename chosen and all the merges. | |
| 1500 | + git update-index --index-info <<EOF | |
| 1501 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-different/sub/y.f | |
| 1502 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-different/sub/z | |
| 1503 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-different/w | |
| 1504 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-different/x.f | |
| 1505 | +EOF | |
| 1506 | + make_resolve_tree ours B A | |
| 1507 | +) | |
| 1508 | + | |
| 1509 | +(cd conflicting-rename-2 | |
| 1510 | + rm .git/index | |
| 1511 | + # Like 'conflicting-rename', but this one only renames a single sub-directory for very much the same effect. | |
| 1512 | + # Thus, keeping the 'ancestor' version is the same as 'main', except for merged content. | |
| 1513 | + git update-index --index-info <<EOF | |
| 1514 | +100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/y.f | |
| 1515 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/z | |
| 1516 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w | |
| 1517 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f | |
| 1518 | +EOF | |
| 1519 | + make_resolve_tree ancestor A B | |
| 1520 | + make_resolve_tree ancestor B A | |
| 1521 | + | |
| 1522 | + rm .git/index | |
| 1523 | + git update-index --index-info <<EOF | |
| 1524 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub-renamed/y.f | |
| 1525 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub-renamed/z | |
| 1526 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w | |
| 1527 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f | |
| 1528 | +EOF | |
| 1529 | + make_resolve_tree ours A B | |
| 1530 | + rm .git/index | |
| 1531 | + git update-index --index-info <<EOF | |
| 1532 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub-different/y.f | |
| 1533 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub-different/z | |
| 1534 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w | |
| 1535 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f | |
| 1536 | +EOF | |
| 1537 | + make_resolve_tree ours B A | |
| 1538 | +) | |
| 1539 | + | |
| 1540 | +(cd conflicting-rename-complex | |
| 1541 | + rm .git/index | |
| 1542 | + # 'A" renames 'a' to 'a-renamed', but 'B' moves 'a/sub/' up one level, and replaces everything in its wake | |
| 1543 | + # so its two files are the only ones left. | |
| 1544 | + # As result, we actually have one unconflicting change which ends up creating the new directory 'a-renamed', | |
| 1545 | + # but everything else is conflicting so it keeps the 'ancestor' version. | |
| 1546 | + git update-index --index-info <<EOF | |
| 1547 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/z | |
| 1548 | +100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/y.f | |
| 1549 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/z | |
| 1550 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w | |
| 1551 | +100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/x.f | |
| 1552 | +EOF | |
| 1553 | + make_resolve_tree ancestor A B | |
| 1554 | + make_resolve_tree ancestor B A | |
| 1555 | + | |
| 1556 | + rm .git/index | |
| 1557 | + # This is some of the rename tracking from `B` making it into the non-clashing portions of 'A". | |
| 1558 | + # Due to different rename tracking, the non-forced version is also a bit of a mess, and that carries on here. | |
| 1559 | + git update-index --index-info <<EOF | |
| 1560 | + 100644 blob 8a1218a1024a212bb3db30becd860315f9f3ac52 a-renamed/sub/y.f | |
| 1561 | + 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/sub/z | |
| 1562 | + 100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-renamed/x.f | |
| 1563 | + 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/z | |
| 1564 | + 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/w | |
| 1565 | +EOF | |
| 1566 | + make_resolve_tree ours A B | |
| 1567 | + | |
| 1568 | + rm .git/index | |
| 1569 | + # It applies the merged result of the content, and interestingly also managed to reconcile the rename from 'A'. | |
| 1570 | + # However, it also drops all of 'their' conflicting changes in favor of 'ours', a respectable result. | |
| 1571 | + git update-index --index-info <<EOF | |
| 1572 | + 100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-renamed/y.f | |
| 1573 | + 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/z | |
| 1574 | +EOF | |
| 1575 | + make_resolve_tree ours B A | |
| 1576 | +) | |
| 1577 | + | |
| 1578 | +(cd renamed-symlink-with-conflict | |
| 1579 | + rm .git/index | |
| 1580 | + # 'A' changes 'a/x.f' and renames the 'link', while 'B' also changes 'a/x.f' in a mergable fashion, | |
| 1581 | + # while renaming 'link' to something else which is where the conflict comes from. | |
| 1582 | + # Choosing the 'ancestor' means to not rename 'link' at all, while merging the file. | |
| 1583 | + git update-index --index-info <<EOF | |
| 1584 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f | |
| 1585 | +120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link | |
| 1586 | +EOF | |
| 1587 | + make_resolve_tree ancestor A B | |
| 1588 | + make_resolve_tree ancestor B A | |
| 1589 | + | |
| 1590 | + rm .git/index | |
| 1591 | + # Here we choose the name of 'link' in 'A'. | |
| 1592 | + git update-index --index-info <<EOF | |
| 1593 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f | |
| 1594 | +120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link-renamed | |
| 1595 | +EOF | |
| 1596 | + make_resolve_tree ours A B | |
| 1597 | + | |
| 1598 | + rm .git/index | |
| 1599 | + # Here we choose the name of 'link' in 'B'. | |
| 1600 | + git update-index --index-info <<EOF | |
| 1601 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f | |
| 1602 | +120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link-different | |
| 1603 | +EOF | |
| 1604 | + make_resolve_tree ours B A | |
| 1605 | +) | |
| 1606 | + | |
| 1607 | +(cd type-change-and-renamed | |
| 1608 | + rm .git/index | |
| 1609 | + # 'A' changes `link` to a file, while 'B' keeps the link, but renames it. | |
| 1610 | + # 'ancestor' just keeps the original version of 'link' | |
| 1611 | + git update-index --index-info <<EOF | |
| 1612 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/x.f | |
| 1613 | +120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link | |
| 1614 | +EOF | |
| 1615 | + make_resolve_tree ancestor A B | |
| 1616 | + make_resolve_tree ancestor B A | |
| 1617 | + | |
| 1618 | + rm .git/index | |
| 1619 | + # 'A' changes the type of 'link' to be a file, and that's what's used here. | |
| 1620 | + git update-index --index-info <<EOF | |
| 1621 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/x.f | |
| 1622 | +100644 blob f89a08d1e226b9a319210641b63b07dcf0bd705f link | |
| 1623 | +EOF | |
| 1624 | + make_resolve_tree ours A B | |
| 1625 | + | |
| 1626 | + rm .git/index | |
| 1627 | + # 'B' renames the link, and that is picked up as well. | |
| 1628 | + git update-index --index-info <<EOF | |
| 1629 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/x.f | |
| 1630 | +120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link-renamed | |
| 1631 | +EOF | |
| 1632 | + make_resolve_tree ours B A | |
| 1633 | +) | |
| 1634 | + | |
| 1635 | +(cd change-and-delete | |
| 1636 | + rm .git/index | |
| 1637 | + # 'A' changes 'link' to be a file, and changes the file, while 'B' deletes everything, | |
| 1638 | + # causing each file to be irreconcilable. | |
| 1639 | + # 'ancestor' keeps everything as is. | |
| 1640 | + git update-index --index-info <<EOF | |
| 1641 | +100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/x.f | |
| 1642 | +120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link | |
| 1643 | +EOF | |
| 1644 | + make_resolve_tree ancestor A B | |
| 1645 | + make_resolve_tree ancestor B A | |
| 1646 | + | |
| 1647 | + rm .git/index | |
| 1648 | + # 'A' changes everything, and that's the change we keep. | |
| 1649 | + git update-index --index-info <<EOF | |
| 1650 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f | |
| 1651 | +100644 blob f89a08d1e226b9a319210641b63b07dcf0bd705f link | |
| 1652 | +EOF | |
| 1653 | + make_resolve_tree ours A B | |
| 1654 | + | |
| 1655 | + rm .git/index | |
| 1656 | + # 'B' deletes everything, which is what we keep. | |
| 1657 | + make_resolve_tree ours B A | |
| 1658 | +) | |
| 1659 | + | |
| 1660 | +(cd submodule-both-modify | |
| 1661 | + rm .git/index | |
| 1662 | + # There is only one submodule. 'A' and 'B' change it in a fast-forwardable manner, | |
| 1663 | + # but we can't handle this at all yet, and thus have to consider it irreconcilable. | |
| 1664 | + # The 'ancestor' resolution just keeps what was. | |
| 1665 | + git update-index --index-info <<EOF | |
| 1666 | +160000 commit e835c0c403c8e494c0ca98f3d25d0b8464c18d38 sub | |
| 1667 | +EOF | |
| 1668 | + make_resolve_tree ancestor A B | |
| 1669 | + make_resolve_tree ancestor B A | |
| 1670 | + | |
| 1671 | + rm .git/index | |
| 1672 | + # Otherwise it's the state of 'A'. | |
| 1673 | + git update-index --index-info <<EOF | |
| 1674 | +160000 commit 64466ebdff775ad618d9cc993cf52840e0af528c sub | |
| 1675 | +EOF | |
| 1676 | + make_resolve_tree ours A B | |
| 1677 | + | |
| 1678 | + rm .git/index | |
| 1679 | + # Otherwise it's the state of 'B'. | |
| 1680 | + git update-index --index-info <<EOF | |
| 1681 | +160000 commit ea6eb701e03c2497915c25a851f3da8f8e362ca0 sub | |
| 1682 | +EOF | |
| 1683 | + make_resolve_tree ours B A | |
| 1684 | +) | |
| 1685 | + | |
| 1686 | +(cd multiple-merge-bases | |
| 1687 | + rm .git/index | |
| 1688 | + # 'A' modifies and 'B' deletes the single file in the tree. | |
| 1689 | + # 'ancestor' keeps the original, which is already the result of the merge of | |
| 1690 | + # the merge-bases. | |
| 1691 | + git update-index --index-info <<EOF | |
| 1692 | +100644 blob 09c277aa66897c58157f57a374eacc63a407dcab content | |
| 1693 | +EOF | |
| 1694 | + make_resolve_tree ancestor A B | |
| 1695 | + make_resolve_tree ancestor B A | |
| 1696 | + | |
| 1697 | + rm .git/index | |
| 1698 | + # 'A' keeps the modified version. | |
| 1699 | + git update-index --index-info <<EOF | |
| 1700 | +100644 blob 0a6a0ba83635bc00e7c79a4b5b6e50381385c1af content | |
| 1701 | +EOF | |
| 1702 | + make_resolve_tree ours A B | |
| 1703 | + | |
| 1704 | + rm .git/index | |
| 1705 | + # 'B' applies the deletion to get an empty tree. | |
| 1706 | + make_resolve_tree ours B A | |
| 1707 | +) | |
| 1708 | + | |
| 1709 | +(cd non-tree-to-tree | |
| 1710 | + rm .git/index | |
| 1711 | + # 'A' changes the single file 'a', while 'B' replaces it with a directory structure 'a', | |
| 1712 | + # without a rename though. | |
| 1713 | + # We manage to pick the 'ancestor', just a single file, while discarding all follow-up changes. | |
| 1714 | + git update-index --index-info <<EOF | |
| 1715 | +100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a | |
| 1716 | +EOF | |
| 1717 | + make_resolve_tree ancestor A B | |
| 1718 | + make_resolve_tree ancestor B A | |
| 1719 | + | |
| 1720 | + rm .git/index | |
| 1721 | + # Picks 'A' which is just a single, modified (and mergable) file. | |
| 1722 | + git update-index --index-info <<EOF | |
| 1723 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a | |
| 1724 | +EOF | |
| 1725 | + make_resolve_tree ours A B | |
| 1726 | + | |
| 1727 | + rm .git/index | |
| 1728 | + # Picks 'B' which is a whole directory tree. | |
| 1729 | + git update-index --index-info <<EOF | |
| 1730 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/d | |
| 1731 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/e | |
| 1732 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/b | |
| 1733 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/c | |
| 1734 | +EOF | |
| 1735 | + make_resolve_tree ours B A | |
| 1736 | +) | |
| 1737 | + | |
| 1738 | +(cd tree-to-non-tree | |
| 1739 | + rm .git/index | |
| 1740 | + # 'A' modifies a nested file 'a/sub/b', while 'B' replaces 'a/' with file 'a'. | |
| 1741 | + # Ignore *their* changes for 'ancestor' resolution, and the modification, | |
| 1742 | + # but apply all others which are deletions. | |
| 1743 | + git update-index --index-info <<EOF | |
| 1744 | +100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/b | |
| 1745 | +EOF | |
| 1746 | + make_resolve_tree ancestor A B | |
| 1747 | + make_resolve_tree ancestor B A | |
| 1748 | + # *ours* is the same as ancestor, we want to keep the tree and changes, but it only | |
| 1749 | + # applies to the one modification that protects the change. | |
| 1750 | + rm .git/index | |
| 1751 | + git update-index --index-info <<EOF | |
| 1752 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub/b | |
| 1753 | +EOF | |
| 1754 | + make_resolve_tree ours A B | |
| 1755 | + | |
| 1756 | + rm .git/index | |
| 1757 | + # Now *ours* is the single file which replaces a tree. | |
| 1758 | + git update-index --index-info <<EOF | |
| 1759 | +100644 blob fa49b077972391ad58037050f2a75f74e3671e92 a | |
| 1760 | +EOF | |
| 1761 | + make_resolve_tree ours B A | |
| 1762 | +) | |
| 1763 | + | |
| 1764 | +(cd tree-to-non-tree-with-rename | |
| 1765 | + rm .git/index | |
| 1766 | + # 'A' modifies a nested file 'a/sub/b', while 'B' replaces 'a/' with file 'a'. | |
| 1767 | + # I let it pass as it's an edge-case to some extent. | |
| 1768 | + git update-index --index-info <<EOF | |
| 1769 | +100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/b | |
| 1770 | +EOF | |
| 1771 | + make_resolve_tree ancestor A B | |
| 1772 | + make_resolve_tree ancestor B A | |
| 1773 | + | |
| 1774 | + rm .git/index | |
| 1775 | + # Thanks to the rename, this version keeps one additional file, 'a/e' | |
| 1776 | + git update-index --index-info <<EOF | |
| 1777 | + 100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub/b | |
| 1778 | +EOF | |
| 1779 | + make_resolve_tree ours A B | |
| 1780 | + | |
| 1781 | + rm .git/index | |
| 1782 | + # Now *ours* is the single file which replaces a tree. | |
| 1783 | + git update-index --index-info <<EOF | |
| 1784 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a | |
| 1785 | +EOF | |
| 1786 | + make_resolve_tree ours B A | |
| 1787 | +) | |
| 1788 | + | |
| 1789 | +(cd non-tree-to-tree-with-rename | |
| 1790 | + # 'A' changes the single file 'a', while 'B' replaces it with a directory structure 'a'. | |
| 1791 | + # The rename now sends this off-course, as it removes the previously 'protective' entry | |
| 1792 | + # and thus makes all changes from 'B' succeed without us detecting any problem with that. | |
| 1793 | + # Also, here we don't actually have irreconcilable tree-changes because of that. | |
| 1794 | + rm .git/index | |
| 1795 | + git update-index --index-info <<EOF | |
| 1796 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/d | |
| 1797 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/e | |
| 1798 | +100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub/b | |
| 1799 | +100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/c | |
| 1800 | +EOF | |
| 1801 | + make_resolve_tree ancestor A B | |
| 1802 | + make_resolve_tree ancestor B A | |
| 1803 | + | |
| 1804 | + # Thanks to the rename, this time there isn't even an irreconcilable conflict. | |
| 1805 | + # This is deactivated as the test-suite can't handle this (one) special case. | |
| 1806 | + # The state really is the one from the ancestors, as there are no irreconcilable | |
| 1807 | + # tree changes, it's all the same. But the test expects to see changes when 'ours' | |
| 1808 | + # is chosen so we can't easily run this here. | |
| 1809 | + # make_resolve_tree ours A B | |
| 1810 | + # make_resolve_tree ours B A | |
| 1811 | +) | |
| 1812 | + | |
| 1813 | +(cd rename-within-rename | |
| 1814 | + # 'A' and 'B' change all content in a mergable manner. 'A' renames 'a' to 'a-renamed', | |
| 1815 | + # and 'B' renames 'a/sub' to 'a/sub-renamed'. | |
| 1816 | + # Ideally, we get both together, but doing so added a lot of complexity so maybe give | |
| 1817 | + # that another go and try to keep it simple. | |
| 1818 | + # In ancestor mode, only those ancestors of conflicts are kept unchanged, so some renames | |
| 1819 | + # go through. | |
| 1820 | + rm .git/index | |
| 1821 | + git update-index --index-info <<EOF | |
| 1822 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w | |
| 1823 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f | |
| 1824 | +100644 44065282f89b9bd6439ed2e4674721383fd987eb 0 a/sub/y.f | |
| 1825 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a/sub/z | |
| 1826 | +EOF | |
| 1827 | + make_resolve_tree ancestor A B | |
| 1828 | + make_resolve_tree ancestor B A | |
| 1829 | + | |
| 1830 | + # *ours* is `a-renamed` everything, with merges. | |
| 1831 | + rm .git/index | |
| 1832 | + git update-index --index-info <<EOF | |
| 1833 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/sub/y.f | |
| 1834 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/sub/z | |
| 1835 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w | |
| 1836 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f | |
| 1837 | +EOF | |
| 1838 | + make_resolve_tree ours A B | |
| 1839 | + | |
| 1840 | + # Now ours is the renamed sub-directory, with merges. It can bring everything together even. | |
| 1841 | + rm .git/index | |
| 1842 | + git update-index --index-info <<EOF | |
| 1843 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/sub-renamed/y.f | |
| 1844 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/sub-renamed/z | |
| 1845 | +100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w | |
| 1846 | +100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f | |
| 1847 | +EOF | |
| 1848 | + make_resolve_tree ours B A | |
| 1849 | +) | |
| @@ -10,16 +10,16 @@ fn binary() { · 9 unmodified lines | ||
| 10 | 10 | ); |
| 11 | 11 | assert_eq!( |
| 12 | 12 | builtin_driver::binary(Some(ResolveWith::Ancestor)), |
| 13 | − (Pick::Ancestor, Resolution::Complete), | |
| 13 | + (Pick::Ancestor, Resolution::CompleteWithAutoResolvedConflict), | |
| 14 | 14 | "Otherwise we can pick anything and it will mark it as complete" |
| 15 | 15 | ); |
| 16 | 16 | assert_eq!( |
| 17 | 17 | builtin_driver::binary(Some(ResolveWith::Ours)), |
| 18 | − (Pick::Ours, Resolution::Complete) | |
| 18 | + (Pick::Ours, Resolution::CompleteWithAutoResolvedConflict) | |
| 19 | 19 | ); |
| 20 | 20 | assert_eq!( |
| 21 | 21 | builtin_driver::binary(Some(ResolveWith::Theirs)), |
| 22 | − (Pick::Theirs, Resolution::Complete) | |
| 22 | + (Pick::Theirs, Resolution::CompleteWithAutoResolvedConflict) | |
| 23 | 23 | ); |
| 24 | 24 | } |
| 25 | 25 | |
| @@ -59,12 +59,61 @@ mod merge { · 58 unmodified lines | ||
| 59 | 59 | let res = platform_ref.merge(&mut buf, default_labels(), &Default::default())?; |
| 60 | 60 | assert_eq!( |
| 61 | 61 | res, |
| 62 | − (Pick::Theirs, Resolution::Complete), | |
| 62 | + (Pick::Theirs, Resolution::CompleteWithAutoResolvedConflict), | |
| 63 | 63 | "the auto-binary driver respects its own options" |
| 64 | 64 | ); |
| 65 | 65 | Ok(()) |
| 66 | 66 | } |
| 67 | 67 | |
| 68 | + #[test] | |
| 69 | + fn same_binaries_do_not_count_as_conflicted() -> crate::Result { | |
| 70 | + let mut platform = new_platform(None, pipeline::Mode::ToGit); | |
| 71 | + platform.set_resource( | |
| 72 | + gix_hash::Kind::Sha1.null(), | |
| 73 | + EntryKind::Blob, | |
| 74 | + "a".into(), | |
| 75 | + ResourceKind::CommonAncestorOrBase, | |
| 76 | + &gix_object::find::Never, | |
| 77 | + )?; | |
| 78 | + | |
| 79 | + let mut db = ObjectDb::default(); | |
| 80 | + for (content, kind) in [ | |
| 81 | + ("any\0", ResourceKind::CurrentOrOurs), | |
| 82 | + ("any\0", ResourceKind::OtherOrTheirs), | |
| 83 | + ] { | |
| 84 | + let id = db.insert(content); | |
| 85 | + platform.set_resource( | |
| 86 | + id, | |
| 87 | + EntryKind::Blob, | |
| 88 | + "path matters only for attribute lookup".into(), | |
| 89 | + kind, | |
| 90 | + &db, | |
| 91 | + )?; | |
| 92 | + } | |
| 93 | + let platform_ref = platform.prepare_merge(&db, Default::default())?; | |
| 94 | + assert_eq!( | |
| 95 | + platform_ref.driver, | |
| 96 | + DriverChoice::BuiltIn(BuiltinDriver::Text), | |
| 97 | + "it starts out at the default text driver" | |
| 98 | + ); | |
| 99 | + | |
| 100 | + let mut buf = Vec::new(); | |
| 101 | + let res = platform_ref.merge(&mut buf, default_labels(), &Default::default())?; | |
| 102 | + assert_eq!( | |
| 103 | + res, | |
| 104 | + (Pick::Ours, Resolution::Complete), | |
| 105 | + "as both are the same, it just picks ours, declaring it non-conflicting" | |
| 106 | + ); | |
| 107 | + | |
| 108 | + let mut input = imara_diff::intern::InternedInput::new(&[][..], &[]); | |
| 109 | + assert_eq!( | |
| 110 | + platform_ref.builtin_merge(BuiltinDriver::Binary, &mut buf, &mut input, default_labels()), | |
| 111 | + res, | |
| 112 | + "no way to work around it - calling the built-in binary merge is the same" | |
| 113 | + ); | |
| 114 | + Ok(()) | |
| 115 | + } | |
| 116 | + | |
| 68 | 117 | #[test] |
| 69 | 118 | fn builtin_with_conflict() -> crate::Result { |
| 70 | 119 | let mut platform = new_platform(None, pipeline::Mode::ToGit); |
| @@ -192,7 +241,7 @@ theirs · 121 unmodified lines | ||
| 192 | 241 | ] { |
| 193 | 242 | platform_ref.options.resolve_binary_with = Some(resolve); |
| 194 | 243 | let res = platform_ref.merge(&mut buf, default_labels(), &Default::default())?; |
| 195 | − assert_eq!(res, (expected_pick, Resolution::Complete)); | |
| 244 | + assert_eq!(res, (expected_pick, Resolution::CompleteWithAutoResolvedConflict)); | |
| 196 | 245 | assert_eq!(platform_ref.buffer_by_pick(res.0).unwrap().unwrap().as_bstr(), expected); |
| 197 | 246 | |
| 198 | 247 | assert_eq!( |
| @@ -48,6 +48,7 @@ pub enum ConflictKind { · 47 unmodified lines | ||
| 48 | 48 | |
| 49 | 49 | /// More loosely structured information about the `Conflict`. |
| 50 | 50 | #[derive(Debug)] |
| 51 | +#[allow(dead_code)] // used only for debugging | |
| 51 | 52 | pub struct ConflictInfo { |
| 52 | 53 | /// All the paths involved in the informational message |
| 53 | 54 | pub paths: Vec<String>, |
| @@ -149,9 +150,11 @@ impl Iterator for Expectations<'_> { · 95 unmodified lines | ||
| 149 | 150 | else { |
| 150 | 151 | unreachable!("invalid line: {line:?}") |
| 151 | 152 | }; |
| 152 | − let deviation = (expected_custom_tree != "expected^{tree}").then(|| { | |
| 153 | − let expected_tree_id = gix_hash::ObjectId::from_hex(expected_custom_tree.as_bytes()) | |
| 154 | − .expect("valid tree id in hex for the expected tree"); | |
| 153 | + let deviation = (!expected_custom_tree.starts_with("expected")).then(|| { | |
| 154 | + let expected_tree_id = | |
| 155 | + gix_hash::ObjectId::from_hex(expected_custom_tree.as_bytes()).unwrap_or_else(|err| { | |
| 156 | + panic!("valid tree id in hex for the expected tree: '{expected_custom_tree}': {err} {line}") | |
| 157 | + }); | |
| 155 | 158 | let message = tokens.collect::<Vec<_>>().join(" ").trim().to_owned(); |
| 156 | 159 | Deviation { |
| 157 | 160 | message, |
| @@ -371,6 +374,22 @@ pub fn show_diff_and_fail( · 213 unmodified lines | ||
| 371 | 374 | ); |
| 372 | 375 | } |
| 373 | 376 | |
| 377 | +pub fn show_diff_trees_and_fail( | |
| 378 | + case_name: &str, | |
| 379 | + actual_id: ObjectId, | |
| 380 | + actual: &gix_merge::tree::Outcome<'_>, | |
| 381 | + expected_tree_id: gix_hash::ObjectId, | |
| 382 | + additional_information: &str, | |
| 383 | + odb: &gix_odb::memory::Proxy<gix_odb::Handle>, | |
| 384 | +) { | |
| 385 | + pretty_assertions::assert_str_eq!( | |
| 386 | + visualize_tree(&actual_id, odb, None).to_string(), | |
| 387 | + visualize_tree(&expected_tree_id, odb, None).to_string(), | |
| 388 | + "{case_name}: merged tree mismatch\n{:#?}\n{additional_information}\n{case_name}", | |
| 389 | + actual.conflicts, | |
| 390 | + ); | |
| 391 | +} | |
| 392 | + | |
| 374 | 393 | pub(crate) fn apply_git_index_entries(conflicts: &[Conflict], state: &mut gix_index::State) { |
| 375 | 394 | let len = state.entries().len(); |
| 376 | 395 | for Conflict { ours, theirs, ancestor } in conflicts { |
| @@ -1,7 +1,8 @@ | ||
| 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 | +use gix_merge::tree::apply_index_entries::RemovalMode; | |
| 5 | +use gix_merge::tree::{treat_as_unresolved, TreatAsUnresolved}; | |
| 5 | 6 | use gix_object::Write; |
| 6 | 7 | use gix_worktree::stack::state::attributes; |
| 7 | 8 | use std::path::Path; |
| @@ -21,7 +22,8 @@ fn run_baseline() -> crate::Result { · 13 unmodified lines | ||
| 21 | 22 | let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?; |
| 22 | 23 | let cases = std::fs::read_to_string(root.join("baseline.cases"))?; |
| 23 | 24 | let mut actual_cases = 0; |
| 24 | − // let new_test = Some("rename-add-symlink-A-B"); | |
| 25 | + let mut skipped_tree_resolve_cases = 0; | |
| 26 | + // let new_test = Some("tree-to-non-tree-with-rename-A-B"); | |
| 25 | 27 | let new_test = None; |
| 26 | 28 | for baseline::Expectation { |
| 27 | 29 | root, |
| @@ -63,12 +65,12 @@ fn run_baseline() -> crate::Result { · 35 unmodified lines | ||
| 63 | 65 | &mut blob_merge, |
| 64 | 66 | &odb, |
| 65 | 67 | &mut |id| id.to_hex_with_len(7).to_string(), |
| 66 | − options, | |
| 68 | + options.clone(), | |
| 67 | 69 | )? |
| 68 | 70 | .tree_merge; |
| 69 | 71 | |
| 70 | 72 | let actual_id = actual.tree.write(|tree| odb.write(tree))?; |
| 71 | − match deviation { | |
| 73 | + match &deviation { | |
| 72 | 74 | None => { |
| 73 | 75 | if actual_id != merge_info.merged_tree { |
| 74 | 76 | baseline::show_diff_and_fail(&case_name, actual_id, &actual, &merge_info, &odb); |
| @@ -78,22 +80,15 @@ fn run_baseline() -> crate::Result { · 3 unmodified lines | ||
| 78 | 80 | message, |
| 79 | 81 | expected_tree_id, |
| 80 | 82 | }) => { |
| 83 | + // TODO: why did this ever work? | |
| 81 | 84 | // Sometimes only the reversed part of a specific test is different. |
| 82 | − if case_name != "same-rename-different-mode-A-B" && case_name != "same-rename-different-mode-A-B-diff3" | |
| 83 | − { | |
| 84 | − assert_ne!( | |
| 85 | − actual_id, merge_info.merged_tree, | |
| 86 | − "{case_name}: Git caught up - adjust expectation {message}" | |
| 87 | − ); | |
| 88 | − } else { | |
| 89 | − assert_eq!( | |
| 90 | − actual_id, merge_info.merged_tree, | |
| 91 | − "{case_name}: Git should match here, it just doesn't match in one of two cases" | |
| 92 | − ); | |
| 93 | − } | |
| 85 | + // assert_eq!( | |
| 86 | + // actual_id, merge_info.merged_tree, | |
| 87 | + // "{case_name}: Git should match here, it just doesn't match in one of two cases" | |
| 88 | + // ); | |
| 94 | 89 | pretty_assertions::assert_str_eq!( |
| 95 | 90 | baseline::visualize_tree(&actual_id, &odb, None).to_string(), |
| 96 | − baseline::visualize_tree(&expected_tree_id, &odb, None).to_string(), | |
| 91 | + baseline::visualize_tree(expected_tree_id, &odb, None).to_string(), | |
| 97 | 92 | "{case_name}: tree mismatch: {message} \n{:#?}\n{case_name}", |
| 98 | 93 | actual.conflicts |
| 99 | 94 | ); |
| @@ -102,10 +97,10 @@ fn run_baseline() -> crate::Result { · 2 unmodified lines | ||
| 102 | 97 | |
| 103 | 98 | let mut actual_index = gix_index::State::from_tree(&actual_id, &odb, Default::default())?; |
| 104 | 99 | let expected_index = { |
| 105 | − let derivative_index_path = root.join(".git").join(format!("{case_name}.index")); | |
| 106 | − if derivative_index_path.exists() { | |
| 100 | + let deviating_index_path = root.join(".git").join(format!("{case_name}.index")); | |
| 101 | + if deviating_index_path.exists() { | |
| 107 | 102 | gix_index::File::at( |
| 108 | − derivative_index_path, | |
| 103 | + deviating_index_path, | |
| 109 | 104 | odb.store().object_hash(), |
| 110 | 105 | true, |
| 111 | 106 | Default::default(), |
| @@ -119,9 +114,9 @@ fn run_baseline() -> crate::Result { · 7 unmodified lines | ||
| 119 | 114 | index |
| 120 | 115 | } |
| 121 | 116 | }; |
| 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)); | |
| 117 | + let conflicts_like_in_git = TreatAsUnresolved::git(); | |
| 118 | + let did_change = | |
| 119 | + actual.index_changed_after_applying_conflicts(&mut actual_index, conflicts_like_in_git, RemovalMode::Prune); | |
| 125 | 120 | |
| 126 | 121 | pretty_assertions::assert_eq!( |
| 127 | 122 | baseline::clear_entries(&actual_index), |
| @@ -130,29 +125,89 @@ fn run_baseline() -> crate::Result { · 2 unmodified lines | ||
| 130 | 125 | actual.conflicts, |
| 131 | 126 | merge_info.conflicts |
| 132 | 127 | ); |
| 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 { | |
| 128 | + if deviation.is_none() { | |
| 144 | 129 | assert_eq!( |
| 145 | 130 | did_change, |
| 146 | 131 | actual.has_unresolved_conflicts(conflicts_like_in_git), |
| 147 | 132 | "{case_name}: If there is any kind of conflict, the index should have been changed" |
| 148 | 133 | ); |
| 149 | 134 | } |
| 135 | + | |
| 136 | + // The content-merge mode is not relevant for the upcoming tree-conflict resolution. | |
| 137 | + if case_name.contains("diff3") { | |
| 138 | + continue; | |
| 139 | + } | |
| 140 | + | |
| 141 | + for tree_resolution in [ | |
| 142 | + gix_merge::tree::ResolveWith::Ancestor, | |
| 143 | + gix_merge::tree::ResolveWith::Ours, | |
| 144 | + ] { | |
| 145 | + let resolution_name = match tree_resolution { | |
| 146 | + gix_merge::tree::ResolveWith::Ancestor => "ancestor", | |
| 147 | + gix_merge::tree::ResolveWith::Ours => "ours", | |
| 148 | + }; | |
| 149 | + let basename = format!("resolve-{our_side_name}-{their_side_name}-with-{resolution_name}"); | |
| 150 | + let tree_path = root.join(".git").join(format!("{basename}.tree")); | |
| 151 | + if !tree_path.exists() { | |
| 152 | + skipped_tree_resolve_cases += 1; | |
| 153 | + continue; | |
| 154 | + }; | |
| 155 | + let expected_tree_id = gix_hash::ObjectId::from_hex(std::fs::read_to_string(tree_path)?.trim().as_bytes())?; | |
| 156 | + options.tree_merge.tree_conflicts = Some(tree_resolution); | |
| 157 | + let resolve_with_ours = tree_resolution == gix_merge::tree::ResolveWith::Ours; | |
| 158 | + if resolve_with_ours { | |
| 159 | + options.tree_merge.blob_merge.text.conflict = | |
| 160 | + gix_merge::blob::builtin_driver::text::Conflict::ResolveWithOurs; | |
| 161 | + } | |
| 162 | + let mut actual = gix_merge::commit( | |
| 163 | + our_commit_id, | |
| 164 | + their_commit_id, | |
| 165 | + gix_merge::blob::builtin_driver::text::Labels { | |
| 166 | + ancestor: None, | |
| 167 | + current: Some(our_side_name.as_str().into()), | |
| 168 | + other: Some(their_side_name.as_str().into()), | |
| 169 | + }, | |
| 170 | + &mut graph, | |
| 171 | + &mut diff_resource_cache, | |
| 172 | + &mut blob_merge, | |
| 173 | + &odb, | |
| 174 | + &mut |id| id.to_hex_with_len(7).to_string(), | |
| 175 | + options.clone(), | |
| 176 | + )? | |
| 177 | + .tree_merge; | |
| 178 | + | |
| 179 | + let actual_id = actual.tree.write(|tree| odb.write(tree))?; | |
| 180 | + if actual_id != expected_tree_id { | |
| 181 | + baseline::show_diff_trees_and_fail(&case_name, actual_id, &actual, expected_tree_id, &basename, &odb); | |
| 182 | + } | |
| 183 | + if resolve_with_ours { | |
| 184 | + assert!( | |
| 185 | + !actual.has_unresolved_conflicts(conflicts_like_in_git), | |
| 186 | + "We have forcefully resolved all conflicts, as far as Git would be concerned\n{:#?}", | |
| 187 | + actual.conflicts | |
| 188 | + ); | |
| 189 | + assert!( | |
| 190 | + actual.has_unresolved_conflicts(TreatAsUnresolved { | |
| 191 | + content_merge: treat_as_unresolved::ContentMerge::ForcedResolution, | |
| 192 | + tree_merge: treat_as_unresolved::TreeMerge::ForcedResolution, | |
| 193 | + }), | |
| 194 | + "But it's possible to adjust the parameter to still learn that a conflict happened,\ | |
| 195 | + and each of these tests has one that is irreconcilable" | |
| 196 | + ); | |
| 197 | + } | |
| 198 | + } | |
| 150 | 199 | } |
| 151 | 200 | |
| 152 | 201 | assert_eq!( |
| 153 | − actual_cases, 107, | |
| 202 | + actual_cases, 117, | |
| 154 | 203 | "BUG: update this number, and don't forget to remove a filter in the end" |
| 155 | 204 | ); |
| 205 | + assert_eq!( | |
| 206 | + skipped_tree_resolve_cases, 102, | |
| 207 | + "this is done when no case is skipped, and we don't want to accidentally skip them.\ | |
| 208 | + Some don't actually have conflicts.\ | |
| 209 | + The ones we skipped don't have irreconcilable conflicts" | |
| 210 | + ); | |
| 156 | 211 | |
| 157 | 212 | Ok(()) |
| 158 | 213 | } |
| @@ -163,16 +218,17 @@ fn basic_merge_options() -> Options { · 4 unmodified lines | ||
| 163 | 218 | use_first_merge_base: false, |
| 164 | 219 | tree_merge: gix_merge::tree::Options { |
| 165 | 220 | symlink_conflicts: None, |
| 221 | + tree_conflicts: None, | |
| 166 | 222 | rewrites: Some(Rewrites { |
| 167 | 223 | copies: None, |
| 168 | 224 | percentage: Some(0.5), |
| 169 | 225 | limit: 0, |
| 226 | + track_empty: false, | |
| 170 | 227 | }), |
| 171 | 228 | blob_merge: gix_merge::blob::platform::merge::Options::default(), |
| 172 | 229 | blob_merge_command_ctx: Default::default(), |
| 173 | 230 | fail_on_conflict: None, |
| 174 | 231 | marker_size_multiplier: 0, |
| 175 | − allow_lossy_resolution: false, | |
| 176 | 232 | }, |
| 177 | 233 | } |
| 178 | 234 | } |
| @@ -224,6 +280,4 @@ fn new_blob_merge_platform( · 45 unmodified lines | ||
| 224 | 280 | ) |
| 225 | 281 | } |
| 226 | 282 | |
| 227 | −// TODO: make sure everything is read eventually, even if only to improve debug messages in case of failure. | |
| 228 | −#[allow(dead_code)] | |
| 229 | 283 | mod baseline; |
| @@ -80,6 +80,7 @@ fn changed_and_untracked_and_renamed() { · 79 unmodified lines | ||
| 80 | 80 | }), |
| 81 | 81 | percentage: Some(0.3), |
| 82 | 82 | limit: 0, |
| 83 | + track_empty: true, | |
| 83 | 84 | }; |
| 84 | 85 | let out = fixture_filtered_detailed( |
| 85 | 86 | "changed-and-untracked-and-renamed", |
| @@ -26,6 +26,7 @@ serde = { version = "1.0.114", optional = true, default-features = false, featur · 25 unmodified lines | ||
| 26 | 26 | thiserror = "2.0.0" |
| 27 | 27 | url = "2.5.2" |
| 28 | 28 | bstr = { version = "1.3.0", default-features = false, features = ["std"] } |
| 29 | +percent-encoding = "2.3.1" | |
| 29 | 30 | |
| 30 | 31 | document-features = { version = "0.2.0", optional = true } |
| 31 | 32 | |
| @@ -81,7 +81,7 @@ impl std::fmt::Display for Url { · 80 unmodified lines | ||
| 81 | 81 | let mut storage; |
| 82 | 82 | let to_print = if self.password.is_some() { |
| 83 | 83 | storage = self.clone(); |
| 84 | − storage.password = Some("<redacted>".into()); | |
| 84 | + storage.password = Some("redacted".into()); | |
| 85 | 85 | &storage |
| 86 | 86 | } else { |
| 87 | 87 | self |
| @@ -308,6 +308,10 @@ impl Url { · 307 unmodified lines | ||
| 308 | 308 | } |
| 309 | 309 | } |
| 310 | 310 | |
| 311 | +fn percent_encode(s: &str) -> Cow<'_, str> { | |
| 312 | + percent_encoding::utf8_percent_encode(s, percent_encoding::NON_ALPHANUMERIC).into() | |
| 313 | +} | |
| 314 | + | |
| 311 | 315 | /// Serialization |
| 312 | 316 | impl Url { |
| 313 | 317 | /// Write this URL losslessly to `out`, ready to be parsed again. |
| @@ -318,10 +322,10 @@ impl Url { · 4 unmodified lines | ||
| 318 | 322 | } |
| 319 | 323 | match (&self.user, &self.host) { |
| 320 | 324 | (Some(user), Some(host)) => { |
| 321 | − out.write_all(user.as_bytes())?; | |
| 325 | + out.write_all(percent_encode(user).as_bytes())?; | |
| 322 | 326 | if let Some(password) = &self.password { |
| 323 | 327 | out.write_all(b":")?; |
| 324 | − out.write_all(password.as_bytes())?; | |
| 328 | + out.write_all(percent_encode(password).as_bytes())?; | |
| 325 | 329 | } |
| 326 | 330 | out.write_all(b"@")?; |
| 327 | 331 | out.write_all(host.as_bytes())?; |
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | use std::convert::Infallible; |
| 2 | 2 | |
| 3 | −use bstr::{BStr, BString, ByteSlice}; | |
| 4 | − | |
| 5 | 3 | use crate::Scheme; |
| 4 | +use bstr::{BStr, BString, ByteSlice}; | |
| 5 | +use percent_encoding::percent_decode_str; | |
| 6 | 6 | |
| 7 | 7 | /// The error returned by [parse()](crate::parse()). |
| 8 | 8 | #[derive(Debug, thiserror::Error)] |
| @@ -114,14 +114,28 @@ pub(crate) fn url(input: &BStr, protocol_end: usize) -> Result<crate::Url, Error · 105 unmodified lines | ||
| 114 | 114 | Ok(crate::Url { |
| 115 | 115 | serialize_alternative_form: false, |
| 116 | 116 | scheme, |
| 117 | − user: url_user(&url), | |
| 118 | − password: url.password().map(Into::into), | |
| 117 | + user: url_user(&url, UrlKind::Url)?, | |
| 118 | + password: url | |
| 119 | + .password() | |
| 120 | + .map(|s| percent_decoded_utf8(s, UrlKind::Url)) | |
| 121 | + .transpose()?, | |
| 119 | 122 | host: url.host_str().map(Into::into), |
| 120 | 123 | port: url.port(), |
| 121 | 124 | path: url.path().into(), |
| 122 | 125 | }) |
| 123 | 126 | } |
| 124 | 127 | |
| 128 | +fn percent_decoded_utf8(s: &str, kind: UrlKind) -> Result<String, Error> { | |
| 129 | + Ok(percent_decode_str(s) | |
| 130 | + .decode_utf8() | |
| 131 | + .map_err(|err| Error::Utf8 { | |
| 132 | + url: s.into(), | |
| 133 | + kind, | |
| 134 | + source: err, | |
| 135 | + })? | |
| 136 | + .into_owned()) | |
| 137 | +} | |
| 138 | + | |
| 125 | 139 | pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> { |
| 126 | 140 | let input = input_to_utf8(input, UrlKind::Scp)?; |
| 127 | 141 | |
| @@ -150,19 +164,22 @@ pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> { · 22 unmodified lines | ||
| 150 | 164 | Ok(crate::Url { |
| 151 | 165 | serialize_alternative_form: true, |
| 152 | 166 | scheme: url.scheme().into(), |
| 153 | − user: url_user(&url), | |
| 154 | − password: url.password().map(Into::into), | |
| 167 | + user: url_user(&url, UrlKind::Scp)?, | |
| 168 | + password: url | |
| 169 | + .password() | |
| 170 | + .map(|s| percent_decoded_utf8(s, UrlKind::Scp)) | |
| 171 | + .transpose()?, | |
| 155 | 172 | host: url.host_str().map(Into::into), |
| 156 | 173 | port: url.port(), |
| 157 | 174 | path: path.into(), |
| 158 | 175 | }) |
| 159 | 176 | } |
| 160 | 177 | |
| 161 | −fn url_user(url: &url::Url) -> Option<String> { | |
| 178 | +fn url_user(url: &url::Url, kind: UrlKind) -> Result<Option<String>, Error> { | |
| 162 | 179 | if url.username().is_empty() && url.password().is_none() { |
| 163 | − None | |
| 180 | + Ok(None) | |
| 164 | 181 | } else { |
| 165 | − Some(url.username().into()) | |
| 182 | + Ok(Some(percent_decoded_utf8(url.username(), kind)?)) | |
| 166 | 183 | } |
| 167 | 184 | } |
| 168 | 185 | |
| @@ -0,0 +1 @@ | ||
| 1 | +BBi%BBBBB,@}:m\ | |
| @@ -174,7 +174,7 @@ fn display() { · 173 unmodified lines | ||
| 174 | 174 | compare("/path/to/repo", "/path/to/repo", "same goes for simple paths"); |
| 175 | 175 | compare( |
| 176 | 176 | "https://user:password@host/path", |
| 177 | − "https://user:<redacted>@host/path", | |
| 178 | − "it visibly redacts passwords though", | |
| 177 | + "https://user:redacted@host/path", | |
| 178 | + "it visibly redacts passwords though, and it's still a valid URL", | |
| 179 | 179 | ); |
| 180 | 180 | } |
| @@ -164,6 +164,7 @@ fn assert_urls_equal(expected: &baseline::GitDiagUrl<'_>, actual: &gix_url::Url) · 163 unmodified lines | ||
| 164 | 164 | assert_eq!(actual.path, expected.path.unwrap_or_default()); |
| 165 | 165 | } |
| 166 | 166 | |
| 167 | +#[allow(clippy::module_inception)] | |
| 167 | 168 | mod baseline { |
| 168 | 169 | use bstr::{BStr, BString, ByteSlice}; |
| 169 | 170 | use gix_testtools::once_cell::sync::Lazy; |
File renamed without changes.
| @@ -5,6 +5,7 @@ use bstr::ByteSlice; · 4 unmodified lines | ||
| 5 | 5 | #[test] |
| 6 | 6 | fn fuzzed() { |
| 7 | 7 | for name in [ |
| 8 | + "illegal-utf8", | |
| 8 | 9 | "short-panic", |
| 9 | 10 | "very-long-abort2", |
| 10 | 11 | "very-long-abort", |
| @@ -2,5 +2,7 @@ pub type Error = Box<dyn std::error::Error>; · 1 unmodified line | ||
| 2 | 2 | pub type Result = std::result::Result<(), Error>; |
| 3 | 3 | |
| 4 | 4 | mod access; |
| 5 | +mod baseline; | |
| 5 | 6 | mod expand_path; |
| 7 | +mod fuzzed; | |
| 6 | 8 | mod parse; |
File renamed without changes.
| @@ -39,6 +39,26 @@ fn username_and_password_and_port() -> crate::Result { · 38 unmodified lines | ||
| 39 | 39 | ) |
| 40 | 40 | } |
| 41 | 41 | |
| 42 | +#[test] | |
| 43 | +fn username_and_password_with_spaces_and_port() -> crate::Result { | |
| 44 | + let expected = gix_url::Url::from_parts( | |
| 45 | + Scheme::Http, | |
| 46 | + Some("user name".into()), | |
| 47 | + Some("password secret".into()), | |
| 48 | + Some("example.com".into()), | |
| 49 | + Some(8080), | |
| 50 | + b"/~byron/hello".into(), | |
| 51 | + false, | |
| 52 | + )?; | |
| 53 | + assert_url_roundtrip( | |
| 54 | + "http://user%20name:password%20secret@example.com:8080/~byron/hello", | |
| 55 | + expected.clone(), | |
| 56 | + )?; | |
| 57 | + assert_eq!(expected.user(), Some("user name")); | |
| 58 | + assert_eq!(expected.password(), Some("password secret")); | |
| 59 | + Ok(()) | |
| 60 | +} | |
| 61 | + | |
| 42 | 62 | #[test] |
| 43 | 63 | fn only_password() -> crate::Result { |
| 44 | 64 | assert_url_roundtrip( |
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -173,7 +173,8 @@ pub mod describe { · 172 unmodified lines | ||
| 173 | 173 | /// A support type to allow configuring a `git describe` operation |
| 174 | 174 | pub struct Platform<'repo> { |
| 175 | 175 | pub(crate) id: gix_hash::ObjectId, |
| 176 | − pub(crate) repo: &'repo crate::Repository, | |
| 176 | + /// The owning repository. | |
| 177 | + pub repo: &'repo crate::Repository, | |
| 177 | 178 | pub(crate) select: SelectRef, |
| 178 | 179 | pub(crate) first_parent: bool, |
| 179 | 180 | pub(crate) id_as_fallback: bool, |
| @@ -156,7 +156,7 @@ impl Cache { · 155 unmodified lines | ||
| 156 | 156 | } |
| 157 | 157 | |
| 158 | 158 | #[cfg(feature = "blob-diff")] |
| 159 | − pub(crate) fn diff_renames(&self) -> Result<Option<gix_diff::Rewrites>, crate::diff::new_rewrites::Error> { | |
| 159 | + pub(crate) fn diff_renames(&self) -> Result<(Option<gix_diff::Rewrites>, bool), crate::diff::new_rewrites::Error> { | |
| 160 | 160 | self.diff_renames |
| 161 | 161 | .get_or_try_init(|| crate::diff::new_rewrites(&self.resolved, self.lenient_config)) |
| 162 | 162 | .copied() |
| @@ -18,7 +18,8 @@ pub use tree::root::Tree; · 17 unmodified lines | ||
| 18 | 18 | /// |
| 19 | 19 | /// Note that these values won't update even if the underlying file(s) change. |
| 20 | 20 | pub struct Snapshot<'repo> { |
| 21 | − pub(crate) repo: &'repo Repository, | |
| 21 | + /// The owning repository. | |
| 22 | + pub repo: &'repo Repository, | |
| 22 | 23 | } |
| 23 | 24 | |
| 24 | 25 | /// A platform to access configuration values and modify them in memory, while making them available when this platform is dropped |
| @@ -31,13 +32,15 @@ pub struct Snapshot<'repo> { · 6 unmodified lines | ||
| 31 | 32 | // TODO: make it possible to load snapshots with reloading via .config() and write mutated snapshots back to disk which should be the way |
| 32 | 33 | // to affect all instances of a repo, probably via `config_mut()` and `config_mut_at()`. |
| 33 | 34 | pub struct SnapshotMut<'repo> { |
| 34 | − pub(crate) repo: Option<&'repo mut Repository>, | |
| 35 | + /// The owning repository. | |
| 36 | + pub repo: Option<&'repo mut Repository>, | |
| 35 | 37 | pub(crate) config: gix_config::File<'static>, |
| 36 | 38 | } |
| 37 | 39 | |
| 38 | 40 | /// A utility structure created by [`SnapshotMut::commit_auto_rollback()`] that restores the previous configuration on drop. |
| 39 | 41 | pub struct CommitAutoRollback<'repo> { |
| 40 | − pub(crate) repo: Option<&'repo mut Repository>, | |
| 42 | + /// The owning repository. | |
| 43 | + pub repo: Option<&'repo mut Repository>, | |
| 41 | 44 | pub(crate) prev_config: crate::Config, |
| 42 | 45 | } |
| 43 | 46 | |
| @@ -602,7 +605,7 @@ pub(crate) struct Cache { · 558 unmodified lines | ||
| 602 | 605 | pub(crate) url_rewrite: OnceCell<crate::remote::url::Rewrite>, |
| 603 | 606 | /// The lazy-loaded rename information for diffs. |
| 604 | 607 | #[cfg(feature = "blob-diff")] |
| 605 | − pub(crate) diff_renames: OnceCell<Option<crate::diff::Rewrites>>, | |
| 608 | + pub(crate) diff_renames: OnceCell<(Option<crate::diff::Rewrites>, bool)>, | |
| 606 | 609 | /// A lazily loaded mapping to know which url schemes to allow |
| 607 | 610 | #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] |
| 608 | 611 | pub(crate) url_scheme: OnceCell<crate::remote::url::SchemePermission>, |
| @@ -51,7 +51,14 @@ impl Options { · 50 unmodified lines | ||
| 51 | 51 | pub(crate) fn from_configuration(config: &crate::config::Cache) -> Result<Self, options::init::Error> { |
| 52 | 52 | Ok(Options { |
| 53 | 53 | location: Some(Location::Path), |
| 54 | − rewrites: config.diff_renames()?.unwrap_or_default().into(), | |
| 54 | + rewrites: { | |
| 55 | + let (rewrites, is_configured) = config.diff_renames()?; | |
| 56 | + if is_configured { | |
| 57 | + rewrites | |
| 58 | + } else { | |
| 59 | + Some(Default::default()) | |
| 60 | + } | |
| 61 | + }, | |
| 55 | 62 | }) |
| 56 | 63 | } |
| 57 | 64 | } |
| @@ -164,14 +171,14 @@ pub(crate) mod utils { · 106 unmodified lines | ||
| 164 | 171 | } |
| 165 | 172 | |
| 166 | 173 | /// Create an instance by reading all relevant information from the `config`uration, while being `lenient` or not. |
| 167 | − /// Returns `Ok(None)` if nothing is configured. | |
| 174 | + /// Returns `Ok((None, false))` if nothing is configured, or `Ok((None, true))` if it's configured and disabled. | |
| 168 | 175 | /// |
| 169 | 176 | /// Note that missing values will be defaulted similar to what git does. |
| 170 | 177 | #[allow(clippy::result_large_err)] |
| 171 | 178 | pub fn new_rewrites( |
| 172 | 179 | config: &gix_config::File<'static>, |
| 173 | 180 | lenient: bool, |
| 174 | − ) -> Result<Option<Rewrites>, new_rewrites::Error> { | |
| 181 | + ) -> Result<(Option<Rewrites>, bool), new_rewrites::Error> { | |
| 175 | 182 | new_rewrites_inner(config, lenient, &Diff::RENAMES, &Diff::RENAME_LIMIT) |
| 176 | 183 | } |
| 177 | 184 | |
| @@ -180,7 +187,7 @@ pub(crate) mod utils { · 2 unmodified lines | ||
| 180 | 187 | lenient: bool, |
| 181 | 188 | renames: &'static crate::config::tree::diff::Renames, |
| 182 | 189 | rename_limit: &'static crate::config::tree::keys::UnsignedInteger, |
| 183 | − ) -> Result<Option<Rewrites>, new_rewrites::Error> { | |
| 190 | + ) -> Result<(Option<Rewrites>, bool), new_rewrites::Error> { | |
| 184 | 191 | let copies = match config |
| 185 | 192 | .boolean(renames) |
| 186 | 193 | .map(|value| renames.try_into_renames(value)) |
| @@ -188,25 +195,28 @@ pub(crate) mod utils { · 1 unmodified line | ||
| 188 | 195 | .with_leniency(lenient)? |
| 189 | 196 | { |
| 190 | 197 | Some(renames) => match renames { |
| 191 | − Tracking::Disabled => return Ok(None), | |
| 198 | + Tracking::Disabled => return Ok((None, true)), | |
| 192 | 199 | Tracking::Renames => None, |
| 193 | 200 | Tracking::RenamesAndCopies => Some(Copies::default()), |
| 194 | 201 | }, |
| 195 | − None => return Ok(None), | |
| 202 | + None => return Ok((None, false)), | |
| 196 | 203 | }; |
| 197 | 204 | |
| 198 | 205 | let default = Rewrites::default(); |
| 199 | − Ok(Rewrites { | |
| 200 | − copies, | |
| 201 | − limit: config | |
| 202 | − .integer(rename_limit) | |
| 203 | − .map(|value| rename_limit.try_into_usize(value)) | |
| 204 | − .transpose() | |
| 205 | − .with_leniency(lenient)? | |
| 206 | − .unwrap_or(default.limit), | |
| 207 | − ..default | |
| 208 | − } | |
| 209 | − .into()) | |
| 206 | + Ok(( | |
| 207 | + Rewrites { | |
| 208 | + copies, | |
| 209 | + limit: config | |
| 210 | + .integer(rename_limit) | |
| 211 | + .map(|value| rename_limit.try_into_usize(value)) | |
| 212 | + .transpose() | |
| 213 | + .with_leniency(lenient)? | |
| 214 | + .unwrap_or(default.limit), | |
| 215 | + ..default | |
| 216 | + } | |
| 217 | + .into(), | |
| 218 | + true, | |
| 219 | + )) | |
| 210 | 220 | } |
| 211 | 221 | |
| 212 | 222 | /// Return a low-level utility to efficiently prepare a blob-level diff operation between two resources, |
| @@ -107,7 +107,10 @@ pub mod commit { · 106 unmodified lines | ||
| 107 | 107 | /// |
| 108 | 108 | pub mod tree { |
| 109 | 109 | use gix_merge::blob::builtin_driver; |
| 110 | − pub use gix_merge::tree::{Conflict, ContentMerge, Resolution, ResolutionFailure, TreatAsUnresolved}; | |
| 110 | + pub use gix_merge::tree::{ | |
| 111 | + apply_index_entries, treat_as_unresolved, Conflict, ContentMerge, Resolution, ResolutionFailure, | |
| 112 | + TreatAsUnresolved, | |
| 113 | + }; | |
| 111 | 114 | |
| 112 | 115 | /// The outcome produced by [`Repository::merge_trees()`](crate::Repository::merge_trees()). |
| 113 | 116 | #[derive(Clone)] |
| @@ -136,22 +139,19 @@ pub mod tree { · 22 unmodified lines | ||
| 136 | 139 | |
| 137 | 140 | /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a |
| 138 | 141 | /// conflict should be considered unresolved. |
| 139 | − /// It's important that `index` is at the state of [`Self::tree`]. | |
| 140 | − /// | |
| 141 | − /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`. | |
| 142 | 142 | /// |
| 143 | − /// ### Important | |
| 143 | + /// `removal_mode` decides how unconflicted entries should be removed if they are superseded by | |
| 144 | + /// their conflicted counterparts. | |
| 144 | 145 | /// |
| 145 | − /// Also, the unconflicted stage of such entries will be removed merely by setting a flag, so the | |
| 146 | − /// in-memory entry is still present. | |
| 147 | − /// One can prune `index` [in-memory](gix_index::State::remove_entries()) or write it to disk, which will | |
| 148 | − /// cause entries marked for removal not to be persisted. | |
| 146 | + /// It's important that `index` is at the state of [`Self::tree`]. | |
| 147 | + /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`. | |
| 149 | 148 | pub fn index_changed_after_applying_conflicts( |
| 150 | 149 | &self, |
| 151 | 150 | index: &mut gix_index::State, |
| 152 | 151 | how: TreatAsUnresolved, |
| 152 | + removal_mode: apply_index_entries::RemovalMode, | |
| 153 | 153 | ) -> bool { |
| 154 | − gix_merge::tree::apply_index_entries(&self.conflicts, how, index) | |
| 154 | + apply_index_entries(&self.conflicts, how, index, removal_mode) | |
| 155 | 155 | } |
| 156 | 156 | } |
| 157 | 157 | |
| @@ -160,6 +160,7 @@ pub mod tree { · 2 unmodified lines | ||
| 160 | 160 | pub struct Options { |
| 161 | 161 | inner: gix_merge::tree::Options, |
| 162 | 162 | file_favor: Option<FileFavor>, |
| 163 | + tree_favor: Option<TreeFavor>, | |
| 163 | 164 | } |
| 164 | 165 | |
| 165 | 166 | impl From<gix_merge::tree::Options> for Options { |
| @@ -167,6 +168,7 @@ pub mod tree { · 1 unmodified line | ||
| 167 | 168 | Options { |
| 168 | 169 | inner: opts, |
| 169 | 170 | file_favor: None, |
| 171 | + tree_favor: None, | |
| 170 | 172 | } |
| 171 | 173 | } |
| 172 | 174 | } |
| @@ -190,6 +192,7 @@ pub mod tree { · 17 unmodified lines | ||
| 190 | 192 | opts.blob_merge.resolve_binary_with = Some(resolve_binary); |
| 191 | 193 | opts.blob_merge.text.conflict = resolve_text; |
| 192 | 194 | } |
| 195 | + opts.tree_conflicts = value.tree_favor.map(Into::into); | |
| 193 | 196 | opts |
| 194 | 197 | } |
| 195 | 198 | } |
| @@ -202,7 +205,7 @@ pub mod tree { · 6 unmodified lines | ||
| 202 | 205 | /// * binary files |
| 203 | 206 | /// * symlinks (a form of file after all) |
| 204 | 207 | /// |
| 205 | − /// Note that that union merges aren't available as they aren't available for binaries or symlinks. | |
| 208 | + /// Note that *union* merges aren't available as they aren't available for binaries or symlinks. | |
| 206 | 209 | #[derive(Debug, Copy, Clone)] |
| 207 | 210 | pub enum FileFavor { |
| 208 | 211 | /// Choose *our* side in case of a conflict. |
| @@ -215,6 +218,35 @@ pub mod tree { · 6 unmodified lines | ||
| 215 | 218 | Theirs, |
| 216 | 219 | } |
| 217 | 220 | |
| 221 | + /// Control how irreconcilable changes to trees should be resolved. | |
| 222 | + /// | |
| 223 | + /// Examples for such issues are: | |
| 224 | + /// | |
| 225 | + /// * *we*: delete, *they*: modify | |
| 226 | + /// * *we*: rename, *they*: rename to something else | |
| 227 | + /// * *we*: delete, *they*: rename | |
| 228 | + /// | |
| 229 | + /// Use this to control which entries are visible to in the resulting tree. | |
| 230 | + /// Also note that this does not apply to the many tree-related changes are reconcilable. | |
| 231 | + #[derive(Debug, Copy, Clone)] | |
| 232 | + pub enum TreeFavor { | |
| 233 | + /// Choose *our* side in case of a conflict. | |
| 234 | + /// Note that content-merges are *still* performed according to the [FileFavor]. | |
| 235 | + Ours, | |
| 236 | + /// Choose the state of the shared common ancestor, dropping both *ours* and *their* changes. | |
| 237 | + /// Content merges are not performed here. | |
| 238 | + Ancestor, | |
| 239 | + } | |
| 240 | + | |
| 241 | + impl From<TreeFavor> for gix_merge::tree::ResolveWith { | |
| 242 | + fn from(value: TreeFavor) -> Self { | |
| 243 | + match value { | |
| 244 | + TreeFavor::Ours => gix_merge::tree::ResolveWith::Ours, | |
| 245 | + TreeFavor::Ancestor => gix_merge::tree::ResolveWith::Ancestor, | |
| 246 | + } | |
| 247 | + } | |
| 248 | + } | |
| 249 | + | |
| 218 | 250 | /// Builder |
| 219 | 251 | impl Options { |
| 220 | 252 | /// If *not* `None`, rename tracking will be performed when determining the changes of each side of the merge. |
| @@ -233,10 +265,22 @@ pub mod tree { · 12 unmodified lines | ||
| 233 | 265 | |
| 234 | 266 | /// When `None`, the default, both sides will be treated equally, and in case of conflict an unbiased representation |
| 235 | 267 | /// is chosen both for content and for trees, causing a conflict. |
| 236 | − /// When `Some(favor)` one can choose a side to prefer in order to automatically resolve a conflict meaningfully. | |
| 268 | + /// | |
| 269 | + /// With `Some(favor)` one can choose a side to prefer in order to forcefully resolve an otherwise irreconcilable conflict, | |
| 270 | + /// loosing information in the process. | |
| 237 | 271 | pub fn with_file_favor(mut self, file_favor: Option<FileFavor>) -> Self { |
| 238 | 272 | self.file_favor = file_favor; |
| 239 | 273 | self |
| 240 | 274 | } |
| 275 | + | |
| 276 | + /// When `None`, the default, both sides will be treated equally, trying to keep both conflicting changes in the tree, possibly | |
| 277 | + /// by renaming one side to move it out of the way. | |
| 278 | + /// | |
| 279 | + /// With `Some(favor)` one can choose a side to prefer in order to forcefully resolve an otherwise irreconcilable conflict, | |
| 280 | + /// loosing information in the process. | |
| 281 | + pub fn with_tree_favor(mut self, tree_favor: Option<TreeFavor>) -> Self { | |
| 282 | + self.tree_favor = tree_favor; | |
| 283 | + self | |
| 284 | + } | |
| 241 | 285 | } |
| 242 | 286 | } |
| @@ -5,8 +5,8 @@ use crate::Repository; · 4 unmodified lines | ||
| 5 | 5 | pub struct EntryRef<'repo, 'a> { |
| 6 | 6 | /// The actual entry ref we are wrapping. |
| 7 | 7 | pub inner: gix_object::tree::EntryRef<'a>, |
| 8 | − | |
| 9 | − pub(crate) repo: &'repo Repository, | |
| 8 | + /// The owning repository. | |
| 9 | + pub repo: &'repo Repository, | |
| 10 | 10 | } |
| 11 | 11 | |
| 12 | 12 | impl<'repo, 'a> EntryRef<'repo, 'a> { |
| @@ -10,7 +10,8 @@ use crate::{object::find, Id, ObjectDetached, Tree}; · 9 unmodified lines | ||
| 10 | 10 | pub struct Editor<'repo> { |
| 11 | 11 | pub(crate) inner: gix_object::tree::Editor<'repo>, |
| 12 | 12 | pub(crate) validate: gix_validate::path::component::Options, |
| 13 | − pub(crate) repo: &'repo crate::Repository, | |
| 13 | + /// The owning repository. | |
| 14 | + pub repo: &'repo crate::Repository, | |
| 14 | 15 | } |
| 15 | 16 | |
| 16 | 17 | /// Initialization |
| @@ -197,7 +198,8 @@ impl std::fmt::Debug for Tree<'_> { · 180 unmodified lines | ||
| 197 | 198 | #[derive(PartialEq, Debug, Clone)] |
| 198 | 199 | pub struct Entry<'repo> { |
| 199 | 200 | pub(crate) inner: gix_object::tree::Entry, |
| 200 | − pub(crate) repo: &'repo crate::Repository, | |
| 201 | + /// The owning repository. | |
| 202 | + pub repo: &'repo crate::Repository, | |
| 201 | 203 | } |
| 202 | 204 | |
| 203 | 205 | mod entry { |
| @@ -8,7 +8,8 @@ use gix_ref::file::ReferenceExt; · 7 unmodified lines | ||
| 8 | 8 | #[must_use = "Iterators should be obtained from this iterator platform"] |
| 9 | 9 | pub struct Platform<'r> { |
| 10 | 10 | pub(crate) platform: gix_ref::file::iter::Platform<'r>, |
| 11 | − pub(crate) repo: &'r crate::Repository, | |
| 11 | + /// The owning repository. | |
| 12 | + pub repo: &'r crate::Repository, | |
| 12 | 13 | } |
| 13 | 14 | |
| 14 | 15 | /// An iterator over references, with or without filter. |
| @@ -88,27 +88,27 @@ impl Repository { · 87 unmodified lines | ||
| 88 | 88 | |
| 89 | 89 | /// Read all relevant configuration options to instantiate options for use in [`merge_trees()`](Self::merge_trees). |
| 90 | 90 | pub fn tree_merge_options(&self) -> Result<crate::merge::tree::Options, tree_merge_options::Error> { |
| 91 | + let (mut rewrites, mut is_configured) = crate::diff::utils::new_rewrites_inner( | |
| 92 | + &self.config.resolved, | |
| 93 | + self.config.lenient_config, | |
| 94 | + &tree::Merge::RENAMES, | |
| 95 | + &tree::Merge::RENAME_LIMIT, | |
| 96 | + )?; | |
| 97 | + if !is_configured { | |
| 98 | + (rewrites, is_configured) = | |
| 99 | + crate::diff::utils::new_rewrites(&self.config.resolved, self.config.lenient_config)?; | |
| 100 | + } | |
| 101 | + if !is_configured { | |
| 102 | + rewrites = Some(Default::default()); | |
| 103 | + } | |
| 91 | 104 | Ok(gix_merge::tree::Options { |
| 92 | − rewrites: Some( | |
| 93 | − crate::diff::utils::new_rewrites_inner( | |
| 94 | − &self.config.resolved, | |
| 95 | − self.config.lenient_config, | |
| 96 | − &tree::Merge::RENAMES, | |
| 97 | − &tree::Merge::RENAME_LIMIT, | |
| 98 | − )? | |
| 99 | − .map(Ok) | |
| 100 | − .or_else(|| { | |
| 101 | − crate::diff::utils::new_rewrites(&self.config.resolved, self.config.lenient_config).transpose() | |
| 102 | − }) | |
| 103 | − .transpose()? | |
| 104 | − .unwrap_or_default(), | |
| 105 | − ), | |
| 105 | + rewrites, | |
| 106 | 106 | blob_merge: self.blob_merge_options()?, |
| 107 | 107 | blob_merge_command_ctx: self.command_context()?, |
| 108 | 108 | fail_on_conflict: None, |
| 109 | 109 | marker_size_multiplier: 0, |
| 110 | 110 | symlink_conflicts: None, |
| 111 | − allow_lossy_resolution: false, | |
| 111 | + tree_conflicts: None, | |
| 112 | 112 | } |
| 113 | 113 | .into()) |
| 114 | 114 | } |
| @@ -28,5 +28,6 @@ pub struct Spec<'repo> { · 27 unmodified lines | ||
| 28 | 28 | pub(crate) first_ref: Option<gix_ref::Reference>, |
| 29 | 29 | /// The second name of a reference as seen while parsing a `RevSpec`, for completeness. |
| 30 | 30 | pub(crate) second_ref: Option<gix_ref::Reference>, |
| 31 | − pub(crate) repo: &'repo crate::Repository, | |
| 31 | + /// The owning repository. | |
| 32 | + pub repo: &'repo crate::Repository, | |
| 32 | 33 | } |
| @@ -152,7 +152,8 @@ impl<'repo> Info<'repo> { · 151 unmodified lines | ||
| 152 | 152 | /// author or commit messages will be required of *all* commits traversed here, it should be better to avoid trying to load it |
| 153 | 153 | /// by [turning commit-graph support off][Platform::use_commit_graph()]. This certainly is a micro-optimization though. |
| 154 | 154 | pub struct Platform<'repo> { |
| 155 | − pub(crate) repo: &'repo Repository, | |
| 155 | + /// The owning repository. | |
| 156 | + pub repo: &'repo Repository, | |
| 156 | 157 | pub(crate) tips: Vec<ObjectId>, |
| 157 | 158 | pub(crate) prune: Vec<ObjectId>, |
| 158 | 159 | pub(crate) sorting: Sorting, |
| @@ -334,7 +335,8 @@ pub mod iter { · 175 unmodified lines | ||
| 334 | 335 | pub(crate) mod iter_impl { |
| 335 | 336 | /// The iterator returned by [`crate::revision::walk::Platform::all()`]. |
| 336 | 337 | pub struct Walk<'repo> { |
| 337 | − pub(crate) repo: &'repo crate::Repository, | |
| 338 | + /// The owning repository. | |
| 339 | + pub repo: &'repo crate::Repository, | |
| 338 | 340 | pub(crate) inner: Box<dyn Iterator<Item = Result<gix_traverse::commit::Info, super::iter::Error>> + 'repo>, |
| 339 | 341 | } |
| 340 | 342 | |
| @@ -23,7 +23,7 @@ pub use errors::*; · 22 unmodified lines | ||
| 23 | 23 | |
| 24 | 24 | /// A platform maintaining state needed to interact with submodules, created by [`Repository::submodules()]. |
| 25 | 25 | pub(crate) struct SharedState<'repo> { |
| 26 | − pub(crate) repo: &'repo Repository, | |
| 26 | + pub repo: &'repo Repository, | |
| 27 | 27 | pub(crate) modules: ModulesSnapshot, |
| 28 | 28 | is_active: RefCell<Option<IsActiveState>>, |
| 29 | 29 | index: RefCell<Option<IndexPersistedOrInMemory>>, |
| @@ -19,7 +19,8 @@ pub struct Worktree<'repo> { · 18 unmodified lines | ||
| 19 | 19 | pub struct Head<'repo> { |
| 20 | 20 | /// One of various possible states for the HEAD reference |
| 21 | 21 | pub kind: head::Kind, |
| 22 | − pub(crate) repo: &'repo Repository, | |
| 22 | + /// The owning repository. | |
| 23 | + pub repo: &'repo Repository, | |
| 23 | 24 | } |
| 24 | 25 | |
| 25 | 26 | /// An [`ObjectId`] with access to a repository. |
| @@ -27,7 +28,8 @@ pub struct Head<'repo> { · 1 unmodified line | ||
| 27 | 28 | pub struct Id<'r> { |
| 28 | 29 | /// The actual object id |
| 29 | 30 | pub(crate) inner: ObjectId, |
| 30 | − pub(crate) repo: &'r Repository, | |
| 31 | + /// The owning repository. | |
| 32 | + pub repo: &'r Repository, | |
| 31 | 33 | } |
| 32 | 34 | |
| 33 | 35 | /// A decoded object with a reference to its owning repository. |
| @@ -39,7 +41,8 @@ pub struct Object<'repo> { · 5 unmodified lines | ||
| 39 | 41 | pub kind: gix_object::Kind, |
| 40 | 42 | /// The fully decoded object data |
| 41 | 43 | pub data: Vec<u8>, |
| 42 | − pub(crate) repo: &'repo Repository, | |
| 44 | + /// The owning repository. | |
| 45 | + pub repo: &'repo Repository, | |
| 43 | 46 | } |
| 44 | 47 | |
| 45 | 48 | impl Drop for Object<'_> { |
| @@ -55,7 +58,8 @@ pub struct Blob<'repo> { · 9 unmodified lines | ||
| 55 | 58 | pub id: ObjectId, |
| 56 | 59 | /// The blob's data. |
| 57 | 60 | pub data: Vec<u8>, |
| 58 | − pub(crate) repo: &'repo Repository, | |
| 61 | + /// The owning repository. | |
| 62 | + pub repo: &'repo Repository, | |
| 59 | 63 | } |
| 60 | 64 | |
| 61 | 65 | impl Drop for Blob<'_> { |
| @@ -71,7 +75,8 @@ pub struct Tree<'repo> { · 9 unmodified lines | ||
| 71 | 75 | pub id: ObjectId, |
| 72 | 76 | /// The fully decoded tree data |
| 73 | 77 | pub data: Vec<u8>, |
| 74 | − pub(crate) repo: &'repo Repository, | |
| 78 | + /// The owning repository. | |
| 79 | + pub repo: &'repo Repository, | |
| 75 | 80 | } |
| 76 | 81 | |
| 77 | 82 | impl Drop for Tree<'_> { |
| @@ -87,7 +92,8 @@ pub struct Tag<'repo> { · 9 unmodified lines | ||
| 87 | 92 | pub id: ObjectId, |
| 88 | 93 | /// The fully decoded tag data |
| 89 | 94 | pub data: Vec<u8>, |
| 90 | − pub(crate) repo: &'repo Repository, | |
| 95 | + /// The owning repository. | |
| 96 | + pub repo: &'repo Repository, | |
| 91 | 97 | } |
| 92 | 98 | |
| 93 | 99 | impl Drop for Tag<'_> { |
| @@ -103,7 +109,8 @@ pub struct Commit<'repo> { · 9 unmodified lines | ||
| 103 | 109 | pub id: ObjectId, |
| 104 | 110 | /// The fully decoded commit data |
| 105 | 111 | pub data: Vec<u8>, |
| 106 | − pub(crate) repo: &'repo Repository, | |
| 112 | + /// The owning repository. | |
| 113 | + pub repo: &'repo Repository, | |
| 107 | 114 | } |
| 108 | 115 | |
| 109 | 116 | impl Drop for Commit<'_> { |
| @@ -132,7 +139,8 @@ pub struct ObjectDetached { · 22 unmodified lines | ||
| 132 | 139 | pub struct Reference<'r> { |
| 133 | 140 | /// The actual reference data |
| 134 | 141 | pub inner: gix_ref::Reference, |
| 135 | − pub(crate) repo: &'r Repository, | |
| 142 | + /// The owning repository. | |
| 143 | + pub repo: &'r Repository, | |
| 136 | 144 | } |
| 137 | 145 | |
| 138 | 146 | /// A thread-local handle to interact with a repository from a single thread. |
| @@ -219,7 +227,8 @@ pub struct Remote<'repo> { · 80 unmodified lines | ||
| 219 | 227 | // pub(crate) prune: bool, |
| 220 | 228 | // /// Delete tags that don't exist on the remote anymore, equivalent to pruning the refspec `refs/tags/*:refs/tags/*`. |
| 221 | 229 | // pub(crate) prune_tags: bool, |
| 222 | − pub(crate) repo: &'repo Repository, | |
| 230 | + /// The owning repository. | |
| 231 | + pub repo: &'repo Repository, | |
| 223 | 232 | } |
| 224 | 233 | |
| 225 | 234 | /// A utility to make matching against pathspecs simple. |
| @@ -231,7 +240,8 @@ pub struct Remote<'repo> { · 5 unmodified lines | ||
| 231 | 240 | #[derive(Clone)] |
| 232 | 241 | #[cfg(feature = "attributes")] |
| 233 | 242 | pub struct Pathspec<'repo> { |
| 234 | − pub(crate) repo: &'repo Repository, | |
| 243 | + /// The owning repository. | |
| 244 | + pub repo: &'repo Repository, | |
| 235 | 245 | /// The cache to power attribute access. It's only initialized if we have a pattern with attributes. |
| 236 | 246 | pub(crate) stack: Option<gix_worktree::Stack>, |
| 237 | 247 | /// The prepared search to use for checking matches. |
| @@ -261,6 +271,7 @@ pub struct Submodule<'repo> { · 23 unmodified lines | ||
| 261 | 271 | /// A utility to access `.gitattributes` and `.gitignore` information efficiently. |
| 262 | 272 | #[cfg(any(feature = "attributes", feature = "excludes"))] |
| 263 | 273 | pub struct AttributeStack<'repo> { |
| 264 | − pub(crate) repo: &'repo Repository, | |
| 274 | + /// The owning repository. | |
| 275 | + pub repo: &'repo Repository, | |
| 265 | 276 | pub(crate) inner: gix_worktree::Stack, |
| 266 | 277 | } |
| @@ -165,6 +165,7 @@ mod track_rewrites { · 164 unmodified lines | ||
| 165 | 165 | }), |
| 166 | 166 | limit: 1000, |
| 167 | 167 | percentage: Some(0.5), |
| 168 | + track_empty: false, | |
| 168 | 169 | }; |
| 169 | 170 | let out = from |
| 170 | 171 | .changes()? |
| @@ -172,9 +172,13 @@ pub fn main() -> Result<()> { · 171 unmodified lines | ||
| 172 | 172 | }, |
| 173 | 173 | ), |
| 174 | 174 | merge::SubCommands::Tree { |
| 175 | − in_memory, | |
| 176 | − file_favor, | |
| 177 | − debug, | |
| 175 | + opts: | |
| 176 | + merge::SharedOptions { | |
| 177 | + in_memory, | |
| 178 | + file_favor, | |
| 179 | + tree_favor, | |
| 180 | + debug, | |
| 181 | + }, | |
| 178 | 182 | ours, |
| 179 | 183 | base, |
| 180 | 184 | theirs, |
| @@ -197,15 +201,20 @@ pub fn main() -> Result<()> { · 16 unmodified lines | ||
| 197 | 201 | format, |
| 198 | 202 | file_favor: file_favor.map(Into::into), |
| 199 | 203 | in_memory, |
| 204 | + tree_favor: tree_favor.map(Into::into), | |
| 200 | 205 | debug, |
| 201 | 206 | }, |
| 202 | 207 | ) |
| 203 | 208 | }, |
| 204 | 209 | ), |
| 205 | 210 | merge::SubCommands::Commit { |
| 206 | − in_memory, | |
| 207 | − file_favor, | |
| 208 | − debug, | |
| 211 | + opts: | |
| 212 | + merge::SharedOptions { | |
| 213 | + in_memory, | |
| 214 | + file_favor, | |
| 215 | + tree_favor, | |
| 216 | + debug, | |
| 217 | + }, | |
| 209 | 218 | ours, |
| 210 | 219 | theirs, |
| 211 | 220 | } => prepare_and_run( |
| @@ -225,6 +234,7 @@ pub fn main() -> Result<()> { · 13 unmodified lines | ||
| 225 | 234 | core::repository::merge::tree::Options { |
| 226 | 235 | format, |
| 227 | 236 | file_favor: file_favor.map(Into::into), |
| 237 | + tree_favor: tree_favor.map(Into::into), | |
| 228 | 238 | in_memory, |
| 229 | 239 | debug, |
| 230 | 240 | }, |
| @@ -375,6 +375,14 @@ pub mod merge { · 374 unmodified lines | ||
| 375 | 375 | Theirs, |
| 376 | 376 | } |
| 377 | 377 | |
| 378 | + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] | |
| 379 | + pub enum TreeFavor { | |
| 380 | + /// Use only the previous tree entry in case of conflict. | |
| 381 | + Ancestor, | |
| 382 | + /// Use only ours tree entry in case of conflict. | |
| 383 | + Ours, | |
| 384 | + } | |
| 385 | + | |
| 378 | 386 | impl From<FileFavor> for gix::merge::tree::FileFavor { |
| 379 | 387 | fn from(value: FileFavor) -> Self { |
| 380 | 388 | match value { |
| @@ -384,6 +392,33 @@ pub mod merge { · 3 unmodified lines | ||
| 384 | 392 | } |
| 385 | 393 | } |
| 386 | 394 | |
| 395 | + impl From<TreeFavor> for gix::merge::tree::TreeFavor { | |
| 396 | + fn from(value: TreeFavor) -> Self { | |
| 397 | + match value { | |
| 398 | + TreeFavor::Ancestor => gix::merge::tree::TreeFavor::Ancestor, | |
| 399 | + TreeFavor::Ours => gix::merge::tree::TreeFavor::Ours, | |
| 400 | + } | |
| 401 | + } | |
| 402 | + } | |
| 403 | + | |
| 404 | + #[derive(Debug, clap::Parser)] | |
| 405 | + pub struct SharedOptions { | |
| 406 | + /// Keep all objects to be written in memory to avoid any disk IO. | |
| 407 | + /// | |
| 408 | + /// Note that the resulting tree won't be available or inspectable. | |
| 409 | + #[clap(long, short = 'm')] | |
| 410 | + pub in_memory: bool, | |
| 411 | + /// Decide how to resolve content conflicts in files. If unset, write conflict markers and fail. | |
| 412 | + #[clap(long, short = 'f')] | |
| 413 | + pub file_favor: Option<FileFavor>, | |
| 414 | + /// Decide how to resolve conflicts in trees, i.e. modification/deletion. If unset, try to preserve both states and fail. | |
| 415 | + #[clap(long, short = 't')] | |
| 416 | + pub tree_favor: Option<TreeFavor>, | |
| 417 | + /// Print additional information about conflicts for debugging. | |
| 418 | + #[clap(long, short = 'd')] | |
| 419 | + pub debug: bool, | |
| 420 | + } | |
| 421 | + | |
| 387 | 422 | #[derive(Debug, clap::Parser)] |
| 388 | 423 | #[command(about = "perform merges of various kinds")] |
| 389 | 424 | pub struct Platform { |
| @@ -412,17 +447,8 @@ pub mod merge { · 22 unmodified lines | ||
| 412 | 447 | |
| 413 | 448 | /// Merge a tree by specifying ours, base and theirs, writing it to the object database. |
| 414 | 449 | Tree { |
| 415 | − /// Keep all objects to be written in memory to avoid any disk IO. | |
| 416 | − /// | |
| 417 | − /// Note that the resulting tree won't be available or inspectable. | |
| 418 | − #[clap(long, short = 'm')] | |
| 419 | − in_memory: bool, | |
| 420 | − /// Decide how to resolve content conflicts in files. If unset, write conflict markers and fail. | |
| 421 | − #[clap(long, short = 'f')] | |
| 422 | − file_favor: Option<FileFavor>, | |
| 423 | − /// Print additional information about conflicts for debugging. | |
| 424 | − #[clap(long, short = 'd')] | |
| 425 | − debug: bool, | |
| 450 | + #[clap(flatten)] | |
| 451 | + opts: SharedOptions, | |
| 426 | 452 | |
| 427 | 453 | /// A revspec to our treeish. |
| 428 | 454 | #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)] |
| @@ -436,17 +462,8 @@ pub mod merge { · 7 unmodified lines | ||
| 436 | 462 | }, |
| 437 | 463 | /// Merge a commits by specifying ours, and theirs, writing the tree to the object database. |
| 438 | 464 | Commit { |
| 439 | − /// Keep all objects to be written in memory to avoid any disk IO. | |
| 440 | − /// | |
| 441 | − /// Note that the resulting tree won't be available or inspectable. | |
| 442 | − #[clap(long, short = 'm')] | |
| 443 | − in_memory: bool, | |
| 444 | − /// Decide how to resolve content conflicts in files. If unset, write conflict markers and fail. | |
| 445 | − #[clap(long, short = 'f')] | |
| 446 | − file_favor: Option<FileFavor>, | |
| 447 | − /// Print additional information about conflicts for debugging. | |
| 448 | − #[clap(long, short = 'd')] | |
| 449 | − debug: bool, | |
| 465 | + #[clap(flatten)] | |
| 466 | + opts: SharedOptions, | |
| 450 | 467 | |
| 451 | 468 | /// A revspec to our committish. |
| 452 | 469 | #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)] |
| @@ -62,6 +62,10 @@ struct Record { · 61 unmodified lines | ||
| 62 | 62 | } |
| 63 | 63 | |
| 64 | 64 | static GIT_CONFIG: &[Record] = &[ |
| 65 | + Record { | |
| 66 | + config: "attr.tree", | |
| 67 | + usage: Planned("Decides where to read git attributes from - this will affect APIs") | |
| 68 | + }, | |
| 65 | 69 | Record { |
| 66 | 70 | config: "core.symlinks", |
| 67 | 71 | usage: Planned("Needed to handle checkouts faithfully") |
| @@ -521,11 +521,18 @@ fn scripted_fixture_read_only_with_args_inner( · 520 unmodified lines | ||
| 521 | 521 | |d| (true, d.to_owned()), |
| 522 | 522 | ); |
| 523 | 523 | |
| 524 | − let _marker = gix_lock::Marker::acquire_to_hold_resource( | |
| 525 | − script_basename, | |
| 526 | − gix_lock::acquire::Fail::AfterDurationWithBackoff(Duration::from_secs(6 * 60)), | |
| 527 | − None, | |
| 528 | − )?; | |
| 524 | + // We may that destination_dir is already unique (i.e. temp-dir) - thus there is no need for a lock, | |
| 525 | + // and we can execute scripts in parallel. | |
| 526 | + let _marker = destination_dir | |
| 527 | + .is_none() | |
| 528 | + .then(|| { | |
| 529 | + gix_lock::Marker::acquire_to_hold_resource( | |
| 530 | + script_basename, | |
| 531 | + gix_lock::acquire::Fail::AfterDurationWithBackoff(Duration::from_secs(6 * 60)), | |
| 532 | + None, | |
| 533 | + ) | |
| 534 | + }) | |
| 535 | + .transpose()?; | |
| 529 | 536 | let failure_marker = script_result_directory.join("_invalid_state_due_to_script_failure_"); |
| 530 | 537 | if force_run || !script_result_directory.is_dir() || failure_marker.is_file() { |
| 531 | 538 | if failure_marker.is_file() { |