History

Merge pull request #1705 from GitoxideLabs/merge

tree merge with tree-related auto-resolution

Sebastian Thiel committed 1 year ago

Commit 520c8322 parents fadf106 960773e61 files · +1988 −507Browse files

Unified · Split

61 files
  1. Cargo.lock +1 −0
  2. crate-status.md +3 −0
  3. gitoxide-core/src/query/engine/update.rs +6 −1
  4. gitoxide-core/src/repository/merge/commit.rs +6 −2
  5. gitoxide-core/src/repository/merge/tree.rs +7 −2
  6. gitoxide-core/src/repository/status.rs +1 −0
  7. gix-diff/src/lib.rs +5 −0
  8. gix-diff/src/rewrites/mod.rs +1 −0
  9. gix-diff/src/rewrites/tracker.rs +44 −7
  10. gix-diff/tests/diff/rewrites/tracker.rs +10 −0
  11. gix-diff/tests/diff/tree_with_rewrites.rs +19 −1
  12. gix-merge/src/blob/builtin_driver/binary.rs +4 −2
  13. gix-merge/src/blob/builtin_driver/text/mod.rs +3 −0
  14. gix-merge/src/blob/builtin_driver/text/utils.rs +3 −2
  15. gix-merge/src/blob/platform/merge.rs +15 −7
  16. gix-merge/src/commit/mod.rs +1 −0
  17. gix-merge/src/commit/virtual_merge_base.rs +6 −6
  18. gix-merge/src/lib.rs +1 −0
  19. gix-merge/src/tree/function.rs +396 −221
  20. gix-merge/src/tree/mod.rs +193 −67
  21. gix-merge/src/tree/utils.rs +14 −17
  22. gix-merge/tests/fixtures/generated-archives/tree-baseline.tar +0 −0
  23. gix-merge/tests/fixtures/tree-baseline.sh +803 −10
  24. gix-merge/tests/merge/blob/builtin_driver.rs +3 −3
  25. gix-merge/tests/merge/blob/platform.rs +51 −2
  26. gix-merge/tests/merge/tree/baseline.rs +22 −3
  27. gix-merge/tests/merge/tree/mod.rs +92 −38
  28. gix-status/tests/status/index_as_worktree_with_renames.rs +1 −0
  29. gix-url/Cargo.toml +1 −0
  30. gix-url/src/impls.rs +1 −1
  31. gix-url/src/lib.rs +6 −2
  32. gix-url/src/parse.rs +26 −9
  33. gix-url/tests/fixtures/fuzzed/illegal-utf8.url +1 −0
  34. gix-url/tests/url/access.rs +2 −2
  35. gix-url/tests/url/baseline.rs +1 −0
  36. gix-url/tests/url/expand_path.rs +0 −0
  37. gix-url/tests/url/fuzzed.rs +1 −0
  38. gix-url/tests/url/main.rs +2 −0
  39. gix-url/tests/url/parse/file.rs +0 −0
  40. gix-url/tests/url/parse/http.rs +20 −0
  41. gix-url/tests/url/parse/invalid.rs +0 −0
  42. gix-url/tests/url/parse/mod.rs +0 −0
  43. gix-url/tests/url/parse/ssh.rs +0 −0
  44. gix/src/commit.rs +2 −1
  45. gix/src/config/cache/access.rs +1 −1
  46. gix/src/config/mod.rs +7 −4
  47. gix/src/diff.rs +27 −17
  48. gix/src/merge.rs +56 −12
  49. gix/src/object/tree/iter.rs +2 −2
  50. gix/src/object/tree/mod.rs +4 −2
  51. gix/src/reference/iter.rs +2 −1
  52. gix/src/repository/merge.rs +15 −15
  53. gix/src/revision/mod.rs +2 −1
  54. gix/src/revision/walk.rs +4 −2
  55. gix/src/submodule/mod.rs +1 −1
  56. gix/src/types.rs +22 −11
  57. gix/tests/gix/object/tree/diff.rs +1 −0
  58. src/plumbing/main.rs +16 −6
  59. src/plumbing/options/mod.rs +39 −22
  60. src/plumbing/progress.rs +4 −0
  61. tests/tools/src/lib.rs +12 −5
