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
27922792 "gix-features 0.39.1",
27932793 "gix-path 0.10.13",
27942794 "gix-testtools",
2795+ "percent-encoding",
27952796 "serde",
27962797 "thiserror 2.0.3",
27972798 "url",
crate-status.md+3−0
@@ -352,8 +352,11 @@ Check out the [performance discussion][gix-diff-performance] as well. · 351 unmodified lines
352352 - [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same)
353353 * [x] **tree**-diff-heuristics match Git for its test-cases
354354 - [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.
355356 - [ ] submodule merges (*right now they count as conflicts if they differ*)
356357 - [ ] 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.
357360 * [x] **commits** - with handling of multiple merge bases by recursive merge-base merge
358361 * [x] API documentation
359362 * [ ] Examples
gitoxide-core/src/query/engine/update.rs+6−1
@@ -139,7 +139,12 @@ pub fn update( · 138 unmodified lines
139139 });
140140
141141 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();
143148 r.copies = Some(gix::diff::rewrites::Copies {
144149 source: if find_copies_harder {
145150 CopySource::FromSetOfModifiedFilesAndAllSources
gitoxide-core/src/repository/merge/commit.rs+6−2
@@ -17,6 +17,7 @@ pub fn commit( · 16 unmodified lines
1717 Options {
1818 format,
1919 file_favor,
20+ tree_favor,
2021 in_memory,
2122 debug,
2223 }: Options,
@@ -31,7 +32,10 @@ pub fn commit( · 8 unmodified lines
3132 let (ours_ref, ours_id) = refname_and_commit(&repo, ours)?;
3233 let (theirs_ref, theirs_id) = refname_and_commit(&repo, theirs)?;
3334
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);
3539 let ours_id_str = ours_id.to_string();
3640 let theirs_id_str = theirs_id.to_string();
3741 let labels = gix::merge::blob::builtin_driver::text::Labels {
@@ -49,7 +53,7 @@ pub fn commit( · 11 unmodified lines
4953 .merge_commits(ours_id, theirs_id, labels, options.into())?
5054 .tree_merge;
5155 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());
5357 {
5458 let _span = gix::trace::detail!("Writing merged tree");
5559 let mut written = 0;
gitoxide-core/src/repository/merge/tree.rs+7−2
@@ -3,6 +3,7 @@ use crate::OutputFormat; · 2 unmodified lines
33 pub struct Options {
44 pub format: OutputFormat,
55 pub file_favor: Option<gix::merge::tree::FileFavor>,
6+ pub tree_favor: Option<gix::merge::tree::TreeFavor>,
67 pub in_memory: bool,
78 pub debug: bool,
89 }
@@ -29,6 +30,7 @@ pub(super) mod function { · 20 unmodified lines
2930 Options {
3031 format,
3132 file_favor,
33+ tree_favor,
3234 in_memory,
3335 debug,
3436 }: Options,
@@ -44,7 +46,10 @@ pub(super) mod function { · 9 unmodified lines
4446 let (ours_ref, ours_id) = refname_and_tree(&repo, ours)?;
4547 let (theirs_ref, theirs_id) = refname_and_tree(&repo, theirs)?;
4648
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);
4853 let base_id_str = base_id.to_string();
4954 let ours_id_str = ours_id.to_string();
5055 let theirs_id_str = theirs_id.to_string();
@@ -64,7 +69,7 @@ pub(super) mod function { · 13 unmodified lines
6469 };
6570 let res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?;
6671 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());
6873 {
6974 let _span = gix::trace::detail!("Writing merged tree");
7075 let mut written = 0;
gitoxide-core/src/repository/status.rs+1−0
@@ -81,6 +81,7 @@ pub fn show( · 80 unmodified lines
8181 copies: None,
8282 percentage: Some(percentage),
8383 limit: 0,
84+ track_empty: false,
8485 });
8586 if opts.rewrites.is_some() {
8687 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
3737 /// If the limit would not be enough to test the entire set of combinations, the algorithm will trade in precision and not
3838 /// run the fuzzy version of identity tests at all. That way results are never partial.
3939 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,
4045 }
4146
4247 /// 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
7070 copies: None,
7171 percentage: Some(0.5),
7272 limit: 1000,
73+ track_empty: false,
7374 }
7475 }
7576 }
gix-diff/src/rewrites/tracker.rs+44−7
@@ -286,12 +286,10 @@ impl<T: Change> Tracker<T> { · 285 unmodified lines
286286 CopySource::FromSetOfModifiedFiles => {}
287287 CopySource::FromSetOfModifiedFilesAndAllSources => {
288288 push_source_tree(&mut |change, location| {
289− assert!(
290− self.try_push_change(change, location).is_none(),
291− "we must accept every change"
292− );
293− // make sure these aren't viable to be emitted anymore.
294− self.items.last_mut().expect("just pushed").emitted = true;
289+ if self.try_push_change(change, location).is_none() {
290+ // make sure these aren't viable to be emitted anymore.
291+ self.items.last_mut().expect("just pushed").emitted = true;
292+ }
295293 })
296294 .map_err(|err| emit::Error::GetItemsForExhaustiveCopyDetection(Box::new(err)))?;
297295 self.items.sort_by(by_id_and_location);
@@ -341,6 +339,10 @@ impl<T: Change> Tracker<T> { · 43 unmodified lines
341339 ) -> Result<(), emit::Error> {
342340 // we try to cheaply reduce the set of possibilities first, before possibly looking more exhaustively.
343341 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.
344346 if self.match_pairs(cb, None /* by identity */, kind, out, diff_cache, objects, filter)? == Action::Cancel {
345347 return Ok(());
346348 }
@@ -384,10 +386,35 @@ impl<T: Change> Tracker<T> { · 37 unmodified lines
384386 filter: Option<fn(&T) -> bool>,
385387 ) -> Result<Action, emit::Error> {
386388 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+
387402 while let Some((mut dest_idx, dest)) = self.items[dest_ofs..].iter().enumerate().find_map(|(idx, item)| {
388403 (!item.emitted
389404 && 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+ ))
391418 .then_some((idx, item))
392419 }) {
393420 dest_idx += dest_ofs;
@@ -403,6 +430,7 @@ impl<T: Change> Tracker<T> { · 9 unmodified lines
403430 objects,
404431 diff_cache,
405432 &self.path_backing,
433+ &mut num_checks,
406434 )?
407435 .map(|(src_idx, src, diff)| {
408436 let (id, entry_mode) = src.change.id_and_entry_mode();
@@ -420,6 +448,12 @@ impl<T: Change> Tracker<T> { · 11 unmodified lines
420448 src_idx,
421449 )
422450 });
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+ }
423457 let Some((src, src_idx)) = src else {
424458 continue;
425459 };
@@ -631,6 +665,7 @@ fn find_match<'a, T: Change>( · 205 unmodified lines
631665 objects: &impl gix_object::FindObjectOrHeader,
632666 diff_cache: &mut crate::blob::Platform,
633667 path_backing: &[u8],
668+ num_checks: &mut usize,
634669 ) -> Result<Option<SourceTuple<'a, T>>, emit::Error> {
635670 let (item_id, item_mode) = item.change.id_and_entry_mode();
636671 if needs_exact_match(percentage) || item_mode.is_link() {
@@ -651,6 +686,7 @@ fn find_match<'a, T: Change>( · 14 unmodified lines
651686 }
652687 let res = items[range.clone()].iter().enumerate().find_map(|(mut src_idx, src)| {
653688 src_idx += range.start;
689+ *num_checks += 1;
654690 (src_idx != item_idx && src.is_source_for_destination_of(kind, item_mode)).then_some((src_idx, src, None))
655691 });
656692 if let Some(src) = res {
@@ -685,6 +721,7 @@ fn find_match<'a, T: Change>( · 28 unmodified lines
685721 )?;
686722 let prep = diff_cache.prepare_diff()?;
687723 stats.num_similarity_checks += 1;
724+ *num_checks += 1;
688725 match prep.operation {
689726 Operation::InternalDiff { algorithm } => {
690727 let tokens =
gix-diff/tests/diff/rewrites/tracker.rs+10−0
@@ -27,6 +27,7 @@ fn rename_by_id() -> crate::Result { · 26 unmodified lines
2727 copies: None,
2828 percentage: None,
2929 limit,
30+ track_empty: false,
3031 };
3132 let mut track = util::new_tracker(rewrites);
3233 assert!(
@@ -80,6 +81,7 @@ fn copy_by_similarity_reports_limit_if_encountered() -> crate::Result { · 47 unmodified lines
8081 }),
8182 percentage: None,
8283 limit: 1,
84+ track_empty: false,
8385 };
8486 let mut track = util::new_tracker(rewrites);
8587 let odb = util::add_retained_blobs(
@@ -132,6 +134,7 @@ fn copy_by_id() -> crate::Result { · 46 unmodified lines
132134 }),
133135 percentage: None,
134136 limit,
137+ track_empty: false,
135138 };
136139 let mut track = util::new_tracker(rewrites);
137140 let odb = util::add_retained_blobs(
@@ -206,6 +209,7 @@ fn copy_by_id_search_in_all_sources() -> crate::Result { · 68 unmodified lines
206209 }),
207210 percentage: None,
208211 limit,
212+ track_empty: false,
209213 };
210214 let mut track = util::new_tracker(rewrites);
211215 let odb = util::add_retained_blobs(
@@ -284,6 +288,7 @@ fn copy_by_50_percent_similarity() -> crate::Result { · 72 unmodified lines
284288 }),
285289 percentage: None,
286290 limit: 0,
291+ track_empty: false,
287292 };
288293 let mut track = util::new_tracker(rewrites);
289294 let odb = util::add_retained_blobs(
@@ -363,6 +368,7 @@ fn copy_by_id_in_additions_only() -> crate::Result { · 73 unmodified lines
363368 }),
364369 percentage: None,
365370 limit: 0,
371+ track_empty: false,
366372 };
367373 let mut track = util::new_tracker(rewrites);
368374 let odb = util::add_retained_blobs(
@@ -413,6 +419,7 @@ fn rename_by_similarity_reports_limit_if_encountered() -> crate::Result { · 44 unmodified lines
413419 copies: None,
414420 percentage: Some(0.5),
415421 limit: 1,
422+ track_empty: false,
416423 };
417424 let mut track = util::new_tracker(rewrites);
418425 let odb = util::add_retained_blobs(
@@ -458,6 +465,7 @@ fn rename_by_50_percent_similarity() -> crate::Result { · 39 unmodified lines
458465 copies: None,
459466 percentage: Some(0.5),
460467 limit: 0,
468+ track_empty: false,
461469 };
462470 let mut track = util::new_tracker(rewrites);
463471 let odb = util::add_retained_blobs(
@@ -547,6 +555,7 @@ fn directory_renames_by_id_can_fail_gracefully() -> crate::Result { · 83 unmodified lines
547555 copies: None,
548556 percentage: Some(0.5),
549557 limit: 0,
558+ track_empty: false,
550559 };
551560 let mut track = util::new_tracker(rename_by_similarity);
552561 let tree_dst_id = 1;
@@ -638,6 +647,7 @@ fn simple_directory_rename_by_id() -> crate::Result { · 85 unmodified lines
638647 copies: None,
639648 percentage: None,
640649 limit: 0,
650+ track_empty: false,
641651 };
642652 let mut track = util::new_tracker(renames_by_identity);
643653 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
192192
193193 #[test]
194194 fn renames_by_identity() -> crate::Result {
195− for (from, to, expected, assert_msg) in [
195+ for (from, to, expected, assert_msg, track_empty) in [
196196 (
197197 "c3-modification",
198198 "r1-identity",
199199 vec![BStr::new("a"), "dir/a-moved".into()],
200200 "one rename and nothing else",
201+ false,
201202 ),
202203 (
203204 "c4 - add identical files",
@@ -211,24 +212,35 @@ fn renames_by_identity() -> crate::Result { · 7 unmodified lines
211212 "z".into(),
212213 ],
213214 "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,
214223 ),
215224 (
216225 "c5 - add links",
217226 "r4-symlinks",
218227 vec!["link-1".into(), "renamed-link-1".into()],
219228 "symlinks are only tracked by identity",
229+ false,
220230 ),
221231 (
222232 "r1-identity",
223233 "c4 - add identical files",
224234 vec![],
225235 "not having any renames is OK as well",
236+ false,
226237 ),
227238 (
228239 "tc1-identity",
229240 "tc1-identity",
230241 vec![],
231242 "copy tracking is off by default",
243+ false,
232244 ),
233245 ] {
234246 for percentage in [None, Some(0.5)] {
@@ -239,6 +251,7 @@ fn renames_by_identity() -> crate::Result { · 4 unmodified lines
239251 location: Some(Location::Path),
240252 rewrites: Some(Rewrites {
241253 percentage,
254+ track_empty,
242255 ..Default::default()
243256 }),
244257 },
@@ -704,6 +717,7 @@ fn copies_in_entire_tree_by_similarity_with_limit() -> crate::Result { · 459 unmodified lines
704717 ..Default::default()
705718 }),
706719 limit: 2, // similarity checks can't be made that way
720+ track_empty: false,
707721 ..Default::default()
708722 }),
709723 },
@@ -833,6 +847,7 @@ fn realistic_renames_by_identity() -> crate::Result { · 123 unmodified lines
833847 rewrites: Some(Rewrites {
834848 copies: Some(Copies::default()),
835849 limit: 1,
850+ track_empty: true,
836851 ..Default::default()
837852 }),
838853 },
@@ -1324,6 +1339,7 @@ fn realistic_renames_by_identity_3() -> crate::Result { · 485 unmodified lines
13241339 rewrites: Some(Rewrites {
13251340 copies: Some(Copies::default()),
13261341 limit: 1,
1342+ track_empty: true,
13271343 ..Default::default()
13281344 }),
13291345 },
@@ -1402,6 +1418,7 @@ fn realistic_renames_2() -> crate::Result { · 72 unmodified lines
14021418 rewrites: Some(Rewrites {
14031419 copies: Some(Copies::default()),
14041420 limit: 1,
1421+ track_empty: false,
14051422 ..Default::default()
14061423 }),
14071424 },
@@ -1665,6 +1682,7 @@ fn realistic_renames_3_without_identity() -> crate::Result { · 257 unmodified lines
16651682 copies: None,
16661683 percentage: None,
16671684 limit: 0,
1685+ track_empty: false,
16681686 }),
16691687 },
16701688 )?;
gix-merge/src/blob/builtin_driver/binary.rs+4−2
@@ -25,8 +25,10 @@ pub(super) mod function { · 24 unmodified lines
2525 use crate::blob::Resolution;
2626
2727 /// 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.
2829 ///
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`].
3032 pub fn merge(on_conflict: Option<ResolveWith>) -> (Pick, Resolution) {
3133 match on_conflict {
3234 None => (Pick::Ours, Resolution::Conflict),
@@ -36,7 +38,7 @@ pub(super) mod function { · 3 unmodified lines
3638 ResolveWith::Theirs => Pick::Theirs,
3739 ResolveWith::Ancestor => Pick::Ancestor,
3840 },
39− Resolution::Complete,
41+ Resolution::CompleteWithAutoResolvedConflict,
4042 ),
4143 }
4244 }
gix-merge/src/blob/builtin_driver/text/mod.rs+3−0
@@ -57,8 +57,11 @@ pub enum ConflictStyle { · 56 unmodified lines
5757 /// That way it becomes clearer where the content of conflicts are originating from.
5858 #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
5959 pub struct Labels<'a> {
60+ /// The label for the common *ancestor*.
6061 pub ancestor: Option<&'a BStr>,
62+ /// The label for the *current* (or *our*) side.
6163 pub current: Option<&'a BStr>,
64+ /// The label for the *other* (or *their*) side.
6265 pub other: Option<&'a BStr>,
6366 }
6467
gix-merge/src/blob/builtin_driver/text/utils.rs+3−2
@@ -16,8 +16,9 @@ pub fn hunks_differ_in_diff3( · 15 unmodified lines
1616 return true;
1717 }
1818
19− let tokens_for_hunk =
20− |hunk: &Hunk| -> &[imara_diff::intern::Token] { tokens_for_side(hunk.side, input, current_tokens) };
19+ let tokens_for_hunk = |hunk: &Hunk| -> &[imara_diff::intern::Token] {
20+ &tokens_for_side(hunk.side, input, current_tokens)[hunk.after.start as usize..hunk.after.end as usize]
21+ };
2122
2223 a.iter()
2324 .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
329329 (Pick::Buffer, resolution)
330330 }
331331 BuiltinDriver::Binary => {
332− let (pick, resolution) = builtin_driver::binary(self.options.resolve_binary_with);
333− let pick = match pick {
334− builtin_driver::binary::Pick::Ours => Pick::Ours,
335− builtin_driver::binary::Pick::Theirs => Pick::Theirs,
336− builtin_driver::binary::Pick::Ancestor => Pick::Ancestor,
337− };
338− (pick, resolution)
332+ // easier to reason about the 'split' compared to merging both conditions
333+ #[allow(clippy::if_same_then_else)]
334+ if !(self.current.id.is_null() || self.other.id.is_null()) && self.current.id == self.other.id {
335+ (Pick::Ours, Resolution::Complete)
336+ } else if (self.current.id.is_null() || self.other.id.is_null()) && ours == theirs {
337+ (Pick::Ours, Resolution::Complete)
338+ } else {
339+ let (pick, resolution) = builtin_driver::binary(self.options.resolve_binary_with);
340+ let pick = match pick {
341+ builtin_driver::binary::Pick::Ours => Pick::Ours,
342+ builtin_driver::binary::Pick::Theirs => Pick::Theirs,
343+ builtin_driver::binary::Pick::Ancestor => Pick::Ancestor,
344+ };
345+ (pick, resolution)
346+ }
339347 }
340348 BuiltinDriver::Union => {
341349 let resolution = builtin_driver::text(
gix-merge/src/commit/mod.rs+1−0
@@ -55,5 +55,6 @@ pub struct Outcome<'a> { · 54 unmodified lines
5555
5656 pub(super) mod function;
5757
58+///
5859 pub mod virtual_merge_base;
5960 pub 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
3030 pub(super) mod function {
3131 use super::Error;
3232 use crate::blob::builtin_driver;
33− use crate::tree::TreatAsUnresolved;
33+ use crate::tree::{treat_as_unresolved, TreatAsUnresolved};
3434 use gix_object::FindExt;
3535
3636 /// 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
5555 let mut merged_commit_id = first_commit;
5656 others.push(second_commit);
5757
58− options.allow_lossy_resolution = true;
58+ options.tree_conflicts = Some(crate::tree::ResolveWith::Ancestor);
5959 options.blob_merge.is_virtual_ancestor = true;
6060 options.blob_merge.text.conflict = builtin_driver::text::Conflict::ResolveWithOurs;
6161 let favor_ancestor = Some(builtin_driver::binary::ResolveWith::Ancestor);
@@ -86,10 +86,10 @@ pub(super) mod function { · 24 unmodified lines
8686 },
8787 )?;
8888 // This shouldn't happen, but if for some buggy reason it does, we rather bail.
89− if out
90− .tree_merge
91− .has_unresolved_conflicts(TreatAsUnresolved::ConflictMarkers)
92− {
89+ if out.tree_merge.has_unresolved_conflicts(TreatAsUnresolved {
90+ content_merge: treat_as_unresolved::ContentMerge::Markers,
91+ tree_merge: treat_as_unresolved::TreeMerge::Undecidable,
92+ }) {
9393 return Err(Error::VirtualMergeBaseConflict.into());
9494 }
9595 let merged_tree_id = out
gix-merge/src/lib.rs+1−0
@@ -4,6 +4,7 @@ · 3 unmodified lines
44 //! * [tree-merges](mod@tree) look at trees and merge them structurally, triggering blob-merges as needed.
55 //! * [commit-merges](mod@commit) are like tree merges, but compute or create the merge-base on the fly.
66 #![deny(rust_2018_idioms)]
7+#![deny(missing_docs)]
78 #![forbid(unsafe_code)]
89
910 ///
gix-merge/src/tree/function.rs+396−221
@@ -5,7 +5,7 @@ use crate::tree::utils::{ · 4 unmodified lines
55 use crate::tree::ConflictMapping::{Original, Swapped};
66 use crate::tree::{
77 Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, ContentMerge, Error, Options, Outcome,
8− Resolution, ResolutionFailure,
8+ Resolution, ResolutionFailure, ResolveWith,
99 };
1010 use bstr::{BString, ByteSlice};
1111 use gix_diff::tree::recorder::Location;
@@ -13,6 +13,7 @@ use gix_diff::tree_with_rewrites::Change; · 1 unmodified line
1313 use gix_hash::ObjectId;
1414 use gix_object::tree::{EntryKind, EntryMode};
1515 use gix_object::{tree, FindExt};
16+use std::borrow::Cow;
1617 use std::convert::Infallible;
1718
1819 /// Perform a merge between `our_tree` and `their_tree`, using `base_tree` as merge-base.
@@ -71,10 +72,9 @@ where · 52 unmodified lines
7172 let _span = gix_trace::coarse!("gix_merge::tree", ?base_tree, ?our_tree, ?their_tree, ?labels);
7273 let (mut base_buf, mut side_buf) = (Vec::new(), Vec::new());
7374 let ancestor_tree = objects.find_tree(base_tree, &mut base_buf)?;
74− let allow_resolution_failure = !options.allow_lossy_resolution;
75−
7675 let mut editor = tree::Editor::new(ancestor_tree.to_owned(), objects, base_tree.kind());
7776 let ancestor_tree = gix_object::TreeRefIter::from_bytes(&base_buf);
77+ let tree_conflicts = options.tree_conflicts;
7878
7979 let mut our_changes = Vec::new();
8080 if ours_needs_diff {
@@ -128,7 +128,12 @@ where · 47 unmodified lines
128128
129129 let mut conflicts = Vec::new();
130130 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+ }
132137 if let Some(how) = options.fail_on_conflict {
133138 if conflict.resolution.is_err() || conflict.is_unresolved(how) {
134139 failed_on_first_conflict = true;
@@ -238,7 +243,7 @@ where · 103 unmodified lines
238243 PossibleConflict::TreeToNonTree { change_idx: Some(idx) }
239244 if matches!(
240245 our_changes[idx].inner,
241− Change::Deletion { .. } | Change::Addition { .. }
246+ Change::Deletion { .. } | Change::Addition { .. } | Change::Rewrite { .. }
242247 ) =>
243248 {
244249 (Some(idx), Some(MatchKind::EraseTree))
@@ -255,34 +260,76 @@ where · 10 unmodified lines
255260 PossibleConflict::Match { change_idx }
256261 | PossibleConflict::PassedRewrittenDirectory { change_idx } => Some(change_idx),
257262 }
258− .map(|idx| &our_changes[idx]);
263+ .map(|idx| &mut our_changes[idx]);
259264
260265 if let Some(ours) = ours {
261266 gix_trace::debug!("Turning a case we could probably handle into a conflict for now. theirs: {theirs:#?} ours: {ours:#?} kind: {match_kind:?}");
262− if allow_resolution_failure
263− && should_fail_on_conflict(Conflict::unknown((
264− &ours.inner,
265− theirs,
266− Original,
267− outer_side,
268− )))
269− {
267+ let conflict = Conflict::unknown((&ours.inner, theirs, Original, outer_side));
268+ if let Some(ResolveWith::Ours) = tree_conflicts {
269+ apply_our_resolution(&ours.inner, theirs, outer_side, &mut editor)?;
270+ *match outer_side {
271+ Original => &mut ours.was_written,
272+ Swapped => &mut their_changes[theirs_idx].was_written,
273+ } = true;
274+ }
275+ if should_fail_on_conflict(conflict) {
270276 break 'outer;
271277 };
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;
273326 } else {
274327 gix_trace::debug!("Couldn't figure out how to handle {match_kind:?} theirs: {theirs:#?} candidate: {candidate:#?}");
275− continue;
276328 }
329+ continue;
277330 };
278331
279332 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− );
286333 match (ours, theirs) {
287334 (
288335 Change::Modification {
@@ -326,7 +373,7 @@ where · 37 unmodified lines
326373 Swapped
327374 };
328375 if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) {
329− assert_eq!(
376+ debug_assert_eq!(
330377 previous_id, their_source_id,
331378 "both refer to the same base, so should always match"
332379 );
@@ -407,9 +454,23 @@ where · 74 unmodified lines
407454 (new_change, None),
408455 pick_our_changes_mut(side, their_changes, our_changes),
409456 );
410− } else if allow_resolution_failure {
411− editor.upsert(toc(our_location), our_mode.kind(), *our_id)?;
412− editor.upsert(toc(their_location), their_mode.kind(), *their_id)?;
457+ } else {
458+ match tree_conflicts {
459+ None => {
460+ // keep both states - 'our_location' is the previous location as well.
461+ editor.upsert(toc(our_location), our_mode.kind(), *our_id)?;
462+ editor.upsert(toc(their_location), their_mode.kind(), *their_id)?;
463+ }
464+ Some(ResolveWith::Ours) => {
465+ editor.remove(toc(source_location))?;
466+ if side.to_global(outer_side).is_swapped() {
467+ editor.upsert(toc(their_location), their_mode.kind(), *their_id)?;
468+ } else {
469+ editor.upsert(toc(our_location), our_mode.kind(), *our_id)?;
470+ }
471+ }
472+ Some(ResolveWith::Ancestor) => {}
473+ }
413474
414475 if should_fail_on_conflict(Conflict::without_resolution(
415476 ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch,
@@ -516,7 +577,7 @@ where · 100 unmodified lines
516577 &options,
517578 )?;
518579 editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?;
519− Some(Conflict::with_resolution(
580+ Conflict::with_resolution(
520581 Resolution::OursModifiedTheirsModifiedThenBlobContentMerge {
521582 merged_blob: ContentMerge {
522583 resolution,
@@ -525,8 +586,8 @@ where · 2 unmodified lines
525586 },
526587 (ours, theirs, Original, outer_side),
527588 [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)],
528− ))
529− } else if allow_resolution_failure {
589+ )
590+ } else {
530591 // Actually this has a preference, as symlinks are always left in place with the other side renamed.
531592 let (
532593 logical_side,
@@ -555,8 +616,7 @@ where · 22 unmodified lines
555616 tree_with_rename,
556617 label_of_side_to_be_moved,
557618 )?;
558− editor.upsert(toc(location), our_mode.kind(), our_id)?;
559− let conflict = Conflict::without_resolution(
619+ let mut conflict = Conflict::without_resolution(
560620 ResolutionFailure::OursAddedTheirsAddedTypeMismatch {
561621 their_unique_location: renamed_location.clone(),
562622 },
@@ -567,27 +627,45 @@ where · 4 unmodified lines
567627 index_entry_at_path(&their_mode, &their_id, their_path_hint),
568628 ],
569629 );
570−
571− let new_change = Change::Addition {
572− location: renamed_location,
573− entry_mode: their_mode,
574− id: their_id,
575− relation: None,
576− };
577− tree_with_rename.remove_existing_leaf(location.as_bstr());
578− push_deferred(
579− (new_change, None),
580− pick_our_changes_mut(logical_side, their_changes, our_changes),
581− );
582− Some(conflict)
583− } else {
584− None
630+ match tree_conflicts {
631+ None => {
632+ let new_change = Change::Addition {
633+ location: renamed_location,
634+ entry_mode: their_mode,
635+ id: their_id,
636+ relation: None,
637+ };
638+ editor.upsert(toc(location), our_mode.kind(), our_id)?;
639+ tree_with_rename.remove_existing_leaf(location.as_bstr());
640+ push_deferred(
641+ (new_change, None),
642+ pick_our_changes_mut(logical_side, their_changes, our_changes),
643+ );
644+ }
645+ Some(resolve) => {
646+ conflict.entries = Default::default();
647+ match resolve {
648+ ResolveWith::Ours => match outer_side {
649+ Original => {
650+ editor.upsert(toc(location), our_mode.kind(), our_id)?;
651+ }
652+ Swapped => {
653+ editor.upsert(toc(location), their_mode.kind(), their_id)?;
654+ }
655+ },
656+ ResolveWith::Ancestor => {
657+ // Do nothing - this discards both sides.
658+ // Note that one of these adds might be the result of a rename, which
659+ // means we effectively loose the original and can't get it back as that information is degenerated.
660+ }
661+ }
662+ }
663+ }
664+ conflict
585665 };
586666
587− if let Some(conflict) = conflict {
588− if should_fail_on_conflict(conflict) {
589− break 'outer;
590− };
667+ if should_fail_on_conflict(conflict) {
668+ break 'outer;
591669 }
592670 }
593671 (
@@ -609,7 +687,7 @@ where · 15 unmodified lines
609687 previous_entry_mode,
610688 previous_id,
611689 },
612− ) if allow_resolution_failure => {
690+ ) => {
613691 let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) {
614692 (labels.current.unwrap_or_default(), Original)
615693 } else {
@@ -622,62 +700,135 @@ where · 6 unmodified lines
622700 };
623701 change_on_right
624702 .map(|change| {
625− change.inner.entry_mode().is_tree() && change.inner.location() == location
703+ change.inner.entry_mode().is_tree()
704+ && change.inner.location() == location
705+ && matches!(change.inner, Change::Addition { .. })
626706 })
627707 .unwrap_or_default()
628708 };
629709
630− if deletion_prefaces_addition_of_directory {
631− let our_tree = pick_our_tree(side, our_tree, their_tree);
632− let renamed_path = unique_path_in_tree(
633− location.as_bstr(),
634− &editor,
635− our_tree,
636− label_of_side_to_be_moved,
637− )?;
638− editor.remove(toc(location))?;
639− our_tree.remove_existing_leaf(location.as_bstr());
710+ let should_break = if deletion_prefaces_addition_of_directory {
711+ let entries = [
712+ index_entry(previous_entry_mode, previous_id),
713+ index_entry(entry_mode, id),
714+ None,
715+ ];
716+ match tree_conflicts {
717+ None => {
718+ let our_tree = pick_our_tree(side, our_tree, their_tree);
719+ let renamed_path = unique_path_in_tree(
720+ location.as_bstr(),
721+ &editor,
722+ our_tree,
723+ label_of_side_to_be_moved,
724+ )?;
725+ editor.remove(toc(location))?;
726+ our_tree.remove_existing_leaf(location.as_bstr());
640727
641− let new_change = Change::Addition {
642− location: renamed_path.clone(),
643− relation: None,
644− entry_mode: *entry_mode,
645− id: *id,
646− };
647− let should_break = should_fail_on_conflict(Conflict::without_resolution(
648− ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed {
649− renamed_unique_path_to_modified_blob: renamed_path,
650− },
651− (ours, theirs, side, outer_side),
652− [
653− index_entry(previous_entry_mode, previous_id),
654− index_entry(entry_mode, id),
655− None,
656− ],
657− ));
728+ let new_change = Change::Addition {
729+ location: renamed_path.clone(),
730+ relation: None,
731+ entry_mode: *entry_mode,
732+ id: *id,
733+ };
734+ let should_break = should_fail_on_conflict(Conflict::without_resolution(
735+ ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed {
736+ renamed_unique_path_to_modified_blob: renamed_path,
737+ },
738+ (ours, theirs, side, outer_side),
739+ entries,
740+ ));
658741
659− // Since we move *our* side, our tree needs to be modified.
660− push_deferred(
661− (new_change, None),
662− pick_our_changes_mut(side, our_changes, their_changes),
663− );
742+ // Since we move *our* side, our tree needs to be modified.
743+ push_deferred(
744+ (new_change, None),
745+ pick_our_changes_mut(side, our_changes, their_changes),
746+ );
747+ should_break
748+ }
749+ Some(ResolveWith::Ours) => {
750+ match side.to_global(outer_side) {
751+ Original => {
752+ // ours is modification
753+ editor.upsert(toc(location), entry_mode.kind(), *id)?;
754+ }
755+ Swapped => {
756+ // ours is deletion
757+ editor.remove(toc(location))?;
758+ }
759+ }
760+ should_fail_on_conflict(Conflict::without_resolution(
761+ ResolutionFailure::OursModifiedTheirsDeleted,
762+ (ours, theirs, side, outer_side),
763+ entries,
764+ ))
765+ }
766+ Some(ResolveWith::Ancestor) => {
767+ should_fail_on_conflict(Conflict::without_resolution(
768+ ResolutionFailure::OursModifiedTheirsDeleted,
769+ (ours, theirs, side, outer_side),
770+ entries,
771+ ))
772+ }
773+ }
774+ } else {
775+ let entries = [
776+ index_entry(previous_entry_mode, previous_id),
777+ index_entry(entry_mode, id),
778+ None,
779+ ];
780+ match tree_conflicts {
781+ None => {
782+ editor.upsert(toc(location), entry_mode.kind(), *id)?;
783+ }
784+ Some(ResolveWith::Ours) => {
785+ let ours = match outer_side {
786+ Original => ours,
787+ Swapped => theirs,
788+ };
664789
665− if should_break {
666− break 'outer;
790+ match ours {
791+ Change::Modification { .. } => {
792+ editor.upsert(toc(location), entry_mode.kind(), *id)?;
793+ }
794+ Change::Deletion { .. } => {
795+ editor.remove(toc(location))?;
796+ }
797+ _ => unreachable!("parent-match assures this"),
798+ };
799+ }
800+ Some(ResolveWith::Ancestor) => {}
667801 };
668− } else {
669− let should_break = should_fail_on_conflict(Conflict::without_resolution(
802+ should_fail_on_conflict(Conflict::without_resolution(
670803 ResolutionFailure::OursModifiedTheirsDeleted,
671804 (ours, theirs, side, outer_side),
672− [
673− index_entry(previous_entry_mode, previous_id),
674− index_entry(entry_mode, id),
675− None,
676− ],
677− ));
678− editor.upsert(toc(location), entry_mode.kind(), *id)?;
679− if should_break {
680− break 'outer;
805+ entries,
806+ ))
807+ };
808+ if should_break {
809+ break 'outer;
810+ };
811+ }
812+ (
813+ Change::Modification { .. },
814+ Change::Addition {
815+ location,
816+ entry_mode,
817+ id,
818+ ..
819+ },
820+ ) if ours.location() != theirs.location() => {
821+ match tree_conflicts {
822+ None => {
823+ unreachable!("modification/deletion pair should prevent modification/addition from happening")
824+ }
825+ Some(ResolveWith::Ancestor) => {}
826+ Some(ResolveWith::Ours) => {
827+ if outer_side.is_swapped() {
828+ editor.upsert(toc(location), entry_mode.kind(), *id)?;
829+ }
830+ // we have already taken care of the 'root' of this -
831+ // everything that follows can safely be ignored
681832 }
682833 }
683834 }
@@ -720,112 +871,89 @@ where · 36 unmodified lines
720871 let merged_mode =
721872 merge_modes(*our_mode, *their_mode).expect("this case was assured earlier");
722873
723− editor.remove(toc(source_location))?;
724− our_tree.remove_existing_leaf(source_location.as_bstr());
725− their_tree.remove_existing_leaf(source_location.as_bstr());
874+ if matches!(tree_conflicts, None | Some(ResolveWith::Ours)) {
875+ editor.remove(toc(source_location))?;
876+ our_tree.remove_existing_leaf(source_location.as_bstr());
877+ their_tree.remove_existing_leaf(source_location.as_bstr());
878+ }
726879
727− let their_rewritten_location =
728− possibly_rewritten_location(our_tree, their_location.as_bstr(), our_changes);
729− let our_rewritten_location =
730− possibly_rewritten_location(their_tree, our_location.as_bstr(), their_changes);
731− let (our_addition, their_addition) =
732− match (our_rewritten_location, their_rewritten_location) {
733− (None, Some(location)) => (
734− None,
735− Some(Change::Addition {
736− location,
880+ let their_location =
881+ possibly_rewritten_location(our_tree, their_location.as_bstr(), our_changes)
882+ .map_or(Cow::Borrowed(their_location.as_bstr()), Cow::Owned);
883+ let our_location =
884+ possibly_rewritten_location(their_tree, our_location.as_bstr(), their_changes)
885+ .map_or(Cow::Borrowed(our_location.as_bstr()), Cow::Owned);
886+ let (our_addition, their_addition) = if our_location == their_location {
887+ (
888+ None,
889+ Some(Change::Addition {
890+ location: our_location.into_owned(),
891+ relation: None,
892+ entry_mode: merged_mode,
893+ id: merged_blob_id,
894+ }),
895+ )
896+ } else {
897+ if should_fail_on_conflict(Conflict::without_resolution(
898+ ResolutionFailure::OursRenamedTheirsRenamedDifferently {
899+ merged_blob: resolution.take().map(|resolution| ContentMerge {
900+ resolution,
901+ merged_blob_id,
902+ }),
903+ },
904+ (ours, theirs, Original, outer_side),
905+ [
906+ index_entry_at_path(
907+ source_entry_mode,
908+ source_id,
909+ ConflictIndexEntryPathHint::Source,
910+ ),
911+ index_entry_at_path(
912+ our_mode,
913+ &merged_blob_id,
914+ ConflictIndexEntryPathHint::Current,
915+ ),
916+ index_entry_at_path(
917+ their_mode,
918+ &merged_blob_id,
919+ ConflictIndexEntryPathHint::RenamedOrTheirs,
920+ ),
921+ ],
922+ )) {
923+ break 'outer;
924+ };
925+ match tree_conflicts {
926+ None => {
927+ let our_addition = Change::Addition {
928+ location: our_location.into_owned(),
737929 relation: None,
738930 entry_mode: merged_mode,
739931 id: merged_blob_id,
740− }),
741− ),
742− (Some(location), None) => (
743− None,
744− Some(Change::Addition {
745− location,
932+ };
933+ let their_addition = Change::Addition {
934+ location: their_location.into_owned(),
746935 relation: None,
747936 entry_mode: merged_mode,
748937 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;
763938 };
764− their_changes[theirs_idx].was_written = true;
765− our_changes[ours_idx].was_written = true;
766− continue;
939+ (Some(our_addition), Some(their_addition))
767940 }
768− (None, None) => {
769− if our_location == their_location {
770− (
771− None,
772− Some(Change::Addition {
773− location: our_location.to_owned(),
774− relation: None,
775− entry_mode: merged_mode,
776− id: merged_blob_id,
777− }),
778− )
779− } else {
780− if !allow_resolution_failure {
781− their_changes[theirs_idx].was_written = true;
782− our_changes[ours_idx].was_written = true;
783− continue;
941+ Some(ResolveWith::Ancestor) => (None, None),
942+ Some(ResolveWith::Ours) => {
943+ let our_addition = Change::Addition {
944+ location: match outer_side {
945+ Original => our_location,
946+ Swapped => their_location,
784947 }
785− if should_fail_on_conflict(Conflict::without_resolution(
786− ResolutionFailure::OursRenamedTheirsRenamedDifferently {
787− merged_blob: resolution.take().map(|resolution| ContentMerge {
788− resolution,
789− merged_blob_id,
790− }),
791− },
792− (ours, theirs, Original, outer_side),
793− [
794− index_entry_at_path(
795− source_entry_mode,
796− source_id,
797− ConflictIndexEntryPathHint::Source,
798− ),
799− index_entry_at_path(
800− our_mode,
801− &merged_blob_id,
802− ConflictIndexEntryPathHint::Current,
803− ),
804− index_entry_at_path(
805− their_mode,
806− &merged_blob_id,
807− ConflictIndexEntryPathHint::RenamedOrTheirs,
808− ),
809− ],
810− )) {
811− break 'outer;
812− };
813− let our_addition = Change::Addition {
814− location: our_location.to_owned(),
815− relation: None,
816− entry_mode: merged_mode,
817− id: merged_blob_id,
818− };
819− let their_addition = Change::Addition {
820− location: their_location.to_owned(),
821− relation: None,
822− entry_mode: merged_mode,
823− id: merged_blob_id,
824− };
825− (Some(our_addition), Some(their_addition))
826− }
948+ .into_owned(),
949+ relation: None,
950+ entry_mode: merged_mode,
951+ id: merged_blob_id,
952+ };
953+ (Some(our_addition), None)
827954 }
828− };
955+ }
956+ };
829957
830958 if let Some(resolution) = resolution {
831959 if should_fail_on_conflict(Conflict::with_resolution(
@@ -883,16 +1011,21 @@ where · 51 unmodified lines
8831011 ..
8841012 },
8851013 Change::Deletion { .. },
886− ) if !rewritten_mode.is_commit() && allow_resolution_failure => {
1014+ ) if !rewritten_mode.is_commit() => {
8871015 let side = if matches!(ours, Change::Deletion { .. }) {
8881016 Original
8891017 } else {
8901018 Swapped
8911019 };
8921020
893− editor.remove(toc(source_location))?;
894− pick_our_tree(side, our_tree, their_tree)
895− .remove_existing_leaf(source_location.as_bstr());
1021+ match tree_conflicts {
1022+ None | Some(ResolveWith::Ours) => {
1023+ editor.remove(toc(source_location))?;
1024+ pick_our_tree(side, our_tree, their_tree)
1025+ .remove_existing_leaf(source_location.as_bstr());
1026+ }
1027+ Some(ResolveWith::Ancestor) => {}
1028+ }
8961029
8971030 let their_rewritten_location = possibly_rewritten_location(
8981031 pick_our_tree(side, our_tree, their_tree),
@@ -923,10 +1056,15 @@ where · 24 unmodified lines
9231056 break 'outer;
9241057 };
9251058
926− push_deferred(
927− (our_addition, None),
928− pick_our_changes_mut(side, their_changes, our_changes),
929− );
1059+ let ours_is_rewrite = side.is_swapped();
1060+ if tree_conflicts.is_none()
1061+ || (matches!(tree_conflicts, Some(ResolveWith::Ours)) && ours_is_rewrite)
1062+ {
1063+ push_deferred(
1064+ (our_addition, None),
1065+ pick_our_changes_mut(side, their_changes, our_changes),
1066+ );
1067+ }
9301068 }
9311069 (
9321070 Change::Rewrite {
@@ -941,6 +1079,7 @@ where · 8 unmodified lines
9411079 Change::Addition {
9421080 id: their_id,
9431081 entry_mode: their_mode,
1082+ location: add_location,
9441083 ..
9451084 },
9461085 )
@@ -948,6 +1087,7 @@ where · 1 unmodified line
9481087 Change::Addition {
9491088 id: their_id,
9501089 entry_mode: their_mode,
1090+ location: add_location,
9511091 ..
9521092 },
9531093 Change::Rewrite {
@@ -1005,9 +1145,17 @@ where · 51 unmodified lines
10051145 // Because this constellation can only be found by the lookup tree, there is
10061146 // no need to put it as addition, we know it's not going to intersect on the other side.
10071147 editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?;
1008− } else if allow_resolution_failure {
1009− editor.remove(toc(source_location))?;
1010− pick_our_tree(side, our_tree, their_tree).remove_leaf(source_location.as_bstr());
1148+ } else {
1149+ // We always remove the source from the tree - it might be re-added later.
1150+ let ours_is_rename =
1151+ tree_conflicts == Some(ResolveWith::Ours) && side == outer_side;
1152+ let remove_rename_source =
1153+ tree_conflicts.is_none() || ours_is_rename || add_location != source_location;
1154+ if remove_rename_source {
1155+ editor.remove(toc(source_location))?;
1156+ pick_our_tree(side, our_tree, their_tree)
1157+ .remove_leaf(source_location.as_bstr());
1158+ }
10111159
10121160 let (
10131161 logical_side,
@@ -1036,7 +1184,13 @@ where · 22 unmodified lines
10361184 tree_with_rename,
10371185 label_of_side_to_be_moved,
10381186 )?;
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+
10401194 let conflict = Conflict::without_resolution(
10411195 ResolutionFailure::OursAddedTheirsAddedTypeMismatch {
10421196 their_unique_location: renamed_location.clone(),
@@ -1049,20 +1203,21 @@ where · 6 unmodified lines
10491203 ],
10501204 );
10511205
1052− let new_change_with_rename = Change::Addition {
1053− location: renamed_location,
1054− entry_mode: their_mode,
1055− id: their_id,
1056− relation: None,
1057− };
1058− tree_with_rename.remove_existing_leaf(location.as_bstr());
1059− push_deferred(
1060− (
1061− new_change_with_rename,
1062− Some(pick_idx(logical_side, theirs_idx, ours_idx)),
1063− ),
1064− pick_our_changes_mut(logical_side, their_changes, our_changes),
1065− );
1206+ if tree_conflicts.is_none() {
1207+ let new_change_with_rename = Change::Addition {
1208+ location: renamed_location,
1209+ entry_mode: their_mode,
1210+ id: their_id,
1211+ relation: None,
1212+ };
1213+ push_deferred(
1214+ (
1215+ new_change_with_rename,
1216+ Some(pick_idx(logical_side, theirs_idx, ours_idx)),
1217+ ),
1218+ pick_our_changes_mut(logical_side, their_changes, our_changes),
1219+ );
1220+ }
10661221
10671222 if should_fail_on_conflict(conflict) {
10681223 break 'outer;
@@ -1070,9 +1225,16 @@ where · 1 unmodified line
10701225 }
10711226 }
10721227 _unknown => {
1073− if allow_resolution_failure
1074− && should_fail_on_conflict(Conflict::unknown((ours, theirs, Original, outer_side)))
1075− {
1228+ debug_assert!(
1229+ match_kind.is_none()
1230+ || (ours.location() == theirs.location()
1231+ || ours.source_location() == theirs.source_location()),
1232+ "BUG: right now it's not known to be possible to match changes from different paths: {match_kind:?} {candidate:?}"
1233+ );
1234+ if let Some(ResolveWith::Ours) = tree_conflicts {
1235+ apply_our_resolution(ours, theirs, outer_side, &mut editor)?;
1236+ }
1237+ if should_fail_on_conflict(Conflict::unknown((ours, theirs, Original, outer_side))) {
10761238 break 'outer;
10771239 };
10781240 }
@@ -1098,6 +1260,19 @@ where · 19 unmodified lines
10981260 })
10991261 }
11001262
1263+fn apply_our_resolution(
1264+ local_ours: &Change,
1265+ local_theirs: &Change,
1266+ outer_side: ConflictMapping,
1267+ editor: &mut gix_object::tree::Editor<'_>,
1268+) -> Result<(), Error> {
1269+ let ours = match outer_side {
1270+ Original => local_ours,
1271+ Swapped => local_theirs,
1272+ };
1273+ Ok(apply_change(editor, ours, None)?)
1274+}
1275+
11011276 fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool {
11021277 a.is_commit() || b.is_commit()
11031278 }
gix-merge/src/tree/mod.rs+193−67
@@ -39,40 +39,77 @@ pub struct Outcome<'a> { · 38 unmodified lines
3939 /// Use [`has_unresolved_conflicts()`](Outcome::has_unresolved_conflicts()) to see if any action is needed
4040 /// before using [`tree`](Outcome::tree).
4141 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.
4344 /// This also makes this outcome a very partial merge that cannot be completed.
4445 /// Only set if [`fail_on_conflict`](Options::fail_on_conflict) is `true`.
4546 pub failed_on_first_unresolved_conflict: bool,
4647 }
4748
4849 /// Determine what should be considered an unresolved conflict.
50+#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
51+pub struct TreatAsUnresolved {
52+ /// Determine which content merges should be considered unresolved.
53+ pub content_merge: treat_as_unresolved::ContentMerge,
54+ /// Determine which tree merges should be considered unresolved.
55+ pub tree_merge: treat_as_unresolved::TreeMerge,
56+}
57+
4958 ///
50−/// Note that no matter which variant, [conflicts](Conflict) with
51−/// [resolution failure](`ResolutionFailure`) will always be unresolved.
52−///
53−/// Also, when one side was modified but the other side renamed it, this will not
54−/// be considered a conflict, even if a non-conflicting merge happened.
55−#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
56−pub enum TreatAsUnresolved {
57− /// Only consider content merges with conflict markers as unresolved.
58− ///
59− /// Auto-resolved tree conflicts will *not* be considered unresolved.
60− ConflictMarkers,
61− /// Consider content merges with conflict markers as unresolved, and content
62− /// merges where conflicts where auto-resolved in any way, like choosing
63− /// *ours*, *theirs* or by their *union*.
64− ///
65− /// Auto-resolved tree conflicts will *not* be considered unresolved.
66− ConflictMarkersAndAutoResolved,
67− /// Whenever there were conflicting renames, or conflict markers, it is unresolved.
68− /// Note that auto-resolved content merges will *not* be considered unresolved.
69− ///
70− /// Also note that files that were changed in one and renamed in another will
71− /// be moved into place, which will be considered resolved.
72− Renames,
73− /// Similar to [`Self::Renames`], but auto-resolved content-merges will
74− /// also be considered unresolved.
75− RenamesAndAutoResolvedContent,
59+pub mod treat_as_unresolved {
60+ use crate::tree::TreatAsUnresolved;
61+
62+ /// Which kind of content merges should be considered unresolved?
63+ #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
64+ pub enum ContentMerge {
65+ /// Content merges that still show conflict markers.
66+ #[default]
67+ Markers,
68+ /// Content merges who would have conflicted if it wasn't for a
69+ /// [resolution strategy](crate::blob::builtin_driver::text::Conflict::ResolveWithOurs).
70+ ForcedResolution,
71+ }
72+
73+ /// Which kind of tree merges should be considered unresolved?
74+ #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
75+ pub enum TreeMerge {
76+ /// All failed renames.
77+ Undecidable,
78+ /// All failed renames, and the ones where a tree item was renamed to avoid a clash.
79+ #[default]
80+ EvasiveRenames,
81+ /// All of `EvasiveRenames`, and tree merges that would have conflicted but which were resolved
82+ /// with a [resolution strategy](super::ResolveWith).
83+ ForcedResolution,
84+ }
85+
86+ /// Instantiation/Presets
87+ impl TreatAsUnresolved {
88+ /// Return an instance with the highest sensitivity to what should be considered unresolved as it
89+ /// includes entries which have been resolved using a [merge strategy](super::ResolveWith).
90+ pub fn forced_resolution() -> Self {
91+ Self {
92+ content_merge: ContentMerge::ForcedResolution,
93+ tree_merge: TreeMerge::ForcedResolution,
94+ }
95+ }
96+
97+ /// Return an instance that considers unresolved any conflict that Git would also consider unresolved.
98+ /// This is the same as the `default()` implementation.
99+ pub fn git() -> Self {
100+ Self::default()
101+ }
102+
103+ /// Only undecidable tree merges and conflict markers are considered unresolved.
104+ /// This also means that renamed entries to make space for a conflicting one is considered acceptable,
105+ /// making this preset the most lenient.
106+ pub fn undecidable() -> Self {
107+ Self {
108+ content_merge: ContentMerge::Markers,
109+ tree_merge: TreeMerge::Undecidable,
110+ }
111+ }
112+ }
76113 }
77114
78115 impl Outcome<'_> {
@@ -84,13 +121,18 @@ impl Outcome<'_> { · 5 unmodified lines
84121
85122 /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a
86123 /// conflict should be considered unresolved.
124+ /// `removal_mode` decides how unconflicted entries should be removed if they are superseded by
125+ /// their conflicted counterparts.
87126 /// It's important that `index` is at the state of [`Self::tree`].
88127 ///
89128 /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`.
90− /// Also, the unconflicted stage of such entries will be removed merely by setting a flag, so the
91− /// in-memory entry is still present.
92− pub fn index_changed_after_applying_conflicts(&self, index: &mut gix_index::State, how: TreatAsUnresolved) -> bool {
93− apply_index_entries(&self.conflicts, how, index)
129+ pub fn index_changed_after_applying_conflicts(
130+ &self,
131+ index: &mut gix_index::State,
132+ how: TreatAsUnresolved,
133+ removal_mode: apply_index_entries::RemovalMode,
134+ ) -> bool {
135+ apply_index_entries(&self.conflicts, how, index, removal_mode)
94136 }
95137 }
96138
@@ -150,7 +192,7 @@ enum ConflictIndexEntryPathHint { · 53 unmodified lines
150192 }
151193
152194 /// 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)]
154196 enum ConflictMapping {
155197 /// The sides are as described in the field documentation, i.e. `ours` is `ours`.
156198 Original,
@@ -168,41 +210,56 @@ impl ConflictMapping { · 11 unmodified lines
168210 ConflictMapping::Swapped => ConflictMapping::Original,
169211 }
170212 }
213+ fn to_global(self, global: ConflictMapping) -> ConflictMapping {
214+ match global {
215+ ConflictMapping::Original => self,
216+ ConflictMapping::Swapped => self.swapped(),
217+ }
218+ }
171219 }
172220
173221 impl Conflict {
174222 /// Return `true` if this instance is considered unresolved based on the criterion specified by `how`.
175223 pub fn is_unresolved(&self, how: TreatAsUnresolved) -> bool {
176224 use crate::blob;
177− let content_merge_matches = |info: &ContentMerge| match how {
178− TreatAsUnresolved::ConflictMarkers | TreatAsUnresolved::Renames => {
179− matches!(info.resolution, blob::Resolution::Conflict)
180− }
181− TreatAsUnresolved::RenamesAndAutoResolvedContent | TreatAsUnresolved::ConflictMarkersAndAutoResolved => {
225+ let content_merge_unresolved = |info: &ContentMerge| match how.content_merge {
226+ treat_as_unresolved::ContentMerge::Markers => matches!(info.resolution, blob::Resolution::Conflict),
227+ treat_as_unresolved::ContentMerge::ForcedResolution => {
182228 matches!(
183229 info.resolution,
184230 blob::Resolution::Conflict | blob::Resolution::CompleteWithAutoResolvedConflict
185231 )
186232 }
187233 };
188− match how {
189− TreatAsUnresolved::ConflictMarkers | TreatAsUnresolved::ConflictMarkersAndAutoResolved => {
190− self.resolution.is_err() || self.content_merge().map_or(false, |info| content_merge_matches(&info))
234+ match how.tree_merge {
235+ treat_as_unresolved::TreeMerge::Undecidable => {
236+ self.resolution.is_err()
237+ || self
238+ .content_merge()
239+ .map_or(false, |info| content_merge_unresolved(&info))
240+ }
241+ treat_as_unresolved::TreeMerge::EvasiveRenames | treat_as_unresolved::TreeMerge::ForcedResolution => {
242+ match &self.resolution {
243+ Ok(success) => match success {
244+ Resolution::SourceLocationAffectedByRename { .. } => false,
245+ Resolution::Forced(_) => {
246+ how.tree_merge == treat_as_unresolved::TreeMerge::ForcedResolution
247+ || self
248+ .content_merge()
249+ .map_or(false, |merged_blob| content_merge_unresolved(&merged_blob))
250+ }
251+ Resolution::OursModifiedTheirsRenamedAndChangedThenRename {
252+ merged_blob,
253+ final_location,
254+ ..
255+ } => final_location.is_some() || merged_blob.as_ref().map_or(false, content_merge_unresolved),
256+ Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => {
257+ content_merge_unresolved(merged_blob)
258+ }
259+ },
260+ Err(_failure) => true,
261+ }
191262 }
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− },
206263 }
207264 }
208265
@@ -237,15 +294,11 @@ impl Conflict { · 28 unmodified lines
237294
238295 /// Return information about the content merge if it was performed.
239296 pub fn content_merge(&self) -> Option<ContentMerge> {
240− match &self.resolution {
241− Ok(success) => match success {
242− Resolution::SourceLocationAffectedByRename { .. } => None,
243− Resolution::OursModifiedTheirsRenamedAndChangedThenRename { merged_blob, .. } => *merged_blob,
244− Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => Some(*merged_blob),
245− },
246− Err(failure) => match failure {
297+ fn failure_merged_blob(failure: &ResolutionFailure) -> Option<ContentMerge> {
298+ match failure {
247299 ResolutionFailure::OursRenamedTheirsRenamedDifferently { merged_blob } => *merged_blob,
248300 ResolutionFailure::Unknown
301+ | ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { .. }
249302 | ResolutionFailure::OursModifiedTheirsDeleted
250303 | ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch
251304 | ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed {
@@ -253,7 +306,16 @@ impl Conflict { · 1 unmodified line
253306 }
254307 | ResolutionFailure::OursAddedTheirsAddedTypeMismatch { .. }
255308 | 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),
256317 },
318+ Err(failure) => failure_merged_blob(failure),
257319 }
258320 }
259321 }
@@ -291,6 +353,9 @@ pub enum Resolution { · 31 unmodified lines
291353 /// The outcome of the content merge.
292354 merged_blob: ContentMerge,
293355 },
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),
294359 }
295360
296361 /// Describes of a conflict involving *our* change and *their* failed to be resolved.
@@ -306,6 +371,17 @@ pub enum ResolutionFailure { · 9 unmodified lines
306371 /// The path at which `ours` can be found in the tree - it's in the same directory that it was in before.
307372 renamed_unique_path_to_modified_blob: BString,
308373 },
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+ },
309385 /// *ours* was added (or renamed into place) with a different mode than theirs, e.g. blob and symlink, and we kept
310386 /// the symlink in its original location, renaming the other side to `their_unique_location`.
311387 OursAddedTheirsAddedTypeMismatch {
@@ -340,6 +416,8 @@ pub struct ContentMerge { · 28 unmodified lines
340416 #[derive(Default, Debug, Clone)]
341417 pub struct Options {
342418 /// 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.
343421 pub rewrites: Option<Rewrites>,
344422 /// Decide how blob-merges should be done. This relates to if conflicts can be resolved or not.
345423 pub blob_merge: crate::blob::platform::merge::Options,
@@ -358,16 +436,51 @@ pub struct Options { · 12 unmodified lines
358436 /// If `None`, when symlinks clash *ours* will be chosen and a conflict will occur.
359437 /// Otherwise, the same logic applies as for the merge of binary resources.
360438 pub symlink_conflicts: Option<crate::blob::builtin_driver::binary::ResolveWith>,
361− /// If `true`, instead of issuing a conflict with [`ResolutionFailure`], do nothing and keep the base/ancestor
362− /// version. This is useful when one wants to avoid any kind of merge-conflict to have *some*, *lossy* resolution.
363− pub allow_lossy_resolution: bool,
439+ /// If `None`, tree irreconcilable tree conflicts will result in [resolution failures](ResolutionFailure).
440+ /// Otherwise, one can choose a side. Note that it's still possible to determine that auto-resolution happened
441+ /// despite this choice, which allows carry forward the conflicting information, possibly for later resolution.
442+ /// If `Some(…)`, irreconcilable conflicts are reconciled by making a choice.
443+ /// Note that [`Conflict::entries()`] will still be set, to not degenerate information, even though they then represent
444+ /// the entries what would fit the index if no forced resolution was performed.
445+ /// It's up to the caller to handle that information mindfully.
446+ pub tree_conflicts: Option<ResolveWith>,
447+}
448+
449+/// Decide how to resolve tree-related conflicts, but only those that have [no way of being correct](ResolutionFailure).
450+#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
451+pub enum ResolveWith {
452+ /// On irreconcilable conflict, choose neither *our* nor *their* state, but keep the common *ancestor* state instead.
453+ Ancestor,
454+ /// On irreconcilable conflict, choose *our* side.
455+ ///
456+ /// Note that in order to get something equivalent to *theirs*, put *theirs* into the side of *ours*,
457+ /// swapping the sides essentially.
458+ Ours,
364459 }
365460
366461 pub(super) mod function;
367462 mod utils;
463+///
368464 pub mod apply_index_entries {
369465
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+
370482 pub(super) mod function {
483+ use crate::tree::apply_index_entries::RemovalMode;
371484 use crate::tree::{Conflict, ConflictIndexEntryPathHint, Resolution, ResolutionFailure, TreatAsUnresolved};
372485 use bstr::{BStr, ByteSlice};
373486 use std::collections::{hash_map, HashMap};
@@ -376,8 +489,9 @@ pub mod apply_index_entries { · 2 unmodified lines
376489 /// conflict should be considered unresolved.
377490 /// Once a stage of a path conflicts, the unconflicting stage is removed even though it might be the one
378491 /// that is currently checked out.
379− /// This removal, however, is only done by flagging it with [gix_index::entry::Flags::REMOVE], which means
380− /// these entries won't be written back to disk but will still be present in the index.
492+ /// This removal is only done by flagging it with [gix_index::entry::Flags::REMOVE], which means
493+ /// these entries won't be written back to disk but will still be present in the index if `removal_mode`
494+ /// is [`RemovalMode::Mark`]. For proper removal, choose [`RemovalMode::Prune`].
381495 /// It's important that `index` matches the tree that was produced as part of the merge that also
382496 /// brought about `conflicts`, or else this function will fail if it cannot find the path matching
383497 /// the conflicting entries.
@@ -388,6 +502,7 @@ pub mod apply_index_entries { · 4 unmodified lines
388502 conflicts: &[Conflict],
389503 how: TreatAsUnresolved,
390504 index: &mut gix_index::State,
505+ removal_mode: RemovalMode,
391506 ) -> bool {
392507 if index.is_sparse() {
393508 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
398513 for conflict in conflicts.iter().filter(|c| c.is_unresolved(how)) {
399514 let (renamed_path, current_path): (Option<&BStr>, &BStr) = match &conflict.resolution {
400515 Ok(success) => match success {
516+ Resolution::Forced(_) => continue,
401517 Resolution::SourceLocationAffectedByRename { final_location } => {
402518 (Some(final_location.as_bstr()), final_location.as_bstr())
403519 }
@@ -410,6 +526,9 @@ pub mod apply_index_entries { · 6 unmodified lines
410526 }
411527 },
412528 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()),
413532 ResolutionFailure::OursRenamedTheirsRenamedDifferently { .. } => {
414533 (Some(conflict.theirs.location()), conflict.ours.location())
415534 }
@@ -492,8 +611,15 @@ pub mod apply_index_entries { · 76 unmodified lines
492611 }
493612 }
494613
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+ }
495621 index.sort_entries();
496− index.entries().len() != len
622+ res
497623 }
498624 }
499625 }
gix-merge/src/tree/utils.rs+14−17
@@ -23,7 +23,7 @@ use std::collections::HashMap; · 22 unmodified lines
2323 /// over a directory rewrite in *our* tree. If so, rewrite it so that we get the path
2424 /// it would have had if it had been renamed along with *our* directory.
2525 pub fn possibly_rewritten_location(
26− check_tree: &mut TreeNodes,
26+ check_tree: &TreeNodes,
2727 their_location: &BStr,
2828 our_changes: &ChangeListRef,
2929 ) -> Option<BString> {
@@ -60,7 +60,7 @@ pub fn rewrite_location_with_renamed_directory(their_location: &BStr, passed_cha · 30 unmodified lines
6060 pub fn unique_path_in_tree(
6161 file_path: &BStr,
6262 editor: &tree::Editor<'_>,
63− tree: &mut TreeNodes,
63+ tree: &TreeNodes,
6464 side_name: &BStr,
6565 ) -> Result<BString, Error> {
6666 let mut buf = file_path.to_owned();
@@ -400,11 +400,11 @@ impl TreeNodes { · 333 unmodified lines
400400 Some(index) => {
401401 cursor = &mut self.0[index];
402402 if is_last && !cursor.is_leaf_node() {
403− assert_eq!(
404− cursor.change_idx, None,
405− "BUG: each node should only see a single change when tracking initially: {path} {change_idx}"
406− );
407− cursor.change_idx = Some(change_idx);
403+ // NOTE: we might encounter the same path multiple times in rare conditions.
404+ // At least we avoid overwriting existing intermediate changes, for good measure.
405+ if cursor.change_idx.is_none() {
406+ cursor.change_idx = Some(change_idx);
407+ }
408408 }
409409 }
410410 }
@@ -414,12 +414,12 @@ impl TreeNodes { · 3 unmodified lines
414414
415415 /// Search the tree with `our` changes for `theirs` by [`source_location()`](Change::source_location())).
416416 /// 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> {
418418 if self.0.len() == 1 {
419419 return None;
420420 }
421421 let components = to_components(theirs_location);
422− let mut cursor = &mut self.0[0];
422+ let mut cursor = &self.0[0];
423423 let mut cursor_idx = 0;
424424 let mut intermediate_change = None;
425425 for component in components {
@@ -436,7 +436,7 @@ impl TreeNodes { · 10 unmodified lines
436436 } else {
437437 // a change somewhere else, i.e. `a/c` and we know `a/b` only.
438438 intermediate_change.and_then(|(change, cursor_idx)| {
439− let cursor = &mut self.0[cursor_idx];
439+ let cursor = &self.0[cursor_idx];
440440 // If this is a destination location of a rename, then the `their_location`
441441 // is already at the right spot, and we can just ignore it.
442442 if matches!(cursor.location, ChangeLocation::CurrentLocation) {
@@ -450,7 +450,7 @@ impl TreeNodes { · 7 unmodified lines
450450 }
451451 Some(child_idx) => {
452452 cursor_idx = child_idx;
453− cursor = &mut self.0[cursor_idx];
453+ cursor = &self.0[cursor_idx];
454454 }
455455 }
456456 }
@@ -496,7 +496,7 @@ impl TreeNodes { · 39 unmodified lines
496496 let mut cursor = &mut self.0[0];
497497 while let Some(component) = components.next() {
498498 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"),
500500 Some(existing_idx) => {
501501 let is_last = components.peek().is_none();
502502 if is_last {
@@ -504,7 +504,7 @@ impl TreeNodes { · 1 unmodified line
504504 cursor = &mut self.0[existing_idx];
505505 debug_assert!(
506506 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:?}"
508508 );
509509 cursor.change_idx = None;
510510 } else {
@@ -578,10 +578,7 @@ impl Conflict { · 67 unmodified lines
578578 ours: ours.clone(),
579579 theirs: theirs.clone(),
580580 entries,
581− map: match outer_map {
582− ConflictMapping::Original => map,
583− ConflictMapping::Swapped => map.swapped(),
584− },
581+ map: map.to_global(outer_map),
585582 }
586583 }
587584
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
3636 cp .git/index .git/"${identifier}".index
3737 }
3838
39+function make_resolve_tree() {
40+ local resolve=${1:?Their 'ancestor' or 'ours'}
41+ local our_side=${2:-}
42+ local their_side=${3:-}
43+
44+ local filename="resolve-${our_side}-${their_side}-with-${resolve}"
45+ git write-tree > ".git/${filename}.tree"
46+}
47+
3948 function baseline () (
4049 local dir=${1:?the directory to enter}
4150 local output_name=${2:?the basename of the output of the merge}
@@ -58,8 +67,13 @@ function baseline () ( · 16 unmodified lines
5867 our_commit_id="$(git rev-parse "$our_committish")"
5968 their_commit_id="$(git rev-parse "$their_committish")"
6069 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
6174 if [ -z "$opt_deviation_message" ]; then
6275 maybe_expected_tree="expected^{tree}"
76+ maybe_expected_reversed_tree="expected^{tree}"
6377 fi
6478
6579 local merge_info="${output_name}.merge-info"
@@ -69,10 +83,108 @@ function baseline () ( · 3 unmodified lines
6983 if [[ "$one_side" != "no-reverse" ]]; then
7084 local merge_info="${output_name}-reversed.merge-info"
7185 git -c merge.conflictStyle=$conflict_style merge-tree -z --write-tree --allow-unrelated-histories "$their_committish" "$our_committish" > "$merge_info" || :
72− echo "$dir" "$conflict_style" "$their_commit_id" "$their_committish" "$our_commit_id" "$our_committish" "$merge_info" "$maybe_expected_tree" "$opt_deviation_message" >> ../baseline.cases
86+ echo "$dir" "$conflict_style" "$their_commit_id" "$their_committish" "$our_commit_id" "$our_committish" "$merge_info" "$maybe_expected_reversed_tree" "$opt_deviation_message" >> ../baseline.cases
7387 fi
7488 )
7589
90+
91+git init non-tree-to-tree
92+(cd non-tree-to-tree
93+ write_lines original 1 2 3 4 5 >a
94+ git add a && git commit -m "init"
95+
96+ git branch A
97+ git branch B
98+
99+ git checkout A
100+ write_lines 1 2 3 4 5 6 >a
101+ git commit -am "'A' changes 'a'"
102+
103+ git checkout B
104+ rm a
105+ mkdir -p a/sub
106+ touch a/sub/b a/sub/c a/d a/e
107+ git add a && git commit -m "mv 'a' to 'a/sub/b', populate 'a/' with empty files"
108+)
109+
110+git init tree-to-non-tree
111+(cd tree-to-non-tree
112+ mkdir -p a/sub
113+ write_lines original 1 2 3 4 5 >a/sub/b
114+ touch a/sub/c a/d a/e
115+ git add a && git commit -m "init"
116+
117+ git branch A
118+ git branch B
119+
120+ git checkout A
121+ write_lines 1 2 3 4 5 6 >a/sub/b
122+ git commit -am "'A' changes 'a/sub/b'"
123+
124+ git checkout B
125+ rm -Rf a
126+ echo "new file" > a
127+ git add a && git commit -m "rm -Rf a/ && add non-empty 'a'"
128+)
129+
130+git init non-tree-to-tree-with-rename
131+(cd non-tree-to-tree-with-rename
132+ write_lines original 1 2 3 4 5 >a
133+ git add a && git commit -m "init"
134+
135+ git branch A
136+ git branch B
137+
138+ git checkout A
139+ write_lines 1 2 3 4 5 6 >a
140+ git commit -am "'A' changes 'a'"
141+
142+ git checkout B
143+ mv a tmp
144+ mkdir -p a/sub
145+ mv tmp a/sub/b
146+ touch a/sub/c a/d a/e
147+ git add a && git commit -m "mv 'a' to 'a/sub/b', populate 'a/' with empty files"
148+)
149+
150+git init tree-to-non-tree-with-rename
151+(cd tree-to-non-tree-with-rename
152+ mkdir -p a/sub
153+ write_lines original 1 2 3 4 5 >a/sub/b
154+ touch a/sub/c a/d a/e
155+ git add a && git commit -m "init"
156+
157+ git branch A
158+ git branch B
159+
160+ git checkout A
161+ write_lines 1 2 3 4 5 6 >a/sub/b
162+ git commit -am "'A' changes 'a/sub/b'"
163+
164+ git checkout B
165+ rm -Rf a
166+ touch a
167+ git add a && git commit -m "rm -Rf a/ && add empty 'a' (which is like a rename from an empty deleted file)"
168+ # And because it's so thrown off, it gets a completely different result if reversed.
169+ git branch expected-reversed
170+
171+ rm .git/index
172+ git update-index --index-info <<EOF
173+100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/b
174+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/sub/b
175+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a~B
176+EOF
177+ make_conflict_index tree-to-non-tree-with-rename-A-B
178+
179+ rm .git/index
180+ git update-index --index-info <<EOF
181+100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/b
182+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/sub/b
183+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a~B
184+EOF
185+ make_conflict_index tree-to-non-tree-with-rename-A-B-reversed
186+)
187+
76188 git init simple
77189 (cd simple
78190 rm -Rf .git/hooks
@@ -222,6 +334,26 @@ git init rename-add-symlink · 143 unmodified lines
222334 git commit -m "rename foo to bar"
223335 )
224336
337+git init rename-add-same-symlink
338+(cd rename-add-same-symlink
339+ touch target
340+ ln -s target link
341+ git add .
342+ git commit -m "original"
343+
344+ git branch A
345+ git branch B
346+
347+ git checkout A
348+ git mv link link-new
349+ git commit -m "rename link to link-new"
350+
351+ git checkout B
352+ ln -s target link-new
353+ git add link-new
354+ git commit -m "create link-new"
355+)
356+
225357 git init rename-rename-plus-content
226358 (cd rename-rename-plus-content
227359 write_lines 1 2 3 4 5 >foo
@@ -401,12 +533,40 @@ git init rename-within-rename · 173 unmodified lines
401533 git checkout expected
402534 write_lines 1 2 3 4 5 6 >a/x.f
403535 write_lines 1 2 3 4 5 6 >a/sub/y.f
404− git mv a/sub a/sub-renamed
536+ cp -Rv a/sub a/sub-renamed
537+ git add .
405538 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"
407540
408− # This means there are no conflicts actually.
541+ # We have duplication just like Git, but our index is definitely more complex. This one seems more plausible.
542+ # The problem is that renames can't be indicated correctly in the index.
543+ rm .git/index
544+ git update-index --index-info <<EOF
545+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-renamed/sub/y.f
546+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z
547+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/sub-renamed/y.f
548+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/sub-renamed/z
549+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w
550+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f
551+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/sub-renamed/y.f
552+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a/sub-renamed/z
553+100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f
554+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z
555+EOF
409556 make_conflict_index rename-within-rename-A-B-deviates
557+ rm .git/index
558+ git update-index --index-info <<EOF
559+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-renamed/sub/y.f
560+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z
561+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/sub-renamed/y.f
562+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/sub-renamed/z
563+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w
564+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f
565+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/sub-renamed/y.f
566+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a/sub-renamed/z
567+100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/sub/y.f
568+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 a/sub/z
569+EOF
410570 make_conflict_index rename-within-rename-A-B-deviates-reversed
411571 )
412572
@@ -586,10 +746,12 @@ git init conflicting-rename-complex · 173 unmodified lines
586746 rm -Rf ./a
587747 mkdir -p a-renamed/sub
588748 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
589750 write_lines 1 2 3 4 5 6 >a-renamed/y.f
590− touch a-renamed/z a-renamed/sub/z
751+ touch a-renamed/z a-renamed/w a-renamed/sub/z
591752 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+
593755
594756 # Since the whole state is very different, the expected index is as well, but at least it should make sense for what it is.
595757 # 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
599761 100644 8a1218a1024a212bb3db30becd860315f9f3ac52 2 a-renamed/sub/y.f
600762 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/sub/z
601763 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f
764+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a-renamed/x.f
765+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 2 a-renamed/w
602766 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z
767+100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/x.f
768+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a/y.f
603769 EOF
604770 make_conflict_index conflicting-rename-complex-A-B
605771
@@ -608,7 +774,11 @@ EOF · 2 unmodified lines
608774 100644 8a1218a1024a212bb3db30becd860315f9f3ac52 3 a-renamed/sub/y.f
609775 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/sub/z
610776 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/y.f
777+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 3 a-renamed/x.f
778+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 3 a-renamed/w
611779 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/z
780+100644 44065282f89b9bd6439ed2e4674721383fd987eb 1 a/x.f
781+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 2 a/y.f
612782 EOF
613783 make_conflict_index conflicting-rename-complex-A-B-reversed
614784 )
@@ -628,7 +798,7 @@ git init same-rename-different-mode · 13 unmodified lines
628798 chmod +x a/x.f a/w
629799 git update-index --chmod=+x a/x.f a/w
630800 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"
632802
633803 git checkout B
634804 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
9931163
9941164
9951165
996−# TODO: Git does not detect the conflict (one turns exe off, the other turns it on), and we do exactly the same.
1166+baseline non-tree-to-tree A-B A B
1167+baseline tree-to-non-tree A-B A B
1168+baseline tree-to-non-tree-with-rename A-B A B
1169+baseline non-tree-to-tree-with-rename A-B A B
1170+baseline rename-add-same-symlink A-B A B
9971171 baseline rename-add-exe-bit-conflict A-B A B
9981172 baseline remove-executable-mode A-B A B
9991173 baseline simple side-1-3-without-conflict side1 side3
@@ -1024,11 +1198,11 @@ baseline super-1 A-B-diff3 A B · 24 unmodified lines
10241198 baseline super-2 A-B A B
10251199 baseline super-2 A-B-diff3 A B
10261200
1027−baseline rename-within-rename A-B-deviates A B "Git doesn't detect the rename-nesting so there is duplication - we achieve the optimal result"
1201+baseline rename-within-rename A-B-deviates A B "Git doesn't detect the rename-nesting, and we do neith, and we do neither"
10281202 baseline rename-within-rename-2 A-B-deviates A B "TBD: Right, something is different documentation was forgotten :/"
10291203 baseline conflicting-rename A-B A B
10301204 baseline conflicting-rename-2 A-B A B
1031−baseline conflicting-rename-complex A-B A B "Git has different rename tracking which is why a-renamed/w disappears - it's still close enough"
1205+baseline conflicting-rename-complex A-B A B "Git has different rename tracking - overall result it's still close enough"
10321206
10331207 baseline same-rename-different-mode A-B A B "Git works for the A/B case, but for B/A it forgets to set the executable bit"
10341208 baseline renamed-symlink-with-conflict A-B A B
@@ -1054,3 +1228,622 @@ baseline rename-and-modification A-B A B · 19 unmodified lines
10541228 baseline symlink-modification A-B A B
10551229 baseline symlink-addition A-B A B
10561230 baseline type-change-to-symlink A-B A B
1231+
1232+##
1233+## Only once the tree-merges were performed can we refer to their objects
1234+## when making tree-conflict resolution expectations. It's important
1235+## to get these right.
1236+##
1237+(cd simple
1238+ rm .git/index
1239+ # 'whatever' is tree-conflict, 'greeting' is content conflict with markers
1240+ git update-index --index-info <<EOF
1241+100644 45b983be36b73c0788dc9cbcb76cbb80fc7bb057 0 greeting
1242+100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers
1243+100644 5716ca5987cbf97d6bb54920bea6adde242d87e6 0 whatever
1244+EOF
1245+ make_resolve_tree ours side1 side2
1246+
1247+ rm .git/index
1248+ git update-index --index-info <<EOF
1249+100644 092bfb9bdf74dd8cfd22e812151281ee9aa6f01a 0 greeting
1250+100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers
1251+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 whatever/empty
1252+EOF
1253+ make_resolve_tree ours side2 side1
1254+
1255+ rm .git/index
1256+ git update-index --index-info <<EOF
1257+100644 9dc97bdc2426e68423360e3e5299280b2cf6b8ff 0 greeting
1258+100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers
1259+100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 whatever
1260+EOF
1261+ make_resolve_tree ancestor side1 side2
1262+
1263+ rm .git/index
1264+ git update-index --index-info <<EOF
1265+100644 1a2664a9924754c698e323f756f9f87f3f2fb337 0 greeting
1266+100644 09c277aa66897c58157f57a374eacc63a407dcab 0 numbers
1267+100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 whatever
1268+EOF
1269+ make_resolve_tree ancestor side2 side1
1270+
1271+ rm .git/index
1272+ git update-index --index-info <<EOF
1273+100644 a4ae6e4709228b5da6001cb9d1cfa7736851e2a6 0 greeting
1274+100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 whatever
1275+100644 542802a799ded74fa01c47ba2f8925e284a369e2 0 Αυτά μου φαίνονται κινέζικα
1276+EOF
1277+ make_resolve_tree ancestor tweak1 side2
1278+
1279+ rm .git/index
1280+ git update-index --index-info <<EOF
1281+100644 45b983be36b73c0788dc9cbcb76cbb80fc7bb057 0 greeting
1282+100644 5716ca5987cbf97d6bb54920bea6adde242d87e6 0 whatever
1283+100644 65bc6a1e238f4bf05b28fd05240636e2cfb657e0 0 Αυτά μου φαίνονται κινέζικα
1284+EOF
1285+ make_resolve_tree ours tweak1 side2
1286+
1287+ rm .git/index
1288+ git update-index --index-info <<EOF
1289+100644 blob ea28dcd7f627a2a7bbd09daa679c452180617c9f greeting
1290+100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 whatever
1291+100644 blob f801a62deed900f8a80ff35e3339474ad6352a93 Αυτά μου φαίνονται κινέζικα
1292+EOF
1293+ make_resolve_tree ancestor side2 tweak1
1294+
1295+ rm .git/index
1296+ git update-index --index-info <<EOF
1297+100644 blob 092bfb9bdf74dd8cfd22e812151281ee9aa6f01a greeting
1298+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 whatever/empty
1299+100644 blob 09c277aa66897c58157f57a374eacc63a407dcab Αυτά μου φαίνονται κινέζικα
1300+EOF
1301+ make_resolve_tree ours side2 tweak1
1302+)
1303+
1304+(cd rename-add-symlink
1305+ rm .git/index
1306+ # the symlink of 'bar' from A
1307+ git update-index --index-info <<EOF
1308+120000 blob 19102815663d23f8b75a47e7a01965dcdc96468c bar
1309+EOF
1310+ make_resolve_tree ours A B
1311+
1312+ rm .git/index
1313+ # the merged form of 'bar' from B, not replaced by symlink
1314+ git update-index --index-info <<EOF
1315+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 bar
1316+EOF
1317+ make_resolve_tree ours B A
1318+
1319+ rm .git/index
1320+ # foo is renamed to bar, type clash means neither A nor B can be added - empty tree
1321+ # It is not able to 'get foo back', it can't track that currently.
1322+ make_resolve_tree ancestor A B
1323+ make_resolve_tree ancestor B A
1324+)
1325+
1326+(cd rename-rename-plus-content
1327+ rm .git/index
1328+ # both sides rename 'foo' into something else.
1329+ git update-index --index-info <<EOF
1330+100644 blob 8a1218a1024a212bb3db30becd860315f9f3ac52 foo
1331+EOF
1332+ make_resolve_tree ancestor A B
1333+ make_resolve_tree ancestor B A
1334+
1335+ rm .git/index
1336+ # 'bar' is the name in 'A', and there is a merge with the content from 'B'
1337+ # which we auto-resolve.
1338+ git update-index --index-info <<EOF
1339+100644 blob d0549c3d3c96a464289f3b820b7d96aedc58924b bar
1340+EOF
1341+ make_resolve_tree ours A B
1342+
1343+ rm .git/index
1344+ # 'baz' is the name in 'B', and there is a merge with the content from 'A'
1345+ # which we auto-resolve.
1346+ git update-index --index-info <<EOF
1347+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 baz
1348+EOF
1349+ make_resolve_tree ours B A
1350+)
1351+
1352+(cd rename-add-delete
1353+ rm .git/index
1354+ # 'foo' is deleted, 'bar' is added in 'A', but renamed to 'bar' in 'B'.
1355+ # Do nothing, *should* keep 'foo', and it's re-added later, which copies it.
1356+ # But instead we keep foo the first time, but then it 'turns' and we process a remaining
1357+ # addition that once again sees the rename from the other side, which is not a conflict
1358+ # and thus removes 'foo' after all, and merges 'bar'.
1359+ # It's not super-correct, but it's only an issue for virtual merge bases, which are kind of
1360+ # hidden anyway.
1361+ git update-index --index-info <<EOF
1362+100644 blob 0cde534c2fca6c92c07b3e7a696665e844b9b933 bar
1363+EOF
1364+ make_resolve_tree ancestor A B
1365+ # B A isn't tested, as it is not an Err() conflict.
1366+
1367+ # This case ends up being exactly the same as the ancestor, but with the merge-conflict
1368+ # auto-resolved to 'ours'.
1369+ # This time, it's expected to not have 'foo', but 'ours' in the clashing pair is a deletion. The rename side
1370+ # is dropped, but what's left is the rename/add pair once the algorithm turns around/flips.
1371+ rm .git/index
1372+ git update-index --index-info <<EOF
1373+100644 blob f286e5cdd97ac6895438ea4548638bb98ac9bd6b bar
1374+EOF
1375+ make_resolve_tree ours A B
1376+)
1377+
1378+(cd rename-rename-delete-delete
1379+ rm .git/index
1380+ # 'A' deletes 'bar' and 'B' turns 'bar' into 'baz'. 'A' renames 'foo' into 'bar', and
1381+ # 'B' deletes 'foo'. 'ancestor' resolves to avoid any edits, leaving the state from 'main'.
1382+ git update-index --index-info <<EOF
1383+100644 blob 5716ca5987cbf97d6bb54920bea6adde242d87e6 bar
1384+100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 foo
1385+EOF
1386+ make_resolve_tree ancestor A B
1387+ # this works in reverse as well (this time).
1388+ make_resolve_tree ancestor B A
1389+
1390+ rm .git/index
1391+ # As 'ours' is a deletion of 'foo', it goes through, but we also acknowledge 'theirs'
1392+ # as it gives better results, so end up with `baz`.
1393+ git update-index --index-info <<EOF
1394+100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 baz
1395+EOF
1396+ make_resolve_tree ours A B
1397+
1398+ rm .git/index
1399+ # Here we end up in exactly the same spot as if we'd do a normal merge,
1400+ # which ends `baz` in a conflict. However, with content-merges set to 'ours'
1401+ # it ends up like it should, giving a good result.
1402+ git update-index --index-info <<EOF
1403+ 100644 blob 5716ca5987cbf97d6bb54920bea6adde242d87e6 baz
1404+EOF
1405+ make_resolve_tree ours B A
1406+)
1407+
1408+(cd super-1
1409+ # Each of the ancestor files are renamed in a conflicting way, and here
1410+ # with ancestor choice, nothing happens, making this equivalent to `main`
1411+ git checkout main
1412+ make_resolve_tree ancestor A B
1413+ make_resolve_tree ancestor B A
1414+
1415+ rm .git/index
1416+ # We do indeed perform the renames like this, and the content is merges as well as possible,
1417+ # (here) configured to content-merge with 'ours' as well where needed.
1418+ git update-index --index-info <<EOF
1419+100644 blob 4b5599c7c2ed4390417d9699bec86144a386873d four
1420+100644 blob 64012489f118cb4011c8902b4a635f70dcb0c0ca six
1421+100644 blob e33f5e94470d3b5fa0220ff6a9cabb78a3f72fa3 two
1422+EOF
1423+ make_resolve_tree ours A B
1424+
1425+ rm .git/index
1426+ # The same, but from the other side.
1427+ git update-index --index-info <<EOF
1428+ 100644 blob 64012489f118cb4011c8902b4a635f70dcb0c0ca four
1429+ 100644 blob e33f5e94470d3b5fa0220ff6a9cabb78a3f72fa3 six
1430+ 100644 blob 4178ea6795c4c3e07b4e17e6a04aa49584b07ecd two
1431+EOF
1432+ make_resolve_tree ours B A
1433+)
1434+
1435+(cd super-2
1436+ rm .git/index
1437+ # 'B' changes foo, and moves it into 'olddir/bar', but `A' deleted 'foo', and adds 'newdir/bar/file'
1438+ # after renaming 'olddir' to 'newdir'.
1439+ # As `B` only has a single change that gets dropped when it clashes with the deletion of 'foo',
1440+ # all other changes of 'A' can just be applied without any conflict whatsoever.
1441+ git update-index --index-info <<EOF
1442+100644 blob 8a1218a1024a212bb3db30becd860315f9f3ac52 foo
1443+100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 newdir/a
1444+100644 blob 61780798228d17af2d34fce4cfbdf35556832472 newdir/b
1445+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 newdir/bar/file
1446+100644 blob f2ad6c76f0115a6ba5b00456a849810e7ec0af20 newdir/c
1447+EOF
1448+ make_resolve_tree ancestor A B
1449+ make_resolve_tree ancestor B A
1450+
1451+ rm .git/index
1452+ # Similar to the ancestor version, but now we choose 'ours', so the rename of 'foo' gets
1453+ # dropped and it just gets deleted. Everything else is then 'A'.
1454+ git update-index --index-info <<EOF
1455+100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 newdir/a
1456+100644 blob 61780798228d17af2d34fce4cfbdf35556832472 newdir/b
1457+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 newdir/bar/file
1458+100644 blob f2ad6c76f0115a6ba5b00456a849810e7ec0af20 newdir/c
1459+EOF
1460+ make_resolve_tree ours A B
1461+
1462+ rm .git/index
1463+ # 'B' changes 'foo' and moves it to 'olddir/bar', which gets tracked to be
1464+ # 'newdir/bar' and is taken verbatim. The clash that it finds it
1465+ # resolves in 'B's favor, leaving only 'newdir/bar'.
1466+ git update-index --index-info <<EOF
1467+100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 newdir/a
1468+100644 blob 61780798228d17af2d34fce4cfbdf35556832472 newdir/b
1469+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 newdir/bar
1470+100644 blob f2ad6c76f0115a6ba5b00456a849810e7ec0af20 newdir/c
1471+EOF
1472+ make_resolve_tree ours B A
1473+)
1474+
1475+(cd conflicting-rename
1476+ rm .git/index
1477+ # 'A' renames 'a' to 'a-renamed', 'B' renames 'a' to 'a-different'.
1478+ # All these conflicts are dropped in favor of keeping the 'ancestor' *location*.
1479+ git update-index --index-info <<EOF
1480+100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/y.f
1481+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/z
1482+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w
1483+100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/x.f
1484+EOF
1485+ make_resolve_tree ancestor A B
1486+ make_resolve_tree ancestor B A
1487+
1488+ rm .git/index
1489+ # Much like the ancestor version, except that it applied the 'A' rename,
1490+ # along with its *merged* content.
1491+ git update-index --index-info <<EOF
1492+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-renamed/sub/y.f
1493+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/sub/z
1494+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/w
1495+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-renamed/x.f
1496+EOF
1497+ make_resolve_tree ours A B
1498+ rm .git/index
1499+ # Just like 'A' above, but with the 'B' rename chosen and all the merges.
1500+ git update-index --index-info <<EOF
1501+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-different/sub/y.f
1502+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-different/sub/z
1503+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-different/w
1504+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-different/x.f
1505+EOF
1506+ make_resolve_tree ours B A
1507+)
1508+
1509+(cd conflicting-rename-2
1510+ rm .git/index
1511+ # Like 'conflicting-rename', but this one only renames a single sub-directory for very much the same effect.
1512+ # Thus, keeping the 'ancestor' version is the same as 'main', except for merged content.
1513+ git update-index --index-info <<EOF
1514+100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/y.f
1515+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/z
1516+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w
1517+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1518+EOF
1519+ make_resolve_tree ancestor A B
1520+ make_resolve_tree ancestor B A
1521+
1522+ rm .git/index
1523+ git update-index --index-info <<EOF
1524+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub-renamed/y.f
1525+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub-renamed/z
1526+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w
1527+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1528+EOF
1529+ make_resolve_tree ours A B
1530+ rm .git/index
1531+ git update-index --index-info <<EOF
1532+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub-different/y.f
1533+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub-different/z
1534+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w
1535+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1536+EOF
1537+ make_resolve_tree ours B A
1538+)
1539+
1540+(cd conflicting-rename-complex
1541+ rm .git/index
1542+ # 'A" renames 'a' to 'a-renamed', but 'B' moves 'a/sub/' up one level, and replaces everything in its wake
1543+ # so its two files are the only ones left.
1544+ # As result, we actually have one unconflicting change which ends up creating the new directory 'a-renamed',
1545+ # but everything else is conflicting so it keeps the 'ancestor' version.
1546+ git update-index --index-info <<EOF
1547+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/z
1548+100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/y.f
1549+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/z
1550+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/w
1551+100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/x.f
1552+EOF
1553+ make_resolve_tree ancestor A B
1554+ make_resolve_tree ancestor B A
1555+
1556+ rm .git/index
1557+ # This is some of the rename tracking from `B` making it into the non-clashing portions of 'A".
1558+ # Due to different rename tracking, the non-forced version is also a bit of a mess, and that carries on here.
1559+ git update-index --index-info <<EOF
1560+ 100644 blob 8a1218a1024a212bb3db30becd860315f9f3ac52 a-renamed/sub/y.f
1561+ 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/sub/z
1562+ 100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-renamed/x.f
1563+ 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/z
1564+ 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/w
1565+EOF
1566+ make_resolve_tree ours A B
1567+
1568+ rm .git/index
1569+ # It applies the merged result of the content, and interestingly also managed to reconcile the rename from 'A'.
1570+ # However, it also drops all of 'their' conflicting changes in favor of 'ours', a respectable result.
1571+ git update-index --index-info <<EOF
1572+ 100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a-renamed/y.f
1573+ 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a-renamed/z
1574+EOF
1575+ make_resolve_tree ours B A
1576+)
1577+
1578+(cd renamed-symlink-with-conflict
1579+ rm .git/index
1580+ # 'A' changes 'a/x.f' and renames the 'link', while 'B' also changes 'a/x.f' in a mergable fashion,
1581+ # while renaming 'link' to something else which is where the conflict comes from.
1582+ # Choosing the 'ancestor' means to not rename 'link' at all, while merging the file.
1583+ git update-index --index-info <<EOF
1584+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1585+120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link
1586+EOF
1587+ make_resolve_tree ancestor A B
1588+ make_resolve_tree ancestor B A
1589+
1590+ rm .git/index
1591+ # Here we choose the name of 'link' in 'A'.
1592+ git update-index --index-info <<EOF
1593+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1594+120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link-renamed
1595+EOF
1596+ make_resolve_tree ours A B
1597+
1598+ rm .git/index
1599+ # Here we choose the name of 'link' in 'B'.
1600+ git update-index --index-info <<EOF
1601+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1602+120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link-different
1603+EOF
1604+ make_resolve_tree ours B A
1605+)
1606+
1607+(cd type-change-and-renamed
1608+ rm .git/index
1609+ # 'A' changes `link` to a file, while 'B' keeps the link, but renames it.
1610+ # 'ancestor' just keeps the original version of 'link'
1611+ git update-index --index-info <<EOF
1612+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/x.f
1613+120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link
1614+EOF
1615+ make_resolve_tree ancestor A B
1616+ make_resolve_tree ancestor B A
1617+
1618+ rm .git/index
1619+ # 'A' changes the type of 'link' to be a file, and that's what's used here.
1620+ git update-index --index-info <<EOF
1621+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/x.f
1622+100644 blob f89a08d1e226b9a319210641b63b07dcf0bd705f link
1623+EOF
1624+ make_resolve_tree ours A B
1625+
1626+ rm .git/index
1627+ # 'B' renames the link, and that is picked up as well.
1628+ git update-index --index-info <<EOF
1629+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/x.f
1630+120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link-renamed
1631+EOF
1632+ make_resolve_tree ours B A
1633+)
1634+
1635+(cd change-and-delete
1636+ rm .git/index
1637+ # 'A' changes 'link' to be a file, and changes the file, while 'B' deletes everything,
1638+ # causing each file to be irreconcilable.
1639+ # 'ancestor' keeps everything as is.
1640+ git update-index --index-info <<EOF
1641+100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/x.f
1642+120000 blob e29fa63dae4ccf0788897a7025da868083178fdf link
1643+EOF
1644+ make_resolve_tree ancestor A B
1645+ make_resolve_tree ancestor B A
1646+
1647+ rm .git/index
1648+ # 'A' changes everything, and that's the change we keep.
1649+ git update-index --index-info <<EOF
1650+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/x.f
1651+100644 blob f89a08d1e226b9a319210641b63b07dcf0bd705f link
1652+EOF
1653+ make_resolve_tree ours A B
1654+
1655+ rm .git/index
1656+ # 'B' deletes everything, which is what we keep.
1657+ make_resolve_tree ours B A
1658+)
1659+
1660+(cd submodule-both-modify
1661+ rm .git/index
1662+ # There is only one submodule. 'A' and 'B' change it in a fast-forwardable manner,
1663+ # but we can't handle this at all yet, and thus have to consider it irreconcilable.
1664+ # The 'ancestor' resolution just keeps what was.
1665+ git update-index --index-info <<EOF
1666+160000 commit e835c0c403c8e494c0ca98f3d25d0b8464c18d38 sub
1667+EOF
1668+ make_resolve_tree ancestor A B
1669+ make_resolve_tree ancestor B A
1670+
1671+ rm .git/index
1672+ # Otherwise it's the state of 'A'.
1673+ git update-index --index-info <<EOF
1674+160000 commit 64466ebdff775ad618d9cc993cf52840e0af528c sub
1675+EOF
1676+ make_resolve_tree ours A B
1677+
1678+ rm .git/index
1679+ # Otherwise it's the state of 'B'.
1680+ git update-index --index-info <<EOF
1681+160000 commit ea6eb701e03c2497915c25a851f3da8f8e362ca0 sub
1682+EOF
1683+ make_resolve_tree ours B A
1684+)
1685+
1686+(cd multiple-merge-bases
1687+ rm .git/index
1688+ # 'A' modifies and 'B' deletes the single file in the tree.
1689+ # 'ancestor' keeps the original, which is already the result of the merge of
1690+ # the merge-bases.
1691+ git update-index --index-info <<EOF
1692+100644 blob 09c277aa66897c58157f57a374eacc63a407dcab content
1693+EOF
1694+ make_resolve_tree ancestor A B
1695+ make_resolve_tree ancestor B A
1696+
1697+ rm .git/index
1698+ # 'A' keeps the modified version.
1699+ git update-index --index-info <<EOF
1700+100644 blob 0a6a0ba83635bc00e7c79a4b5b6e50381385c1af content
1701+EOF
1702+ make_resolve_tree ours A B
1703+
1704+ rm .git/index
1705+ # 'B' applies the deletion to get an empty tree.
1706+ make_resolve_tree ours B A
1707+)
1708+
1709+(cd non-tree-to-tree
1710+ rm .git/index
1711+ # 'A' changes the single file 'a', while 'B' replaces it with a directory structure 'a',
1712+ # without a rename though.
1713+ # We manage to pick the 'ancestor', just a single file, while discarding all follow-up changes.
1714+ git update-index --index-info <<EOF
1715+100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a
1716+EOF
1717+ make_resolve_tree ancestor A B
1718+ make_resolve_tree ancestor B A
1719+
1720+ rm .git/index
1721+ # Picks 'A' which is just a single, modified (and mergable) file.
1722+ git update-index --index-info <<EOF
1723+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a
1724+EOF
1725+ make_resolve_tree ours A B
1726+
1727+ rm .git/index
1728+ # Picks 'B' which is a whole directory tree.
1729+ git update-index --index-info <<EOF
1730+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/d
1731+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/e
1732+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/b
1733+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/c
1734+EOF
1735+ make_resolve_tree ours B A
1736+)
1737+
1738+(cd tree-to-non-tree
1739+ rm .git/index
1740+ # 'A' modifies a nested file 'a/sub/b', while 'B' replaces 'a/' with file 'a'.
1741+ # Ignore *their* changes for 'ancestor' resolution, and the modification,
1742+ # but apply all others which are deletions.
1743+ git update-index --index-info <<EOF
1744+100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/b
1745+EOF
1746+ make_resolve_tree ancestor A B
1747+ make_resolve_tree ancestor B A
1748+ # *ours* is the same as ancestor, we want to keep the tree and changes, but it only
1749+ # applies to the one modification that protects the change.
1750+ rm .git/index
1751+ git update-index --index-info <<EOF
1752+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub/b
1753+EOF
1754+ make_resolve_tree ours A B
1755+
1756+ rm .git/index
1757+ # Now *ours* is the single file which replaces a tree.
1758+ git update-index --index-info <<EOF
1759+100644 blob fa49b077972391ad58037050f2a75f74e3671e92 a
1760+EOF
1761+ make_resolve_tree ours B A
1762+)
1763+
1764+(cd tree-to-non-tree-with-rename
1765+ rm .git/index
1766+ # 'A' modifies a nested file 'a/sub/b', while 'B' replaces 'a/' with file 'a'.
1767+ # I let it pass as it's an edge-case to some extent.
1768+ git update-index --index-info <<EOF
1769+100644 blob 44065282f89b9bd6439ed2e4674721383fd987eb a/sub/b
1770+EOF
1771+ make_resolve_tree ancestor A B
1772+ make_resolve_tree ancestor B A
1773+
1774+ rm .git/index
1775+ # Thanks to the rename, this version keeps one additional file, 'a/e'
1776+ git update-index --index-info <<EOF
1777+ 100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub/b
1778+EOF
1779+ make_resolve_tree ours A B
1780+
1781+ rm .git/index
1782+ # Now *ours* is the single file which replaces a tree.
1783+ git update-index --index-info <<EOF
1784+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a
1785+EOF
1786+ make_resolve_tree ours B A
1787+)
1788+
1789+(cd non-tree-to-tree-with-rename
1790+ # 'A' changes the single file 'a', while 'B' replaces it with a directory structure 'a'.
1791+ # The rename now sends this off-course, as it removes the previously 'protective' entry
1792+ # and thus makes all changes from 'B' succeed without us detecting any problem with that.
1793+ # Also, here we don't actually have irreconcilable tree-changes because of that.
1794+ rm .git/index
1795+ git update-index --index-info <<EOF
1796+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/d
1797+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/e
1798+100644 blob b414108e81e5091fe0974a1858b4d0d22b107f70 a/sub/b
1799+100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a/sub/c
1800+EOF
1801+ make_resolve_tree ancestor A B
1802+ make_resolve_tree ancestor B A
1803+
1804+ # Thanks to the rename, this time there isn't even an irreconcilable conflict.
1805+ # This is deactivated as the test-suite can't handle this (one) special case.
1806+ # The state really is the one from the ancestors, as there are no irreconcilable
1807+ # tree changes, it's all the same. But the test expects to see changes when 'ours'
1808+ # is chosen so we can't easily run this here.
1809+ # make_resolve_tree ours A B
1810+ # make_resolve_tree ours B A
1811+)
1812+
1813+(cd rename-within-rename
1814+ # 'A' and 'B' change all content in a mergable manner. 'A' renames 'a' to 'a-renamed',
1815+ # and 'B' renames 'a/sub' to 'a/sub-renamed'.
1816+ # Ideally, we get both together, but doing so added a lot of complexity so maybe give
1817+ # that another go and try to keep it simple.
1818+ # In ancestor mode, only those ancestors of conflicts are kept unchanged, so some renames
1819+ # go through.
1820+ rm .git/index
1821+ git update-index --index-info <<EOF
1822+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w
1823+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f
1824+100644 44065282f89b9bd6439ed2e4674721383fd987eb 0 a/sub/y.f
1825+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a/sub/z
1826+EOF
1827+ make_resolve_tree ancestor A B
1828+ make_resolve_tree ancestor B A
1829+
1830+ # *ours* is `a-renamed` everything, with merges.
1831+ rm .git/index
1832+ git update-index --index-info <<EOF
1833+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/sub/y.f
1834+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/sub/z
1835+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w
1836+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f
1837+EOF
1838+ make_resolve_tree ours A B
1839+
1840+ # Now ours is the renamed sub-directory, with merges. It can bring everything together even.
1841+ rm .git/index
1842+ git update-index --index-info <<EOF
1843+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/sub-renamed/y.f
1844+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/sub-renamed/z
1845+100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a-renamed/w
1846+100644 b414108e81e5091fe0974a1858b4d0d22b107f70 0 a-renamed/x.f
1847+EOF
1848+ make_resolve_tree ours B A
1849+)
gix-merge/tests/merge/blob/builtin_driver.rs+3−3
@@ -10,16 +10,16 @@ fn binary() { · 9 unmodified lines
1010 );
1111 assert_eq!(
1212 builtin_driver::binary(Some(ResolveWith::Ancestor)),
13− (Pick::Ancestor, Resolution::Complete),
13+ (Pick::Ancestor, Resolution::CompleteWithAutoResolvedConflict),
1414 "Otherwise we can pick anything and it will mark it as complete"
1515 );
1616 assert_eq!(
1717 builtin_driver::binary(Some(ResolveWith::Ours)),
18− (Pick::Ours, Resolution::Complete)
18+ (Pick::Ours, Resolution::CompleteWithAutoResolvedConflict)
1919 );
2020 assert_eq!(
2121 builtin_driver::binary(Some(ResolveWith::Theirs)),
22− (Pick::Theirs, Resolution::Complete)
22+ (Pick::Theirs, Resolution::CompleteWithAutoResolvedConflict)
2323 );
2424 }
2525
gix-merge/tests/merge/blob/platform.rs+51−2
@@ -59,12 +59,61 @@ mod merge { · 58 unmodified lines
5959 let res = platform_ref.merge(&mut buf, default_labels(), &Default::default())?;
6060 assert_eq!(
6161 res,
62− (Pick::Theirs, Resolution::Complete),
62+ (Pick::Theirs, Resolution::CompleteWithAutoResolvedConflict),
6363 "the auto-binary driver respects its own options"
6464 );
6565 Ok(())
6666 }
6767
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+
68117 #[test]
69118 fn builtin_with_conflict() -> crate::Result {
70119 let mut platform = new_platform(None, pipeline::Mode::ToGit);
@@ -192,7 +241,7 @@ theirs · 121 unmodified lines
192241 ] {
193242 platform_ref.options.resolve_binary_with = Some(resolve);
194243 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));
196245 assert_eq!(platform_ref.buffer_by_pick(res.0).unwrap().unwrap().as_bstr(), expected);
197246
198247 assert_eq!(
gix-merge/tests/merge/tree/baseline.rs+22−3
@@ -48,6 +48,7 @@ pub enum ConflictKind { · 47 unmodified lines
4848
4949 /// More loosely structured information about the `Conflict`.
5050 #[derive(Debug)]
51+#[allow(dead_code)] // used only for debugging
5152 pub struct ConflictInfo {
5253 /// All the paths involved in the informational message
5354 pub paths: Vec<String>,
@@ -149,9 +150,11 @@ impl Iterator for Expectations<'_> { · 95 unmodified lines
149150 else {
150151 unreachable!("invalid line: {line:?}")
151152 };
152− let deviation = (expected_custom_tree != "expected^{tree}").then(|| {
153− let expected_tree_id = gix_hash::ObjectId::from_hex(expected_custom_tree.as_bytes())
154− .expect("valid tree id in hex for the expected tree");
153+ let deviation = (!expected_custom_tree.starts_with("expected")).then(|| {
154+ let expected_tree_id =
155+ gix_hash::ObjectId::from_hex(expected_custom_tree.as_bytes()).unwrap_or_else(|err| {
156+ panic!("valid tree id in hex for the expected tree: '{expected_custom_tree}': {err} {line}")
157+ });
155158 let message = tokens.collect::<Vec<_>>().join(" ").trim().to_owned();
156159 Deviation {
157160 message,
@@ -371,6 +374,22 @@ pub fn show_diff_and_fail( · 213 unmodified lines
371374 );
372375 }
373376
377+pub fn show_diff_trees_and_fail(
378+ case_name: &str,
379+ actual_id: ObjectId,
380+ actual: &gix_merge::tree::Outcome<'_>,
381+ expected_tree_id: gix_hash::ObjectId,
382+ additional_information: &str,
383+ odb: &gix_odb::memory::Proxy<gix_odb::Handle>,
384+) {
385+ pretty_assertions::assert_str_eq!(
386+ visualize_tree(&actual_id, odb, None).to_string(),
387+ visualize_tree(&expected_tree_id, odb, None).to_string(),
388+ "{case_name}: merged tree mismatch\n{:#?}\n{additional_information}\n{case_name}",
389+ actual.conflicts,
390+ );
391+}
392+
374393 pub(crate) fn apply_git_index_entries(conflicts: &[Conflict], state: &mut gix_index::State) {
375394 let len = state.entries().len();
376395 for Conflict { ours, theirs, ancestor } in conflicts {
gix-merge/tests/merge/tree/mod.rs+92−38
@@ -1,7 +1,8 @@
11 use crate::tree::baseline::Deviation;
22 use gix_diff::Rewrites;
33 use gix_merge::commit::Options;
4−use gix_merge::tree::TreatAsUnresolved;
4+use gix_merge::tree::apply_index_entries::RemovalMode;
5+use gix_merge::tree::{treat_as_unresolved, TreatAsUnresolved};
56 use gix_object::Write;
67 use gix_worktree::stack::state::attributes;
78 use std::path::Path;
@@ -21,7 +22,8 @@ fn run_baseline() -> crate::Result { · 13 unmodified lines
2122 let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?;
2223 let cases = std::fs::read_to_string(root.join("baseline.cases"))?;
2324 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");
2527 let new_test = None;
2628 for baseline::Expectation {
2729 root,
@@ -63,12 +65,12 @@ fn run_baseline() -> crate::Result { · 35 unmodified lines
6365 &mut blob_merge,
6466 &odb,
6567 &mut |id| id.to_hex_with_len(7).to_string(),
66− options,
68+ options.clone(),
6769 )?
6870 .tree_merge;
6971
7072 let actual_id = actual.tree.write(|tree| odb.write(tree))?;
71− match deviation {
73+ match &deviation {
7274 None => {
7375 if actual_id != merge_info.merged_tree {
7476 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
7880 message,
7981 expected_tree_id,
8082 }) => {
83+ // TODO: why did this ever work?
8184 // Sometimes only the reversed part of a specific test is different.
82− if case_name != "same-rename-different-mode-A-B" && case_name != "same-rename-different-mode-A-B-diff3"
83− {
84− assert_ne!(
85− actual_id, merge_info.merged_tree,
86− "{case_name}: Git caught up - adjust expectation {message}"
87− );
88− } else {
89− assert_eq!(
90− actual_id, merge_info.merged_tree,
91− "{case_name}: Git should match here, it just doesn't match in one of two cases"
92− );
93− }
85+ // assert_eq!(
86+ // actual_id, merge_info.merged_tree,
87+ // "{case_name}: Git should match here, it just doesn't match in one of two cases"
88+ // );
9489 pretty_assertions::assert_str_eq!(
9590 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(),
9792 "{case_name}: tree mismatch: {message} \n{:#?}\n{case_name}",
9893 actual.conflicts
9994 );
@@ -102,10 +97,10 @@ fn run_baseline() -> crate::Result { · 2 unmodified lines
10297
10398 let mut actual_index = gix_index::State::from_tree(&actual_id, &odb, Default::default())?;
10499 let expected_index = {
105− let derivative_index_path = root.join(".git").join(format!("{case_name}.index"));
106− if derivative_index_path.exists() {
100+ let deviating_index_path = root.join(".git").join(format!("{case_name}.index"));
101+ if deviating_index_path.exists() {
107102 gix_index::File::at(
108− derivative_index_path,
103+ deviating_index_path,
109104 odb.store().object_hash(),
110105 true,
111106 Default::default(),
@@ -119,9 +114,9 @@ fn run_baseline() -> crate::Result { · 7 unmodified lines
119114 index
120115 }
121116 };
122− let conflicts_like_in_git = TreatAsUnresolved::Renames;
123− let did_change = actual.index_changed_after_applying_conflicts(&mut actual_index, conflicts_like_in_git);
124− actual_index.remove_entries(|_, _, e| e.flags.contains(gix_index::entry::Flags::REMOVE));
117+ let conflicts_like_in_git = TreatAsUnresolved::git();
118+ let did_change =
119+ actual.index_changed_after_applying_conflicts(&mut actual_index, conflicts_like_in_git, RemovalMode::Prune);
125120
126121 pretty_assertions::assert_eq!(
127122 baseline::clear_entries(&actual_index),
@@ -130,29 +125,89 @@ fn run_baseline() -> crate::Result { · 2 unmodified lines
130125 actual.conflicts,
131126 merge_info.conflicts
132127 );
133− // if case_name.starts_with("submodule-both-modify-A-B") {
134− if false {
135− assert!(
136− !did_change,
137− "{case_name}: We can't handle submodules, so there is no index change"
138− );
139− assert!(
140− actual.has_unresolved_conflicts(conflicts_like_in_git),
141− "{case_name}: submodules currently result in an unresolved (unknown) conflict"
142− );
143− } else {
128+ if deviation.is_none() {
144129 assert_eq!(
145130 did_change,
146131 actual.has_unresolved_conflicts(conflicts_like_in_git),
147132 "{case_name}: If there is any kind of conflict, the index should have been changed"
148133 );
149134 }
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+ }
150199 }
151200
152201 assert_eq!(
153− actual_cases, 107,
202+ actual_cases, 117,
154203 "BUG: update this number, and don't forget to remove a filter in the end"
155204 );
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+ );
156211
157212 Ok(())
158213 }
@@ -163,16 +218,17 @@ fn basic_merge_options() -> Options { · 4 unmodified lines
163218 use_first_merge_base: false,
164219 tree_merge: gix_merge::tree::Options {
165220 symlink_conflicts: None,
221+ tree_conflicts: None,
166222 rewrites: Some(Rewrites {
167223 copies: None,
168224 percentage: Some(0.5),
169225 limit: 0,
226+ track_empty: false,
170227 }),
171228 blob_merge: gix_merge::blob::platform::merge::Options::default(),
172229 blob_merge_command_ctx: Default::default(),
173230 fail_on_conflict: None,
174231 marker_size_multiplier: 0,
175− allow_lossy_resolution: false,
176232 },
177233 }
178234 }
@@ -224,6 +280,4 @@ fn new_blob_merge_platform( · 45 unmodified lines
224280 )
225281 }
226282
227−// TODO: make sure everything is read eventually, even if only to improve debug messages in case of failure.
228−#[allow(dead_code)]
229283 mod 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
8080 }),
8181 percentage: Some(0.3),
8282 limit: 0,
83+ track_empty: true,
8384 };
8485 let out = fixture_filtered_detailed(
8586 "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
2626 thiserror = "2.0.0"
2727 url = "2.5.2"
2828 bstr = { version = "1.3.0", default-features = false, features = ["std"] }
29+percent-encoding = "2.3.1"
2930
3031 document-features = { version = "0.2.0", optional = true }
3132
gix-url/src/impls.rs+1−1
@@ -81,7 +81,7 @@ impl std::fmt::Display for Url { · 80 unmodified lines
8181 let mut storage;
8282 let to_print = if self.password.is_some() {
8383 storage = self.clone();
84− storage.password = Some("<redacted>".into());
84+ storage.password = Some("redacted".into());
8585 &storage
8686 } else {
8787 self
gix-url/src/lib.rs+6−2
@@ -308,6 +308,10 @@ impl Url { · 307 unmodified lines
308308 }
309309 }
310310
311+fn percent_encode(s: &str) -> Cow<'_, str> {
312+ percent_encoding::utf8_percent_encode(s, percent_encoding::NON_ALPHANUMERIC).into()
313+}
314+
311315 /// Serialization
312316 impl Url {
313317 /// Write this URL losslessly to `out`, ready to be parsed again.
@@ -318,10 +322,10 @@ impl Url { · 4 unmodified lines
318322 }
319323 match (&self.user, &self.host) {
320324 (Some(user), Some(host)) => {
321− out.write_all(user.as_bytes())?;
325+ out.write_all(percent_encode(user).as_bytes())?;
322326 if let Some(password) = &self.password {
323327 out.write_all(b":")?;
324− out.write_all(password.as_bytes())?;
328+ out.write_all(percent_encode(password).as_bytes())?;
325329 }
326330 out.write_all(b"@")?;
327331 out.write_all(host.as_bytes())?;
gix-url/src/parse.rs+26−9
@@ -1,8 +1,8 @@
11 use std::convert::Infallible;
22
3−use bstr::{BStr, BString, ByteSlice};
4−
53 use crate::Scheme;
4+use bstr::{BStr, BString, ByteSlice};
5+use percent_encoding::percent_decode_str;
66
77 /// The error returned by [parse()](crate::parse()).
88 #[derive(Debug, thiserror::Error)]
@@ -114,14 +114,28 @@ pub(crate) fn url(input: &BStr, protocol_end: usize) -> Result<crate::Url, Error · 105 unmodified lines
114114 Ok(crate::Url {
115115 serialize_alternative_form: false,
116116 scheme,
117− user: url_user(&url),
118− password: url.password().map(Into::into),
117+ user: url_user(&url, UrlKind::Url)?,
118+ password: url
119+ .password()
120+ .map(|s| percent_decoded_utf8(s, UrlKind::Url))
121+ .transpose()?,
119122 host: url.host_str().map(Into::into),
120123 port: url.port(),
121124 path: url.path().into(),
122125 })
123126 }
124127
128+fn percent_decoded_utf8(s: &str, kind: UrlKind) -> Result<String, Error> {
129+ Ok(percent_decode_str(s)
130+ .decode_utf8()
131+ .map_err(|err| Error::Utf8 {
132+ url: s.into(),
133+ kind,
134+ source: err,
135+ })?
136+ .into_owned())
137+}
138+
125139 pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> {
126140 let input = input_to_utf8(input, UrlKind::Scp)?;
127141
@@ -150,19 +164,22 @@ pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> { · 22 unmodified lines
150164 Ok(crate::Url {
151165 serialize_alternative_form: true,
152166 scheme: url.scheme().into(),
153− user: url_user(&url),
154− password: url.password().map(Into::into),
167+ user: url_user(&url, UrlKind::Scp)?,
168+ password: url
169+ .password()
170+ .map(|s| percent_decoded_utf8(s, UrlKind::Scp))
171+ .transpose()?,
155172 host: url.host_str().map(Into::into),
156173 port: url.port(),
157174 path: path.into(),
158175 })
159176 }
160177
161−fn url_user(url: &url::Url) -> Option<String> {
178+fn url_user(url: &url::Url, kind: UrlKind) -> Result<Option<String>, Error> {
162179 if url.username().is_empty() && url.password().is_none() {
163− None
180+ Ok(None)
164181 } else {
165− Some(url.username().into())
182+ Ok(Some(percent_decoded_utf8(url.username(), kind)?))
166183 }
167184 }
168185
gix-url/tests/fixtures/fuzzed/illegal-utf8.url+1−0
@@ -0,0 +1 @@
1+BBi%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
174174 compare("/path/to/repo", "/path/to/repo", "same goes for simple paths");
175175 compare(
176176 "https://user:password@host/path",
177− "https://user:<redacted>@host/path",
178− "it visibly redacts passwords though",
177+ "https://user:redacted@host/path",
178+ "it visibly redacts passwords though, and it's still a valid URL",
179179 );
180180 }
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
164164 assert_eq!(actual.path, expected.path.unwrap_or_default());
165165 }
166166
167+#[allow(clippy::module_inception)]
167168 mod baseline {
168169 use bstr::{BStr, BString, ByteSlice};
169170 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
55 #[test]
66 fn fuzzed() {
77 for name in [
8+ "illegal-utf8",
89 "short-panic",
910 "very-long-abort2",
1011 "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
22 pub type Result = std::result::Result<(), Error>;
33
44 mod access;
5+mod baseline;
56 mod expand_path;
7+mod fuzzed;
68 mod 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
3939 )
4040 }
4141
42+#[test]
43+fn username_and_password_with_spaces_and_port() -> crate::Result {
44+ let expected = gix_url::Url::from_parts(
45+ Scheme::Http,
46+ Some("user name".into()),
47+ Some("password secret".into()),
48+ Some("example.com".into()),
49+ Some(8080),
50+ b"/~byron/hello".into(),
51+ false,
52+ )?;
53+ assert_url_roundtrip(
54+ "http://user%20name:password%20secret@example.com:8080/~byron/hello",
55+ expected.clone(),
56+ )?;
57+ assert_eq!(expected.user(), Some("user name"));
58+ assert_eq!(expected.password(), Some("password secret"));
59+ Ok(())
60+}
61+
4262 #[test]
4363 fn only_password() -> crate::Result {
4464 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
173173 /// A support type to allow configuring a `git describe` operation
174174 pub struct Platform<'repo> {
175175 pub(crate) id: gix_hash::ObjectId,
176− pub(crate) repo: &'repo crate::Repository,
176+ /// The owning repository.
177+ pub repo: &'repo crate::Repository,
177178 pub(crate) select: SelectRef,
178179 pub(crate) first_parent: bool,
179180 pub(crate) id_as_fallback: bool,
gix/src/config/cache/access.rs+1−1
@@ -156,7 +156,7 @@ impl Cache { · 155 unmodified lines
156156 }
157157
158158 #[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> {
160160 self.diff_renames
161161 .get_or_try_init(|| crate::diff::new_rewrites(&self.resolved, self.lenient_config))
162162 .copied()
gix/src/config/mod.rs+7−4
@@ -18,7 +18,8 @@ pub use tree::root::Tree; · 17 unmodified lines
1818 ///
1919 /// Note that these values won't update even if the underlying file(s) change.
2020 pub struct Snapshot<'repo> {
21− pub(crate) repo: &'repo Repository,
21+ /// The owning repository.
22+ pub repo: &'repo Repository,
2223 }
2324
2425 /// 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
3132 // TODO: make it possible to load snapshots with reloading via .config() and write mutated snapshots back to disk which should be the way
3233 // to affect all instances of a repo, probably via `config_mut()` and `config_mut_at()`.
3334 pub struct SnapshotMut<'repo> {
34− pub(crate) repo: Option<&'repo mut Repository>,
35+ /// The owning repository.
36+ pub repo: Option<&'repo mut Repository>,
3537 pub(crate) config: gix_config::File<'static>,
3638 }
3739
3840 /// A utility structure created by [`SnapshotMut::commit_auto_rollback()`] that restores the previous configuration on drop.
3941 pub struct CommitAutoRollback<'repo> {
40− pub(crate) repo: Option<&'repo mut Repository>,
42+ /// The owning repository.
43+ pub repo: Option<&'repo mut Repository>,
4144 pub(crate) prev_config: crate::Config,
4245 }
4346
@@ -602,7 +605,7 @@ pub(crate) struct Cache { · 558 unmodified lines
602605 pub(crate) url_rewrite: OnceCell<crate::remote::url::Rewrite>,
603606 /// The lazy-loaded rename information for diffs.
604607 #[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)>,
606609 /// A lazily loaded mapping to know which url schemes to allow
607610 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
608611 pub(crate) url_scheme: OnceCell<crate::remote::url::SchemePermission>,
gix/src/diff.rs+27−17
@@ -51,7 +51,14 @@ impl Options { · 50 unmodified lines
5151 pub(crate) fn from_configuration(config: &crate::config::Cache) -> Result<Self, options::init::Error> {
5252 Ok(Options {
5353 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+ },
5562 })
5663 }
5764 }
@@ -164,14 +171,14 @@ pub(crate) mod utils { · 106 unmodified lines
164171 }
165172
166173 /// 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.
168175 ///
169176 /// Note that missing values will be defaulted similar to what git does.
170177 #[allow(clippy::result_large_err)]
171178 pub fn new_rewrites(
172179 config: &gix_config::File<'static>,
173180 lenient: bool,
174− ) -> Result<Option<Rewrites>, new_rewrites::Error> {
181+ ) -> Result<(Option<Rewrites>, bool), new_rewrites::Error> {
175182 new_rewrites_inner(config, lenient, &Diff::RENAMES, &Diff::RENAME_LIMIT)
176183 }
177184
@@ -180,7 +187,7 @@ pub(crate) mod utils { · 2 unmodified lines
180187 lenient: bool,
181188 renames: &'static crate::config::tree::diff::Renames,
182189 rename_limit: &'static crate::config::tree::keys::UnsignedInteger,
183− ) -> Result<Option<Rewrites>, new_rewrites::Error> {
190+ ) -> Result<(Option<Rewrites>, bool), new_rewrites::Error> {
184191 let copies = match config
185192 .boolean(renames)
186193 .map(|value| renames.try_into_renames(value))
@@ -188,25 +195,28 @@ pub(crate) mod utils { · 1 unmodified line
188195 .with_leniency(lenient)?
189196 {
190197 Some(renames) => match renames {
191− Tracking::Disabled => return Ok(None),
198+ Tracking::Disabled => return Ok((None, true)),
192199 Tracking::Renames => None,
193200 Tracking::RenamesAndCopies => Some(Copies::default()),
194201 },
195− None => return Ok(None),
202+ None => return Ok((None, false)),
196203 };
197204
198205 let default = Rewrites::default();
199− Ok(Rewrites {
200− copies,
201− limit: config
202− .integer(rename_limit)
203− .map(|value| rename_limit.try_into_usize(value))
204− .transpose()
205− .with_leniency(lenient)?
206− .unwrap_or(default.limit),
207− ..default
208− }
209− .into())
206+ Ok((
207+ Rewrites {
208+ copies,
209+ limit: config
210+ .integer(rename_limit)
211+ .map(|value| rename_limit.try_into_usize(value))
212+ .transpose()
213+ .with_leniency(lenient)?
214+ .unwrap_or(default.limit),
215+ ..default
216+ }
217+ .into(),
218+ true,
219+ ))
210220 }
211221
212222 /// 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
107107 ///
108108 pub mod tree {
109109 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+ };
111114
112115 /// The outcome produced by [`Repository::merge_trees()`](crate::Repository::merge_trees()).
113116 #[derive(Clone)]
@@ -136,22 +139,19 @@ pub mod tree { · 22 unmodified lines
136139
137140 /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a
138141 /// 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`.
142142 ///
143− /// ### Important
143+ /// `removal_mode` decides how unconflicted entries should be removed if they are superseded by
144+ /// their conflicted counterparts.
144145 ///
145− /// Also, the unconflicted stage of such entries will be removed merely by setting a flag, so the
146− /// in-memory entry is still present.
147− /// One can prune `index` [in-memory](gix_index::State::remove_entries()) or write it to disk, which will
148− /// cause entries marked for removal not to be persisted.
146+ /// It's important that `index` is at the state of [`Self::tree`].
147+ /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`.
149148 pub fn index_changed_after_applying_conflicts(
150149 &self,
151150 index: &mut gix_index::State,
152151 how: TreatAsUnresolved,
152+ removal_mode: apply_index_entries::RemovalMode,
153153 ) -> bool {
154− gix_merge::tree::apply_index_entries(&self.conflicts, how, index)
154+ apply_index_entries(&self.conflicts, how, index, removal_mode)
155155 }
156156 }
157157
@@ -160,6 +160,7 @@ pub mod tree { · 2 unmodified lines
160160 pub struct Options {
161161 inner: gix_merge::tree::Options,
162162 file_favor: Option<FileFavor>,
163+ tree_favor: Option<TreeFavor>,
163164 }
164165
165166 impl From<gix_merge::tree::Options> for Options {
@@ -167,6 +168,7 @@ pub mod tree { · 1 unmodified line
167168 Options {
168169 inner: opts,
169170 file_favor: None,
171+ tree_favor: None,
170172 }
171173 }
172174 }
@@ -190,6 +192,7 @@ pub mod tree { · 17 unmodified lines
190192 opts.blob_merge.resolve_binary_with = Some(resolve_binary);
191193 opts.blob_merge.text.conflict = resolve_text;
192194 }
195+ opts.tree_conflicts = value.tree_favor.map(Into::into);
193196 opts
194197 }
195198 }
@@ -202,7 +205,7 @@ pub mod tree { · 6 unmodified lines
202205 /// * binary files
203206 /// * symlinks (a form of file after all)
204207 ///
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.
206209 #[derive(Debug, Copy, Clone)]
207210 pub enum FileFavor {
208211 /// Choose *our* side in case of a conflict.
@@ -215,6 +218,35 @@ pub mod tree { · 6 unmodified lines
215218 Theirs,
216219 }
217220
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+
218250 /// Builder
219251 impl Options {
220252 /// 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
233265
234266 /// When `None`, the default, both sides will be treated equally, and in case of conflict an unbiased representation
235267 /// 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.
237271 pub fn with_file_favor(mut self, file_favor: Option<FileFavor>) -> Self {
238272 self.file_favor = file_favor;
239273 self
240274 }
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+ }
241285 }
242286 }
gix/src/object/tree/iter.rs+2−2
@@ -5,8 +5,8 @@ use crate::Repository; · 4 unmodified lines
55 pub struct EntryRef<'repo, 'a> {
66 /// The actual entry ref we are wrapping.
77 pub inner: gix_object::tree::EntryRef<'a>,
8−
9− pub(crate) repo: &'repo Repository,
8+ /// The owning repository.
9+ pub repo: &'repo Repository,
1010 }
1111
1212 impl<'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
1010 pub struct Editor<'repo> {
1111 pub(crate) inner: gix_object::tree::Editor<'repo>,
1212 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,
1415 }
1516
1617 /// Initialization
@@ -197,7 +198,8 @@ impl std::fmt::Debug for Tree<'_> { · 180 unmodified lines
197198 #[derive(PartialEq, Debug, Clone)]
198199 pub struct Entry<'repo> {
199200 pub(crate) inner: gix_object::tree::Entry,
200− pub(crate) repo: &'repo crate::Repository,
201+ /// The owning repository.
202+ pub repo: &'repo crate::Repository,
201203 }
202204
203205 mod entry {
gix/src/reference/iter.rs+2−1
@@ -8,7 +8,8 @@ use gix_ref::file::ReferenceExt; · 7 unmodified lines
88 #[must_use = "Iterators should be obtained from this iterator platform"]
99 pub struct Platform<'r> {
1010 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,
1213 }
1314
1415 /// An iterator over references, with or without filter.
gix/src/repository/merge.rs+15−15
@@ -88,27 +88,27 @@ impl Repository { · 87 unmodified lines
8888
8989 /// Read all relevant configuration options to instantiate options for use in [`merge_trees()`](Self::merge_trees).
9090 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+ }
91104 Ok(gix_merge::tree::Options {
92− rewrites: Some(
93− crate::diff::utils::new_rewrites_inner(
94− &self.config.resolved,
95− self.config.lenient_config,
96− &tree::Merge::RENAMES,
97− &tree::Merge::RENAME_LIMIT,
98− )?
99− .map(Ok)
100− .or_else(|| {
101− crate::diff::utils::new_rewrites(&self.config.resolved, self.config.lenient_config).transpose()
102− })
103− .transpose()?
104− .unwrap_or_default(),
105− ),
105+ rewrites,
106106 blob_merge: self.blob_merge_options()?,
107107 blob_merge_command_ctx: self.command_context()?,
108108 fail_on_conflict: None,
109109 marker_size_multiplier: 0,
110110 symlink_conflicts: None,
111− allow_lossy_resolution: false,
111+ tree_conflicts: None,
112112 }
113113 .into())
114114 }
gix/src/revision/mod.rs+2−1
@@ -28,5 +28,6 @@ pub struct Spec<'repo> { · 27 unmodified lines
2828 pub(crate) first_ref: Option<gix_ref::Reference>,
2929 /// The second name of a reference as seen while parsing a `RevSpec`, for completeness.
3030 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,
3233 }
gix/src/revision/walk.rs+4−2
@@ -152,7 +152,8 @@ impl<'repo> Info<'repo> { · 151 unmodified lines
152152 /// author or commit messages will be required of *all* commits traversed here, it should be better to avoid trying to load it
153153 /// by [turning commit-graph support off][Platform::use_commit_graph()]. This certainly is a micro-optimization though.
154154 pub struct Platform<'repo> {
155− pub(crate) repo: &'repo Repository,
155+ /// The owning repository.
156+ pub repo: &'repo Repository,
156157 pub(crate) tips: Vec<ObjectId>,
157158 pub(crate) prune: Vec<ObjectId>,
158159 pub(crate) sorting: Sorting,
@@ -334,7 +335,8 @@ pub mod iter { · 175 unmodified lines
334335 pub(crate) mod iter_impl {
335336 /// The iterator returned by [`crate::revision::walk::Platform::all()`].
336337 pub struct Walk<'repo> {
337− pub(crate) repo: &'repo crate::Repository,
338+ /// The owning repository.
339+ pub repo: &'repo crate::Repository,
338340 pub(crate) inner: Box<dyn Iterator<Item = Result<gix_traverse::commit::Info, super::iter::Error>> + 'repo>,
339341 }
340342
gix/src/submodule/mod.rs+1−1
@@ -23,7 +23,7 @@ pub use errors::*; · 22 unmodified lines
2323
2424 /// A platform maintaining state needed to interact with submodules, created by [`Repository::submodules()].
2525 pub(crate) struct SharedState<'repo> {
26− pub(crate) repo: &'repo Repository,
26+ pub repo: &'repo Repository,
2727 pub(crate) modules: ModulesSnapshot,
2828 is_active: RefCell<Option<IsActiveState>>,
2929 index: RefCell<Option<IndexPersistedOrInMemory>>,
gix/src/types.rs+22−11
@@ -19,7 +19,8 @@ pub struct Worktree<'repo> { · 18 unmodified lines
1919 pub struct Head<'repo> {
2020 /// One of various possible states for the HEAD reference
2121 pub kind: head::Kind,
22− pub(crate) repo: &'repo Repository,
22+ /// The owning repository.
23+ pub repo: &'repo Repository,
2324 }
2425
2526 /// An [`ObjectId`] with access to a repository.
@@ -27,7 +28,8 @@ pub struct Head<'repo> { · 1 unmodified line
2728 pub struct Id<'r> {
2829 /// The actual object id
2930 pub(crate) inner: ObjectId,
30− pub(crate) repo: &'r Repository,
31+ /// The owning repository.
32+ pub repo: &'r Repository,
3133 }
3234
3335 /// A decoded object with a reference to its owning repository.
@@ -39,7 +41,8 @@ pub struct Object<'repo> { · 5 unmodified lines
3941 pub kind: gix_object::Kind,
4042 /// The fully decoded object data
4143 pub data: Vec<u8>,
42− pub(crate) repo: &'repo Repository,
44+ /// The owning repository.
45+ pub repo: &'repo Repository,
4346 }
4447
4548 impl Drop for Object<'_> {
@@ -55,7 +58,8 @@ pub struct Blob<'repo> { · 9 unmodified lines
5558 pub id: ObjectId,
5659 /// The blob's data.
5760 pub data: Vec<u8>,
58− pub(crate) repo: &'repo Repository,
61+ /// The owning repository.
62+ pub repo: &'repo Repository,
5963 }
6064
6165 impl Drop for Blob<'_> {
@@ -71,7 +75,8 @@ pub struct Tree<'repo> { · 9 unmodified lines
7175 pub id: ObjectId,
7276 /// The fully decoded tree data
7377 pub data: Vec<u8>,
74− pub(crate) repo: &'repo Repository,
78+ /// The owning repository.
79+ pub repo: &'repo Repository,
7580 }
7681
7782 impl Drop for Tree<'_> {
@@ -87,7 +92,8 @@ pub struct Tag<'repo> { · 9 unmodified lines
8792 pub id: ObjectId,
8893 /// The fully decoded tag data
8994 pub data: Vec<u8>,
90− pub(crate) repo: &'repo Repository,
95+ /// The owning repository.
96+ pub repo: &'repo Repository,
9197 }
9298
9399 impl Drop for Tag<'_> {
@@ -103,7 +109,8 @@ pub struct Commit<'repo> { · 9 unmodified lines
103109 pub id: ObjectId,
104110 /// The fully decoded commit data
105111 pub data: Vec<u8>,
106− pub(crate) repo: &'repo Repository,
112+ /// The owning repository.
113+ pub repo: &'repo Repository,
107114 }
108115
109116 impl Drop for Commit<'_> {
@@ -132,7 +139,8 @@ pub struct ObjectDetached { · 22 unmodified lines
132139 pub struct Reference<'r> {
133140 /// The actual reference data
134141 pub inner: gix_ref::Reference,
135− pub(crate) repo: &'r Repository,
142+ /// The owning repository.
143+ pub repo: &'r Repository,
136144 }
137145
138146 /// A thread-local handle to interact with a repository from a single thread.
@@ -219,7 +227,8 @@ pub struct Remote<'repo> { · 80 unmodified lines
219227 // pub(crate) prune: bool,
220228 // /// Delete tags that don't exist on the remote anymore, equivalent to pruning the refspec `refs/tags/*:refs/tags/*`.
221229 // pub(crate) prune_tags: bool,
222− pub(crate) repo: &'repo Repository,
230+ /// The owning repository.
231+ pub repo: &'repo Repository,
223232 }
224233
225234 /// A utility to make matching against pathspecs simple.
@@ -231,7 +240,8 @@ pub struct Remote<'repo> { · 5 unmodified lines
231240 #[derive(Clone)]
232241 #[cfg(feature = "attributes")]
233242 pub struct Pathspec<'repo> {
234− pub(crate) repo: &'repo Repository,
243+ /// The owning repository.
244+ pub repo: &'repo Repository,
235245 /// The cache to power attribute access. It's only initialized if we have a pattern with attributes.
236246 pub(crate) stack: Option<gix_worktree::Stack>,
237247 /// The prepared search to use for checking matches.
@@ -261,6 +271,7 @@ pub struct Submodule<'repo> { · 23 unmodified lines
261271 /// A utility to access `.gitattributes` and `.gitignore` information efficiently.
262272 #[cfg(any(feature = "attributes", feature = "excludes"))]
263273 pub struct AttributeStack<'repo> {
264− pub(crate) repo: &'repo Repository,
274+ /// The owning repository.
275+ pub repo: &'repo Repository,
265276 pub(crate) inner: gix_worktree::Stack,
266277 }
gix/tests/gix/object/tree/diff.rs+1−0
@@ -165,6 +165,7 @@ mod track_rewrites { · 164 unmodified lines
165165 }),
166166 limit: 1000,
167167 percentage: Some(0.5),
168+ track_empty: false,
168169 };
169170 let out = from
170171 .changes()?
src/plumbing/main.rs+16−6
@@ -172,9 +172,13 @@ pub fn main() -> Result<()> { · 171 unmodified lines
172172 },
173173 ),
174174 merge::SubCommands::Tree {
175− in_memory,
176− file_favor,
177− debug,
175+ opts:
176+ merge::SharedOptions {
177+ in_memory,
178+ file_favor,
179+ tree_favor,
180+ debug,
181+ },
178182 ours,
179183 base,
180184 theirs,
@@ -197,15 +201,20 @@ pub fn main() -> Result<()> { · 16 unmodified lines
197201 format,
198202 file_favor: file_favor.map(Into::into),
199203 in_memory,
204+ tree_favor: tree_favor.map(Into::into),
200205 debug,
201206 },
202207 )
203208 },
204209 ),
205210 merge::SubCommands::Commit {
206− in_memory,
207− file_favor,
208− debug,
211+ opts:
212+ merge::SharedOptions {
213+ in_memory,
214+ file_favor,
215+ tree_favor,
216+ debug,
217+ },
209218 ours,
210219 theirs,
211220 } => prepare_and_run(
@@ -225,6 +234,7 @@ pub fn main() -> Result<()> { · 13 unmodified lines
225234 core::repository::merge::tree::Options {
226235 format,
227236 file_favor: file_favor.map(Into::into),
237+ tree_favor: tree_favor.map(Into::into),
228238 in_memory,
229239 debug,
230240 },
src/plumbing/options/mod.rs+39−22
@@ -375,6 +375,14 @@ pub mod merge { · 374 unmodified lines
375375 Theirs,
376376 }
377377
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+
378386 impl From<FileFavor> for gix::merge::tree::FileFavor {
379387 fn from(value: FileFavor) -> Self {
380388 match value {
@@ -384,6 +392,33 @@ pub mod merge { · 3 unmodified lines
384392 }
385393 }
386394
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+
387422 #[derive(Debug, clap::Parser)]
388423 #[command(about = "perform merges of various kinds")]
389424 pub struct Platform {
@@ -412,17 +447,8 @@ pub mod merge { · 22 unmodified lines
412447
413448 /// Merge a tree by specifying ours, base and theirs, writing it to the object database.
414449 Tree {
415− /// Keep all objects to be written in memory to avoid any disk IO.
416− ///
417− /// Note that the resulting tree won't be available or inspectable.
418− #[clap(long, short = 'm')]
419− in_memory: bool,
420− /// Decide how to resolve content conflicts in files. If unset, write conflict markers and fail.
421− #[clap(long, short = 'f')]
422− file_favor: Option<FileFavor>,
423− /// Print additional information about conflicts for debugging.
424− #[clap(long, short = 'd')]
425− debug: bool,
450+ #[clap(flatten)]
451+ opts: SharedOptions,
426452
427453 /// A revspec to our treeish.
428454 #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]
@@ -436,17 +462,8 @@ pub mod merge { · 7 unmodified lines
436462 },
437463 /// Merge a commits by specifying ours, and theirs, writing the tree to the object database.
438464 Commit {
439− /// Keep all objects to be written in memory to avoid any disk IO.
440− ///
441− /// Note that the resulting tree won't be available or inspectable.
442− #[clap(long, short = 'm')]
443− in_memory: bool,
444− /// Decide how to resolve content conflicts in files. If unset, write conflict markers and fail.
445− #[clap(long, short = 'f')]
446− file_favor: Option<FileFavor>,
447− /// Print additional information about conflicts for debugging.
448− #[clap(long, short = 'd')]
449− debug: bool,
465+ #[clap(flatten)]
466+ opts: SharedOptions,
450467
451468 /// A revspec to our committish.
452469 #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]
src/plumbing/progress.rs+4−0
@@ -62,6 +62,10 @@ struct Record { · 61 unmodified lines
6262 }
6363
6464 static GIT_CONFIG: &[Record] = &[
65+ Record {
66+ config: "attr.tree",
67+ usage: Planned("Decides where to read git attributes from - this will affect APIs")
68+ },
6569 Record {
6670 config: "core.symlinks",
6771 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
521521 |d| (true, d.to_owned()),
522522 );
523523
524− let _marker = gix_lock::Marker::acquire_to_hold_resource(
525− script_basename,
526− gix_lock::acquire::Fail::AfterDurationWithBackoff(Duration::from_secs(6 * 60)),
527− None,
528− )?;
524+ // We may that destination_dir is already unique (i.e. temp-dir) - thus there is no need for a lock,
525+ // and we can execute scripts in parallel.
526+ let _marker = destination_dir
527+ .is_none()
528+ .then(|| {
529+ gix_lock::Marker::acquire_to_hold_resource(
530+ script_basename,
531+ gix_lock::acquire::Fail::AfterDurationWithBackoff(Duration::from_secs(6 * 60)),
532+ None,
533+ )
534+ })
535+ .transpose()?;
529536 let failure_marker = script_result_directory.join("_invalid_state_due_to_script_failure_");
530537 if force_run || !script_result_directory.is_dir() || failure_marker.is_file() {
531538 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