tree merge with tree-related auto-resolution
Unified · Split
| @@ -2792,6 +2792,7 @@ dependencies = [ · 2791 unmodified lines | |||
| 2792 | "gix-features 0.39.1", | 2792 | "gix-features 0.39.1", |
| 2793 | "gix-path 0.10.13", | 2793 | "gix-path 0.10.13", |
| 2794 | "gix-testtools", | 2794 | "gix-testtools", |
| 2795 | "percent-encoding", | ||
| 2795 | "serde", | 2796 | "serde", |
| 2796 | "thiserror 2.0.3", | 2797 | "thiserror 2.0.3", |
| 2797 | "url", | 2798 | "url", |
| @@ -352,8 +352,11 @@ Check out the [performance discussion][gix-diff-performance] as well. · 351 unmodified lines | |||
| 352 | - [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same) | 352 | - [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same) |
| 353 | * [x] **tree**-diff-heuristics match Git for its test-cases | 353 | * [x] **tree**-diff-heuristics match Git for its test-cases |
| 354 | - [x] a way to generate an index with stages, mostly conforming with Git. | 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 | - [ ] submodule merges (*right now they count as conflicts if they differ*) | 356 | - [ ] submodule merges (*right now they count as conflicts if they differ*) |
| 356 | - [ ] assure sparse indices are handled correctly during application - right now we refuse. | 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 | * [x] **commits** - with handling of multiple merge bases by recursive merge-base merge | 360 | * [x] **commits** - with handling of multiple merge bases by recursive merge-base merge |
| 358 | * [x] API documentation | 361 | * [x] API documentation |
| 359 | * [ ] Examples | 362 | * [ ] Examples |
| @@ -139,7 +139,12 @@ pub fn update( · 138 unmodified lines | |||
| 139 | }); | 139 | }); |
| 140 | 140 | ||
| 141 | let rewrites = { | 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 | r.copies = Some(gix::diff::rewrites::Copies { | 148 | r.copies = Some(gix::diff::rewrites::Copies { |
| 144 | source: if find_copies_harder { | 149 | source: if find_copies_harder { |
| 145 | CopySource::FromSetOfModifiedFilesAndAllSources | 150 | CopySource::FromSetOfModifiedFilesAndAllSources |
| @@ -17,6 +17,7 @@ pub fn commit( · 16 unmodified lines | |||
| 17 | Options { | 17 | Options { |
| 18 | format, | 18 | format, |
| 19 | file_favor, | 19 | file_favor, |
| 20 | tree_favor, | ||
| 20 | in_memory, | 21 | in_memory, |
| 21 | debug, | 22 | debug, |
| 22 | }: Options, | 23 | }: Options, |
| @@ -31,7 +32,10 @@ pub fn commit( · 8 unmodified lines | |||
| 31 | let (ours_ref, ours_id) = refname_and_commit(&repo, ours)?; | 32 | let (ours_ref, ours_id) = refname_and_commit(&repo, ours)?; |
| 32 | let (theirs_ref, theirs_id) = refname_and_commit(&repo, theirs)?; | 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 | let ours_id_str = ours_id.to_string(); | 39 | let ours_id_str = ours_id.to_string(); |
| 36 | let theirs_id_str = theirs_id.to_string(); | 40 | let theirs_id_str = theirs_id.to_string(); |
| 37 | let labels = gix::merge::blob::builtin_driver::text::Labels { | 41 | let labels = gix::merge::blob::builtin_driver::text::Labels { |
| @@ -49,7 +53,7 @@ pub fn commit( · 11 unmodified lines | |||
| 49 | .merge_commits(ours_id, theirs_id, labels, options.into())? | 53 | .merge_commits(ours_id, theirs_id, labels, options.into())? |
| 50 | .tree_merge; | 54 | .tree_merge; |
| 51 | let has_conflicts = res.conflicts.is_empty(); | 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 | let _span = gix::trace::detail!("Writing merged tree"); | 58 | let _span = gix::trace::detail!("Writing merged tree"); |
| 55 | let mut written = 0; | 59 | let mut written = 0; |
| @@ -3,6 +3,7 @@ use crate::OutputFormat; · 2 unmodified lines | |||
| 3 | pub struct Options { | 3 | pub struct Options { |
| 4 | pub format: OutputFormat, | 4 | pub format: OutputFormat, |
| 5 | pub file_favor: Option<gix::merge::tree::FileFavor>, | 5 | pub file_favor: Option<gix::merge::tree::FileFavor>, |
| 6 | pub tree_favor: Option<gix::merge::tree::TreeFavor>, | ||
| 6 | pub in_memory: bool, | 7 | pub in_memory: bool, |
| 7 | pub debug: bool, | 8 | pub debug: bool, |
| 8 | } | 9 | } |
| @@ -29,6 +30,7 @@ pub(super) mod function { · 20 unmodified lines | |||
| 29 | Options { | 30 | Options { |
| 30 | format, | 31 | format, |
| 31 | file_favor, | 32 | file_favor, |
| 33 | tree_favor, | ||
| 32 | in_memory, | 34 | in_memory, |
| 33 | debug, | 35 | debug, |
| 34 | }: Options, | 36 | }: Options, |
| @@ -44,7 +46,10 @@ pub(super) mod function { · 9 unmodified lines | |||
| 44 | let (ours_ref, ours_id) = refname_and_tree(&repo, ours)?; | 46 | let (ours_ref, ours_id) = refname_and_tree(&repo, ours)?; |
| 45 | let (theirs_ref, theirs_id) = refname_and_tree(&repo, theirs)?; | 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 | let base_id_str = base_id.to_string(); | 53 | let base_id_str = base_id.to_string(); |
| 49 | let ours_id_str = ours_id.to_string(); | 54 | let ours_id_str = ours_id.to_string(); |
| 50 | let theirs_id_str = theirs_id.to_string(); | 55 | let theirs_id_str = theirs_id.to_string(); |
| @@ -64,7 +69,7 @@ pub(super) mod function { · 13 unmodified lines | |||
| 64 | }; | 69 | }; |
| 65 | let res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?; | 70 | let res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?; |
| 66 | let has_conflicts = res.conflicts.is_empty(); | 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 | let _span = gix::trace::detail!("Writing merged tree"); | 74 | let _span = gix::trace::detail!("Writing merged tree"); |
| 70 | let mut written = 0; | 75 | let mut written = 0; |
| @@ -81,6 +81,7 @@ pub fn show( · 80 unmodified lines | |||
| 81 | copies: None, | 81 | copies: None, |
| 82 | percentage: Some(percentage), | 82 | percentage: Some(percentage), |
| 83 | limit: 0, | 83 | limit: 0, |
| 84 | track_empty: false, | ||
| 84 | }); | 85 | }); |
| 85 | if opts.rewrites.is_some() { | 86 | if opts.rewrites.is_some() { |
| 86 | if let Some(opts) = opts.dirwalk_options.as_mut() { | 87 | if let Some(opts) = opts.dirwalk_options.as_mut() { |
| @@ -37,6 +37,11 @@ pub struct Rewrites { · 36 unmodified lines | |||
| 37 | /// If the limit would not be enough to test the entire set of combinations, the algorithm will trade in precision and not | 37 | /// If the limit would not be enough to test the entire set of combinations, the algorithm will trade in precision and not |
| 38 | /// run the fuzzy version of identity tests at all. That way results are never partial. | 38 | /// run the fuzzy version of identity tests at all. That way results are never partial. |
| 39 | pub limit: usize, | 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 | /// Contains a [Tracker](rewrites::Tracker) to detect rewrites. | 47 | /// Contains a [Tracker](rewrites::Tracker) to detect rewrites. |
| @@ -70,6 +70,7 @@ impl Default for Rewrites { · 69 unmodified lines | |||
| 70 | copies: None, | 70 | copies: None, |
| 71 | percentage: Some(0.5), | 71 | percentage: Some(0.5), |
| 72 | limit: 1000, | 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 | CopySource::FromSetOfModifiedFiles => {} | 286 | CopySource::FromSetOfModifiedFiles => {} |
| 287 | CopySource::FromSetOfModifiedFilesAndAllSources => { | 287 | CopySource::FromSetOfModifiedFilesAndAllSources => { |
| 288 | push_source_tree(&mut |change, location| { | 288 | push_source_tree(&mut |change, location| { |
| 289 | assert!( | 289 | if self.try_push_change(change, location).is_none() { |
| 290 | self.try_push_change(change, location).is_none(), | 290 | // make sure these aren't viable to be emitted anymore. |
| 291 | "we must accept every change" | 291 | self.items.last_mut().expect("just pushed").emitted = true; |
| 292 | ); | 292 | } |
| 293 | // make sure these aren't viable to be emitted anymore. | ||
| 294 | self.items.last_mut().expect("just pushed").emitted = true; | ||
| 295 | }) | 293 | }) |
| 296 | .map_err(|err| emit::Error::GetItemsForExhaustiveCopyDetection(Box::new(err)))?; | 294 | .map_err(|err| emit::Error::GetItemsForExhaustiveCopyDetection(Box::new(err)))?; |
| 297 | self.items.sort_by(by_id_and_location); | 295 | self.items.sort_by(by_id_and_location); |
| @@ -341,6 +339,10 @@ impl<T: Change> Tracker<T> { · 43 unmodified lines | |||
| 341 | ) -> Result<(), emit::Error> { | 339 | ) -> Result<(), emit::Error> { |
| 342 | // we try to cheaply reduce the set of possibilities first, before possibly looking more exhaustively. | 340 | // we try to cheaply reduce the set of possibilities first, before possibly looking more exhaustively. |
| 343 | let needs_second_pass = !needs_exact_match(percentage); | 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 | if self.match_pairs(cb, None /* by identity */, kind, out, diff_cache, objects, filter)? == Action::Cancel { | 346 | if self.match_pairs(cb, None /* by identity */, kind, out, diff_cache, objects, filter)? == Action::Cancel { |
| 345 | return Ok(()); | 347 | return Ok(()); |
| 346 | } | 348 | } |
| @@ -384,10 +386,35 @@ impl<T: Change> Tracker<T> { · 37 unmodified lines | |||
| 384 | filter: Option<fn(&T) -> bool>, | 386 | filter: Option<fn(&T) -> bool>, |
| 385 | ) -> Result<Action, emit::Error> { | 387 | ) -> Result<Action, emit::Error> { |
| 386 | let mut dest_ofs = 0; | 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 | while let Some((mut dest_idx, dest)) = self.items[dest_ofs..].iter().enumerate().find_map(|(idx, item)| { | 402 | while let Some((mut dest_idx, dest)) = self.items[dest_ofs..].iter().enumerate().find_map(|(idx, item)| { |
| 388 | (!item.emitted | 403 | (!item.emitted |
| 389 | && matches!(item.change.kind(), ChangeKind::Addition) | 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 | .then_some((idx, item)) | 418 | .then_some((idx, item)) |
| 392 | }) { | 419 | }) { |
| 393 | dest_idx += dest_ofs; | 420 | dest_idx += dest_ofs; |
| @@ -403,6 +430,7 @@ impl<T: Change> Tracker<T> { · 9 unmodified lines | |||
| 403 | objects, | 430 | objects, |
| 404 | diff_cache, | 431 | diff_cache, |
| 405 | &self.path_backing, | 432 | &self.path_backing, |
| 433 | &mut num_checks, | ||
| 406 | )? | 434 | )? |
| 407 | .map(|(src_idx, src, diff)| { | 435 | .map(|(src_idx, src, diff)| { |
| 408 | let (id, entry_mode) = src.change.id_and_entry_mode(); | 436 | let (id, entry_mode) = src.change.id_and_entry_mode(); |
| @@ -420,6 +448,12 @@ impl<T: Change> Tracker<T> { · 11 unmodified lines | |||
| 420 | src_idx, | 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 | let Some((src, src_idx)) = src else { | 457 | let Some((src, src_idx)) = src else { |
| 424 | continue; | 458 | continue; |
| 425 | }; | 459 | }; |
| @@ -631,6 +665,7 @@ fn find_match<'a, T: Change>( · 205 unmodified lines | |||
| 631 | objects: &impl gix_object::FindObjectOrHeader, | 665 | objects: &impl gix_object::FindObjectOrHeader, |
| 632 | diff_cache: &mut crate::blob::Platform, | 666 | diff_cache: &mut crate::blob::Platform, |
| 633 | path_backing: &[u8], | 667 | path_backing: &[u8], |
| 668 | num_checks: &mut usize, | ||
| 634 | ) -> Result<Option<SourceTuple<'a, T>>, emit::Error> { | 669 | ) -> Result<Option<SourceTuple<'a, T>>, emit::Error> { |
| 635 | let (item_id, item_mode) = item.change.id_and_entry_mode(); | 670 | let (item_id, item_mode) = item.change.id_and_entry_mode(); |
| 636 | if needs_exact_match(percentage) || item_mode.is_link() { | 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 | let res = items[range.clone()].iter().enumerate().find_map(|(mut src_idx, src)| { | 687 | let res = items[range.clone()].iter().enumerate().find_map(|(mut src_idx, src)| { |
| 653 | src_idx += range.start; | 688 | src_idx += range.start; |
| 689 | *num_checks += 1; | ||
| 654 | (src_idx != item_idx && src.is_source_for_destination_of(kind, item_mode)).then_some((src_idx, src, None)) | 690 | (src_idx != item_idx && src.is_source_for_destination_of(kind, item_mode)).then_some((src_idx, src, None)) |
| 655 | }); | 691 | }); |
| 656 | if let Some(src) = res { | 692 | if let Some(src) = res { |
| @@ -685,6 +721,7 @@ fn find_match<'a, T: Change>( · 28 unmodified lines | |||
| 685 | )?; | 721 | )?; |
| 686 | let prep = diff_cache.prepare_diff()?; | 722 | let prep = diff_cache.prepare_diff()?; |
| 687 | stats.num_similarity_checks += 1; | 723 | stats.num_similarity_checks += 1; |
| 724 | *num_checks += 1; | ||
| 688 | match prep.operation { | 725 | match prep.operation { |
| 689 | Operation::InternalDiff { algorithm } => { | 726 | Operation::InternalDiff { algorithm } => { |
| 690 | let tokens = | 727 | let tokens = |
| @@ -27,6 +27,7 @@ fn rename_by_id() -> crate::Result { · 26 unmodified lines | |||
| 27 | copies: None, | 27 | copies: None, |
| 28 | percentage: None, | 28 | percentage: None, |
| 29 | limit, | 29 | limit, |
| 30 | track_empty: false, | ||
| 30 | }; | 31 | }; |
| 31 | let mut track = util::new_tracker(rewrites); | 32 | let mut track = util::new_tracker(rewrites); |
| 32 | assert!( | 33 | assert!( |
| @@ -80,6 +81,7 @@ fn copy_by_similarity_reports_limit_if_encountered() -> crate::Result { · 47 unmodified lines | |||
| 80 | }), | 81 | }), |
| 81 | percentage: None, | 82 | percentage: None, |
| 82 | limit: 1, | 83 | limit: 1, |
| 84 | track_empty: false, | ||
| 83 | }; | 85 | }; |
| 84 | let mut track = util::new_tracker(rewrites); | 86 | let mut track = util::new_tracker(rewrites); |
| 85 | let odb = util::add_retained_blobs( | 87 | let odb = util::add_retained_blobs( |
| @@ -132,6 +134,7 @@ fn copy_by_id() -> crate::Result { · 46 unmodified lines | |||
| 132 | }), | 134 | }), |
| 133 | percentage: None, | 135 | percentage: None, |
| 134 | limit, | 136 | limit, |
| 137 | track_empty: false, | ||
| 135 | }; | 138 | }; |
| 136 | let mut track = util::new_tracker(rewrites); | 139 | let mut track = util::new_tracker(rewrites); |
| 137 | let odb = util::add_retained_blobs( | 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 | percentage: None, | 210 | percentage: None, |
| 208 | limit, | 211 | limit, |
| 212 | track_empty: false, | ||
| 209 | }; | 213 | }; |
| 210 | let mut track = util::new_tracker(rewrites); | 214 | let mut track = util::new_tracker(rewrites); |
| 211 | let odb = util::add_retained_blobs( | 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 | percentage: None, | 289 | percentage: None, |
| 286 | limit: 0, | 290 | limit: 0, |
| 291 | track_empty: false, | ||
| 287 | }; | 292 | }; |
| 288 | let mut track = util::new_tracker(rewrites); | 293 | let mut track = util::new_tracker(rewrites); |
| 289 | let odb = util::add_retained_blobs( | 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 | percentage: None, | 369 | percentage: None, |
| 365 | limit: 0, | 370 | limit: 0, |
| 371 | track_empty: false, | ||
| 366 | }; | 372 | }; |
| 367 | let mut track = util::new_tracker(rewrites); | 373 | let mut track = util::new_tracker(rewrites); |
| 368 | let odb = util::add_retained_blobs( | 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 | copies: None, | 419 | copies: None, |
| 414 | percentage: Some(0.5), | 420 | percentage: Some(0.5), |
| 415 | limit: 1, | 421 | limit: 1, |
| 422 | track_empty: false, | ||
| 416 | }; | 423 | }; |
| 417 | let mut track = util::new_tracker(rewrites); | 424 | let mut track = util::new_tracker(rewrites); |
| 418 | let odb = util::add_retained_blobs( | 425 | let odb = util::add_retained_blobs( |
| @@ -458,6 +465,7 @@ fn rename_by_50_percent_similarity() -> crate::Result { · 39 unmodified lines | |||
| 458 | copies: None, | 465 | copies: None, |
| 459 | percentage: Some(0.5), | 466 | percentage: Some(0.5), |
| 460 | limit: 0, | 467 | limit: 0, |
| 468 | track_empty: false, | ||
| 461 | }; | 469 | }; |
| 462 | let mut track = util::new_tracker(rewrites); | 470 | let mut track = util::new_tracker(rewrites); |
| 463 | let odb = util::add_retained_blobs( | 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 | copies: None, | 555 | copies: None, |
| 548 | percentage: Some(0.5), | 556 | percentage: Some(0.5), |
| 549 | limit: 0, | 557 | limit: 0, |
| 558 | track_empty: false, | ||
| 550 | }; | 559 | }; |
| 551 | let mut track = util::new_tracker(rename_by_similarity); | 560 | let mut track = util::new_tracker(rename_by_similarity); |
| 552 | let tree_dst_id = 1; | 561 | let tree_dst_id = 1; |
| @@ -638,6 +647,7 @@ fn simple_directory_rename_by_id() -> crate::Result { · 85 unmodified lines | |||
| 638 | copies: None, | 647 | copies: None, |
| 639 | percentage: None, | 648 | percentage: None, |
| 640 | limit: 0, | 649 | limit: 0, |
| 650 | track_empty: false, | ||
| 641 | }; | 651 | }; |
| 642 | let mut track = util::new_tracker(renames_by_identity); | 652 | let mut track = util::new_tracker(renames_by_identity); |
| 643 | let tree_dst_id = 1; | 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 | #[test] | 193 | #[test] |
| 194 | fn renames_by_identity() -> crate::Result { | 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 | "c3-modification", | 197 | "c3-modification", |
| 198 | "r1-identity", | 198 | "r1-identity", |
| 199 | vec![BStr::new("a"), "dir/a-moved".into()], | 199 | vec![BStr::new("a"), "dir/a-moved".into()], |
| 200 | "one rename and nothing else", | 200 | "one rename and nothing else", |
| 201 | false, | ||
| 201 | ), | 202 | ), |
| 202 | ( | 203 | ( |
| 203 | "c4 - add identical files", | 204 | "c4 - add identical files", |
| @@ -211,24 +212,35 @@ fn renames_by_identity() -> crate::Result { · 7 unmodified lines | |||
| 211 | "z".into(), | 212 | "z".into(), |
| 212 | ], | 213 | ], |
| 213 | "multiple possible sources decide by ordering everything lexicographically", | 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 | "c5 - add links", | 225 | "c5 - add links", |
| 217 | "r4-symlinks", | 226 | "r4-symlinks", |
| 218 | vec!["link-1".into(), "renamed-link-1".into()], | 227 | vec!["link-1".into(), "renamed-link-1".into()], |
| 219 | "symlinks are only tracked by identity", | 228 | "symlinks are only tracked by identity", |
| 229 | false, | ||
| 220 | ), | 230 | ), |
| 221 | ( | 231 | ( |
| 222 | "r1-identity", | 232 | "r1-identity", |
| 223 | "c4 - add identical files", | 233 | "c4 - add identical files", |
| 224 | vec![], | 234 | vec![], |
| 225 | "not having any renames is OK as well", | 235 | "not having any renames is OK as well", |
| 236 | false, | ||
| 226 | ), | 237 | ), |
| 227 | ( | 238 | ( |
| 228 | "tc1-identity", | 239 | "tc1-identity", |
| 229 | "tc1-identity", | 240 | "tc1-identity", |
| 230 | vec![], | 241 | vec![], |
| 231 | "copy tracking is off by default", | 242 | "copy tracking is off by default", |
| 243 | false, | ||
| 232 | ), | 244 | ), |
| 233 | ] { | 245 | ] { |
| 234 | for percentage in [None, Some(0.5)] { | 246 | for percentage in [None, Some(0.5)] { |
| @@ -239,6 +251,7 @@ fn renames_by_identity() -> crate::Result { · 4 unmodified lines | |||
| 239 | location: Some(Location::Path), | 251 | location: Some(Location::Path), |
| 240 | rewrites: Some(Rewrites { | 252 | rewrites: Some(Rewrites { |
| 241 | percentage, | 253 | percentage, |
| 254 | track_empty, | ||
| 242 | ..Default::default() | 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 | ..Default::default() | 717 | ..Default::default() |
| 705 | }), | 718 | }), |
| 706 | limit: 2, // similarity checks can't be made that way | 719 | limit: 2, // similarity checks can't be made that way |
| 720 | track_empty: false, | ||
| 707 | ..Default::default() | 721 | ..Default::default() |
| 708 | }), | 722 | }), |
| 709 | }, | 723 | }, |
| @@ -833,6 +847,7 @@ fn realistic_renames_by_identity() -> crate::Result { · 123 unmodified lines | |||
| 833 | rewrites: Some(Rewrites { | 847 | rewrites: Some(Rewrites { |
| 834 | copies: Some(Copies::default()), | 848 | copies: Some(Copies::default()), |
| 835 | limit: 1, | 849 | limit: 1, |
| 850 | track_empty: true, | ||
| 836 | ..Default::default() | 851 | ..Default::default() |
| 837 | }), | 852 | }), |
| 838 | }, | 853 | }, |
| @@ -1324,6 +1339,7 @@ fn realistic_renames_by_identity_3() -> crate::Result { · 485 unmodified lines | |||
| 1324 | rewrites: Some(Rewrites { | 1339 | rewrites: Some(Rewrites { |
| 1325 | copies: Some(Copies::default()), | 1340 | copies: Some(Copies::default()), |
| 1326 | limit: 1, | 1341 | limit: 1, |
| 1342 | track_empty: true, | ||
| 1327 | ..Default::default() | 1343 | ..Default::default() |
| 1328 | }), | 1344 | }), |
| 1329 | }, | 1345 | }, |
| @@ -1402,6 +1418,7 @@ fn realistic_renames_2() -> crate::Result { · 72 unmodified lines | |||
| 1402 | rewrites: Some(Rewrites { | 1418 | rewrites: Some(Rewrites { |
| 1403 | copies: Some(Copies::default()), | 1419 | copies: Some(Copies::default()), |
| 1404 | limit: 1, | 1420 | limit: 1, |
| 1421 | track_empty: false, | ||
| 1405 | ..Default::default() | 1422 | ..Default::default() |
| 1406 | }), | 1423 | }), |
| 1407 | }, | 1424 | }, |
| @@ -1665,6 +1682,7 @@ fn realistic_renames_3_without_identity() -> crate::Result { · 257 unmodified lines | |||
| 1665 | copies: None, | 1682 | copies: None, |
| 1666 | percentage: None, | 1683 | percentage: None, |
| 1667 | limit: 0, | 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 | use crate::blob::Resolution; | 25 | use crate::blob::Resolution; |
| 26 | 26 | ||
| 27 | /// As this algorithm doesn't look at the actual data, it returns a choice solely based on logic. | 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 | pub fn merge(on_conflict: Option<ResolveWith>) -> (Pick, Resolution) { | 32 | pub fn merge(on_conflict: Option<ResolveWith>) -> (Pick, Resolution) { |
| 31 | match on_conflict { | 33 | match on_conflict { |
| 32 | None => (Pick::Ours, Resolution::Conflict), | 34 | None => (Pick::Ours, Resolution::Conflict), |
| @@ -36,7 +38,7 @@ pub(super) mod function { · 3 unmodified lines | |||
| 36 | ResolveWith::Theirs => Pick::Theirs, | 38 | ResolveWith::Theirs => Pick::Theirs, |
| 37 | ResolveWith::Ancestor => Pick::Ancestor, | 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 | /// That way it becomes clearer where the content of conflicts are originating from. | 57 | /// That way it becomes clearer where the content of conflicts are originating from. |
| 58 | #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] | 58 | #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] |
| 59 | pub struct Labels<'a> { | 59 | pub struct Labels<'a> { |
| 60 | /// The label for the common *ancestor*. | ||
| 60 | pub ancestor: Option<&'a BStr>, | 61 | pub ancestor: Option<&'a BStr>, |
| 62 | /// The label for the *current* (or *our*) side. | ||
| 61 | pub current: Option<&'a BStr>, | 63 | pub current: Option<&'a BStr>, |
| 64 | /// The label for the *other* (or *their*) side. | ||
| 62 | pub other: Option<&'a BStr>, | 65 | pub other: Option<&'a BStr>, |
| 63 | } | 66 | } |
| 64 | 67 | ||
| @@ -16,8 +16,9 @@ pub fn hunks_differ_in_diff3( · 15 unmodified lines | |||
| 16 | return true; | 16 | return true; |
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | let tokens_for_hunk = | 19 | let tokens_for_hunk = |hunk: &Hunk| -> &[imara_diff::intern::Token] { |
| 20 | |hunk: &Hunk| -> &[imara_diff::intern::Token] { tokens_for_side(hunk.side, input, current_tokens) }; | 20 | &tokens_for_side(hunk.side, input, current_tokens)[hunk.after.start as usize..hunk.after.end as usize] |
| 21 | }; | ||
| 21 | 22 | ||
| 22 | a.iter() | 23 | a.iter() |
| 23 | .flat_map(tokens_for_hunk) | 24 | .flat_map(tokens_for_hunk) |
| @@ -329,13 +329,21 @@ pub(super) mod inner { · 328 unmodified lines | |||
| 329 | (Pick::Buffer, resolution) | 329 | (Pick::Buffer, resolution) |
| 330 | } | 330 | } |
| 331 | BuiltinDriver::Binary => { | 331 | BuiltinDriver::Binary => { |
| 332 | let (pick, resolution) = builtin_driver::binary(self.options.resolve_binary_with); | 332 | // easier to reason about the 'split' compared to merging both conditions |
| 333 | let pick = match pick { | 333 | #[allow(clippy::if_same_then_else)] |
| 334 | builtin_driver::binary::Pick::Ours => Pick::Ours, | 334 | if !(self.current.id.is_null() || self.other.id.is_null()) && self.current.id == self.other.id { |
| 335 | builtin_driver::binary::Pick::Theirs => Pick::Theirs, | 335 | (Pick::Ours, Resolution::Complete) |
| 336 | builtin_driver::binary::Pick::Ancestor => Pick::Ancestor, | 336 | } else if (self.current.id.is_null() || self.other.id.is_null()) && ours == theirs { |
| 337 | }; | 337 | (Pick::Ours, Resolution::Complete) |
| 338 | (pick, resolution) | 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 | BuiltinDriver::Union => { | 348 | BuiltinDriver::Union => { |
| 341 | let resolution = builtin_driver::text( | 349 | let resolution = builtin_driver::text( |
| @@ -55,5 +55,6 @@ pub struct Outcome<'a> { · 54 unmodified lines | |||
| 55 | 55 | ||
| 56 | pub(super) mod function; | 56 | pub(super) mod function; |
| 57 | 57 | ||
| 58 | /// | ||
| 58 | pub mod virtual_merge_base; | 59 | pub mod virtual_merge_base; |
| 59 | pub use virtual_merge_base::function::virtual_merge_base; | 60 | pub use virtual_merge_base::function::virtual_merge_base; |
| @@ -30,7 +30,7 @@ pub enum Error { · 29 unmodified lines | |||
| 30 | pub(super) mod function { | 30 | pub(super) mod function { |
| 31 | use super::Error; | 31 | use super::Error; |
| 32 | use crate::blob::builtin_driver; | 32 | use crate::blob::builtin_driver; |
| 33 | use crate::tree::TreatAsUnresolved; | 33 | use crate::tree::{treat_as_unresolved, TreatAsUnresolved}; |
| 34 | use gix_object::FindExt; | 34 | use gix_object::FindExt; |
| 35 | 35 | ||
| 36 | /// Create a single virtual merge-base by merging `first_commit`, `second_commit` and `others` into one. | 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 | let mut merged_commit_id = first_commit; | 55 | let mut merged_commit_id = first_commit; |
| 56 | others.push(second_commit); | 56 | others.push(second_commit); |
| 57 | 57 | ||
| 58 | options.allow_lossy_resolution = true; | 58 | options.tree_conflicts = Some(crate::tree::ResolveWith::Ancestor); |
| 59 | options.blob_merge.is_virtual_ancestor = true; | 59 | options.blob_merge.is_virtual_ancestor = true; |
| 60 | options.blob_merge.text.conflict = builtin_driver::text::Conflict::ResolveWithOurs; | 60 | options.blob_merge.text.conflict = builtin_driver::text::Conflict::ResolveWithOurs; |
| 61 | let favor_ancestor = Some(builtin_driver::binary::ResolveWith::Ancestor); | 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 | // This shouldn't happen, but if for some buggy reason it does, we rather bail. | 88 | // This shouldn't happen, but if for some buggy reason it does, we rather bail. |
| 89 | if out | 89 | if out.tree_merge.has_unresolved_conflicts(TreatAsUnresolved { |
| 90 | .tree_merge | 90 | content_merge: treat_as_unresolved::ContentMerge::Markers, |
| 91 | .has_unresolved_conflicts(TreatAsUnresolved::ConflictMarkers) | 91 | tree_merge: treat_as_unresolved::TreeMerge::Undecidable, |
| 92 | { | 92 | }) { |
| 93 | return Err(Error::VirtualMergeBaseConflict.into()); | 93 | return Err(Error::VirtualMergeBaseConflict.into()); |
| 94 | } | 94 | } |
| 95 | let merged_tree_id = out | 95 | let merged_tree_id = out |
| @@ -4,6 +4,7 @@ · 3 unmodified lines | |||
| 4 | //! * [tree-merges](mod@tree) look at trees and merge them structurally, triggering blob-merges as needed. | 4 | //! * [tree-merges](mod@tree) look at trees and merge them structurally, triggering blob-merges as needed. |
| 5 | //! * [commit-merges](mod@commit) are like tree merges, but compute or create the merge-base on the fly. | 5 | //! * [commit-merges](mod@commit) are like tree merges, but compute or create the merge-base on the fly. |
| 6 | #![deny(rust_2018_idioms)] | 6 | #![deny(rust_2018_idioms)] |
| 7 | #![deny(missing_docs)] | ||
| 7 | #![forbid(unsafe_code)] | 8 | #![forbid(unsafe_code)] |
| 8 | 9 | ||
| 9 | /// | 10 | /// |
| @@ -5,7 +5,7 @@ use crate::tree::utils::{ · 4 unmodified lines | |||
| 5 | use crate::tree::ConflictMapping::{Original, Swapped}; | 5 | use crate::tree::ConflictMapping::{Original, Swapped}; |
| 6 | use crate::tree::{ | 6 | use crate::tree::{ |
| 7 | Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, ContentMerge, Error, Options, Outcome, | 7 | Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, ContentMerge, Error, Options, Outcome, |
| 8 | Resolution, ResolutionFailure, | 8 | Resolution, ResolutionFailure, ResolveWith, |
| 9 | }; | 9 | }; |
| 10 | use bstr::{BString, ByteSlice}; | 10 | use bstr::{BString, ByteSlice}; |
| 11 | use gix_diff::tree::recorder::Location; | 11 | use gix_diff::tree::recorder::Location; |
| @@ -13,6 +13,7 @@ use gix_diff::tree_with_rewrites::Change; · 1 unmodified line | |||
| 13 | use gix_hash::ObjectId; | 13 | use gix_hash::ObjectId; |
| 14 | use gix_object::tree::{EntryKind, EntryMode}; | 14 | use gix_object::tree::{EntryKind, EntryMode}; |
| 15 | use gix_object::{tree, FindExt}; | 15 | use gix_object::{tree, FindExt}; |
| 16 | use std::borrow::Cow; | ||
| 16 | use std::convert::Infallible; | 17 | use std::convert::Infallible; |
| 17 | 18 | ||
| 18 | /// Perform a merge between `our_tree` and `their_tree`, using `base_tree` as merge-base. | 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 | let _span = gix_trace::coarse!("gix_merge::tree", ?base_tree, ?our_tree, ?their_tree, ?labels); | 72 | let _span = gix_trace::coarse!("gix_merge::tree", ?base_tree, ?our_tree, ?their_tree, ?labels); |
| 72 | let (mut base_buf, mut side_buf) = (Vec::new(), Vec::new()); | 73 | let (mut base_buf, mut side_buf) = (Vec::new(), Vec::new()); |
| 73 | let ancestor_tree = objects.find_tree(base_tree, &mut base_buf)?; | 74 | let ancestor_tree = objects.find_tree(base_tree, &mut base_buf)?; |
| 74 | let allow_resolution_failure = !options.allow_lossy_resolution; | ||
| 75 | |||
| 76 | let mut editor = tree::Editor::new(ancestor_tree.to_owned(), objects, base_tree.kind()); | 75 | let mut editor = tree::Editor::new(ancestor_tree.to_owned(), objects, base_tree.kind()); |
| 77 | let ancestor_tree = gix_object::TreeRefIter::from_bytes(&base_buf); | 76 | let ancestor_tree = gix_object::TreeRefIter::from_bytes(&base_buf); |
| 77 | let tree_conflicts = options.tree_conflicts; | ||
| 78 | 78 | ||
| 79 | let mut our_changes = Vec::new(); | 79 | let mut our_changes = Vec::new(); |
| 80 | if ours_needs_diff { | 80 | if ours_needs_diff { |
| @@ -128,7 +128,12 @@ where · 47 unmodified lines | |||
| 128 | 128 | ||
| 129 | let mut conflicts = Vec::new(); | 129 | let mut conflicts = Vec::new(); |
| 130 | let mut failed_on_first_conflict = false; | 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 | if let Some(how) = options.fail_on_conflict { | 137 | if let Some(how) = options.fail_on_conflict { |
| 133 | if conflict.resolution.is_err() || conflict.is_unresolved(how) { | 138 | if conflict.resolution.is_err() || conflict.is_unresolved(how) { |
| 134 | failed_on_first_conflict = true; | 139 | failed_on_first_conflict = true; |
| @@ -238,7 +243,7 @@ where · 103 unmodified lines | |||
| 238 | PossibleConflict::TreeToNonTree { change_idx: Some(idx) } | 243 | PossibleConflict::TreeToNonTree { change_idx: Some(idx) } |
| 239 | if matches!( | 244 | if matches!( |
| 240 | our_changes[idx].inner, | 245 | our_changes[idx].inner, |
| 241 | Change::Deletion { .. } | Change::Addition { .. } | 246 | Change::Deletion { .. } | Change::Addition { .. } | Change::Rewrite { .. } |
| 242 | ) => | 247 | ) => |
| 243 | { | 248 | { |
| 244 | (Some(idx), Some(MatchKind::EraseTree)) | 249 | (Some(idx), Some(MatchKind::EraseTree)) |
| @@ -255,34 +260,76 @@ where · 10 unmodified lines | |||
| 255 | PossibleConflict::Match { change_idx } | 260 | PossibleConflict::Match { change_idx } |
| 256 | | PossibleConflict::PassedRewrittenDirectory { change_idx } => Some(change_idx), | 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 | if let Some(ours) = ours { | 265 | if let Some(ours) = ours { |
| 261 | gix_trace::debug!("Turning a case we could probably handle into a conflict for now. theirs: {theirs:#?} ours: {ours:#?} kind: {match_kind:?}"); | 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 | 267 | let conflict = Conflict::unknown((&ours.inner, theirs, Original, outer_side)); |
| 263 | && should_fail_on_conflict(Conflict::unknown(( | 268 | if let Some(ResolveWith::Ours) = tree_conflicts { |
| 264 | &ours.inner, | 269 | apply_our_resolution(&ours.inner, theirs, outer_side, &mut editor)?; |
| 265 | theirs, | 270 | *match outer_side { |
| 266 | Original, | 271 | Original => &mut ours.was_written, |
| 267 | outer_side, | 272 | Swapped => &mut their_changes[theirs_idx].was_written, |
| 268 | ))) | 273 | } = true; |
| 269 | { | 274 | } |
| 275 | if should_fail_on_conflict(conflict) { | ||
| 270 | break 'outer; | 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 | } else { | 326 | } else { |
| 274 | gix_trace::debug!("Couldn't figure out how to handle {match_kind:?} theirs: {theirs:#?} candidate: {candidate:#?}"); | 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 | let ours = &our_changes[ours_idx].inner; | 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 | match (ours, theirs) { | 333 | match (ours, theirs) { |
| 287 | ( | 334 | ( |
| 288 | Change::Modification { | 335 | Change::Modification { |
| @@ -326,7 +373,7 @@ where · 37 unmodified lines | |||
| 326 | Swapped | 373 | Swapped |
| 327 | }; | 374 | }; |
| 328 | if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { | 375 | if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { |
| 329 | assert_eq!( | 376 | debug_assert_eq!( |
| 330 | previous_id, their_source_id, | 377 | previous_id, their_source_id, |
| 331 | "both refer to the same base, so should always match" | 378 | "both refer to the same base, so should always match" |
| 332 | ); | 379 | ); |
| @@ -407,9 +454,23 @@ where · 74 unmodified lines | |||
| 407 | (new_change, None), | 454 | (new_change, None), |
| 408 | pick_our_changes_mut(side, their_changes, our_changes), | 455 | pick_our_changes_mut(side, their_changes, our_changes), |
| 409 | ); | 456 | ); |
| 410 | } else if allow_resolution_failure { | 457 | } else { |
| 411 | editor.upsert(toc(our_location), our_mode.kind(), *our_id)?; | 458 | match tree_conflicts { |
| 412 | editor.upsert(toc(their_location), their_mode.kind(), *their_id)?; | 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 | if should_fail_on_conflict(Conflict::without_resolution( | 475 | if should_fail_on_conflict(Conflict::without_resolution( |
| 415 | ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch, | 476 | ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch, |
| @@ -516,7 +577,7 @@ where · 100 unmodified lines | |||
| 516 | &options, | 577 | &options, |
| 517 | )?; | 578 | )?; |
| 518 | editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; | 579 | editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; |
| 519 | Some(Conflict::with_resolution( | 580 | Conflict::with_resolution( |
| 520 | Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { | 581 | Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { |
| 521 | merged_blob: ContentMerge { | 582 | merged_blob: ContentMerge { |
| 522 | resolution, | 583 | resolution, |
| @@ -525,8 +586,8 @@ where · 2 unmodified lines | |||
| 525 | }, | 586 | }, |
| 526 | (ours, theirs, Original, outer_side), | 587 | (ours, theirs, Original, outer_side), |
| 527 | [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], | 588 | [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], |
| 528 | )) | 589 | ) |
| 529 | } else if allow_resolution_failure { | 590 | } else { |
| 530 | // Actually this has a preference, as symlinks are always left in place with the other side renamed. | 591 | // Actually this has a preference, as symlinks are always left in place with the other side renamed. |
| 531 | let ( | 592 | let ( |
| 532 | logical_side, | 593 | logical_side, |
| @@ -555,8 +616,7 @@ where · 22 unmodified lines | |||
| 555 | tree_with_rename, | 616 | tree_with_rename, |
| 556 | label_of_side_to_be_moved, | 617 | label_of_side_to_be_moved, |
| 557 | )?; | 618 | )?; |
| 558 | editor.upsert(toc(location), our_mode.kind(), our_id)?; | 619 | let mut conflict = Conflict::without_resolution( |
| 559 | let conflict = Conflict::without_resolution( | ||
| 560 | ResolutionFailure::OursAddedTheirsAddedTypeMismatch { | 620 | ResolutionFailure::OursAddedTheirsAddedTypeMismatch { |
| 561 | their_unique_location: renamed_location.clone(), | 621 | their_unique_location: renamed_location.clone(), |
| 562 | }, | 622 | }, |
| @@ -567,27 +627,45 @@ where · 4 unmodified lines | |||
| 567 | index_entry_at_path(&their_mode, &their_id, their_path_hint), | 627 | index_entry_at_path(&their_mode, &their_id, their_path_hint), |
| 568 | ], | 628 | ], |
| 569 | ); | 629 | ); |
| 570 | 630 | match tree_conflicts { | |
| 571 | let new_change = Change::Addition { | 631 | None => { |
| 572 | location: renamed_location, | 632 | let new_change = Change::Addition { |
| 573 | entry_mode: their_mode, | 633 | location: renamed_location, |
| 574 | id: their_id, | 634 | entry_mode: their_mode, |
| 575 | relation: None, | 635 | id: their_id, |
| 576 | }; | 636 | relation: None, |
| 577 | tree_with_rename.remove_existing_leaf(location.as_bstr()); | 637 | }; |
| 578 | push_deferred( | 638 | editor.upsert(toc(location), our_mode.kind(), our_id)?; |
| 579 | (new_change, None), | 639 | tree_with_rename.remove_existing_leaf(location.as_bstr()); |
| 580 | pick_our_changes_mut(logical_side, their_changes, our_changes), | 640 | push_deferred( |
| 581 | ); | 641 | (new_change, None), |
| 582 | Some(conflict) | 642 | pick_our_changes_mut(logical_side, their_changes, our_changes), |
| 583 | } else { | 643 | ); |
| 584 | None | 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 { | 667 | if should_fail_on_conflict(conflict) { |
| 588 | if should_fail_on_conflict(conflict) { | 668 | break 'outer; |
| 589 | break 'outer; | ||
| 590 | }; | ||
| 591 | } | 669 | } |
| 592 | } | 670 | } |
| 593 | ( | 671 | ( |
| @@ -609,7 +687,7 @@ where · 15 unmodified lines | |||
| 609 | previous_entry_mode, | 687 | previous_entry_mode, |
| 610 | previous_id, | 688 | previous_id, |
| 611 | }, | 689 | }, |
| 612 | ) if allow_resolution_failure => { | 690 | ) => { |
| 613 | let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) { | 691 | let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) { |
| 614 | (labels.current.unwrap_or_default(), Original) | 692 | (labels.current.unwrap_or_default(), Original) |
| 615 | } else { | 693 | } else { |
| @@ -622,62 +700,135 @@ where · 6 unmodified lines | |||
| 622 | }; | 700 | }; |
| 623 | change_on_right | 701 | change_on_right |
| 624 | .map(|change| { | 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 | .unwrap_or_default() | 707 | .unwrap_or_default() |
| 628 | }; | 708 | }; |
| 629 | 709 | ||
| 630 | if deletion_prefaces_addition_of_directory { | 710 | let should_break = if deletion_prefaces_addition_of_directory { |
| 631 | let our_tree = pick_our_tree(side, our_tree, their_tree); | 711 | let entries = [ |
| 632 | let renamed_path = unique_path_in_tree( | 712 | index_entry(previous_entry_mode, previous_id), |
| 633 | location.as_bstr(), | 713 | index_entry(entry_mode, id), |
| 634 | &editor, | 714 | None, |
| 635 | our_tree, | 715 | ]; |
| 636 | label_of_side_to_be_moved, | 716 | match tree_conflicts { |
| 637 | )?; | 717 | None => { |
| 638 | editor.remove(toc(location))?; | 718 | let our_tree = pick_our_tree(side, our_tree, their_tree); |
| 639 | our_tree.remove_existing_leaf(location.as_bstr()); | 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 { | 728 | let new_change = Change::Addition { |
| 642 | location: renamed_path.clone(), | 729 | location: renamed_path.clone(), |
| 643 | relation: None, | 730 | relation: None, |
| 644 | entry_mode: *entry_mode, | 731 | entry_mode: *entry_mode, |
| 645 | id: *id, | 732 | id: *id, |
| 646 | }; | 733 | }; |
| 647 | let should_break = should_fail_on_conflict(Conflict::without_resolution( | 734 | let should_break = should_fail_on_conflict(Conflict::without_resolution( |
| 648 | ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { | 735 | ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { |
| 649 | renamed_unique_path_to_modified_blob: renamed_path, | 736 | renamed_unique_path_to_modified_blob: renamed_path, |
| 650 | }, | 737 | }, |
| 651 | (ours, theirs, side, outer_side), | 738 | (ours, theirs, side, outer_side), |
| 652 | [ | 739 | entries, |
| 653 | index_entry(previous_entry_mode, previous_id), | 740 | )); |
| 654 | index_entry(entry_mode, id), | ||
| 655 | None, | ||
| 656 | ], | ||
| 657 | )); | ||
| 658 | 741 | ||
| 659 | // Since we move *our* side, our tree needs to be modified. | 742 | // Since we move *our* side, our tree needs to be modified. |
| 660 | push_deferred( | 743 | push_deferred( |
| 661 | (new_change, None), | 744 | (new_change, None), |
| 662 | pick_our_changes_mut(side, our_changes, their_changes), | 745 | pick_our_changes_mut(side, our_changes, their_changes), |
| 663 | ); | 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 { | 790 | match ours { |
| 666 | break 'outer; | 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 { | 802 | should_fail_on_conflict(Conflict::without_resolution( |
| 669 | let should_break = should_fail_on_conflict(Conflict::without_resolution( | ||
| 670 | ResolutionFailure::OursModifiedTheirsDeleted, | 803 | ResolutionFailure::OursModifiedTheirsDeleted, |
| 671 | (ours, theirs, side, outer_side), | 804 | (ours, theirs, side, outer_side), |
| 672 | [ | 805 | entries, |
| 673 | index_entry(previous_entry_mode, previous_id), | 806 | )) |
| 674 | index_entry(entry_mode, id), | 807 | }; |
| 675 | None, | 808 | if should_break { |
| 676 | ], | 809 | break 'outer; |
| 677 | )); | 810 | }; |
| 678 | editor.upsert(toc(location), entry_mode.kind(), *id)?; | 811 | } |
| 679 | if should_break { | 812 | ( |
| 680 | break 'outer; | 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 | let merged_mode = | 871 | let merged_mode = |
| 721 | merge_modes(*our_mode, *their_mode).expect("this case was assured earlier"); | 872 | merge_modes(*our_mode, *their_mode).expect("this case was assured earlier"); |
| 722 | 873 | ||
| 723 | editor.remove(toc(source_location))?; | 874 | if matches!(tree_conflicts, None | Some(ResolveWith::Ours)) { |
| 724 | our_tree.remove_existing_leaf(source_location.as_bstr()); | 875 | editor.remove(toc(source_location))?; |
| 725 | their_tree.remove_existing_leaf(source_location.as_bstr()); | 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 = | 880 | let their_location = |
| 728 | possibly_rewritten_location(our_tree, their_location.as_bstr(), our_changes); | 881 | possibly_rewritten_location(our_tree, their_location.as_bstr(), our_changes) |
| 729 | let our_rewritten_location = | 882 | .map_or(Cow::Borrowed(their_location.as_bstr()), Cow::Owned); |
| 730 | possibly_rewritten_location(their_tree, our_location.as_bstr(), their_changes); | 883 | let our_location = |
| 731 | let (our_addition, their_addition) = | 884 | possibly_rewritten_location(their_tree, our_location.as_bstr(), their_changes) |
| 732 | match (our_rewritten_location, their_rewritten_location) { | 885 | .map_or(Cow::Borrowed(our_location.as_bstr()), Cow::Owned); |
| 733 | (None, Some(location)) => ( | 886 | let (our_addition, their_addition) = if our_location == their_location { |
| 734 | None, | 887 | ( |
| 735 | Some(Change::Addition { | 888 | None, |
| 736 | location, | 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 | relation: None, | 929 | relation: None, |
| 738 | entry_mode: merged_mode, | 930 | entry_mode: merged_mode, |
| 739 | id: merged_blob_id, | 931 | id: merged_blob_id, |
| 740 | }), | 932 | }; |
| 741 | ), | 933 | let their_addition = Change::Addition { |
| 742 | (Some(location), None) => ( | 934 | location: their_location.into_owned(), |
| 743 | None, | ||
| 744 | Some(Change::Addition { | ||
| 745 | location, | ||
| 746 | relation: None, | 935 | relation: None, |
| 747 | entry_mode: merged_mode, | 936 | entry_mode: merged_mode, |
| 748 | id: merged_blob_id, | 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; | 939 | (Some(our_addition), Some(their_addition)) |
| 765 | our_changes[ours_idx].was_written = true; | ||
| 766 | continue; | ||
| 767 | } | 940 | } |
| 768 | (None, None) => { | 941 | Some(ResolveWith::Ancestor) => (None, None), |
| 769 | if our_location == their_location { | 942 | Some(ResolveWith::Ours) => { |
| 770 | ( | 943 | let our_addition = Change::Addition { |
| 771 | None, | 944 | location: match outer_side { |
| 772 | Some(Change::Addition { | 945 | Original => our_location, |
| 773 | location: our_location.to_owned(), | 946 | Swapped => their_location, |
| 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; | ||
| 784 | } | 947 | } |
| 785 | if should_fail_on_conflict(Conflict::without_resolution( | 948 | .into_owned(), |
| 786 | ResolutionFailure::OursRenamedTheirsRenamedDifferently { | 949 | relation: None, |
| 787 | merged_blob: resolution.take().map(|resolution| ContentMerge { | 950 | entry_mode: merged_mode, |
| 788 | resolution, | 951 | id: merged_blob_id, |
| 789 | merged_blob_id, | 952 | }; |
| 790 | }), | 953 | (Some(our_addition), None) |
| 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 | } | ||
| 827 | } | 954 | } |
| 828 | }; | 955 | } |
| 956 | }; | ||
| 829 | 957 | ||
| 830 | if let Some(resolution) = resolution { | 958 | if let Some(resolution) = resolution { |
| 831 | if should_fail_on_conflict(Conflict::with_resolution( | 959 | if should_fail_on_conflict(Conflict::with_resolution( |
| @@ -883,16 +1011,21 @@ where · 51 unmodified lines | |||
| 883 | .. | 1011 | .. |
| 884 | }, | 1012 | }, |
| 885 | Change::Deletion { .. }, | 1013 | Change::Deletion { .. }, |
| 886 | ) if !rewritten_mode.is_commit() && allow_resolution_failure => { | 1014 | ) if !rewritten_mode.is_commit() => { |
| 887 | let side = if matches!(ours, Change::Deletion { .. }) { | 1015 | let side = if matches!(ours, Change::Deletion { .. }) { |
| 888 | Original | 1016 | Original |
| 889 | } else { | 1017 | } else { |
| 890 | Swapped | 1018 | Swapped |
| 891 | }; | 1019 | }; |
| 892 | 1020 | ||
| 893 | editor.remove(toc(source_location))?; | 1021 | match tree_conflicts { |
| 894 | pick_our_tree(side, our_tree, their_tree) | 1022 | None | Some(ResolveWith::Ours) => { |
| 895 | .remove_existing_leaf(source_location.as_bstr()); | 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 | let their_rewritten_location = possibly_rewritten_location( | 1030 | let their_rewritten_location = possibly_rewritten_location( |
| 898 | pick_our_tree(side, our_tree, their_tree), | 1031 | pick_our_tree(side, our_tree, their_tree), |
| @@ -923,10 +1056,15 @@ where · 24 unmodified lines | |||
| 923 | break 'outer; | 1056 | break 'outer; |
| 924 | }; | 1057 | }; |
| 925 | 1058 | ||
| 926 | push_deferred( | 1059 | let ours_is_rewrite = side.is_swapped(); |
| 927 | (our_addition, None), | 1060 | if tree_conflicts.is_none() |
| 928 | pick_our_changes_mut(side, their_changes, our_changes), | 1061 | || (matches!(tree_conflicts, Some(ResolveWith::Ours)) && ours_is_rewrite) |
| 929 | ); | 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 | Change::Rewrite { | 1070 | Change::Rewrite { |
| @@ -941,6 +1079,7 @@ where · 8 unmodified lines | |||
| 941 | Change::Addition { | 1079 | Change::Addition { |
| 942 | id: their_id, | 1080 | id: their_id, |
| 943 | entry_mode: their_mode, | 1081 | entry_mode: their_mode, |
| 1082 | location: add_location, | ||
| 944 | .. | 1083 | .. |
| 945 | }, | 1084 | }, |
| 946 | ) | 1085 | ) |
| @@ -948,6 +1087,7 @@ where · 1 unmodified line | |||
| 948 | Change::Addition { | 1087 | Change::Addition { |
| 949 | id: their_id, | 1088 | id: their_id, |
| 950 | entry_mode: their_mode, | 1089 | entry_mode: their_mode, |
| 1090 | location: add_location, | ||
| 951 | .. | 1091 | .. |
| 952 | }, | 1092 | }, |
| 953 | Change::Rewrite { | 1093 | Change::Rewrite { |
| @@ -1005,9 +1145,17 @@ where · 51 unmodified lines | |||
| 1005 | // Because this constellation can only be found by the lookup tree, there is | 1145 | // Because this constellation can only be found by the lookup tree, there is |
| 1006 | // no need to put it as addition, we know it's not going to intersect on the other side. | 1146 | // no need to put it as addition, we know it's not going to intersect on the other side. |
| 1007 | editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; | 1147 | editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; |
| 1008 | } else if allow_resolution_failure { | 1148 | } else { |
| 1009 | editor.remove(toc(source_location))?; | 1149 | // We always remove the source from the tree - it might be re-added later. |
| 1010 | pick_our_tree(side, our_tree, their_tree).remove_leaf(source_location.as_bstr()); | 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 | let ( | 1160 | let ( |
| 1013 | logical_side, | 1161 | logical_side, |
| @@ -1036,7 +1184,13 @@ where · 22 unmodified lines | |||
| 1036 | tree_with_rename, | 1184 | tree_with_rename, |
| 1037 | label_of_side_to_be_moved, | 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 | let conflict = Conflict::without_resolution( | 1194 | let conflict = Conflict::without_resolution( |
| 1041 | ResolutionFailure::OursAddedTheirsAddedTypeMismatch { | 1195 | ResolutionFailure::OursAddedTheirsAddedTypeMismatch { |
| 1042 | their_unique_location: renamed_location.clone(), | 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 { | 1206 | if tree_conflicts.is_none() { |
| 1053 | location: renamed_location, | 1207 | let new_change_with_rename = Change::Addition { |
| 1054 | entry_mode: their_mode, | 1208 | location: renamed_location, |
| 1055 | id: their_id, | 1209 | entry_mode: their_mode, |
| 1056 | relation: None, | 1210 | id: their_id, |
| 1057 | }; | 1211 | relation: None, |
| 1058 | tree_with_rename.remove_existing_leaf(location.as_bstr()); | 1212 | }; |
| 1059 | push_deferred( | 1213 | push_deferred( |
| 1060 | ( | 1214 | ( |
| 1061 | new_change_with_rename, | 1215 | new_change_with_rename, |
| 1062 | Some(pick_idx(logical_side, theirs_idx, ours_idx)), | 1216 | Some(pick_idx(logical_side, theirs_idx, ours_idx)), |
| 1063 | ), | 1217 | ), |
| 1064 | pick_our_changes_mut(logical_side, their_changes, our_changes), | 1218 | pick_our_changes_mut(logical_side, their_changes, our_changes), |
| 1065 | ); | 1219 | ); |
| 1220 | } | ||
| 1066 | 1221 | ||
| 1067 | if should_fail_on_conflict(conflict) { | 1222 | if should_fail_on_conflict(conflict) { |
| 1068 | break 'outer; | 1223 | break 'outer; |
| @@ -1070,9 +1225,16 @@ where · 1 unmodified line | |||
| 1070 | } | 1225 | } |
| 1071 | } | 1226 | } |
| 1072 | _unknown => { | 1227 | _unknown => { |
| 1073 | if allow_resolution_failure | 1228 | debug_assert!( |
| 1074 | && should_fail_on_conflict(Conflict::unknown((ours, theirs, Original, outer_side))) | 1229 | match_kind.is_none() |
| 1075 | { | 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 | break 'outer; | 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 | fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool { | 1276 | fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool { |
| 1102 | a.is_commit() || b.is_commit() | 1277 | a.is_commit() || b.is_commit() |
| 1103 | } | 1278 | } |
| @@ -39,40 +39,77 @@ pub struct Outcome<'a> { · 38 unmodified lines | |||
| 39 | /// Use [`has_unresolved_conflicts()`](Outcome::has_unresolved_conflicts()) to see if any action is needed | 39 | /// Use [`has_unresolved_conflicts()`](Outcome::has_unresolved_conflicts()) to see if any action is needed |
| 40 | /// before using [`tree`](Outcome::tree). | 40 | /// before using [`tree`](Outcome::tree). |
| 41 | pub conflicts: Vec<Conflict>, | 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 | /// This also makes this outcome a very partial merge that cannot be completed. | 44 | /// This also makes this outcome a very partial merge that cannot be completed. |
| 44 | /// Only set if [`fail_on_conflict`](Options::fail_on_conflict) is `true`. | 45 | /// Only set if [`fail_on_conflict`](Options::fail_on_conflict) is `true`. |
| 45 | pub failed_on_first_unresolved_conflict: bool, | 46 | pub failed_on_first_unresolved_conflict: bool, |
| 46 | } | 47 | } |
| 47 | 48 | ||
| 48 | /// Determine what should be considered an unresolved conflict. | 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 | 59 | pub mod treat_as_unresolved { |
| 51 | /// [resolution failure](`ResolutionFailure`) will always be unresolved. | 60 | use crate::tree::TreatAsUnresolved; |
| 52 | /// | 61 | |
| 53 | /// Also, when one side was modified but the other side renamed it, this will not | 62 | /// Which kind of content merges should be considered unresolved? |
| 54 | /// be considered a conflict, even if a non-conflicting merge happened. | 63 | #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] |
| 55 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] | 64 | pub enum ContentMerge { |
| 56 | pub enum TreatAsUnresolved { | 65 | /// Content merges that still show conflict markers. |
| 57 | /// Only consider content merges with conflict markers as unresolved. | 66 | #[default] |
| 58 | /// | 67 | Markers, |
| 59 | /// Auto-resolved tree conflicts will *not* be considered unresolved. | 68 | /// Content merges who would have conflicted if it wasn't for a |
| 60 | ConflictMarkers, | 69 | /// [resolution strategy](crate::blob::builtin_driver::text::Conflict::ResolveWithOurs). |
| 61 | /// Consider content merges with conflict markers as unresolved, and content | 70 | ForcedResolution, |
| 62 | /// merges where conflicts where auto-resolved in any way, like choosing | 71 | } |
| 63 | /// *ours*, *theirs* or by their *union*. | 72 | |
| 64 | /// | 73 | /// Which kind of tree merges should be considered unresolved? |
| 65 | /// Auto-resolved tree conflicts will *not* be considered unresolved. | 74 | #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] |
| 66 | ConflictMarkersAndAutoResolved, | 75 | pub enum TreeMerge { |
| 67 | /// Whenever there were conflicting renames, or conflict markers, it is unresolved. | 76 | /// All failed renames. |
| 68 | /// Note that auto-resolved content merges will *not* be considered unresolved. | 77 | Undecidable, |
| 69 | /// | 78 | /// All failed renames, and the ones where a tree item was renamed to avoid a clash. |
| 70 | /// Also note that files that were changed in one and renamed in another will | 79 | #[default] |
| 71 | /// be moved into place, which will be considered resolved. | 80 | EvasiveRenames, |
| 72 | Renames, | 81 | /// All of `EvasiveRenames`, and tree merges that would have conflicted but which were resolved |
| 73 | /// Similar to [`Self::Renames`], but auto-resolved content-merges will | 82 | /// with a [resolution strategy](super::ResolveWith). |
| 74 | /// also be considered unresolved. | 83 | ForcedResolution, |
| 75 | RenamesAndAutoResolvedContent, | 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 | impl Outcome<'_> { | 115 | impl Outcome<'_> { |
| @@ -84,13 +121,18 @@ impl Outcome<'_> { · 5 unmodified lines | |||
| 84 | 121 | ||
| 85 | /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a | 122 | /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a |
| 86 | /// conflict should be considered unresolved. | 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 | /// It's important that `index` is at the state of [`Self::tree`]. | 126 | /// It's important that `index` is at the state of [`Self::tree`]. |
| 88 | /// | 127 | /// |
| 89 | /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`. | 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 | 129 | pub fn index_changed_after_applying_conflicts( |
| 91 | /// in-memory entry is still present. | 130 | &self, |
| 92 | pub fn index_changed_after_applying_conflicts(&self, index: &mut gix_index::State, how: TreatAsUnresolved) -> bool { | 131 | index: &mut gix_index::State, |
| 93 | apply_index_entries(&self.conflicts, how, index) | 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 | /// A utility to help define which side is what in the [`Conflict`] type. | 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 | enum ConflictMapping { | 196 | enum ConflictMapping { |
| 155 | /// The sides are as described in the field documentation, i.e. `ours` is `ours`. | 197 | /// The sides are as described in the field documentation, i.e. `ours` is `ours`. |
| 156 | Original, | 198 | Original, |
| @@ -168,41 +210,56 @@ impl ConflictMapping { · 11 unmodified lines | |||
| 168 | ConflictMapping::Swapped => ConflictMapping::Original, | 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 | impl Conflict { | 221 | impl Conflict { |
| 174 | /// Return `true` if this instance is considered unresolved based on the criterion specified by `how`. | 222 | /// Return `true` if this instance is considered unresolved based on the criterion specified by `how`. |
| 175 | pub fn is_unresolved(&self, how: TreatAsUnresolved) -> bool { | 223 | pub fn is_unresolved(&self, how: TreatAsUnresolved) -> bool { |
| 176 | use crate::blob; | 224 | use crate::blob; |
| 177 | let content_merge_matches = |info: &ContentMerge| match how { | 225 | let content_merge_unresolved = |info: &ContentMerge| match how.content_merge { |
| 178 | TreatAsUnresolved::ConflictMarkers | TreatAsUnresolved::Renames => { | 226 | treat_as_unresolved::ContentMerge::Markers => matches!(info.resolution, blob::Resolution::Conflict), |
| 179 | matches!(info.resolution, blob::Resolution::Conflict) | 227 | treat_as_unresolved::ContentMerge::ForcedResolution => { |
| 180 | } | ||
| 181 | TreatAsUnresolved::RenamesAndAutoResolvedContent | TreatAsUnresolved::ConflictMarkersAndAutoResolved => { | ||
| 182 | matches!( | 228 | matches!( |
| 183 | info.resolution, | 229 | info.resolution, |
| 184 | blob::Resolution::Conflict | blob::Resolution::CompleteWithAutoResolvedConflict | 230 | blob::Resolution::Conflict | blob::Resolution::CompleteWithAutoResolvedConflict |
| 185 | ) | 231 | ) |
| 186 | } | 232 | } |
| 187 | }; | 233 | }; |
| 188 | match how { | 234 | match how.tree_merge { |
| 189 | TreatAsUnresolved::ConflictMarkers | TreatAsUnresolved::ConflictMarkersAndAutoResolved => { | 235 | treat_as_unresolved::TreeMerge::Undecidable => { |
| 190 | self.resolution.is_err() || self.content_merge().map_or(false, |info| content_merge_matches(&info)) | 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 | /// Return information about the content merge if it was performed. | 295 | /// Return information about the content merge if it was performed. |
| 239 | pub fn content_merge(&self) -> Option<ContentMerge> { | 296 | pub fn content_merge(&self) -> Option<ContentMerge> { |
| 240 | match &self.resolution { | 297 | fn failure_merged_blob(failure: &ResolutionFailure) -> Option<ContentMerge> { |
| 241 | Ok(success) => match success { | 298 | match failure { |
| 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 { | ||
| 247 | ResolutionFailure::OursRenamedTheirsRenamedDifferently { merged_blob } => *merged_blob, | 299 | ResolutionFailure::OursRenamedTheirsRenamedDifferently { merged_blob } => *merged_blob, |
| 248 | ResolutionFailure::Unknown | 300 | ResolutionFailure::Unknown |
| 301 | | ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { .. } | ||
| 249 | | ResolutionFailure::OursModifiedTheirsDeleted | 302 | | ResolutionFailure::OursModifiedTheirsDeleted |
| 250 | | ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch | 303 | | ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch |
| 251 | | ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { | 304 | | ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { |
| @@ -253,7 +306,16 @@ impl Conflict { · 1 unmodified line | |||
| 253 | } | 306 | } |
| 254 | | ResolutionFailure::OursAddedTheirsAddedTypeMismatch { .. } | 307 | | ResolutionFailure::OursAddedTheirsAddedTypeMismatch { .. } |
| 255 | | ResolutionFailure::OursDeletedTheirsRenamed => None, | 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 | /// The outcome of the content merge. | 353 | /// The outcome of the content merge. |
| 292 | merged_blob: ContentMerge, | 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 | /// Describes of a conflict involving *our* change and *their* failed to be resolved. | 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 | /// The path at which `ours` can be found in the tree - it's in the same directory that it was in before. | 371 | /// The path at which `ours` can be found in the tree - it's in the same directory that it was in before. |
| 307 | renamed_unique_path_to_modified_blob: BString, | 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 | /// *ours* was added (or renamed into place) with a different mode than theirs, e.g. blob and symlink, and we kept | 385 | /// *ours* was added (or renamed into place) with a different mode than theirs, e.g. blob and symlink, and we kept |
| 310 | /// the symlink in its original location, renaming the other side to `their_unique_location`. | 386 | /// the symlink in its original location, renaming the other side to `their_unique_location`. |
| 311 | OursAddedTheirsAddedTypeMismatch { | 387 | OursAddedTheirsAddedTypeMismatch { |
| @@ -340,6 +416,8 @@ pub struct ContentMerge { · 28 unmodified lines | |||
| 340 | #[derive(Default, Debug, Clone)] | 416 | #[derive(Default, Debug, Clone)] |
| 341 | pub struct Options { | 417 | pub struct Options { |
| 342 | /// If *not* `None`, rename tracking will be performed when determining the changes of each side of the merge. | 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 | pub rewrites: Option<Rewrites>, | 421 | pub rewrites: Option<Rewrites>, |
| 344 | /// Decide how blob-merges should be done. This relates to if conflicts can be resolved or not. | 422 | /// Decide how blob-merges should be done. This relates to if conflicts can be resolved or not. |
| 345 | pub blob_merge: crate::blob::platform::merge::Options, | 423 | pub blob_merge: crate::blob::platform::merge::Options, |
| @@ -358,16 +436,51 @@ pub struct Options { · 12 unmodified lines | |||
| 358 | /// If `None`, when symlinks clash *ours* will be chosen and a conflict will occur. | 436 | /// If `None`, when symlinks clash *ours* will be chosen and a conflict will occur. |
| 359 | /// Otherwise, the same logic applies as for the merge of binary resources. | 437 | /// Otherwise, the same logic applies as for the merge of binary resources. |
| 360 | pub symlink_conflicts: Option<crate::blob::builtin_driver::binary::ResolveWith>, | 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 | 439 | /// If `None`, tree irreconcilable tree conflicts will result in [resolution failures](ResolutionFailure). |
| 362 | /// version. This is useful when one wants to avoid any kind of merge-conflict to have *some*, *lossy* resolution. | 440 | /// Otherwise, one can choose a side. Note that it's still possible to determine that auto-resolution happened |
| 363 | pub allow_lossy_resolution: bool, | 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 | pub(super) mod function; | 461 | pub(super) mod function; |
| 367 | mod utils; | 462 | mod utils; |
| 463 | /// | ||
| 368 | pub mod apply_index_entries { | 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 | pub(super) mod function { | 482 | pub(super) mod function { |
| 483 | use crate::tree::apply_index_entries::RemovalMode; | ||
| 371 | use crate::tree::{Conflict, ConflictIndexEntryPathHint, Resolution, ResolutionFailure, TreatAsUnresolved}; | 484 | use crate::tree::{Conflict, ConflictIndexEntryPathHint, Resolution, ResolutionFailure, TreatAsUnresolved}; |
| 372 | use bstr::{BStr, ByteSlice}; | 485 | use bstr::{BStr, ByteSlice}; |
| 373 | use std::collections::{hash_map, HashMap}; | 486 | use std::collections::{hash_map, HashMap}; |
| @@ -376,8 +489,9 @@ pub mod apply_index_entries { · 2 unmodified lines | |||
| 376 | /// conflict should be considered unresolved. | 489 | /// conflict should be considered unresolved. |
| 377 | /// Once a stage of a path conflicts, the unconflicting stage is removed even though it might be the one | 490 | /// Once a stage of a path conflicts, the unconflicting stage is removed even though it might be the one |
| 378 | /// that is currently checked out. | 491 | /// that is currently checked out. |
| 379 | /// This removal, however, is only done by flagging it with [gix_index::entry::Flags::REMOVE], which means | 492 | /// This removal 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. | 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 | /// It's important that `index` matches the tree that was produced as part of the merge that also | 495 | /// It's important that `index` matches the tree that was produced as part of the merge that also |
| 382 | /// brought about `conflicts`, or else this function will fail if it cannot find the path matching | 496 | /// brought about `conflicts`, or else this function will fail if it cannot find the path matching |
| 383 | /// the conflicting entries. | 497 | /// the conflicting entries. |
| @@ -388,6 +502,7 @@ pub mod apply_index_entries { · 4 unmodified lines | |||
| 388 | conflicts: &[Conflict], | 502 | conflicts: &[Conflict], |
| 389 | how: TreatAsUnresolved, | 503 | how: TreatAsUnresolved, |
| 390 | index: &mut gix_index::State, | 504 | index: &mut gix_index::State, |
| 505 | removal_mode: RemovalMode, | ||
| 391 | ) -> bool { | 506 | ) -> bool { |
| 392 | if index.is_sparse() { | 507 | if index.is_sparse() { |
| 393 | gix_trace::error!("Refusing to apply index entries to sparse index - it's not tested yet"); | 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 | for conflict in conflicts.iter().filter(|c| c.is_unresolved(how)) { | 513 | for conflict in conflicts.iter().filter(|c| c.is_unresolved(how)) { |
| 399 | let (renamed_path, current_path): (Option<&BStr>, &BStr) = match &conflict.resolution { | 514 | let (renamed_path, current_path): (Option<&BStr>, &BStr) = match &conflict.resolution { |
| 400 | Ok(success) => match success { | 515 | Ok(success) => match success { |
| 516 | Resolution::Forced(_) => continue, | ||
| 401 | Resolution::SourceLocationAffectedByRename { final_location } => { | 517 | Resolution::SourceLocationAffectedByRename { final_location } => { |
| 402 | (Some(final_location.as_bstr()), final_location.as_bstr()) | 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 | Err(failure) => match failure { | 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 | ResolutionFailure::OursRenamedTheirsRenamedDifferently { .. } => { | 532 | ResolutionFailure::OursRenamedTheirsRenamedDifferently { .. } => { |
| 414 | (Some(conflict.theirs.location()), conflict.ours.location()) | 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 | index.sort_entries(); | 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 | /// over a directory rewrite in *our* tree. If so, rewrite it so that we get the path | 23 | /// over a directory rewrite in *our* tree. If so, rewrite it so that we get the path |
| 24 | /// it would have had if it had been renamed along with *our* directory. | 24 | /// it would have had if it had been renamed along with *our* directory. |
| 25 | pub fn possibly_rewritten_location( | 25 | pub fn possibly_rewritten_location( |
| 26 | check_tree: &mut TreeNodes, | 26 | check_tree: &TreeNodes, |
| 27 | their_location: &BStr, | 27 | their_location: &BStr, |
| 28 | our_changes: &ChangeListRef, | 28 | our_changes: &ChangeListRef, |
| 29 | ) -> Option<BString> { | 29 | ) -> Option<BString> { |
| @@ -60,7 +60,7 @@ pub fn rewrite_location_with_renamed_directory(their_location: &BStr, passed_cha · 30 unmodified lines | |||
| 60 | pub fn unique_path_in_tree( | 60 | pub fn unique_path_in_tree( |
| 61 | file_path: &BStr, | 61 | file_path: &BStr, |
| 62 | editor: &tree::Editor<'_>, | 62 | editor: &tree::Editor<'_>, |
| 63 | tree: &mut TreeNodes, | 63 | tree: &TreeNodes, |
| 64 | side_name: &BStr, | 64 | side_name: &BStr, |
| 65 | ) -> Result<BString, Error> { | 65 | ) -> Result<BString, Error> { |
| 66 | let mut buf = file_path.to_owned(); | 66 | let mut buf = file_path.to_owned(); |
| @@ -400,11 +400,11 @@ impl TreeNodes { · 333 unmodified lines | |||
| 400 | Some(index) => { | 400 | Some(index) => { |
| 401 | cursor = &mut self.0[index]; | 401 | cursor = &mut self.0[index]; |
| 402 | if is_last && !cursor.is_leaf_node() { | 402 | if is_last && !cursor.is_leaf_node() { |
| 403 | assert_eq!( | 403 | // NOTE: we might encounter the same path multiple times in rare conditions. |
| 404 | cursor.change_idx, None, | 404 | // At least we avoid overwriting existing intermediate changes, for good measure. |
| 405 | "BUG: each node should only see a single change when tracking initially: {path} {change_idx}" | 405 | if cursor.change_idx.is_none() { |
| 406 | ); | 406 | cursor.change_idx = Some(change_idx); |
| 407 | 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 | /// Search the tree with `our` changes for `theirs` by [`source_location()`](Change::source_location())). | 415 | /// Search the tree with `our` changes for `theirs` by [`source_location()`](Change::source_location())). |
| 416 | /// If there is an entry but both are the same, or if there is no entry, return `None`. | 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 | if self.0.len() == 1 { | 418 | if self.0.len() == 1 { |
| 419 | return None; | 419 | return None; |
| 420 | } | 420 | } |
| 421 | let components = to_components(theirs_location); | 421 | let components = to_components(theirs_location); |
| 422 | let mut cursor = &mut self.0[0]; | 422 | let mut cursor = &self.0[0]; |
| 423 | let mut cursor_idx = 0; | 423 | let mut cursor_idx = 0; |
| 424 | let mut intermediate_change = None; | 424 | let mut intermediate_change = None; |
| 425 | for component in components { | 425 | for component in components { |
| @@ -436,7 +436,7 @@ impl TreeNodes { · 10 unmodified lines | |||
| 436 | } else { | 436 | } else { |
| 437 | // a change somewhere else, i.e. `a/c` and we know `a/b` only. | 437 | // a change somewhere else, i.e. `a/c` and we know `a/b` only. |
| 438 | intermediate_change.and_then(|(change, cursor_idx)| { | 438 | intermediate_change.and_then(|(change, cursor_idx)| { |
| 439 | let cursor = &mut self.0[cursor_idx]; | 439 | let cursor = &self.0[cursor_idx]; |
| 440 | // If this is a destination location of a rename, then the `their_location` | 440 | // If this is a destination location of a rename, then the `their_location` |
| 441 | // is already at the right spot, and we can just ignore it. | 441 | // is already at the right spot, and we can just ignore it. |
| 442 | if matches!(cursor.location, ChangeLocation::CurrentLocation) { | 442 | if matches!(cursor.location, ChangeLocation::CurrentLocation) { |
| @@ -450,7 +450,7 @@ impl TreeNodes { · 7 unmodified lines | |||
| 450 | } | 450 | } |
| 451 | Some(child_idx) => { | 451 | Some(child_idx) => { |
| 452 | cursor_idx = child_idx; | 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 | let mut cursor = &mut self.0[0]; | 496 | let mut cursor = &mut self.0[0]; |
| 497 | while let Some(component) = components.next() { | 497 | while let Some(component) = components.next() { |
| 498 | match cursor.children.get(component).copied() { | 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 | Some(existing_idx) => { | 500 | Some(existing_idx) => { |
| 501 | let is_last = components.peek().is_none(); | 501 | let is_last = components.peek().is_none(); |
| 502 | if is_last { | 502 | if is_last { |
| @@ -504,7 +504,7 @@ impl TreeNodes { · 1 unmodified line | |||
| 504 | cursor = &mut self.0[existing_idx]; | 504 | cursor = &mut self.0[existing_idx]; |
| 505 | debug_assert!( | 505 | debug_assert!( |
| 506 | cursor.is_leaf_node(), | 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 | cursor.change_idx = None; | 509 | cursor.change_idx = None; |
| 510 | } else { | 510 | } else { |
| @@ -578,10 +578,7 @@ impl Conflict { · 67 unmodified lines | |||
| 578 | ours: ours.clone(), | 578 | ours: ours.clone(), |
| 579 | theirs: theirs.clone(), | 579 | theirs: theirs.clone(), |
| 580 | entries, | 580 | entries, |
| 581 | map: match outer_map { | 581 | map: map.to_global(outer_map), |
| 582 | ConflictMapping::Original => map, | ||
| 583 | ConflictMapping::Swapped => map.swapped(), | ||
| 584 | }, | ||
| 585 | } | 582 | } |
| 586 | } | 583 | } |
| 587 | 584 | ||
Binary file not shown.
| @@ -36,6 +36,15 @@ function make_conflict_index() { · 35 unmodified lines | |||
| 36 | cp .git/index .git/"${identifier}".index | 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 | function baseline () ( | 48 | function baseline () ( |
| 40 | local dir=${1:?the directory to enter} | 49 | local dir=${1:?the directory to enter} |
| 41 | local output_name=${2:?the basename of the output of the merge} | 50 | local output_name=${2:?the basename of the output of the merge} |
| @@ -58,8 +67,13 @@ function baseline () ( · 16 unmodified lines | |||
| 58 | our_commit_id="$(git rev-parse "$our_committish")" | 67 | our_commit_id="$(git rev-parse "$our_committish")" |
| 59 | their_commit_id="$(git rev-parse "$their_committish")" | 68 | their_commit_id="$(git rev-parse "$their_committish")" |
| 60 | local maybe_expected_tree="$(git rev-parse expected^{tree})" | 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 | if [ -z "$opt_deviation_message" ]; then | 74 | if [ -z "$opt_deviation_message" ]; then |
| 62 | maybe_expected_tree="expected^{tree}" | 75 | maybe_expected_tree="expected^{tree}" |
| 76 | maybe_expected_reversed_tree="expected^{tree}" | ||
| 63 | fi | 77 | fi |
| 64 | 78 | ||
| 65 | local merge_info="${output_name}.merge-info" | 79 | local merge_info="${output_name}.merge-info" |
| @@ -69,10 +83,108 @@ function baseline () ( · 3 unmodified lines | |||
| 69 | if [[ "$one_side" != "no-reverse" ]]; then | 83 | if [[ "$one_side" != "no-reverse" ]]; then |
| 70 | local merge_info="${output_name}-reversed.merge-info" | 84 | local merge_info="${output_name}-reversed.merge-info" |
| 71 | git -c merge.conflictStyle=$conflict_style merge-tree -z --write-tree --allow-unrelated-histories "$their_committish" "$our_committish" > "$merge_info" || : | 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 | fi | 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 | git init simple | 188 | git init simple |
| 77 | (cd simple | 189 | (cd simple |
| 78 | rm -Rf .git/hooks | 190 | rm -Rf .git/hooks |
| @@ -222,6 +334,26 @@ git init rename-add-symlink · 143 unmodified lines | |||
| 222 | git commit -m "rename foo to bar" | 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 | git init rename-rename-plus-content | 357 | git init rename-rename-plus-content |
| 226 | (cd rename-rename-plus-content | 358 | (cd rename-rename-plus-content |
| 227 | write_lines 1 2 3 4 5 >foo | 359 | write_lines 1 2 3 4 5 >foo |
| @@ -401,12 +533,40 @@ git init rename-within-rename · 173 unmodified lines | |||
| 401 | git checkout expected | 533 | git checkout expected |
| 402 | write_lines 1 2 3 4 5 6 >a/x.f | 534 | write_lines 1 2 3 4 5 6 >a/x.f |
| 403 | write_lines 1 2 3 4 5 6 >a/sub/y.f | 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 | git mv a a-renamed | 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 | make_conflict_index rename-within-rename-A-B-deviates | 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 | make_conflict_index rename-within-rename-A-B-deviates-reversed | 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 | rm -Rf ./a | 746 | rm -Rf ./a |
| 587 | mkdir -p a-renamed/sub | 747 | mkdir -p a-renamed/sub |
| 588 | write_lines 1 2 3 4 5 >a-renamed/sub/y.f | 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 | write_lines 1 2 3 4 5 6 >a-renamed/y.f | 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 | git add . | 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 | # Since the whole state is very different, the expected index is as well, but at least it should make sense for what it is. | 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 | # 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 | 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 | 100644 8a1218a1024a212bb3db30becd860315f9f3ac52 2 a-renamed/sub/y.f | 761 | 100644 8a1218a1024a212bb3db30becd860315f9f3ac52 2 a-renamed/sub/y.f |
| 600 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z | 762 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z |
| 601 | 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f | 763 | 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f |
| 764 | 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-renamed/x.f | ||
| 765 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/w | ||
| 602 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z | 766 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z |
| 767 | 100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/x.f | ||
| 768 | 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/y.f | ||
| 603 | EOF | 769 | EOF |
| 604 | make_conflict_index conflicting-rename-complex-A-B | 770 | make_conflict_index conflicting-rename-complex-A-B |
| 605 | 771 | ||
| @@ -608,7 +774,11 @@ EOF · 2 unmodified lines | |||
| 608 | 100644 8a1218a1024a212bb3db30becd860315f9f3ac52 3 a-renamed/sub/y.f | 774 | 100644 8a1218a1024a212bb3db30becd860315f9f3ac52 3 a-renamed/sub/y.f |
| 609 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z | 775 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z |
| 610 | 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f | 776 | 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f |
| 777 | 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-renamed/x.f | ||
| 778 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/w | ||
| 611 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z | 779 | 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z |
| 780 | 100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/x.f | ||
| 781 | 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/y.f | ||
| 612 | EOF | 782 | EOF |
| 613 | make_conflict_index conflicting-rename-complex-A-B-reversed | 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 | chmod +x a/x.f a/w | 798 | chmod +x a/x.f a/w |
| 629 | git update-index --chmod=+x a/x.f a/w | 799 | git update-index --chmod=+x a/x.f a/w |
| 630 | git mv a a-renamed | 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 | git checkout B | 803 | git checkout B |
| 634 | write_lines original 1 2 3 4 5 6 >a/x.f | 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 | baseline rename-add-exe-bit-conflict A-B A B | 1171 | baseline rename-add-exe-bit-conflict A-B A B |
| 998 | baseline remove-executable-mode A-B A B | 1172 | baseline remove-executable-mode A-B A B |
| 999 | baseline simple side-1-3-without-conflict side1 side3 | 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 | baseline super-2 A-B A B | 1198 | baseline super-2 A-B A B |
| 1025 | baseline super-2 A-B-diff3 A B | 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 | baseline rename-within-rename-2 A-B-deviates A B "TBD: Right, something is different documentation was forgotten :/" | 1202 | baseline rename-within-rename-2 A-B-deviates A B "TBD: Right, something is different documentation was forgotten :/" |
| 1029 | baseline conflicting-rename A-B A B | 1203 | baseline conflicting-rename A-B A B |
| 1030 | baseline conflicting-rename-2 A-B A B | 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 | 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" | 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 | baseline renamed-symlink-with-conflict A-B A B | 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 | baseline symlink-modification A-B A B | 1228 | baseline symlink-modification A-B A B |
| 1055 | baseline symlink-addition A-B A B | 1229 | baseline symlink-addition A-B A B |
| 1056 | baseline type-change-to-symlink A-B A B | 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 | assert_eq!( | 11 | assert_eq!( |
| 12 | builtin_driver::binary(Some(ResolveWith::Ancestor)), | 12 | builtin_driver::binary(Some(ResolveWith::Ancestor)), |
| 13 | (Pick::Ancestor, Resolution::Complete), | 13 | (Pick::Ancestor, Resolution::CompleteWithAutoResolvedConflict), |
| 14 | "Otherwise we can pick anything and it will mark it as complete" | 14 | "Otherwise we can pick anything and it will mark it as complete" |
| 15 | ); | 15 | ); |
| 16 | assert_eq!( | 16 | assert_eq!( |
| 17 | builtin_driver::binary(Some(ResolveWith::Ours)), | 17 | builtin_driver::binary(Some(ResolveWith::Ours)), |
| 18 | (Pick::Ours, Resolution::Complete) | 18 | (Pick::Ours, Resolution::CompleteWithAutoResolvedConflict) |
| 19 | ); | 19 | ); |
| 20 | assert_eq!( | 20 | assert_eq!( |
| 21 | builtin_driver::binary(Some(ResolveWith::Theirs)), | 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 | let res = platform_ref.merge(&mut buf, default_labels(), &Default::default())?; | 59 | let res = platform_ref.merge(&mut buf, default_labels(), &Default::default())?; |
| 60 | assert_eq!( | 60 | assert_eq!( |
| 61 | res, | 61 | res, |
| 62 | (Pick::Theirs, Resolution::Complete), | 62 | (Pick::Theirs, Resolution::CompleteWithAutoResolvedConflict), |
| 63 | "the auto-binary driver respects its own options" | 63 | "the auto-binary driver respects its own options" |
| 64 | ); | 64 | ); |
| 65 | Ok(()) | 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 | #[test] | 117 | #[test] |
| 69 | fn builtin_with_conflict() -> crate::Result { | 118 | fn builtin_with_conflict() -> crate::Result { |
| 70 | let mut platform = new_platform(None, pipeline::Mode::ToGit); | 119 | let mut platform = new_platform(None, pipeline::Mode::ToGit); |
| @@ -192,7 +241,7 @@ theirs · 121 unmodified lines | |||
| 192 | ] { | 241 | ] { |
| 193 | platform_ref.options.resolve_binary_with = Some(resolve); | 242 | platform_ref.options.resolve_binary_with = Some(resolve); |
| 194 | let res = platform_ref.merge(&mut buf, default_labels(), &Default::default())?; | 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 | assert_eq!(platform_ref.buffer_by_pick(res.0).unwrap().unwrap().as_bstr(), expected); | 245 | assert_eq!(platform_ref.buffer_by_pick(res.0).unwrap().unwrap().as_bstr(), expected); |
| 197 | 246 | ||
| 198 | assert_eq!( | 247 | assert_eq!( |
| @@ -48,6 +48,7 @@ pub enum ConflictKind { · 47 unmodified lines | |||
| 48 | 48 | ||
| 49 | /// More loosely structured information about the `Conflict`. | 49 | /// More loosely structured information about the `Conflict`. |
| 50 | #[derive(Debug)] | 50 | #[derive(Debug)] |
| 51 | #[allow(dead_code)] // used only for debugging | ||
| 51 | pub struct ConflictInfo { | 52 | pub struct ConflictInfo { |
| 52 | /// All the paths involved in the informational message | 53 | /// All the paths involved in the informational message |
| 53 | pub paths: Vec<String>, | 54 | pub paths: Vec<String>, |
| @@ -149,9 +150,11 @@ impl Iterator for Expectations<'_> { · 95 unmodified lines | |||
| 149 | else { | 150 | else { |
| 150 | unreachable!("invalid line: {line:?}") | 151 | unreachable!("invalid line: {line:?}") |
| 151 | }; | 152 | }; |
| 152 | let deviation = (expected_custom_tree != "expected^{tree}").then(|| { | 153 | let deviation = (!expected_custom_tree.starts_with("expected")).then(|| { |
| 153 | let expected_tree_id = gix_hash::ObjectId::from_hex(expected_custom_tree.as_bytes()) | 154 | let expected_tree_id = |
| 154 | .expect("valid tree id in hex for the expected tree"); | 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 | let message = tokens.collect::<Vec<_>>().join(" ").trim().to_owned(); | 158 | let message = tokens.collect::<Vec<_>>().join(" ").trim().to_owned(); |
| 156 | Deviation { | 159 | Deviation { |
| 157 | message, | 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 | pub(crate) fn apply_git_index_entries(conflicts: &[Conflict], state: &mut gix_index::State) { | 393 | pub(crate) fn apply_git_index_entries(conflicts: &[Conflict], state: &mut gix_index::State) { |
| 375 | let len = state.entries().len(); | 394 | let len = state.entries().len(); |
| 376 | for Conflict { ours, theirs, ancestor } in conflicts { | 395 | for Conflict { ours, theirs, ancestor } in conflicts { |
| @@ -1,7 +1,8 @@ | |||
| 1 | use crate::tree::baseline::Deviation; | 1 | use crate::tree::baseline::Deviation; |
| 2 | use gix_diff::Rewrites; | 2 | use gix_diff::Rewrites; |
| 3 | use gix_merge::commit::Options; | 3 | use gix_merge::commit::Options; |
| 4 | use gix_merge::tree::TreatAsUnresolved; | 4 | use gix_merge::tree::apply_index_entries::RemovalMode; |
| 5 | use gix_merge::tree::{treat_as_unresolved, TreatAsUnresolved}; | ||
| 5 | use gix_object::Write; | 6 | use gix_object::Write; |
| 6 | use gix_worktree::stack::state::attributes; | 7 | use gix_worktree::stack::state::attributes; |
| 7 | use std::path::Path; | 8 | use std::path::Path; |
| @@ -21,7 +22,8 @@ fn run_baseline() -> crate::Result { · 13 unmodified lines | |||
| 21 | let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?; | 22 | let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?; |
| 22 | let cases = std::fs::read_to_string(root.join("baseline.cases"))?; | 23 | let cases = std::fs::read_to_string(root.join("baseline.cases"))?; |
| 23 | let mut actual_cases = 0; | 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 | let new_test = None; | 27 | let new_test = None; |
| 26 | for baseline::Expectation { | 28 | for baseline::Expectation { |
| 27 | root, | 29 | root, |
| @@ -63,12 +65,12 @@ fn run_baseline() -> crate::Result { · 35 unmodified lines | |||
| 63 | &mut blob_merge, | 65 | &mut blob_merge, |
| 64 | &odb, | 66 | &odb, |
| 65 | &mut |id| id.to_hex_with_len(7).to_string(), | 67 | &mut |id| id.to_hex_with_len(7).to_string(), |
| 66 | options, | 68 | options.clone(), |
| 67 | )? | 69 | )? |
| 68 | .tree_merge; | 70 | .tree_merge; |
| 69 | 71 | ||
| 70 | let actual_id = actual.tree.write(|tree| odb.write(tree))?; | 72 | let actual_id = actual.tree.write(|tree| odb.write(tree))?; |
| 71 | match deviation { | 73 | match &deviation { |
| 72 | None => { | 74 | None => { |
| 73 | if actual_id != merge_info.merged_tree { | 75 | if actual_id != merge_info.merged_tree { |
| 74 | baseline::show_diff_and_fail(&case_name, actual_id, &actual, &merge_info, &odb); | 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 | message, | 80 | message, |
| 79 | expected_tree_id, | 81 | expected_tree_id, |
| 80 | }) => { | 82 | }) => { |
| 83 | // TODO: why did this ever work? | ||
| 81 | // Sometimes only the reversed part of a specific test is different. | 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" | 85 | // assert_eq!( |
| 83 | { | 86 | // actual_id, merge_info.merged_tree, |
| 84 | assert_ne!( | 87 | // "{case_name}: Git should match here, it just doesn't match in one of two cases" |
| 85 | actual_id, merge_info.merged_tree, | 88 | // ); |
| 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 | } | ||
| 94 | pretty_assertions::assert_str_eq!( | 89 | pretty_assertions::assert_str_eq!( |
| 95 | baseline::visualize_tree(&actual_id, &odb, None).to_string(), | 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 | "{case_name}: tree mismatch: {message} \n{:#?}\n{case_name}", | 92 | "{case_name}: tree mismatch: {message} \n{:#?}\n{case_name}", |
| 98 | actual.conflicts | 93 | actual.conflicts |
| 99 | ); | 94 | ); |
| @@ -102,10 +97,10 @@ fn run_baseline() -> crate::Result { · 2 unmodified lines | |||
| 102 | 97 | ||
| 103 | let mut actual_index = gix_index::State::from_tree(&actual_id, &odb, Default::default())?; | 98 | let mut actual_index = gix_index::State::from_tree(&actual_id, &odb, Default::default())?; |
| 104 | let expected_index = { | 99 | let expected_index = { |
| 105 | let derivative_index_path = root.join(".git").join(format!("{case_name}.index")); | 100 | let deviating_index_path = root.join(".git").join(format!("{case_name}.index")); |
| 106 | if derivative_index_path.exists() { | 101 | if deviating_index_path.exists() { |
| 107 | gix_index::File::at( | 102 | gix_index::File::at( |
| 108 | derivative_index_path, | 103 | deviating_index_path, |
| 109 | odb.store().object_hash(), | 104 | odb.store().object_hash(), |
| 110 | true, | 105 | true, |
| 111 | Default::default(), | 106 | Default::default(), |
| @@ -119,9 +114,9 @@ fn run_baseline() -> crate::Result { · 7 unmodified lines | |||
| 119 | index | 114 | index |
| 120 | } | 115 | } |
| 121 | }; | 116 | }; |
| 122 | let conflicts_like_in_git = TreatAsUnresolved::Renames; | 117 | let conflicts_like_in_git = TreatAsUnresolved::git(); |
| 123 | let did_change = actual.index_changed_after_applying_conflicts(&mut actual_index, conflicts_like_in_git); | 118 | let did_change = |
| 124 | actual_index.remove_entries(|_, _, e| e.flags.contains(gix_index::entry::Flags::REMOVE)); | 119 | actual.index_changed_after_applying_conflicts(&mut actual_index, conflicts_like_in_git, RemovalMode::Prune); |
| 125 | 120 | ||
| 126 | pretty_assertions::assert_eq!( | 121 | pretty_assertions::assert_eq!( |
| 127 | baseline::clear_entries(&actual_index), | 122 | baseline::clear_entries(&actual_index), |
| @@ -130,29 +125,89 @@ fn run_baseline() -> crate::Result { · 2 unmodified lines | |||
| 130 | actual.conflicts, | 125 | actual.conflicts, |
| 131 | merge_info.conflicts | 126 | merge_info.conflicts |
| 132 | ); | 127 | ); |
| 133 | // if case_name.starts_with("submodule-both-modify-A-B") { | 128 | if deviation.is_none() { |
| 134 | if false { | ||
| 135 | assert!( | ||
| 136 | !did_change, | ||
| 137 | "{case_name}: We can't handle submodules, so there is no index change" | ||
| 138 | ); | ||
| 139 | assert!( | ||
| 140 | actual.has_unresolved_conflicts(conflicts_like_in_git), | ||
| 141 | "{case_name}: submodules currently result in an unresolved (unknown) conflict" | ||
| 142 | ); | ||
| 143 | } else { | ||
| 144 | assert_eq!( | 129 | assert_eq!( |
| 145 | did_change, | 130 | did_change, |
| 146 | actual.has_unresolved_conflicts(conflicts_like_in_git), | 131 | actual.has_unresolved_conflicts(conflicts_like_in_git), |
| 147 | "{case_name}: If there is any kind of conflict, the index should have been changed" | 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 | assert_eq!( | 201 | assert_eq!( |
| 153 | actual_cases, 107, | 202 | actual_cases, 117, |
| 154 | "BUG: update this number, and don't forget to remove a filter in the end" | 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 | Ok(()) | 212 | Ok(()) |
| 158 | } | 213 | } |
| @@ -163,16 +218,17 @@ fn basic_merge_options() -> Options { · 4 unmodified lines | |||
| 163 | use_first_merge_base: false, | 218 | use_first_merge_base: false, |
| 164 | tree_merge: gix_merge::tree::Options { | 219 | tree_merge: gix_merge::tree::Options { |
| 165 | symlink_conflicts: None, | 220 | symlink_conflicts: None, |
| 221 | tree_conflicts: None, | ||
| 166 | rewrites: Some(Rewrites { | 222 | rewrites: Some(Rewrites { |
| 167 | copies: None, | 223 | copies: None, |
| 168 | percentage: Some(0.5), | 224 | percentage: Some(0.5), |
| 169 | limit: 0, | 225 | limit: 0, |
| 226 | track_empty: false, | ||
| 170 | }), | 227 | }), |
| 171 | blob_merge: gix_merge::blob::platform::merge::Options::default(), | 228 | blob_merge: gix_merge::blob::platform::merge::Options::default(), |
| 172 | blob_merge_command_ctx: Default::default(), | 229 | blob_merge_command_ctx: Default::default(), |
| 173 | fail_on_conflict: None, | 230 | fail_on_conflict: None, |
| 174 | marker_size_multiplier: 0, | 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 | mod baseline; | 283 | mod baseline; |
| @@ -80,6 +80,7 @@ fn changed_and_untracked_and_renamed() { · 79 unmodified lines | |||
| 80 | }), | 80 | }), |
| 81 | percentage: Some(0.3), | 81 | percentage: Some(0.3), |
| 82 | limit: 0, | 82 | limit: 0, |
| 83 | track_empty: true, | ||
| 83 | }; | 84 | }; |
| 84 | let out = fixture_filtered_detailed( | 85 | let out = fixture_filtered_detailed( |
| 85 | "changed-and-untracked-and-renamed", | 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 | thiserror = "2.0.0" | 26 | thiserror = "2.0.0" |
| 27 | url = "2.5.2" | 27 | url = "2.5.2" |
| 28 | bstr = { version = "1.3.0", default-features = false, features = ["std"] } | 28 | bstr = { version = "1.3.0", default-features = false, features = ["std"] } |
| 29 | percent-encoding = "2.3.1" | ||
| 29 | 30 | ||
| 30 | document-features = { version = "0.2.0", optional = true } | 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 | let mut storage; | 81 | let mut storage; |
| 82 | let to_print = if self.password.is_some() { | 82 | let to_print = if self.password.is_some() { |
| 83 | storage = self.clone(); | 83 | storage = self.clone(); |
| 84 | storage.password = Some("<redacted>".into()); | 84 | storage.password = Some("redacted".into()); |
| 85 | &storage | 85 | &storage |
| 86 | } else { | 86 | } else { |
| 87 | self | 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 | /// Serialization | 315 | /// Serialization |
| 312 | impl Url { | 316 | impl Url { |
| 313 | /// Write this URL losslessly to `out`, ready to be parsed again. | 317 | /// Write this URL losslessly to `out`, ready to be parsed again. |
| @@ -318,10 +322,10 @@ impl Url { · 4 unmodified lines | |||
| 318 | } | 322 | } |
| 319 | match (&self.user, &self.host) { | 323 | match (&self.user, &self.host) { |
| 320 | (Some(user), Some(host)) => { | 324 | (Some(user), Some(host)) => { |
| 321 | out.write_all(user.as_bytes())?; | 325 | out.write_all(percent_encode(user).as_bytes())?; |
| 322 | if let Some(password) = &self.password { | 326 | if let Some(password) = &self.password { |
| 323 | out.write_all(b":")?; | 327 | out.write_all(b":")?; |
| 324 | out.write_all(password.as_bytes())?; | 328 | out.write_all(percent_encode(password).as_bytes())?; |
| 325 | } | 329 | } |
| 326 | out.write_all(b"@")?; | 330 | out.write_all(b"@")?; |
| 327 | out.write_all(host.as_bytes())?; | 331 | out.write_all(host.as_bytes())?; |
| @@ -1,8 +1,8 @@ | |||
| 1 | use std::convert::Infallible; | 1 | use std::convert::Infallible; |
| 2 | 2 | ||
| 3 | use bstr::{BStr, BString, ByteSlice}; | ||
| 4 | |||
| 5 | use crate::Scheme; | 3 | use crate::Scheme; |
| 4 | use bstr::{BStr, BString, ByteSlice}; | ||
| 5 | use percent_encoding::percent_decode_str; | ||
| 6 | 6 | ||
| 7 | /// The error returned by [parse()](crate::parse()). | 7 | /// The error returned by [parse()](crate::parse()). |
| 8 | #[derive(Debug, thiserror::Error)] | 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 | Ok(crate::Url { | 114 | Ok(crate::Url { |
| 115 | serialize_alternative_form: false, | 115 | serialize_alternative_form: false, |
| 116 | scheme, | 116 | scheme, |
| 117 | user: url_user(&url), | 117 | user: url_user(&url, UrlKind::Url)?, |
| 118 | password: url.password().map(Into::into), | 118 | password: url |
| 119 | .password() | ||
| 120 | .map(|s| percent_decoded_utf8(s, UrlKind::Url)) | ||
| 121 | .transpose()?, | ||
| 119 | host: url.host_str().map(Into::into), | 122 | host: url.host_str().map(Into::into), |
| 120 | port: url.port(), | 123 | port: url.port(), |
| 121 | path: url.path().into(), | 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 | pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> { | 139 | pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> { |
| 126 | let input = input_to_utf8(input, UrlKind::Scp)?; | 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 | Ok(crate::Url { | 164 | Ok(crate::Url { |
| 151 | serialize_alternative_form: true, | 165 | serialize_alternative_form: true, |
| 152 | scheme: url.scheme().into(), | 166 | scheme: url.scheme().into(), |
| 153 | user: url_user(&url), | 167 | user: url_user(&url, UrlKind::Scp)?, |
| 154 | password: url.password().map(Into::into), | 168 | password: url |
| 169 | .password() | ||
| 170 | .map(|s| percent_decoded_utf8(s, UrlKind::Scp)) | ||
| 171 | .transpose()?, | ||
| 155 | host: url.host_str().map(Into::into), | 172 | host: url.host_str().map(Into::into), |
| 156 | port: url.port(), | 173 | port: url.port(), |
| 157 | path: path.into(), | 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 | if url.username().is_empty() && url.password().is_none() { | 179 | if url.username().is_empty() && url.password().is_none() { |
| 163 | None | 180 | Ok(None) |
| 164 | } else { | 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 | compare("/path/to/repo", "/path/to/repo", "same goes for simple paths"); | 174 | compare("/path/to/repo", "/path/to/repo", "same goes for simple paths"); |
| 175 | compare( | 175 | compare( |
| 176 | "https://user:password@host/path", | 176 | "https://user:password@host/path", |
| 177 | "https://user:<redacted>@host/path", | 177 | "https://user:redacted@host/path", |
| 178 | "it visibly redacts passwords though", | 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 | assert_eq!(actual.path, expected.path.unwrap_or_default()); | 164 | assert_eq!(actual.path, expected.path.unwrap_or_default()); |
| 165 | } | 165 | } |
| 166 | 166 | ||
| 167 | #[allow(clippy::module_inception)] | ||
| 167 | mod baseline { | 168 | mod baseline { |
| 168 | use bstr::{BStr, BString, ByteSlice}; | 169 | use bstr::{BStr, BString, ByteSlice}; |
| 169 | use gix_testtools::once_cell::sync::Lazy; | 170 | use gix_testtools::once_cell::sync::Lazy; |
File renamed without changes.
| @@ -5,6 +5,7 @@ use bstr::ByteSlice; · 4 unmodified lines | |||
| 5 | #[test] | 5 | #[test] |
| 6 | fn fuzzed() { | 6 | fn fuzzed() { |
| 7 | for name in [ | 7 | for name in [ |
| 8 | "illegal-utf8", | ||
| 8 | "short-panic", | 9 | "short-panic", |
| 9 | "very-long-abort2", | 10 | "very-long-abort2", |
| 10 | "very-long-abort", | 11 | "very-long-abort", |
| @@ -2,5 +2,7 @@ pub type Error = Box<dyn std::error::Error>; · 1 unmodified line | |||
| 2 | pub type Result = std::result::Result<(), Error>; | 2 | pub type Result = std::result::Result<(), Error>; |
| 3 | 3 | ||
| 4 | mod access; | 4 | mod access; |
| 5 | mod baseline; | ||
| 5 | mod expand_path; | 6 | mod expand_path; |
| 7 | mod fuzzed; | ||
| 6 | mod parse; | 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 | #[test] | 62 | #[test] |
| 43 | fn only_password() -> crate::Result { | 63 | fn only_password() -> crate::Result { |
| 44 | assert_url_roundtrip( | 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 | /// A support type to allow configuring a `git describe` operation | 173 | /// A support type to allow configuring a `git describe` operation |
| 174 | pub struct Platform<'repo> { | 174 | pub struct Platform<'repo> { |
| 175 | pub(crate) id: gix_hash::ObjectId, | 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 | pub(crate) select: SelectRef, | 178 | pub(crate) select: SelectRef, |
| 178 | pub(crate) first_parent: bool, | 179 | pub(crate) first_parent: bool, |
| 179 | pub(crate) id_as_fallback: bool, | 180 | pub(crate) id_as_fallback: bool, |
| @@ -156,7 +156,7 @@ impl Cache { · 155 unmodified lines | |||
| 156 | } | 156 | } |
| 157 | 157 | ||
| 158 | #[cfg(feature = "blob-diff")] | 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 | self.diff_renames | 160 | self.diff_renames |
| 161 | .get_or_try_init(|| crate::diff::new_rewrites(&self.resolved, self.lenient_config)) | 161 | .get_or_try_init(|| crate::diff::new_rewrites(&self.resolved, self.lenient_config)) |
| 162 | .copied() | 162 | .copied() |
| @@ -18,7 +18,8 @@ pub use tree::root::Tree; · 17 unmodified lines | |||
| 18 | /// | 18 | /// |
| 19 | /// Note that these values won't update even if the underlying file(s) change. | 19 | /// Note that these values won't update even if the underlying file(s) change. |
| 20 | pub struct Snapshot<'repo> { | 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 | /// A platform to access configuration values and modify them in memory, while making them available when this platform is dropped | 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 | // TODO: make it possible to load snapshots with reloading via .config() and write mutated snapshots back to disk which should be the way | 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 | // to affect all instances of a repo, probably via `config_mut()` and `config_mut_at()`. | 33 | // to affect all instances of a repo, probably via `config_mut()` and `config_mut_at()`. |
| 33 | pub struct SnapshotMut<'repo> { | 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 | pub(crate) config: gix_config::File<'static>, | 37 | pub(crate) config: gix_config::File<'static>, |
| 36 | } | 38 | } |
| 37 | 39 | ||
| 38 | /// A utility structure created by [`SnapshotMut::commit_auto_rollback()`] that restores the previous configuration on drop. | 40 | /// A utility structure created by [`SnapshotMut::commit_auto_rollback()`] that restores the previous configuration on drop. |
| 39 | pub struct CommitAutoRollback<'repo> { | 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 | pub(crate) prev_config: crate::Config, | 44 | pub(crate) prev_config: crate::Config, |
| 42 | } | 45 | } |
| 43 | 46 | ||
| @@ -602,7 +605,7 @@ pub(crate) struct Cache { · 558 unmodified lines | |||
| 602 | pub(crate) url_rewrite: OnceCell<crate::remote::url::Rewrite>, | 605 | pub(crate) url_rewrite: OnceCell<crate::remote::url::Rewrite>, |
| 603 | /// The lazy-loaded rename information for diffs. | 606 | /// The lazy-loaded rename information for diffs. |
| 604 | #[cfg(feature = "blob-diff")] | 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 | /// A lazily loaded mapping to know which url schemes to allow | 609 | /// A lazily loaded mapping to know which url schemes to allow |
| 607 | #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] | 610 | #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] |
| 608 | pub(crate) url_scheme: OnceCell<crate::remote::url::SchemePermission>, | 611 | pub(crate) url_scheme: OnceCell<crate::remote::url::SchemePermission>, |
| @@ -51,7 +51,14 @@ impl Options { · 50 unmodified lines | |||
| 51 | pub(crate) fn from_configuration(config: &crate::config::Cache) -> Result<Self, options::init::Error> { | 51 | pub(crate) fn from_configuration(config: &crate::config::Cache) -> Result<Self, options::init::Error> { |
| 52 | Ok(Options { | 52 | Ok(Options { |
| 53 | location: Some(Location::Path), | 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 | /// Create an instance by reading all relevant information from the `config`uration, while being `lenient` or not. | 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 | /// Note that missing values will be defaulted similar to what git does. | 176 | /// Note that missing values will be defaulted similar to what git does. |
| 170 | #[allow(clippy::result_large_err)] | 177 | #[allow(clippy::result_large_err)] |
| 171 | pub fn new_rewrites( | 178 | pub fn new_rewrites( |
| 172 | config: &gix_config::File<'static>, | 179 | config: &gix_config::File<'static>, |
| 173 | lenient: bool, | 180 | lenient: bool, |
| 174 | ) -> Result<Option<Rewrites>, new_rewrites::Error> { | 181 | ) -> Result<(Option<Rewrites>, bool), new_rewrites::Error> { |
| 175 | new_rewrites_inner(config, lenient, &Diff::RENAMES, &Diff::RENAME_LIMIT) | 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 | lenient: bool, | 187 | lenient: bool, |
| 181 | renames: &'static crate::config::tree::diff::Renames, | 188 | renames: &'static crate::config::tree::diff::Renames, |
| 182 | rename_limit: &'static crate::config::tree::keys::UnsignedInteger, | 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 | let copies = match config | 191 | let copies = match config |
| 185 | .boolean(renames) | 192 | .boolean(renames) |
| 186 | .map(|value| renames.try_into_renames(value)) | 193 | .map(|value| renames.try_into_renames(value)) |
| @@ -188,25 +195,28 @@ pub(crate) mod utils { · 1 unmodified line | |||
| 188 | .with_leniency(lenient)? | 195 | .with_leniency(lenient)? |
| 189 | { | 196 | { |
| 190 | Some(renames) => match renames { | 197 | Some(renames) => match renames { |
| 191 | Tracking::Disabled => return Ok(None), | 198 | Tracking::Disabled => return Ok((None, true)), |
| 192 | Tracking::Renames => None, | 199 | Tracking::Renames => None, |
| 193 | Tracking::RenamesAndCopies => Some(Copies::default()), | 200 | Tracking::RenamesAndCopies => Some(Copies::default()), |
| 194 | }, | 201 | }, |
| 195 | None => return Ok(None), | 202 | None => return Ok((None, false)), |
| 196 | }; | 203 | }; |
| 197 | 204 | ||
| 198 | let default = Rewrites::default(); | 205 | let default = Rewrites::default(); |
| 199 | Ok(Rewrites { | 206 | Ok(( |
| 200 | copies, | 207 | Rewrites { |
| 201 | limit: config | 208 | copies, |
| 202 | .integer(rename_limit) | 209 | limit: config |
| 203 | .map(|value| rename_limit.try_into_usize(value)) | 210 | .integer(rename_limit) |
| 204 | .transpose() | 211 | .map(|value| rename_limit.try_into_usize(value)) |
| 205 | .with_leniency(lenient)? | 212 | .transpose() |
| 206 | .unwrap_or(default.limit), | 213 | .with_leniency(lenient)? |
| 207 | ..default | 214 | .unwrap_or(default.limit), |
| 208 | } | 215 | ..default |
| 209 | .into()) | 216 | } |
| 217 | .into(), | ||
| 218 | true, | ||
| 219 | )) | ||
| 210 | } | 220 | } |
| 211 | 221 | ||
| 212 | /// Return a low-level utility to efficiently prepare a blob-level diff operation between two resources, | 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 | pub mod tree { | 108 | pub mod tree { |
| 109 | use gix_merge::blob::builtin_driver; | 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 | /// The outcome produced by [`Repository::merge_trees()`](crate::Repository::merge_trees()). | 115 | /// The outcome produced by [`Repository::merge_trees()`](crate::Repository::merge_trees()). |
| 113 | #[derive(Clone)] | 116 | #[derive(Clone)] |
| @@ -136,22 +139,19 @@ pub mod tree { · 22 unmodified lines | |||
| 136 | 139 | ||
| 137 | /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a | 140 | /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a |
| 138 | /// conflict should be considered unresolved. | 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 | /// It's important that `index` is at the state of [`Self::tree`]. |
| 146 | /// in-memory entry is still present. | 147 | /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`. |
| 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. | ||
| 149 | pub fn index_changed_after_applying_conflicts( | 148 | pub fn index_changed_after_applying_conflicts( |
| 150 | &self, | 149 | &self, |
| 151 | index: &mut gix_index::State, | 150 | index: &mut gix_index::State, |
| 152 | how: TreatAsUnresolved, | 151 | how: TreatAsUnresolved, |
| 152 | removal_mode: apply_index_entries::RemovalMode, | ||
| 153 | ) -> bool { | 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 | pub struct Options { | 160 | pub struct Options { |
| 161 | inner: gix_merge::tree::Options, | 161 | inner: gix_merge::tree::Options, |
| 162 | file_favor: Option<FileFavor>, | 162 | file_favor: Option<FileFavor>, |
| 163 | tree_favor: Option<TreeFavor>, | ||
| 163 | } | 164 | } |
| 164 | 165 | ||
| 165 | impl From<gix_merge::tree::Options> for Options { | 166 | impl From<gix_merge::tree::Options> for Options { |
| @@ -167,6 +168,7 @@ pub mod tree { · 1 unmodified line | |||
| 167 | Options { | 168 | Options { |
| 168 | inner: opts, | 169 | inner: opts, |
| 169 | file_favor: None, | 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 | opts.blob_merge.resolve_binary_with = Some(resolve_binary); | 192 | opts.blob_merge.resolve_binary_with = Some(resolve_binary); |
| 191 | opts.blob_merge.text.conflict = resolve_text; | 193 | opts.blob_merge.text.conflict = resolve_text; |
| 192 | } | 194 | } |
| 195 | opts.tree_conflicts = value.tree_favor.map(Into::into); | ||
| 193 | opts | 196 | opts |
| 194 | } | 197 | } |
| 195 | } | 198 | } |
| @@ -202,7 +205,7 @@ pub mod tree { · 6 unmodified lines | |||
| 202 | /// * binary files | 205 | /// * binary files |
| 203 | /// * symlinks (a form of file after all) | 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 | #[derive(Debug, Copy, Clone)] | 209 | #[derive(Debug, Copy, Clone)] |
| 207 | pub enum FileFavor { | 210 | pub enum FileFavor { |
| 208 | /// Choose *our* side in case of a conflict. | 211 | /// Choose *our* side in case of a conflict. |
| @@ -215,6 +218,35 @@ pub mod tree { · 6 unmodified lines | |||
| 215 | Theirs, | 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 | /// Builder | 250 | /// Builder |
| 219 | impl Options { | 251 | impl Options { |
| 220 | /// If *not* `None`, rename tracking will be performed when determining the changes of each side of the merge. | 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 | /// When `None`, the default, both sides will be treated equally, and in case of conflict an unbiased representation | 266 | /// When `None`, the default, both sides will be treated equally, and in case of conflict an unbiased representation |
| 235 | /// is chosen both for content and for trees, causing a conflict. | 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 | pub fn with_file_favor(mut self, file_favor: Option<FileFavor>) -> Self { | 271 | pub fn with_file_favor(mut self, file_favor: Option<FileFavor>) -> Self { |
| 238 | self.file_favor = file_favor; | 272 | self.file_favor = file_favor; |
| 239 | self | 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 | pub struct EntryRef<'repo, 'a> { | 5 | pub struct EntryRef<'repo, 'a> { |
| 6 | /// The actual entry ref we are wrapping. | 6 | /// The actual entry ref we are wrapping. |
| 7 | pub inner: gix_object::tree::EntryRef<'a>, | 7 | pub inner: gix_object::tree::EntryRef<'a>, |
| 8 | 8 | /// The owning repository. | |
| 9 | pub(crate) repo: &'repo Repository, | 9 | pub repo: &'repo Repository, |
| 10 | } | 10 | } |
| 11 | 11 | ||
| 12 | impl<'repo, 'a> EntryRef<'repo, 'a> { | 12 | impl<'repo, 'a> EntryRef<'repo, 'a> { |
| @@ -10,7 +10,8 @@ use crate::{object::find, Id, ObjectDetached, Tree}; · 9 unmodified lines | |||
| 10 | pub struct Editor<'repo> { | 10 | pub struct Editor<'repo> { |
| 11 | pub(crate) inner: gix_object::tree::Editor<'repo>, | 11 | pub(crate) inner: gix_object::tree::Editor<'repo>, |
| 12 | pub(crate) validate: gix_validate::path::component::Options, | 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 | /// Initialization | 17 | /// Initialization |
| @@ -197,7 +198,8 @@ impl std::fmt::Debug for Tree<'_> { · 180 unmodified lines | |||
| 197 | #[derive(PartialEq, Debug, Clone)] | 198 | #[derive(PartialEq, Debug, Clone)] |
| 198 | pub struct Entry<'repo> { | 199 | pub struct Entry<'repo> { |
| 199 | pub(crate) inner: gix_object::tree::Entry, | 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 | mod entry { | 205 | mod entry { |
| @@ -8,7 +8,8 @@ use gix_ref::file::ReferenceExt; · 7 unmodified lines | |||
| 8 | #[must_use = "Iterators should be obtained from this iterator platform"] | 8 | #[must_use = "Iterators should be obtained from this iterator platform"] |
| 9 | pub struct Platform<'r> { | 9 | pub struct Platform<'r> { |
| 10 | pub(crate) platform: gix_ref::file::iter::Platform<'r>, | 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 | /// An iterator over references, with or without filter. | 15 | /// An iterator over references, with or without filter. |
| @@ -88,27 +88,27 @@ impl Repository { · 87 unmodified lines | |||
| 88 | 88 | ||
| 89 | /// Read all relevant configuration options to instantiate options for use in [`merge_trees()`](Self::merge_trees). | 89 | /// Read all relevant configuration options to instantiate options for use in [`merge_trees()`](Self::merge_trees). |
| 90 | pub fn tree_merge_options(&self) -> Result<crate::merge::tree::Options, tree_merge_options::Error> { | 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 | Ok(gix_merge::tree::Options { | 104 | Ok(gix_merge::tree::Options { |
| 92 | rewrites: Some( | 105 | rewrites, |
| 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 | ), | ||
| 106 | blob_merge: self.blob_merge_options()?, | 106 | blob_merge: self.blob_merge_options()?, |
| 107 | blob_merge_command_ctx: self.command_context()?, | 107 | blob_merge_command_ctx: self.command_context()?, |
| 108 | fail_on_conflict: None, | 108 | fail_on_conflict: None, |
| 109 | marker_size_multiplier: 0, | 109 | marker_size_multiplier: 0, |
| 110 | symlink_conflicts: None, | 110 | symlink_conflicts: None, |
| 111 | allow_lossy_resolution: false, | 111 | tree_conflicts: None, |
| 112 | } | 112 | } |
| 113 | .into()) | 113 | .into()) |
| 114 | } | 114 | } |
| @@ -28,5 +28,6 @@ pub struct Spec<'repo> { · 27 unmodified lines | |||
| 28 | pub(crate) first_ref: Option<gix_ref::Reference>, | 28 | pub(crate) first_ref: Option<gix_ref::Reference>, |
| 29 | /// The second name of a reference as seen while parsing a `RevSpec`, for completeness. | 29 | /// The second name of a reference as seen while parsing a `RevSpec`, for completeness. |
| 30 | pub(crate) second_ref: Option<gix_ref::Reference>, | 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 | /// author or commit messages will be required of *all* commits traversed here, it should be better to avoid trying to load it | 152 | /// author or commit messages will be required of *all* commits traversed here, it should be better to avoid trying to load it |
| 153 | /// by [turning commit-graph support off][Platform::use_commit_graph()]. This certainly is a micro-optimization though. | 153 | /// by [turning commit-graph support off][Platform::use_commit_graph()]. This certainly is a micro-optimization though. |
| 154 | pub struct Platform<'repo> { | 154 | pub struct Platform<'repo> { |
| 155 | pub(crate) repo: &'repo Repository, | 155 | /// The owning repository. |
| 156 | pub repo: &'repo Repository, | ||
| 156 | pub(crate) tips: Vec<ObjectId>, | 157 | pub(crate) tips: Vec<ObjectId>, |
| 157 | pub(crate) prune: Vec<ObjectId>, | 158 | pub(crate) prune: Vec<ObjectId>, |
| 158 | pub(crate) sorting: Sorting, | 159 | pub(crate) sorting: Sorting, |
| @@ -334,7 +335,8 @@ pub mod iter { · 175 unmodified lines | |||
| 334 | pub(crate) mod iter_impl { | 335 | pub(crate) mod iter_impl { |
| 335 | /// The iterator returned by [`crate::revision::walk::Platform::all()`]. | 336 | /// The iterator returned by [`crate::revision::walk::Platform::all()`]. |
| 336 | pub struct Walk<'repo> { | 337 | pub struct Walk<'repo> { |
| 337 | pub(crate) repo: &'repo crate::Repository, | 338 | /// The owning repository. |
| 339 | pub repo: &'repo crate::Repository, | ||
| 338 | pub(crate) inner: Box<dyn Iterator<Item = Result<gix_traverse::commit::Info, super::iter::Error>> + 'repo>, | 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 | /// A platform maintaining state needed to interact with submodules, created by [`Repository::submodules()]. | 24 | /// A platform maintaining state needed to interact with submodules, created by [`Repository::submodules()]. |
| 25 | pub(crate) struct SharedState<'repo> { | 25 | pub(crate) struct SharedState<'repo> { |
| 26 | pub(crate) repo: &'repo Repository, | 26 | pub repo: &'repo Repository, |
| 27 | pub(crate) modules: ModulesSnapshot, | 27 | pub(crate) modules: ModulesSnapshot, |
| 28 | is_active: RefCell<Option<IsActiveState>>, | 28 | is_active: RefCell<Option<IsActiveState>>, |
| 29 | index: RefCell<Option<IndexPersistedOrInMemory>>, | 29 | index: RefCell<Option<IndexPersistedOrInMemory>>, |
| @@ -19,7 +19,8 @@ pub struct Worktree<'repo> { · 18 unmodified lines | |||
| 19 | pub struct Head<'repo> { | 19 | pub struct Head<'repo> { |
| 20 | /// One of various possible states for the HEAD reference | 20 | /// One of various possible states for the HEAD reference |
| 21 | pub kind: head::Kind, | 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 | /// An [`ObjectId`] with access to a repository. | 26 | /// An [`ObjectId`] with access to a repository. |
| @@ -27,7 +28,8 @@ pub struct Head<'repo> { · 1 unmodified line | |||
| 27 | pub struct Id<'r> { | 28 | pub struct Id<'r> { |
| 28 | /// The actual object id | 29 | /// The actual object id |
| 29 | pub(crate) inner: ObjectId, | 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 | /// A decoded object with a reference to its owning repository. | 35 | /// A decoded object with a reference to its owning repository. |
| @@ -39,7 +41,8 @@ pub struct Object<'repo> { · 5 unmodified lines | |||
| 39 | pub kind: gix_object::Kind, | 41 | pub kind: gix_object::Kind, |
| 40 | /// The fully decoded object data | 42 | /// The fully decoded object data |
| 41 | pub data: Vec<u8>, | 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 | impl Drop for Object<'_> { | 48 | impl Drop for Object<'_> { |
| @@ -55,7 +58,8 @@ pub struct Blob<'repo> { · 9 unmodified lines | |||
| 55 | pub id: ObjectId, | 58 | pub id: ObjectId, |
| 56 | /// The blob's data. | 59 | /// The blob's data. |
| 57 | pub data: Vec<u8>, | 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 | impl Drop for Blob<'_> { | 65 | impl Drop for Blob<'_> { |
| @@ -71,7 +75,8 @@ pub struct Tree<'repo> { · 9 unmodified lines | |||
| 71 | pub id: ObjectId, | 75 | pub id: ObjectId, |
| 72 | /// The fully decoded tree data | 76 | /// The fully decoded tree data |
| 73 | pub data: Vec<u8>, | 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 | impl Drop for Tree<'_> { | 82 | impl Drop for Tree<'_> { |
| @@ -87,7 +92,8 @@ pub struct Tag<'repo> { · 9 unmodified lines | |||
| 87 | pub id: ObjectId, | 92 | pub id: ObjectId, |
| 88 | /// The fully decoded tag data | 93 | /// The fully decoded tag data |
| 89 | pub data: Vec<u8>, | 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 | impl Drop for Tag<'_> { | 99 | impl Drop for Tag<'_> { |
| @@ -103,7 +109,8 @@ pub struct Commit<'repo> { · 9 unmodified lines | |||
| 103 | pub id: ObjectId, | 109 | pub id: ObjectId, |
| 104 | /// The fully decoded commit data | 110 | /// The fully decoded commit data |
| 105 | pub data: Vec<u8>, | 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 | impl Drop for Commit<'_> { | 116 | impl Drop for Commit<'_> { |
| @@ -132,7 +139,8 @@ pub struct ObjectDetached { · 22 unmodified lines | |||
| 132 | pub struct Reference<'r> { | 139 | pub struct Reference<'r> { |
| 133 | /// The actual reference data | 140 | /// The actual reference data |
| 134 | pub inner: gix_ref::Reference, | 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 | /// A thread-local handle to interact with a repository from a single thread. | 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 | // pub(crate) prune: bool, | 227 | // pub(crate) prune: bool, |
| 220 | // /// Delete tags that don't exist on the remote anymore, equivalent to pruning the refspec `refs/tags/*:refs/tags/*`. | 228 | // /// Delete tags that don't exist on the remote anymore, equivalent to pruning the refspec `refs/tags/*:refs/tags/*`. |
| 221 | // pub(crate) prune_tags: bool, | 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 | /// A utility to make matching against pathspecs simple. | 234 | /// A utility to make matching against pathspecs simple. |
| @@ -231,7 +240,8 @@ pub struct Remote<'repo> { · 5 unmodified lines | |||
| 231 | #[derive(Clone)] | 240 | #[derive(Clone)] |
| 232 | #[cfg(feature = "attributes")] | 241 | #[cfg(feature = "attributes")] |
| 233 | pub struct Pathspec<'repo> { | 242 | pub struct Pathspec<'repo> { |
| 234 | pub(crate) repo: &'repo Repository, | 243 | /// The owning repository. |
| 244 | pub repo: &'repo Repository, | ||
| 235 | /// The cache to power attribute access. It's only initialized if we have a pattern with attributes. | 245 | /// The cache to power attribute access. It's only initialized if we have a pattern with attributes. |
| 236 | pub(crate) stack: Option<gix_worktree::Stack>, | 246 | pub(crate) stack: Option<gix_worktree::Stack>, |
| 237 | /// The prepared search to use for checking matches. | 247 | /// The prepared search to use for checking matches. |
| @@ -261,6 +271,7 @@ pub struct Submodule<'repo> { · 23 unmodified lines | |||
| 261 | /// A utility to access `.gitattributes` and `.gitignore` information efficiently. | 271 | /// A utility to access `.gitattributes` and `.gitignore` information efficiently. |
| 262 | #[cfg(any(feature = "attributes", feature = "excludes"))] | 272 | #[cfg(any(feature = "attributes", feature = "excludes"))] |
| 263 | pub struct AttributeStack<'repo> { | 273 | pub struct AttributeStack<'repo> { |
| 264 | pub(crate) repo: &'repo Repository, | 274 | /// The owning repository. |
| 275 | pub repo: &'repo Repository, | ||
| 265 | pub(crate) inner: gix_worktree::Stack, | 276 | pub(crate) inner: gix_worktree::Stack, |
| 266 | } | 277 | } |
| @@ -165,6 +165,7 @@ mod track_rewrites { · 164 unmodified lines | |||
| 165 | }), | 165 | }), |
| 166 | limit: 1000, | 166 | limit: 1000, |
| 167 | percentage: Some(0.5), | 167 | percentage: Some(0.5), |
| 168 | track_empty: false, | ||
| 168 | }; | 169 | }; |
| 169 | let out = from | 170 | let out = from |
| 170 | .changes()? | 171 | .changes()? |
| @@ -172,9 +172,13 @@ pub fn main() -> Result<()> { · 171 unmodified lines | |||
| 172 | }, | 172 | }, |
| 173 | ), | 173 | ), |
| 174 | merge::SubCommands::Tree { | 174 | merge::SubCommands::Tree { |
| 175 | in_memory, | 175 | opts: |
| 176 | file_favor, | 176 | merge::SharedOptions { |
| 177 | debug, | 177 | in_memory, |
| 178 | file_favor, | ||
| 179 | tree_favor, | ||
| 180 | debug, | ||
| 181 | }, | ||
| 178 | ours, | 182 | ours, |
| 179 | base, | 183 | base, |
| 180 | theirs, | 184 | theirs, |
| @@ -197,15 +201,20 @@ pub fn main() -> Result<()> { · 16 unmodified lines | |||
| 197 | format, | 201 | format, |
| 198 | file_favor: file_favor.map(Into::into), | 202 | file_favor: file_favor.map(Into::into), |
| 199 | in_memory, | 203 | in_memory, |
| 204 | tree_favor: tree_favor.map(Into::into), | ||
| 200 | debug, | 205 | debug, |
| 201 | }, | 206 | }, |
| 202 | ) | 207 | ) |
| 203 | }, | 208 | }, |
| 204 | ), | 209 | ), |
| 205 | merge::SubCommands::Commit { | 210 | merge::SubCommands::Commit { |
| 206 | in_memory, | 211 | opts: |
| 207 | file_favor, | 212 | merge::SharedOptions { |
| 208 | debug, | 213 | in_memory, |
| 214 | file_favor, | ||
| 215 | tree_favor, | ||
| 216 | debug, | ||
| 217 | }, | ||
| 209 | ours, | 218 | ours, |
| 210 | theirs, | 219 | theirs, |
| 211 | } => prepare_and_run( | 220 | } => prepare_and_run( |
| @@ -225,6 +234,7 @@ pub fn main() -> Result<()> { · 13 unmodified lines | |||
| 225 | core::repository::merge::tree::Options { | 234 | core::repository::merge::tree::Options { |
| 226 | format, | 235 | format, |
| 227 | file_favor: file_favor.map(Into::into), | 236 | file_favor: file_favor.map(Into::into), |
| 237 | tree_favor: tree_favor.map(Into::into), | ||
| 228 | in_memory, | 238 | in_memory, |
| 229 | debug, | 239 | debug, |
| 230 | }, | 240 | }, |
| @@ -375,6 +375,14 @@ pub mod merge { · 374 unmodified lines | |||
| 375 | Theirs, | 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 | impl From<FileFavor> for gix::merge::tree::FileFavor { | 386 | impl From<FileFavor> for gix::merge::tree::FileFavor { |
| 379 | fn from(value: FileFavor) -> Self { | 387 | fn from(value: FileFavor) -> Self { |
| 380 | match value { | 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 | #[derive(Debug, clap::Parser)] | 422 | #[derive(Debug, clap::Parser)] |
| 388 | #[command(about = "perform merges of various kinds")] | 423 | #[command(about = "perform merges of various kinds")] |
| 389 | pub struct Platform { | 424 | pub struct Platform { |
| @@ -412,17 +447,8 @@ pub mod merge { · 22 unmodified lines | |||
| 412 | 447 | ||
| 413 | /// Merge a tree by specifying ours, base and theirs, writing it to the object database. | 448 | /// Merge a tree by specifying ours, base and theirs, writing it to the object database. |
| 414 | Tree { | 449 | Tree { |
| 415 | /// Keep all objects to be written in memory to avoid any disk IO. | 450 | #[clap(flatten)] |
| 416 | /// | 451 | opts: SharedOptions, |
| 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, | ||
| 426 | 452 | ||
| 427 | /// A revspec to our treeish. | 453 | /// A revspec to our treeish. |
| 428 | #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)] | 454 | #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)] |
| @@ -436,17 +462,8 @@ pub mod merge { · 7 unmodified lines | |||
| 436 | }, | 462 | }, |
| 437 | /// Merge a commits by specifying ours, and theirs, writing the tree to the object database. | 463 | /// Merge a commits by specifying ours, and theirs, writing the tree to the object database. |
| 438 | Commit { | 464 | Commit { |
| 439 | /// Keep all objects to be written in memory to avoid any disk IO. | 465 | #[clap(flatten)] |
| 440 | /// | 466 | opts: SharedOptions, |
| 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, | ||
| 450 | 467 | ||
| 451 | /// A revspec to our committish. | 468 | /// A revspec to our committish. |
| 452 | #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)] | 469 | #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)] |
| @@ -62,6 +62,10 @@ struct Record { · 61 unmodified lines | |||
| 62 | } | 62 | } |
| 63 | 63 | ||
| 64 | static GIT_CONFIG: &[Record] = &[ | 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 | Record { | 69 | Record { |
| 66 | config: "core.symlinks", | 70 | config: "core.symlinks", |
| 67 | usage: Planned("Needed to handle checkouts faithfully") | 71 | usage: Planned("Needed to handle checkouts faithfully") |
| @@ -521,11 +521,18 @@ fn scripted_fixture_read_only_with_args_inner( · 520 unmodified lines | |||
| 521 | |d| (true, d.to_owned()), | 521 | |d| (true, d.to_owned()), |
| 522 | ); | 522 | ); |
| 523 | 523 | ||
| 524 | let _marker = gix_lock::Marker::acquire_to_hold_resource( | 524 | // We may that destination_dir is already unique (i.e. temp-dir) - thus there is no need for a lock, |
| 525 | script_basename, | 525 | // and we can execute scripts in parallel. |
| 526 | gix_lock::acquire::Fail::AfterDurationWithBackoff(Duration::from_secs(6 * 60)), | 526 | let _marker = destination_dir |
| 527 | None, | 527 | .is_none() |
| 528 | )?; | 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 | let failure_marker = script_result_directory.join("_invalid_state_due_to_script_failure_"); | 536 | let failure_marker = script_result_directory.join("_invalid_state_due_to_script_failure_"); |
| 530 | if force_run || !script_result_directory.is_dir() || failure_marker.is_file() { | 537 | if force_run || !script_result_directory.is_dir() || failure_marker.is_file() { |
| 531 | if failure_marker.is_file() { | 538 | if failure_marker.is_file() { |