Cargo.lock+1−0
@@ -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",
crate-status.md+3−0
@@ -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-cases353* [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 merge360* [x] **commits** - with handling of multiple merge bases by recursive merge-base merge
358* [x] API documentation361* [x] API documentation
359 * [ ] Examples362 * [ ] Examples
gitoxide-core/src/query/engine/update.rs+6−1
@@ -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::FromSetOfModifiedFilesAndAllSources150 CopySource::FromSetOfModifiedFilesAndAllSources
gitoxide-core/src/repository/merge/commit.rs+6−2
@@ -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;
gitoxide-core/src/repository/merge/tree.rs+7−2
@@ -3,6 +3,7 @@ use crate::OutputFormat; · 2 unmodified lines
3pub struct Options {3pub 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;
gitoxide-core/src/repository/status.rs+1−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() {
gix-diff/src/lib.rs+5−0
@@ -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 not37 /// 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.
gix-diff/src/rewrites/mod.rs+1−0
@@ -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}
gix-diff/src/rewrites/tracker.rs+44−7
@@ -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.emitted403 (!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 =
gix-diff/tests/diff/rewrites/tracker.rs+10−0
@@ -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;
gix-diff/tests/diff/tree_with_rewrites.rs+19−1
@@ -192,12 +192,13 @@ fn changes_against_modified_tree_with_filename_tracking() -> crate::Result { · 191 unmodified lines
192 192
193#[test]193#[test]
194fn renames_by_identity() -> crate::Result {194fn 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 way719 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 )?;
gix-merge/src/blob/builtin_driver/binary.rs+4−2
@@ -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 }
gix-merge/src/blob/builtin_driver/text/mod.rs+3−0
@@ -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)]
59pub struct Labels<'a> {59pub 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
gix-merge/src/blob/builtin_driver/text/utils.rs+3−2
@@ -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)
gix-merge/src/blob/platform/merge.rs+15−7
@@ -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(
gix-merge/src/commit/mod.rs+1−0
@@ -55,5 +55,6 @@ pub struct Outcome<'a> { · 54 unmodified lines
55 55
56pub(super) mod function;56pub(super) mod function;
57 57
58///
58pub mod virtual_merge_base;59pub mod virtual_merge_base;
59pub use virtual_merge_base::function::virtual_merge_base;60pub use virtual_merge_base::function::virtual_merge_base;
gix-merge/src/commit/virtual_merge_base.rs+6−6
@@ -30,7 +30,7 @@ pub enum Error { · 29 unmodified lines
30pub(super) mod function {30pub(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 out89 if out.tree_merge.has_unresolved_conflicts(TreatAsUnresolved {
90 .tree_merge90 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 = out95 let merged_tree_id = out
gix-merge/src/lib.rs+1−0
@@ -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///
gix-merge/src/tree/function.rs+396−221
@@ -5,7 +5,7 @@ use crate::tree::utils::{ · 4 unmodified lines
5use crate::tree::ConflictMapping::{Original, Swapped};5use crate::tree::ConflictMapping::{Original, Swapped};
6use crate::tree::{6use 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};
10use bstr::{BString, ByteSlice};10use bstr::{BString, ByteSlice};
11use gix_diff::tree::recorder::Location;11use gix_diff::tree::recorder::Location;
@@ -13,6 +13,7 @@ use gix_diff::tree_with_rewrites::Change; · 1 unmodified line
13use gix_hash::ObjectId;13use gix_hash::ObjectId;
14use gix_object::tree::{EntryKind, EntryMode};14use gix_object::tree::{EntryKind, EntryMode};
15use gix_object::{tree, FindExt};15use gix_object::{tree, FindExt};
16use std::borrow::Cow;
16use std::convert::Infallible;17use 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_failure267 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 Swapped373 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 None644 }
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_right701 change_on_right
624 .map(|change| {702 .map(|change| {
625 change.inner.entry_mode().is_tree() && change.inner.location() == location703 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 Original1016 Original
889 } else {1017 } else {
890 Swapped1018 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 is1145 // 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_failure1228 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
1263fn 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
1101fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool {1276fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool {
1102 a.is_commit() || b.is_commit()1277 a.is_commit() || b.is_commit()
1103}1278}
gix-merge/src/tree/mod.rs+193−67
@@ -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 needed39 /// 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)]
51pub 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) with59pub 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 not62 /// 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 {
56pub 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 content70 ForcedResolution,
62 /// merges where conflicts where auto-resolved in any way, like choosing71 }
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 will79 #[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 will82 /// 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
78impl Outcome<'_> {115impl 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 a122 /// 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 the129 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)]
154enum ConflictMapping {196enum 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
173impl Conflict {221impl 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::CompleteWithAutoResolvedConflict230 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::Unknown300 ResolutionFailure::Unknown
301 | ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { .. }
249 | ResolutionFailure::OursModifiedTheirsDeleted302 | ResolutionFailure::OursModifiedTheirsDeleted
250 | ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch303 | 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 kept385 /// *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)]
341pub struct Options {417pub 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/ancestor439 /// 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)]
451pub 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
366pub(super) mod function;461pub(super) mod function;
367mod utils;462mod utils;
463///
368pub mod apply_index_entries {464pub 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 one490 /// 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 means492 /// 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 also495 /// 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 matching496 /// 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() != len622 res
497 }623 }
498 }624 }
499}625}
gix-merge/src/tree/utils.rs+14−17
@@ -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 path23/// 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.
25pub fn possibly_rewritten_location(25pub 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
60pub fn unique_path_in_tree(60pub 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
gix-merge/tests/fixtures/generated-archives/tree-baseline.tar+0−0

Binary file not shown.

gix-merge/tests/fixtures/tree-baseline.sh+803−10
@@ -36,6 +36,15 @@ function make_conflict_index() { · 35 unmodified lines
36 cp .git/index .git/"${identifier}".index36 cp .git/index .git/"${identifier}".index
37}37}
38 38
39function 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
39function baseline () (48function 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" ]; then74 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 fi77 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" ]]; then83 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.cases86 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 fi87 fi
74)88)
75 89
90
91git 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
110git 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
130git 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
150git 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
173100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/b
174100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/sub/b
175100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a~B
176EOF
177 make_conflict_index tree-to-non-tree-with-rename-A-B
178
179 rm .git/index
180 git update-index --index-info <<EOF
181100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/b
182100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/sub/b
183100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a~B
184EOF
185 make_conflict_index tree-to-non-tree-with-rename-A-B-reversed
186)
187
76git init simple188git init simple
77(cd simple189(cd simple
78 rm -Rf .git/hooks190 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
337git 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
225git init rename-rename-plus-content357git init rename-rename-plus-content
226(cd rename-rename-plus-content358(cd rename-rename-plus-content
227 write_lines 1 2 3 4 5 >foo359 write_lines 1 2 3 4 5 >foo
@@ -401,12 +533,40 @@ git init rename-within-rename · 173 unmodified lines
401 git checkout expected533 git checkout expected
402 write_lines 1 2 3 4 5 6 >a/x.f534 write_lines 1 2 3 4 5 6 >a/x.f
403 write_lines 1 2 3 4 5 6 >a/sub/y.f535 write_lines 1 2 3 4 5 6 >a/sub/y.f
404 git mv a/sub a/sub-renamed536 cp -Rv a/sub a/sub-renamed
537 git add .
405 git mv a a-renamed538 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
545100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-renamed/sub/y.f
546100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z
547100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/sub-renamed/y.f
548100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/sub-renamed/z
549100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w
550100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f
551100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/sub-renamed/y.f
552100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a/sub-renamed/z
553100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f
554100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z
555EOF
409 make_conflict_index rename-within-rename-A-B-deviates556 make_conflict_index rename-within-rename-A-B-deviates
557 rm .git/index
558 git update-index --index-info <<EOF
559100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-renamed/sub/y.f
560100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z
561100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/sub-renamed/y.f
562100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/sub-renamed/z
563100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w
564100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f
565100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/sub-renamed/y.f
566100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a/sub-renamed/z
567100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f
568100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z
569EOF
410 make_conflict_index rename-within-rename-A-B-deviates-reversed570 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 ./a746 rm -Rf ./a
587 mkdir -p a-renamed/sub747 mkdir -p a-renamed/sub
588 write_lines 1 2 3 4 5 >a-renamed/sub/y.f748 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.f750 write_lines 1 2 3 4 5 6 >a-renamed/y.f
590 touch a-renamed/z a-renamed/sub/z751 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 matching757 # 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
599100644 8a1218a1024a212bb3db30becd860315f9f3ac52 2 a-renamed/sub/y.f761100644 8a1218a1024a212bb3db30becd860315f9f3ac52 2 a-renamed/sub/y.f
600100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z762100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z
601100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f763100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f
764100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-renamed/x.f
765100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/w
602100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z766100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z
767100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/x.f
768100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/y.f
603EOF769EOF
604 make_conflict_index conflicting-rename-complex-A-B770 make_conflict_index conflicting-rename-complex-A-B
605 771
@@ -608,7 +774,11 @@ EOF · 2 unmodified lines
608100644 8a1218a1024a212bb3db30becd860315f9f3ac52 3 a-renamed/sub/y.f774100644 8a1218a1024a212bb3db30becd860315f9f3ac52 3 a-renamed/sub/y.f
609100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z775100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z
610100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f776100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f
777100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-renamed/x.f
778100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/w
611100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z779100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z
780100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/x.f
781100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/y.f
612EOF782EOF
613 make_conflict_index conflicting-rename-complex-A-B-reversed783 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/w798 chmod +x a/x.f a/w
629 git update-index --chmod=+x a/x.f a/w799 git update-index --chmod=+x a/x.f a/w
630 git mv a a-renamed800 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 B803 git checkout B
634 write_lines original 1 2 3 4 5 6 >a/x.f804 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.1166baseline non-tree-to-tree A-B A B
1167baseline tree-to-non-tree A-B A B
1168baseline tree-to-non-tree-with-rename A-B A B
1169baseline non-tree-to-tree-with-rename A-B A B
1170baseline rename-add-same-symlink A-B A B
997baseline rename-add-exe-bit-conflict A-B A B1171baseline rename-add-exe-bit-conflict A-B A B
998baseline remove-executable-mode A-B A B1172baseline remove-executable-mode A-B A B
999baseline simple side-1-3-without-conflict side1 side31173baseline simple side-1-3-without-conflict side1 side3
@@ -1024,11 +1198,11 @@ baseline super-1 A-B-diff3 A B · 24 unmodified lines
1024baseline super-2 A-B A B1198baseline super-2 A-B A B
1025baseline super-2 A-B-diff3 A B1199baseline super-2 A-B-diff3 A B
1026 1200
1027baseline rename-within-rename A-B-deviates A B "Git doesn't detect the rename-nesting so there is duplication - we achieve the optimal result"1201baseline rename-within-rename A-B-deviates A B "Git doesn't detect the rename-nesting, and we do neith, and we do neither"
1028baseline rename-within-rename-2 A-B-deviates A B "TBD: Right, something is different documentation was forgotten :/"1202baseline rename-within-rename-2 A-B-deviates A B "TBD: Right, something is different documentation was forgotten :/"
1029baseline conflicting-rename A-B A B1203baseline conflicting-rename A-B A B
1030baseline conflicting-rename-2 A-B A B1204baseline conflicting-rename-2 A-B A B
1031baseline conflicting-rename-complex A-B A B "Git has different rename tracking which is why a-renamed/w disappears - it's still close enough"1205baseline conflicting-rename-complex A-B A B "Git has different rename tracking - overall result it's still close enough"
1032 1206
1033baseline same-rename-different-mode A-B A B "Git works for the A/B case, but for B/A it forgets to set the executable bit"1207baseline 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"
1034baseline renamed-symlink-with-conflict A-B A B1208baseline renamed-symlink-with-conflict A-B A B
@@ -1054,3 +1228,622 @@ baseline rename-and-modification A-B A B · 19 unmodified lines
1054baseline symlink-modification A-B A B1228baseline symlink-modification A-B A B
1055baseline symlink-addition A-B A B1229baseline symlink-addition A-B A B
1056baseline type-change-to-symlink A-B A B1230baseline 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
1241100644 45b983be36b73c0788dc9cbcb76cbb80fc7bb057 0 greeting
1242100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers
1243100644 5716ca5987cbf97d6bb54920bea6adde242d87e6 0 whatever
1244EOF
1245 make_resolve_tree ours side1 side2
1246
1247 rm .git/index
1248 git update-index --index-info <<EOF
1249100644 092bfb9bdf74dd8cfd22e812151281ee9aa6f01a 0 greeting
1250100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers
1251100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 whatever/empty
1252EOF
1253 make_resolve_tree ours side2 side1
1254
1255 rm .git/index
1256 git update-index --index-info <<EOF
1257100644 9dc97bdc2426e68423360e3e5299280b2cf6b8ff 0 greeting
1258100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers
1259100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 whatever
1260EOF
1261 make_resolve_tree ancestor side1 side2
1262
1263 rm .git/index
1264 git update-index --index-info <<EOF
1265100644 1a2664a9924754c698e323f756f9f87f3f2fb337 0 greeting
1266100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers
1267100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 whatever
1268EOF
1269 make_resolve_tree ancestor side2 side1
1270
1271 rm .git/index
1272 git update-index --index-info <<EOF
1273100644 a4ae6e4709228b5da6001cb9d1cfa7736851e2a6 0 greeting
1274100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 whatever
1275100644 542802a799ded74fa01c47ba2f8925e284a369e2 0 Αυτά μου φαίνονται κινέζικα
1276EOF
1277 make_resolve_tree ancestor tweak1 side2
1278
1279 rm .git/index
1280 git update-index --index-info <<EOF
1281100644 45b983be36b73c0788dc9cbcb76cbb80fc7bb057 0 greeting
1282100644 5716ca5987cbf97d6bb54920bea6adde242d87e6 0 whatever
1283100644 65bc6a1e238f4bf05b28fd05240636e2cfb657e0 0 Αυτά μου φαίνονται κινέζικα
1284EOF
1285 make_resolve_tree ours tweak1 side2
1286
1287 rm .git/index
1288 git update-index --index-info <<EOF
1289100644 blob ea28dcd7f627a2a7bbd09daa679c452180617c9f greeting
1290100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 whatever
1291100644 blob f801a62deed900f8a80ff35e3339474ad6352a93 Αυτά μου φαίνονται κινέζικα
1292EOF
1293 make_resolve_tree ancestor side2 tweak1
1294
1295 rm .git/index
1296 git update-index --index-info <<EOF
1297100644 blob 092bfb9bdf74dd8cfd22e812151281ee9aa6f01a greeting
1298100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 whatever/empty
1299100644 blob 09c277aa66897c58157f57a374eacc63a407dcab Αυτά μου φαίνονται κινέζικα
1300EOF
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
1308120000 blob 19102815663d23f8b75a47e7a01965dcdc96468c bar
1309EOF
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
1315100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 bar
1316EOF
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
1330100644 blob 8a1218a1024a212bb3db30becd860315f9f3ac52 foo
1331EOF
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
1339100644 blob d0549c3d3c96a464289f3b820b7d96aedc58924b bar
1340EOF
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
1347100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 baz
1348EOF
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
1362100644 blob 0cde534c2fca6c92c07b3e7a696665e844b9b933 bar
1363EOF
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
1373100644 blob f286e5cdd97ac6895438ea4548638bb98ac9bd6b bar
1374EOF
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
1383100644 blob 5716ca5987cbf97d6bb54920bea6adde242d87e6 bar
1384100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 foo
1385EOF
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
1394100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 baz
1395EOF
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
1404EOF
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
1419100644 blob 4b5599c7c2ed4390417d9699bec86144a386873d four
1420100644 blob 64012489f118cb4011c8902b4a635f70dcb0c0ca six
1421100644 blob e33f5e94470d3b5fa0220ff6a9cabb78a3f72fa3 two
1422EOF
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
1431EOF
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
1442100644 blob 8a1218a1024a212bb3db30becd860315f9f3ac52 foo
1443100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 newdir/a
1444100644 blob 61780798228d17af2d34fce4cfbdf35556832472 newdir/b
1445100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 newdir/bar/file
1446100644 blob f2ad6c76f0115a6ba5b00456a849810e7ec0af20 newdir/c
1447EOF
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
1455100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 newdir/a
1456100644 blob 61780798228d17af2d34fce4cfbdf35556832472 newdir/b
1457100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 newdir/bar/file
1458100644 blob f2ad6c76f0115a6ba5b00456a849810e7ec0af20 newdir/c
1459EOF
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
1467100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 newdir/a
1468100644 blob 61780798228d17af2d34fce4cfbdf35556832472 newdir/b
1469100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 newdir/bar
1470100644 blob f2ad6c76f0115a6ba5b00456a849810e7ec0af20 newdir/c
1471EOF
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
1480100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/y.f
1481100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/z
1482100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w
1483100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/x.f
1484EOF
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
1492100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-renamed/sub/y.f
1493100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/sub/z
1494100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/w
1495100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-renamed/x.f
1496EOF
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
1501100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-different/sub/y.f
1502100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-different/sub/z
1503100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-different/w
1504100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-different/x.f
1505EOF
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
1514100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/y.f
1515100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/z
1516100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w
1517100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1518EOF
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
1524100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub-renamed/y.f
1525100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub-renamed/z
1526100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w
1527100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1528EOF
1529 make_resolve_tree ours A B
1530 rm .git/index
1531 git update-index --index-info <<EOF
1532100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub-different/y.f
1533100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub-different/z
1534100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w
1535100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1536EOF
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
1547100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/z
1548100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/y.f
1549100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/z
1550100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w
1551100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/x.f
1552EOF
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
1565EOF
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
1574EOF
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
1584100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1585120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link
1586EOF
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
1593100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1594120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link-renamed
1595EOF
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
1601100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1602120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link-different
1603EOF
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
1612100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/x.f
1613120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link
1614EOF
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
1621100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/x.f
1622100644 blob f89a08d1e226b9a319210641b63b07dcf0bd705f link
1623EOF
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
1629100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/x.f
1630120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link-renamed
1631EOF
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
1641100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/x.f
1642120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link
1643EOF
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
1650100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1651100644 blob f89a08d1e226b9a319210641b63b07dcf0bd705f link
1652EOF
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
1666160000 commit e835c0c403c8e494c0ca98f3d25d0b8464c18d38 sub
1667EOF
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
1674160000 commit 64466ebdff775ad618d9cc993cf52840e0af528c sub
1675EOF
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
1681160000 commit ea6eb701e03c2497915c25a851f3da8f8e362ca0 sub
1682EOF
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
1692100644 blob 09c277aa66897c58157f57a374eacc63a407dcab content
1693EOF
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
1700100644 blob 0a6a0ba83635bc00e7c79a4b5b6e50381385c1af content
1701EOF
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
1715100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a
1716EOF
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
1723100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a
1724EOF
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
1730100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/d
1731100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/e
1732100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/b
1733100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/c
1734EOF
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
1744100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/b
1745EOF
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
1752100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub/b
1753EOF
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
1759100644 blob fa49b077972391ad58037050f2a75f74e3671e92 a
1760EOF
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
1769100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/b
1770EOF
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
1778EOF
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
1784100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a
1785EOF
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
1796100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/d
1797100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/e
1798100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub/b
1799100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/c
1800EOF
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
1822100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w
1823100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f
1824100644 44065282f89b9bd6439ed2e4674721383fd987eb 0 a/sub/y.f
1825100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a/sub/z
1826EOF
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
1833100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/sub/y.f
1834100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/sub/z
1835100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w
1836100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f
1837EOF
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
1843100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/sub-renamed/y.f
1844100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/sub-renamed/z
1845100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w
1846100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f
1847EOF
1848 make_resolve_tree ours B A
1849)
gix-merge/tests/merge/blob/builtin_driver.rs+3−3
@@ -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
gix-merge/tests/merge/blob/platform.rs+51−2
@@ -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!(
gix-merge/tests/merge/tree/baseline.rs+22−3
@@ -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
51pub struct ConflictInfo {52pub struct ConflictInfo {
52 /// All the paths involved in the informational message53 /// 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
377pub 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
374pub(crate) fn apply_git_index_entries(conflicts: &[Conflict], state: &mut gix_index::State) {393pub(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 {
gix-merge/tests/merge/tree/mod.rs+92−38
@@ -1,7 +1,8 @@
1use crate::tree::baseline::Deviation;1use crate::tree::baseline::Deviation;
2use gix_diff::Rewrites;2use gix_diff::Rewrites;
3use gix_merge::commit::Options;3use gix_merge::commit::Options;
4use gix_merge::tree::TreatAsUnresolved;4use gix_merge::tree::apply_index_entries::RemovalMode;
5use gix_merge::tree::{treat_as_unresolved, TreatAsUnresolved};
5use gix_object::Write;6use gix_object::Write;
6use gix_worktree::stack::state::attributes;7use gix_worktree::stack::state::attributes;
7use std::path::Path;8use 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.conflicts93 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 index114 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.conflicts126 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)]
229mod baseline;283mod baseline;
gix-status/tests/status/index_as_worktree_with_renames.rs+1−0
@@ -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",
gix-url/Cargo.toml+1−0
@@ -26,6 +26,7 @@ serde = { version = "1.0.114", optional = true, default-features = false, featur · 25 unmodified lines
26thiserror = "2.0.0"26thiserror = "2.0.0"
27url = "2.5.2"27url = "2.5.2"
28bstr = { version = "1.3.0", default-features = false, features = ["std"] }28bstr = { version = "1.3.0", default-features = false, features = ["std"] }
29percent-encoding = "2.3.1"
29 30
30document-features = { version = "0.2.0", optional = true }31document-features = { version = "0.2.0", optional = true }
31 32
gix-url/src/impls.rs+1−1
@@ -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 &storage85 &storage
86 } else {86 } else {
87 self87 self
gix-url/src/lib.rs+6−2
@@ -308,6 +308,10 @@ impl Url { · 307 unmodified lines
308 }308 }
309}309}
310 310
311fn percent_encode(s: &str) -> Cow<'_, str> {
312 percent_encoding::utf8_percent_encode(s, percent_encoding::NON_ALPHANUMERIC).into()
313}
314
311/// Serialization315/// Serialization
312impl Url {316impl 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())?;
gix-url/src/parse.rs+26−9
@@ -1,8 +1,8 @@
1use std::convert::Infallible;1use std::convert::Infallible;
2 2
3use bstr::{BStr, BString, ByteSlice};
4
5use crate::Scheme;3use crate::Scheme;
4use bstr::{BStr, BString, ByteSlice};
5use 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
128fn 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
125pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> {139pub(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
161fn url_user(url: &url::Url) -> Option<String> {178fn 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 None180 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
gix-url/tests/fixtures/fuzzed/illegal-utf8.url+1−0
@@ -0,0 +1 @@
1BBi%BBBBB,@}:m\
gix-url/tests/url/access.rsrenamed from gix-url/tests/access/mod.rs+2−2
@@ -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}
gix-url/tests/url/baseline.rsrenamed from gix-url/tests/baseline.rs+1−0
@@ -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)]
167mod baseline {168mod 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;
gix-url/tests/url/expand_path.rsrenamed from gix-url/tests/expand_path/mod.rs+0−0

File renamed without changes.

gix-url/tests/url/fuzzed.rsrenamed from gix-url/tests/fuzzed.rs+1−0
@@ -5,6 +5,7 @@ use bstr::ByteSlice; · 4 unmodified lines
5#[test]5#[test]
6fn fuzzed() {6fn 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",
gix-url/tests/url/main.rsrenamed from gix-url/tests/url.rs+2−0
@@ -2,5 +2,7 @@ pub type Error = Box<dyn std::error::Error>; · 1 unmodified line
2pub type Result = std::result::Result<(), Error>;2pub type Result = std::result::Result<(), Error>;
3 3
4mod access;4mod access;
5mod baseline;
5mod expand_path;6mod expand_path;
7mod fuzzed;
6mod parse;8mod parse;
gix-url/tests/url/parse/file.rsrenamed from gix-url/tests/parse/file.rs+0−0

File renamed without changes.

gix-url/tests/url/parse/http.rsrenamed from gix-url/tests/parse/http.rs+20−0
@@ -39,6 +39,26 @@ fn username_and_password_and_port() -> crate::Result { · 38 unmodified lines
39 )39 )
40}40}
41 41
42#[test]
43fn 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]
43fn only_password() -> crate::Result {63fn only_password() -> crate::Result {
44 assert_url_roundtrip(64 assert_url_roundtrip(
gix-url/tests/url/parse/invalid.rsrenamed from gix-url/tests/parse/invalid.rs+0−0

File renamed without changes.

gix-url/tests/url/parse/mod.rsrenamed from gix-url/tests/parse/mod.rs+0−0

File renamed without changes.

gix-url/tests/url/parse/ssh.rsrenamed from gix-url/tests/parse/ssh.rs+0−0

File renamed without changes.

gix/src/commit.rs+2−1
@@ -173,7 +173,8 @@ pub mod describe { · 172 unmodified lines
173 /// A support type to allow configuring a `git describe` operation173 /// 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,
gix/src/config/cache/access.rs+1−1
@@ -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_renames160 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()
gix/src/config/mod.rs+7−4
@@ -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.
20pub struct Snapshot<'repo> {20pub 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 dropped25/// 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 way32// 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()`.
33pub struct SnapshotMut<'repo> {34pub 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.
39pub struct CommitAutoRollback<'repo> {41pub 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 allow609 /// 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>,
gix/src/diff.rs+27−17
@@ -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 config191 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: config208 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 ..default214 .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,
gix/src/merge.rs+56−12
@@ -107,7 +107,10 @@ pub mod commit { · 106 unmodified lines
107///107///
108pub mod tree {108pub 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 a140 /// 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 /// ### Important143 /// `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 the146 /// 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 opts196 opts
194 }197 }
195 }198 }
@@ -202,7 +205,7 @@ pub mod tree { · 6 unmodified lines
202 /// * binary files205 /// * 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 /// Builder250 /// 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 representation266 /// 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 self273 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}
gix/src/object/tree/iter.rs+2−2
@@ -5,8 +5,8 @@ use crate::Repository; · 4 unmodified lines
5pub struct EntryRef<'repo, 'a> {5pub 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
12impl<'repo, 'a> EntryRef<'repo, 'a> {12impl<'repo, 'a> EntryRef<'repo, 'a> {
gix/src/object/tree/mod.rs+4−2
@@ -10,7 +10,8 @@ use crate::{object::find, Id, ObjectDetached, Tree}; · 9 unmodified lines
10pub struct Editor<'repo> {10pub 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/// Initialization17/// Initialization
@@ -197,7 +198,8 @@ impl std::fmt::Debug for Tree<'_> { · 180 unmodified lines
197#[derive(PartialEq, Debug, Clone)]198#[derive(PartialEq, Debug, Clone)]
198pub struct Entry<'repo> {199pub 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
203mod entry {205mod entry {
gix/src/reference/iter.rs+2−1
@@ -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"]
9pub struct Platform<'r> {9pub 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.
gix/src/repository/merge.rs+15−15
@@ -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 }
gix/src/revision/mod.rs+2−1
@@ -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}
gix/src/revision/walk.rs+4−2
@@ -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 it152/// 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.
154pub struct Platform<'repo> {154pub 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
334pub(crate) mod iter_impl {335pub(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
gix/src/submodule/mod.rs+1−1
@@ -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()].
25pub(crate) struct SharedState<'repo> {25pub(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>>,
gix/src/types.rs+22−11
@@ -19,7 +19,8 @@ pub struct Worktree<'repo> { · 18 unmodified lines
19pub struct Head<'repo> {19pub struct Head<'repo> {
20 /// One of various possible states for the HEAD reference20 /// 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
27pub struct Id<'r> {28pub struct Id<'r> {
28 /// The actual object id29 /// 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 data42 /// 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
45impl Drop for Object<'_> {48impl 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
61impl Drop for Blob<'_> {65impl 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 data76 /// 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
77impl Drop for Tree<'_> {82impl 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 data93 /// 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
93impl Drop for Tag<'_> {99impl 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 data110 /// 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
109impl Drop for Commit<'_> {116impl Drop for Commit<'_> {
@@ -132,7 +139,8 @@ pub struct ObjectDetached { · 22 unmodified lines
132pub struct Reference<'r> {139pub struct Reference<'r> {
133 /// The actual reference data140 /// 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")]
233pub struct Pathspec<'repo> {242pub 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"))]
263pub struct AttributeStack<'repo> {273pub 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}
gix/tests/gix/object/tree/diff.rs+1−0
@@ -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 = from170 let out = from
170 .changes()?171 .changes()?
src/plumbing/main.rs+16−6
@@ -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 },
src/plumbing/options/mod.rs+39−22
@@ -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)]
src/plumbing/progress.rs+4−0
@@ -62,6 +62,10 @@ struct Record { · 61 unmodified lines
62}62}
63 63
64static GIT_CONFIG: &[Record] = &[64static 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")
tests/tools/src/lib.rs+12−5
@@ -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() {

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t