diff --git a/Cargo.lock b/Cargo.lock index dcfc0729ce5..21a56b4c908 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2114,6 +2114,7 @@ dependencies = [ "gix-filter", "gix-fs 0.12.0", "gix-hash 0.15.0", + "gix-index 0.36.0", "gix-object 0.45.0", "gix-odb", "gix-path 0.10.12", diff --git a/crate-status.md b/crate-status.md index b0316d7bf5c..9027fb8240d 100644 --- a/crate-status.md +++ b/crate-status.md @@ -318,6 +318,8 @@ Check out the [performance discussion][gix-diff-performance] as well. * [x] find blobs by similarity check * [ ] heuristics to find best candidate * [ ] find by basename to support similarity check + - Not having it can lead to issues when files with the same or similar content are part of a move + as files can be lost that way. * [x] directory tracking - [x] by identity - [ ] by similarity @@ -349,9 +351,9 @@ Check out the [performance discussion][gix-diff-performance] as well. - [ ] various newlines-related options during the merge (see https://git-scm.com/docs/git-merge#Documentation/git-merge.txt-ignore-space-change). - [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same) * [x] **tree**-diff-heuristics match Git for its test-cases - - [ ] a way to generate an index with stages - - *currently the data it provides won't generate index entries, and possibly can't be used for it yet* + - [x] a way to generate an index with stages, mostly conforming with Git. - [ ] submodule merges (*right now they count as conflicts if they differ*) + - [ ] assure sparse indices are handled correctly during application - right now we refuse. * [x] **commits** - with handling of multiple merge bases by recursive merge-base merge * [x] API documentation * [ ] Examples @@ -462,6 +464,7 @@ Check out the [performance discussion][gix-traverse-performance] as well. * [x] delegate can support for all fetch features, including shallow, deepen, etc. * [x] receive parsed shallow refs * [ ] push +* [ ] remote helper protocol and integration * [x] API documentation * [ ] Some examples diff --git a/gitoxide-core/src/repository/merge/commit.rs b/gitoxide-core/src/repository/merge/commit.rs index a50876ac3f4..e95d4803a3c 100644 --- a/gitoxide-core/src/repository/merge/commit.rs +++ b/gitoxide-core/src/repository/merge/commit.rs @@ -2,7 +2,7 @@ use crate::OutputFormat; use anyhow::{anyhow, bail, Context}; use gix::bstr::BString; use gix::bstr::ByteSlice; -use gix::merge::tree::UnresolvedConflict; +use gix::merge::tree::TreatAsUnresolved; use gix::prelude::Write; use super::tree::Options; @@ -18,6 +18,7 @@ pub fn commit( format, file_favor, in_memory, + debug, }: Options, ) -> anyhow::Result<()> { if format != OutputFormat::Human { @@ -48,7 +49,7 @@ pub fn commit( .merge_commits(ours_id, theirs_id, labels, options.into())? .tree_merge; let has_conflicts = res.conflicts.is_empty(); - let has_unresolved_conflicts = res.has_unresolved_conflicts(UnresolvedConflict::Renames); + let has_unresolved_conflicts = res.has_unresolved_conflicts(TreatAsUnresolved::Renames); { let _span = gix::trace::detail!("Writing merged tree"); let mut written = 0; @@ -63,6 +64,9 @@ pub fn commit( writeln!(out, "{tree_id} (wrote {written} trees)")?; } + if debug { + writeln!(err, "{:#?}", &res.conflicts)?; + } if !has_conflicts { writeln!(err, "{} possibly resolved conflicts", res.conflicts.len())?; } diff --git a/gitoxide-core/src/repository/merge/tree.rs b/gitoxide-core/src/repository/merge/tree.rs index edc7820819f..924b581048b 100644 --- a/gitoxide-core/src/repository/merge/tree.rs +++ b/gitoxide-core/src/repository/merge/tree.rs @@ -4,6 +4,7 @@ pub struct Options { pub format: OutputFormat, pub file_favor: Option, pub in_memory: bool, + pub debug: bool, } pub(super) mod function { @@ -12,7 +13,7 @@ pub(super) mod function { use anyhow::{anyhow, bail, Context}; use gix::bstr::BString; use gix::bstr::ByteSlice; - use gix::merge::tree::UnresolvedConflict; + use gix::merge::tree::TreatAsUnresolved; use gix::prelude::Write; use super::Options; @@ -29,6 +30,7 @@ pub(super) mod function { format, file_favor, in_memory, + debug, }: Options, ) -> anyhow::Result<()> { if format != OutputFormat::Human { @@ -62,7 +64,7 @@ pub(super) mod function { }; let res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?; let has_conflicts = res.conflicts.is_empty(); - let has_unresolved_conflicts = res.has_unresolved_conflicts(UnresolvedConflict::Renames); + let has_unresolved_conflicts = res.has_unresolved_conflicts(TreatAsUnresolved::Renames); { let _span = gix::trace::detail!("Writing merged tree"); let mut written = 0; @@ -77,6 +79,9 @@ pub(super) mod function { writeln!(out, "{tree_id} (wrote {written} trees)")?; } + if debug { + writeln!(err, "{:#?}", &res.conflicts)?; + } if !has_conflicts { writeln!(err, "{} possibly resolved conflicts", res.conflicts.len())?; } diff --git a/gix-diff/src/tree/function.rs b/gix-diff/src/tree/function.rs index 71b5224edd1..8dfc4dfb079 100644 --- a/gix-diff/src/tree/function.rs +++ b/gix-diff/src/tree/function.rs @@ -377,7 +377,7 @@ fn handle_lhs_and_rhs_with_equal_filenames( (false, false) => { delegate.push_path_component(lhs.filename); debug_assert!(lhs.mode.is_no_tree() && lhs.mode.is_no_tree()); - if lhs.oid != rhs.oid + if (lhs.oid != rhs.oid || lhs.mode != rhs.mode) && delegate .visit(Change::Modification { previous_entry_mode: lhs.mode, diff --git a/gix-diff/src/tree_with_rewrites/change.rs b/gix-diff/src/tree_with_rewrites/change.rs index 508c1137519..76749090126 100644 --- a/gix-diff/src/tree_with_rewrites/change.rs +++ b/gix-diff/src/tree_with_rewrites/change.rs @@ -434,6 +434,34 @@ impl<'a> ChangeRef<'a> { } } + /// Return the current mode of this instance, along with its object id. + pub fn entry_mode_and_id(&self) -> (gix_object::tree::EntryMode, &gix_hash::oid) { + match self { + ChangeRef::Addition { entry_mode, id, .. } + | ChangeRef::Deletion { entry_mode, id, .. } + | ChangeRef::Modification { entry_mode, id, .. } + | ChangeRef::Rewrite { entry_mode, id, .. } => (*entry_mode, id), + } + } + + /// Return the *previous* mode and id of the resource where possible, i.e. the source of a rename or copy, or a modification. + pub fn source_entry_mode_and_id(&self) -> (gix_object::tree::EntryMode, &gix_hash::oid) { + match self { + ChangeRef::Addition { entry_mode, id, .. } + | ChangeRef::Deletion { entry_mode, id, .. } + | ChangeRef::Modification { + previous_entry_mode: entry_mode, + previous_id: id, + .. + } + | ChangeRef::Rewrite { + source_entry_mode: entry_mode, + source_id: id, + .. + } => (*entry_mode, id), + } + } + /// Return the *current* location of the resource, i.e. the destination of a rename or copy, or the /// location at which an addition, deletion or modification took place. pub fn location(&self) -> &'a BStr { @@ -478,6 +506,34 @@ impl Change { } } + /// Return the current mode of this instance, along with its object id. + pub fn entry_mode_and_id(&self) -> (gix_object::tree::EntryMode, &gix_hash::oid) { + match self { + Change::Addition { entry_mode, id, .. } + | Change::Deletion { entry_mode, id, .. } + | Change::Modification { entry_mode, id, .. } + | Change::Rewrite { entry_mode, id, .. } => (*entry_mode, id), + } + } + + /// Return the *previous* mode and id of the resource where possible, i.e. the source of a rename or copy, or a modification. + pub fn source_entry_mode_and_id(&self) -> (gix_object::tree::EntryMode, &gix_hash::oid) { + match self { + Change::Addition { entry_mode, id, .. } + | Change::Deletion { entry_mode, id, .. } + | Change::Modification { + previous_entry_mode: entry_mode, + previous_id: id, + .. + } + | Change::Rewrite { + source_entry_mode: entry_mode, + source_id: id, + .. + } => (*entry_mode, id), + } + } + /// Return the *current* location of the resource, i.e. the destination of a rename or copy, or the /// location at which an addition, deletion or modification took place. pub fn location(&self) -> &BStr { diff --git a/gix-diff/tests/diff/tree_with_rewrites.rs b/gix-diff/tests/diff/tree_with_rewrites.rs index 5f30c26b8fd..a67e48862f4 100644 --- a/gix-diff/tests/diff/tree_with_rewrites.rs +++ b/gix-diff/tests/diff/tree_with_rewrites.rs @@ -14,25 +14,19 @@ fn empty_to_new_tree_without_rename_tracking() -> crate::Result { Addition { location: "a", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { location: "b", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { location: "d", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { @@ -42,9 +36,7 @@ fn empty_to_new_tree_without_rename_tracking() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(587ff082e0b98914788500eae5dd6a33f04883c9), }, Addition { @@ -54,9 +46,7 @@ fn empty_to_new_tree_without_rename_tracking() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, ] @@ -76,25 +66,19 @@ fn empty_to_new_tree_without_rename_tracking() -> crate::Result { Addition { location: "a", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { location: "b", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { location: "d", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { @@ -104,9 +88,7 @@ fn empty_to_new_tree_without_rename_tracking() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(587ff082e0b98914788500eae5dd6a33f04883c9), }, Addition { @@ -116,9 +98,7 @@ fn empty_to_new_tree_without_rename_tracking() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, ] @@ -153,35 +133,23 @@ fn changes_against_modified_tree_with_filename_tracking() -> crate::Result { [ Modification { location: "a", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(78981922613b2afb6025042ff6bd878ac1994e85), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(b4f17b61de71d9b2e54ac9e62b1629ae2d97a6a7), }, Modification { location: "dir", - previous_entry_mode: EntryMode( - 16384, - ), + previous_entry_mode: EntryMode(0o40000), previous_id: Sha1(e5c63aefe4327cb1c780c71966b678ce8e4225da), - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(c7ac5f82f536976f3561c9999b5f11e5893358be), }, Modification { location: "dir/c", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(6695780ceb14b05e076a99bbd2babf34723b3464), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), }, ] @@ -198,35 +166,23 @@ fn changes_against_modified_tree_with_filename_tracking() -> crate::Result { [ Modification { location: "a", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(78981922613b2afb6025042ff6bd878ac1994e85), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(b4f17b61de71d9b2e54ac9e62b1629ae2d97a6a7), }, Modification { location: "dir", - previous_entry_mode: EntryMode( - 16384, - ), + previous_entry_mode: EntryMode(0o40000), previous_id: Sha1(e5c63aefe4327cb1c780c71966b678ce8e4225da), - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(c7ac5f82f536976f3561c9999b5f11e5893358be), }, Modification { location: "c", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(6695780ceb14b05e076a99bbd2babf34723b3464), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), }, ] @@ -340,40 +296,28 @@ fn rename_by_similarity() -> crate::Result { [ Modification { location: "b", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(61780798228d17af2d34fce4cfbdf35556832472), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(54781fa52cf133fa9d0bf59cfe2ef2621b5ad29f), }, Modification { location: "dir", - previous_entry_mode: EntryMode( - 16384, - ), + previous_entry_mode: EntryMode(0o40000), previous_id: Sha1(d1622e275dbb2cb3215a0bdcd2fc77273891f360), - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(6602e61ea053525e4907e155c0b3da3a269e1385), }, Deletion { location: "dir/c", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), }, Addition { location: "dir/c-moved", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(f01e8ddf5adc56985b9a1cda6d7c7ef9e3abe034), }, ] @@ -404,31 +348,21 @@ fn rename_by_similarity() -> crate::Result { [ Modification { location: "b", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(61780798228d17af2d34fce4cfbdf35556832472), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(54781fa52cf133fa9d0bf59cfe2ef2621b5ad29f), }, Modification { location: "dir", - previous_entry_mode: EntryMode( - 16384, - ), + previous_entry_mode: EntryMode(0o40000), previous_id: Sha1(d1622e275dbb2cb3215a0bdcd2fc77273891f360), - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(6602e61ea053525e4907e155c0b3da3a269e1385), }, Rewrite { source_location: "dir/c", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), diff: Some( @@ -440,9 +374,7 @@ fn rename_by_similarity() -> crate::Result { similarity: 0.65, }, ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(f01e8ddf5adc56985b9a1cda6d7c7ef9e3abe034), location: "dir/c-moved", relation: None, @@ -508,26 +440,18 @@ fn copies_by_identity() -> crate::Result { [ Modification { location: "dir", - previous_entry_mode: EntryMode( - 16384, - ), + previous_entry_mode: EntryMode(0o40000), previous_id: Sha1(6602e61ea053525e4907e155c0b3da3a269e1385), - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(f01fd5b4d733a4ae749cbb58a828cdb3f342f298), }, Rewrite { source_location: "base", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(f00c965d8307308469e537302baa73048488f162), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(f00c965d8307308469e537302baa73048488f162), location: "c1", relation: None, @@ -535,15 +459,11 @@ fn copies_by_identity() -> crate::Result { }, Rewrite { source_location: "base", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(f00c965d8307308469e537302baa73048488f162), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(f00c965d8307308469e537302baa73048488f162), location: "c2", relation: None, @@ -551,15 +471,11 @@ fn copies_by_identity() -> crate::Result { }, Rewrite { source_location: "base", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(f00c965d8307308469e537302baa73048488f162), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(f00c965d8307308469e537302baa73048488f162), location: "dir/c3", relation: None, @@ -592,26 +508,18 @@ fn copies_by_similarity() -> crate::Result { [ Modification { location: "dir", - previous_entry_mode: EntryMode( - 16384, - ), + previous_entry_mode: EntryMode(0o40000), previous_id: Sha1(f01fd5b4d733a4ae749cbb58a828cdb3f342f298), - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(1d7e20e07562a54af0408fd2669b0c56a6faa6f0), }, Rewrite { source_location: "base", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), location: "c4", relation: None, @@ -619,9 +527,7 @@ fn copies_by_similarity() -> crate::Result { }, Rewrite { source_location: "base", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), diff: Some( @@ -633,9 +539,7 @@ fn copies_by_similarity() -> crate::Result { similarity: 0.8888889, }, ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(08fe19ca4d2f79624f35333157d610811efc1aed), location: "c5", relation: None, @@ -643,9 +547,7 @@ fn copies_by_similarity() -> crate::Result { }, Rewrite { source_location: "base", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), diff: Some( @@ -657,9 +559,7 @@ fn copies_by_similarity() -> crate::Result { similarity: 0.8888889, }, ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), location: "dir/c6", relation: None, @@ -729,15 +629,11 @@ fn copies_in_entire_tree_by_similarity() -> crate::Result { [ Rewrite { source_location: "base", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), location: "c6", relation: None, @@ -745,15 +641,11 @@ fn copies_in_entire_tree_by_similarity() -> crate::Result { }, Rewrite { source_location: "dir/c6", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), location: "c7", relation: None, @@ -761,9 +653,7 @@ fn copies_in_entire_tree_by_similarity() -> crate::Result { }, Rewrite { source_location: "c5", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(08fe19ca4d2f79624f35333157d610811efc1aed), diff: Some( @@ -775,9 +665,7 @@ fn copies_in_entire_tree_by_similarity() -> crate::Result { similarity: 0.75, }, ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(97b3d1a5707f8a11fa5fa8bc6c3bd7b3965601fd), location: "newly-added", relation: None, @@ -785,13 +673,9 @@ fn copies_in_entire_tree_by_similarity() -> crate::Result { }, Modification { location: "b", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(54781fa52cf133fa9d0bf59cfe2ef2621b5ad29f), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(f198d0640214092732566fb00543163845c8252c), }, ] @@ -828,15 +712,11 @@ fn copies_in_entire_tree_by_similarity_with_limit() -> crate::Result { [ Rewrite { source_location: "base", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), location: "c6", relation: None, @@ -844,15 +724,11 @@ fn copies_in_entire_tree_by_similarity_with_limit() -> crate::Result { }, Rewrite { source_location: "dir/c6", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), location: "c7", relation: None, @@ -860,21 +736,15 @@ fn copies_in_entire_tree_by_similarity_with_limit() -> crate::Result { }, Modification { location: "b", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(54781fa52cf133fa9d0bf59cfe2ef2621b5ad29f), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(f198d0640214092732566fb00543163845c8252c), }, Addition { location: "newly-added", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(97b3d1a5707f8a11fa5fa8bc6c3bd7b3965601fd), }, ] @@ -910,26 +780,18 @@ fn copies_by_similarity_with_limit() -> crate::Result { [ Modification { location: "dir", - previous_entry_mode: EntryMode( - 16384, - ), + previous_entry_mode: EntryMode(0o40000), previous_id: Sha1(f01fd5b4d733a4ae749cbb58a828cdb3f342f298), - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(1d7e20e07562a54af0408fd2669b0c56a6faa6f0), }, Rewrite { source_location: "base", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), location: "c4", relation: None, @@ -938,17 +800,13 @@ fn copies_by_similarity_with_limit() -> crate::Result { Addition { location: "c5", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(08fe19ca4d2f79624f35333157d610811efc1aed), }, Addition { location: "dir/c6", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), }, ] @@ -984,15 +842,11 @@ fn realistic_renames_by_identity() -> crate::Result { [ Rewrite { source_location: "git-index/src/file.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), location: "git-index/src/file/mod.rs", relation: None, @@ -1001,20 +855,14 @@ fn realistic_renames_by_identity() -> crate::Result { Addition { location: "git-index/tests/index/file/access.rs", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Modification { location: "git-index/tests/index/file/mod.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(8ba3a16384aacc37d01564b28401755ce8053f51), }, ] @@ -1070,36 +918,26 @@ fn realistic_renames_disabled() -> crate::Result { Deletion { location: "git-index/src/file.rs", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { location: "git-index/src/file/mod.rs", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { location: "git-index/tests/index/file/access.rs", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Modification { location: "git-index/tests/index/file/mod.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(8ba3a16384aacc37d01564b28401755ce8053f51), }, ] @@ -1161,9 +999,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(0026010e87631065a2739f627622feb14f903fd4), }, Addition { @@ -1173,9 +1009,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 2, ), ), - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(0026010e87631065a2739f627622feb14f903fd4), }, Deletion { @@ -1185,9 +1019,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Deletion { @@ -1197,9 +1029,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { @@ -1209,9 +1039,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 2, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { @@ -1221,9 +1049,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 2, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Deletion { @@ -1233,9 +1059,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Deletion { @@ -1245,9 +1069,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Deletion { @@ -1257,9 +1079,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Deletion { @@ -1269,9 +1089,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Deletion { @@ -1281,9 +1099,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { @@ -1293,9 +1109,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 2, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { @@ -1305,9 +1119,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 2, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { @@ -1317,9 +1129,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 2, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { @@ -1329,9 +1139,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 2, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { @@ -1341,9 +1149,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 2, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Deletion { @@ -1353,9 +1159,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 1, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { @@ -1365,9 +1169,7 @@ fn realistic_renames_disabled_2() -> crate::Result { 2, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, ] @@ -1456,33 +1258,25 @@ fn realistic_renames_disabled_3() -> crate::Result { Addition { location: "src/ein.rs", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Addition { location: "src/gix.rs", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Deletion { location: "src/plumbing-cli.rs", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, Deletion { location: "src/porcelain-cli.rs", relation: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, ] @@ -1539,15 +1333,11 @@ fn realistic_renames_by_identity_3() -> crate::Result { [ Rewrite { source_location: "src/plumbing-cli.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), location: "src/ein.rs", relation: None, @@ -1555,15 +1345,11 @@ fn realistic_renames_by_identity_3() -> crate::Result { }, Rewrite { source_location: "src/porcelain-cli.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), location: "src/gix.rs", relation: None, @@ -1629,9 +1415,7 @@ fn realistic_renames_2() -> crate::Result { [ Rewrite { source_location: "git-sec", - source_entry_mode: EntryMode( - 16384, - ), + source_entry_mode: EntryMode(0o40000), source_relation: Some( Parent( 1, @@ -1639,9 +1423,7 @@ fn realistic_renames_2() -> crate::Result { ), source_id: Sha1(0026010e87631065a2739f627622feb14f903fd4), diff: None, - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(0026010e87631065a2739f627622feb14f903fd4), location: "gix-sec", relation: Some( @@ -1653,9 +1435,7 @@ fn realistic_renames_2() -> crate::Result { }, Rewrite { source_location: "git-sec/CHANGELOG.md", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, @@ -1663,9 +1443,7 @@ fn realistic_renames_2() -> crate::Result { ), source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), location: "gix-sec/CHANGELOG.md", relation: Some( @@ -1677,9 +1455,7 @@ fn realistic_renames_2() -> crate::Result { }, Rewrite { source_location: "git-sec/Cargo.toml", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, @@ -1687,9 +1463,7 @@ fn realistic_renames_2() -> crate::Result { ), source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), location: "gix-sec/Cargo.toml", relation: Some( @@ -1701,9 +1475,7 @@ fn realistic_renames_2() -> crate::Result { }, Rewrite { source_location: "git-sec/src/identity.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, @@ -1711,9 +1483,7 @@ fn realistic_renames_2() -> crate::Result { ), source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), location: "gix-sec/src/identity.rs", relation: Some( @@ -1725,9 +1495,7 @@ fn realistic_renames_2() -> crate::Result { }, Rewrite { source_location: "git-sec/src/lib.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, @@ -1735,9 +1503,7 @@ fn realistic_renames_2() -> crate::Result { ), source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), location: "gix-sec/src/lib.rs", relation: Some( @@ -1749,9 +1515,7 @@ fn realistic_renames_2() -> crate::Result { }, Rewrite { source_location: "git-sec/src/permission.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, @@ -1759,9 +1523,7 @@ fn realistic_renames_2() -> crate::Result { ), source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), location: "gix-sec/src/permission.rs", relation: Some( @@ -1773,9 +1535,7 @@ fn realistic_renames_2() -> crate::Result { }, Rewrite { source_location: "git-sec/src/trust.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, @@ -1783,9 +1543,7 @@ fn realistic_renames_2() -> crate::Result { ), source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), location: "gix-sec/src/trust.rs", relation: Some( @@ -1797,9 +1555,7 @@ fn realistic_renames_2() -> crate::Result { }, Rewrite { source_location: "git-sec/tests/sec.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, @@ -1807,9 +1563,7 @@ fn realistic_renames_2() -> crate::Result { ), source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), location: "gix-sec/tests/sec.rs", relation: Some( @@ -1821,9 +1575,7 @@ fn realistic_renames_2() -> crate::Result { }, Rewrite { source_location: "git-sec/tests/identity/mod.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, @@ -1831,9 +1583,7 @@ fn realistic_renames_2() -> crate::Result { ), source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), location: "gix-sec/tests/identity/mod.rs", relation: Some( @@ -1927,9 +1677,7 @@ fn realistic_renames_3_without_identity() -> crate::Result { [ Rewrite { source_location: "src/plumbing/options.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -1937,9 +1685,7 @@ fn realistic_renames_3_without_identity() -> crate::Result { ), source_id: Sha1(00750edc07d6415dcc07ae0351e9397b0222b7ba), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(00750edc07d6415dcc07ae0351e9397b0222b7ba), location: "src/plumbing-renamed/options/mod.rs", relation: Some( @@ -1951,9 +1697,7 @@ fn realistic_renames_3_without_identity() -> crate::Result { }, Rewrite { source_location: "src/plumbing/mod.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -1961,9 +1705,7 @@ fn realistic_renames_3_without_identity() -> crate::Result { ), source_id: Sha1(0cfbf08886fca9a91cb753ec8734c84fcbe52c9f), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(0cfbf08886fca9a91cb753ec8734c84fcbe52c9f), location: "src/plumbing-renamed/mod.rs", relation: Some( @@ -1975,9 +1717,7 @@ fn realistic_renames_3_without_identity() -> crate::Result { }, Rewrite { source_location: "src/plumbing/main.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -1985,9 +1725,7 @@ fn realistic_renames_3_without_identity() -> crate::Result { ), source_id: Sha1(d00491fd7e5bb6fa28c517a0bb32b8b506539d4d), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(d00491fd7e5bb6fa28c517a0bb32b8b506539d4d), location: "src/plumbing-renamed/main.rs", relation: Some( @@ -1999,9 +1737,7 @@ fn realistic_renames_3_without_identity() -> crate::Result { }, Rewrite { source_location: "src/plumbing", - source_entry_mode: EntryMode( - 16384, - ), + source_entry_mode: EntryMode(0o40000), source_relation: Some( Parent( 2, @@ -2009,9 +1745,7 @@ fn realistic_renames_3_without_identity() -> crate::Result { ), source_id: Sha1(b9d41dcdbd92fcab2fb6594d04f2ad99b3472621), diff: None, - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(202702465d7bb291153629dc2e8b353afe9cbdae), location: "src/plumbing-renamed", relation: Some( diff --git a/gix-index/src/decode/entries.rs b/gix-index/src/decode/entries.rs index d3bda1cb6a3..f32074208f5 100644 --- a/gix-index/src/decode/entries.rs +++ b/gix-index/src/decode/entries.rs @@ -64,9 +64,7 @@ pub fn chunk<'a>( .ok_or(decode::Error::Entry { index: idx })?; data = remaining; - if entry.mode.is_sparse() { - is_sparse = true; - } + is_sparse |= entry.mode.is_sparse(); // TODO: entries are actually in an intrusive collection, with path as key. Could be set for us. This affects 'ignore_case' which we // also don't yet handle but probably could, maybe even smartly with the collection. // For now it's unclear to me how they access the index, they could iterate quickly, and have fast access by path. diff --git a/gix-index/src/entry/flags.rs b/gix-index/src/entry/flags.rs index d003dfc8c39..dacd311b3ed 100644 --- a/gix-index/src/entry/flags.rs +++ b/gix-index/src/entry/flags.rs @@ -60,6 +60,11 @@ bitflags! { } impl Flags { + /// Create a new instance whose stage is set to `stage`. + pub fn from_stage(stage: Stage) -> Self { + Flags::from_bits((stage as u32) << 12).expect("stage can only be valid flags") + } + /// Return the stage as extracted from the bits of this instance. pub fn stage(&self) -> Stage { match self.stage_raw() { @@ -96,6 +101,12 @@ impl Flags { } } +impl From for Flags { + fn from(value: Stage) -> Self { + Flags::from_stage(value) + } +} + pub(crate) mod at_rest { use bitflags::bitflags; diff --git a/gix-index/tests/index/entry/mod.rs b/gix-index/tests/index/entry/mod.rs index 184093c3503..831e1eacfd8 100644 --- a/gix-index/tests/index/entry/mod.rs +++ b/gix-index/tests/index/entry/mod.rs @@ -1,3 +1,16 @@ +mod flags { + use gix_index::entry::{Flags, Stage}; + + #[test] + fn from_stage() { + for stage in [Stage::Unconflicted, Stage::Base, Stage::Ours, Stage::Theirs] { + let actual = Flags::from_stage(stage); + assert_eq!(actual.stage(), stage); + let actual: Flags = stage.into(); + assert_eq!(actual.stage(), stage); + } + } +} mod mode; mod stat; mod time; diff --git a/gix-index/tests/index/entry/mode.rs b/gix-index/tests/index/entry/mode.rs index 08ca6932b7f..ef4975a7c9a 100644 --- a/gix-index/tests/index/entry/mode.rs +++ b/gix-index/tests/index/entry/mode.rs @@ -12,3 +12,18 @@ fn apply() { Mode::SYMLINK ); } + +#[test] +fn debug() { + assert_eq!( + format!("{:?}", Mode::FILE), + "Mode(FILE)", + "Assure the debug output is easy to understand" + ); + + assert_eq!( + format!("{:?}", Mode::from_bits(0o120744)), + "Some(Mode(FILE | SYMLINK | 0x40))", + "strange modes are also mostly legible" + ); +} diff --git a/gix-merge/Cargo.toml b/gix-merge/Cargo.toml index b8439ba982b..5f3f0740cf9 100644 --- a/gix-merge/Cargo.toml +++ b/gix-merge/Cargo.toml @@ -32,6 +32,7 @@ gix-quote = { version = "^0.4.13", path = "../gix-quote" } gix-revision = { version = "^0.30.0", path = "../gix-revision", default-features = false, features = ["merge_base"] } gix-revwalk = { version = "^0.16.0", path = "../gix-revwalk" } gix-diff = { version = "^0.47.0", path = "../gix-diff", default-features = false, features = ["blob"] } +gix-index = { version = "^0.36.0", path = "../gix-index" } thiserror = "2.0.0" imara-diff = { version = "0.1.7" } diff --git a/gix-merge/src/blob/builtin_driver/text/function.rs b/gix-merge/src/blob/builtin_driver/text/function.rs index 2b0a2e7522c..2ff6ed3a3ad 100644 --- a/gix-merge/src/blob/builtin_driver/text/function.rs +++ b/gix-merge/src/blob/builtin_driver/text/function.rs @@ -31,14 +31,17 @@ pub fn merge<'a>( current: &'a [u8], ancestor: &'a [u8], other: &'a [u8], - opts: Options, + Options { + diff_algorithm, + conflict, + }: Options, ) -> Resolution { out.clear(); input.update_before(tokens(ancestor)); input.update_after(tokens(current)); let hunks = imara_diff::diff( - opts.diff_algorithm, + diff_algorithm, input, CollectHunks { side: Side::Current, @@ -50,7 +53,7 @@ pub fn merge<'a>( input.update_after(tokens(other)); let mut hunks = imara_diff::diff( - opts.diff_algorithm, + diff_algorithm, input, CollectHunks { side: Side::Other, @@ -86,7 +89,7 @@ pub fn merge<'a>( fill_ancestor(&extended_range, &mut current_hunks); fill_ancestor(&extended_range, &mut intersecting); } - match opts.conflict { + match conflict { Conflict::Keep { style, marker_size } => { let marker_size = marker_size.get(); let (hunks_front_and_back, num_hunks_front) = match style { @@ -177,7 +180,10 @@ pub fn merge<'a>( unreachable!("initial hunks are never ancestors") } }; - let hunks_to_write = if opts.conflict == Conflict::ResolveWithOurs { + if hunks_differ_in_diff3(ConflictStyle::Diff3, our_hunks, their_hunks, input, ¤t_tokens) { + resolution = Resolution::CompleteWithAutoResolvedConflict; + } + let hunks_to_write = if conflict == Conflict::ResolveWithOurs { our_hunks } else { their_hunks @@ -201,6 +207,9 @@ pub fn merge<'a>( unreachable!("initial hunks are never ancestors") } }; + if hunks_differ_in_diff3(ConflictStyle::Diff3, our_hunks, their_hunks, input, ¤t_tokens) { + resolution = Resolution::CompleteWithAutoResolvedConflict; + } let (front_hunks, back_hunks) = hunks_front_and_back.split_at(num_hunks_front); let first_hunk = first_hunk(front_hunks, our_hunks, their_hunks, back_hunks); write_ancestor(input, ancestor_integrated_until, first_hunk.before.start as usize, out); diff --git a/gix-merge/src/blob/mod.rs b/gix-merge/src/blob/mod.rs index fbf578e054d..05a806bf6da 100644 --- a/gix-merge/src/blob/mod.rs +++ b/gix-merge/src/blob/mod.rs @@ -15,13 +15,13 @@ pub mod platform; /// Define if a merge is conflicted or not. #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Resolution { - /// Everything could be resolved during the merge. - /// - /// Conflicts may have been resolved automatically, depending on the options. + /// Everything could be resolved during the merge, and there was no conflict. Complete, + /// Conflicts were resolved automatically, even thought the result is complete + /// and free of conflict markers. + /// This can only be the case for text-file content merges. + CompleteWithAutoResolvedConflict, /// A conflict is still present in the form of conflict markers. - /// - /// Note that this won't be the case if conflicts were automatically resolved. Conflict, } diff --git a/gix-merge/src/commit/virtual_merge_base.rs b/gix-merge/src/commit/virtual_merge_base.rs index 29009d3d11a..86ca96a3f50 100644 --- a/gix-merge/src/commit/virtual_merge_base.rs +++ b/gix-merge/src/commit/virtual_merge_base.rs @@ -30,7 +30,7 @@ pub enum Error { pub(super) mod function { use super::Error; use crate::blob::builtin_driver; - use crate::tree::UnresolvedConflict; + use crate::tree::TreatAsUnresolved; use gix_object::FindExt; /// Create a single virtual merge-base by merging `first_commit`, `second_commit` and `others` into one. @@ -88,7 +88,7 @@ pub(super) mod function { // This shouldn't happen, but if for some buggy reason it does, we rather bail. if out .tree_merge - .has_unresolved_conflicts(UnresolvedConflict::ConflictMarkers) + .has_unresolved_conflicts(TreatAsUnresolved::ConflictMarkers) { return Err(Error::VirtualMergeBaseConflict.into()); } diff --git a/gix-merge/src/tree/function.rs b/gix-merge/src/tree/function.rs index eaa0ea933ba..ca131db27a7 100644 --- a/gix-merge/src/tree/function.rs +++ b/gix-merge/src/tree/function.rs @@ -3,7 +3,10 @@ use crate::tree::utils::{ to_components, track, unique_path_in_tree, ChangeList, ChangeListRef, PossibleConflict, TrackedChange, TreeNodes, }; use crate::tree::ConflictMapping::{Original, Swapped}; -use crate::tree::{Conflict, ConflictMapping, ContentMerge, Error, Options, Outcome, Resolution, ResolutionFailure}; +use crate::tree::{ + Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, ContentMerge, Error, Options, Outcome, + Resolution, ResolutionFailure, +}; use bstr::{BString, ByteSlice}; use gix_diff::tree::recorder::Location; use gix_diff::tree_with_rewrites::Change; @@ -190,11 +193,14 @@ where }) { None => { if let Some((rewritten_location, ours_idx)) = rewritten_location { + // `no_entry` to the index because that's not a conflict at all, + // but somewhat advanced rename tracking. if should_fail_on_conflict(Conflict::with_resolution( Resolution::SourceLocationAffectedByRename { final_location: rewritten_location.to_owned(), }, (&our_changes[*ours_idx].inner, theirs, Original, outer_side), + [None, None, None], )) { break 'outer; }; @@ -254,10 +260,12 @@ where if let Some(ours) = ours { gix_trace::debug!("Turning a case we could probably handle into a conflict for now. theirs: {theirs:#?} ours: {ours:#?} kind: {match_kind:?}"); if allow_resolution_failure - && should_fail_on_conflict(Conflict::without_resolution( - ResolutionFailure::Unknown, - (&ours.inner, theirs, Original, outer_side), - )) + && should_fail_on_conflict(Conflict::unknown(( + &ours.inner, + theirs, + Original, + outer_side, + ))) { break 'outer; }; @@ -328,7 +336,7 @@ where pick_our_changes(side, our_changes, their_changes), ); let renamed_without_change = their_source_id == their_id; - let (our_id, resolution) = if renamed_without_change { + let (merged_blob_id, resolution) = if renamed_without_change { (*our_id, None) } else { let (our_location, our_id, our_mode, their_location, their_id, their_mode) = @@ -373,18 +381,23 @@ where location: their_rewritten_location.unwrap_or_else(|| their_location.to_owned()), relation: None, entry_mode: merged_mode, - id: our_id, + id: merged_blob_id, }; if should_fail_on_conflict(Conflict::with_resolution( Resolution::OursModifiedTheirsRenamedAndChangedThenRename { merged_mode: (merged_mode != *their_mode).then_some(merged_mode), merged_blob: resolution.map(|resolution| ContentMerge { resolution, - merged_blob_id: our_id, + merged_blob_id, }), final_location, }, (ours, theirs, side, outer_side), + [ + index_entry(previous_entry_mode, previous_id), + index_entry(our_mode, our_id), + index_entry(their_mode, their_id), + ], )) { break 'outer; } @@ -401,6 +414,19 @@ where if should_fail_on_conflict(Conflict::without_resolution( ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch, (ours, theirs, side, outer_side), + [ + index_entry_at_path( + previous_entry_mode, + previous_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + None, + index_entry_at_path( + their_mode, + their_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], )) { break 'outer; } @@ -421,7 +447,7 @@ where .. }, ) if !involves_submodule(our_mode, their_mode) - && our_mode.kind() == their_mode.kind() + && merge_modes(*our_mode, *their_mode).is_some() && our_id != their_id => { let (merged_blob_id, resolution) = perform_blob_merge( @@ -436,7 +462,11 @@ where (0, outer_side), &options, )?; - editor.upsert(toc(location), our_mode.kind(), merged_blob_id)?; + + let merged_mode = merge_modes_prev(*our_mode, *their_mode, *previous_entry_mode) + .expect("BUG: merge_modes() reports a valid mode, this one should do too"); + + editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; if should_fail_on_conflict(Conflict::with_resolution( Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob: ContentMerge { @@ -445,6 +475,11 @@ where }, }, (ours, theirs, Original, outer_side), + [ + index_entry(previous_entry_mode, previous_id), + index_entry(our_mode, our_id), + index_entry(their_mode, their_id), + ], )) { break 'outer; }; @@ -489,27 +524,28 @@ where }, }, (ours, theirs, Original, outer_side), + [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], )) } else if allow_resolution_failure { // Actually this has a preference, as symlinks are always left in place with the other side renamed. let ( logical_side, label_of_side_to_be_moved, - (our_mode, our_id), - (their_mode, their_id), + (our_mode, our_id, our_path_hint), + (their_mode, their_id, their_path_hint), ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) { ( Original, labels.other.unwrap_or_default(), - (*our_mode, *our_id), - (*their_mode, *their_id), + (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), + (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), ) } else { ( Swapped, labels.current.unwrap_or_default(), - (*their_mode, *their_id), - (*our_mode, *our_id), + (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), + (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), ) }; let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree); @@ -525,6 +561,11 @@ where their_unique_location: renamed_location.clone(), }, (ours, theirs, logical_side, outer_side), + [ + None, + index_entry_at_path(&our_mode, &our_id, our_path_hint), + index_entry_at_path(&their_mode, &their_id, their_path_hint), + ], ); let new_change = Change::Addition { @@ -554,7 +595,8 @@ where location, entry_mode, id, - .. + previous_entry_mode, + previous_id, }, Change::Deletion { .. }, ) @@ -564,7 +606,8 @@ where location, entry_mode, id, - .. + previous_entry_mode, + previous_id, }, ) if allow_resolution_failure => { let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) { @@ -606,6 +649,11 @@ where renamed_unique_path_to_modified_blob: renamed_path, }, (ours, theirs, side, outer_side), + [ + index_entry(previous_entry_mode, previous_id), + index_entry(entry_mode, id), + None, + ], )); // Since we move *our* side, our tree needs to be modified. @@ -621,6 +669,11 @@ where let should_break = should_fail_on_conflict(Conflict::without_resolution( ResolutionFailure::OursModifiedTheirsDeleted, (ours, theirs, side, outer_side), + [ + index_entry(previous_entry_mode, previous_id), + index_entry(entry_mode, id), + None, + ], )); editor.upsert(toc(location), entry_mode.kind(), *id)?; if should_break { @@ -702,10 +755,9 @@ where // Pretend this is the end of the loop and keep this as conflict. // If this happens in the wild, we'd want to reproduce it. if allow_resolution_failure - && should_fail_on_conflict(Conflict::without_resolution( - ResolutionFailure::Unknown, - (ours, theirs, Original, outer_side), - )) + && should_fail_on_conflict(Conflict::unknown(( + ours, theirs, Original, outer_side, + ))) { break 'outer; }; @@ -738,6 +790,23 @@ where }), }, (ours, theirs, Original, outer_side), + [ + index_entry_at_path( + source_entry_mode, + source_id, + ConflictIndexEntryPathHint::Source, + ), + index_entry_at_path( + our_mode, + &merged_blob_id, + ConflictIndexEntryPathHint::Current, + ), + index_entry_at_path( + their_mode, + &merged_blob_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], )) { break 'outer; }; @@ -767,6 +836,23 @@ where }, }, (ours, theirs, Original, outer_side), + [ + index_entry_at_path( + source_entry_mode, + source_id, + ConflictIndexEntryPathHint::Source, + ), + index_entry_at_path( + our_mode, + &merged_blob_id, + ConflictIndexEntryPathHint::Current, + ), + index_entry_at_path( + their_mode, + &merged_blob_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], )) { break 'outer; }; @@ -824,6 +910,15 @@ where if should_fail_on_conflict(Conflict::without_resolution( ResolutionFailure::OursDeletedTheirsRenamed, (ours, theirs, side, outer_side), + [ + None, + None, + index_entry_at_path( + rewritten_mode, + rewritten_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], )) { break 'outer; }; @@ -901,6 +996,7 @@ where }, }, (ours, theirs, Original, outer_side), + [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], )) { break 'outer; }; @@ -916,21 +1012,21 @@ where let ( logical_side, label_of_side_to_be_moved, - (our_mode, our_id), - (their_mode, their_id), + (our_mode, our_id, our_path_hint), + (their_mode, their_id, their_path_hint), ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) { ( Original, labels.other.unwrap_or_default(), - (*our_mode, *our_id), - (*their_mode, *their_id), + (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), + (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), ) } else { ( Swapped, labels.current.unwrap_or_default(), - (*their_mode, *their_id), - (*our_mode, *our_id), + (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), + (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), ) }; let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree); @@ -946,6 +1042,11 @@ where their_unique_location: renamed_location.clone(), }, (ours, theirs, side, outer_side), + [ + None, + index_entry_at_path(&our_mode, &our_id, our_path_hint), + index_entry_at_path(&their_mode, &their_id, their_path_hint), + ], ); let new_change_with_rename = Change::Addition { @@ -970,10 +1071,7 @@ where } _unknown => { if allow_resolution_failure - && should_fail_on_conflict(Conflict::without_resolution( - ResolutionFailure::Unknown, - (ours, theirs, Original, outer_side), - )) + && should_fail_on_conflict(Conflict::unknown((ours, theirs, Original, outer_side))) { break 'outer; }; @@ -1004,12 +1102,40 @@ fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool { a.is_commit() || b.is_commit() } -/// Allows equal modes or preferes executables bits in case of blobs +/// Allows equal modes or prefers executables bits in case of blobs +/// +/// Note that this is often not correct as the previous mode of each side should be taken into account so that: +/// +/// on | on = on +/// off | off = off +/// on | off || off | on = conflict fn merge_modes(a: EntryMode, b: EntryMode) -> Option { match (a.kind(), b.kind()) { + (_, _) if a == b => Some(a), (EntryKind::BlobExecutable, EntryKind::BlobExecutable | EntryKind::Blob) | (EntryKind::Blob, EntryKind::BlobExecutable) => Some(EntryKind::BlobExecutable.into()), + _ => None, + } +} + +/// Use this version if there is a single common `prev` value for both `a` and `b` to detect +/// if the mode was turned on or off. +fn merge_modes_prev(a: EntryMode, b: EntryMode, prev: EntryMode) -> Option { + match (a.kind(), b.kind()) { (_, _) if a == b => Some(a), + (a @ EntryKind::BlobExecutable, b @ (EntryKind::BlobExecutable | EntryKind::Blob)) + | (a @ EntryKind::Blob, b @ EntryKind::BlobExecutable) => { + let prev = prev.kind(); + let changed = if a == prev { b } else { a }; + Some( + match (prev, changed) { + (EntryKind::Blob, EntryKind::BlobExecutable) => EntryKind::BlobExecutable, + (EntryKind::BlobExecutable, EntryKind::Blob) => EntryKind::Blob, + _ => unreachable!("upper match already assured we only deal with blobs"), + } + .into(), + ) + } _ => None, } } @@ -1066,3 +1192,23 @@ fn pick_our_changes_mut<'a>( Swapped => theirs, } } + +fn index_entry(mode: &gix_object::tree::EntryMode, id: &gix_hash::ObjectId) -> Option { + Some(ConflictIndexEntry { + mode: *mode, + id: *id, + path_hint: None, + }) +} + +fn index_entry_at_path( + mode: &gix_object::tree::EntryMode, + id: &gix_hash::ObjectId, + hint: ConflictIndexEntryPathHint, +) -> Option { + Some(ConflictIndexEntry { + mode: *mode, + id: *id, + path_hint: Some(hint), + }) +} diff --git a/gix-merge/src/tree/mod.rs b/gix-merge/src/tree/mod.rs index f1f52146481..f6a534e916e 100644 --- a/gix-merge/src/tree/mod.rs +++ b/gix-merge/src/tree/mod.rs @@ -47,22 +47,51 @@ pub struct Outcome<'a> { /// Determine what should be considered an unresolved conflict. /// -/// Note that no matter which variant, [conflicts](Conflict) with [resolution failure](`ResolutionFailure`) -/// will always be unresolved. +/// Note that no matter which variant, [conflicts](Conflict) with +/// [resolution failure](`ResolutionFailure`) will always be unresolved. +/// +/// Also, when one side was modified but the other side renamed it, this will not +/// be considered a conflict, even if a non-conflicting merge happened. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum UnresolvedConflict { +pub enum TreatAsUnresolved { /// Only consider content merges with conflict markers as unresolved. + /// + /// Auto-resolved tree conflicts will *not* be considered unresolved. ConflictMarkers, - /// Whenever there was any rename, or conflict markers, it is unresolved. + /// Consider content merges with conflict markers as unresolved, and content + /// merges where conflicts where auto-resolved in any way, like choosing + /// *ours*, *theirs* or by their *union*. + /// + /// Auto-resolved tree conflicts will *not* be considered unresolved. + ConflictMarkersAndAutoResolved, + /// Whenever there were conflicting renames, or conflict markers, it is unresolved. + /// Note that auto-resolved content merges will *not* be considered unresolved. + /// + /// Also note that files that were changed in one and renamed in another will + /// be moved into place, which will be considered resolved. Renames, + /// Similar to [`Self::Renames`], but auto-resolved content-merges will + /// also be considered unresolved. + RenamesAndAutoResolvedContent, } impl Outcome<'_> { /// Return `true` if there is any conflict that would still need to be resolved as they would yield undesirable trees. /// This is based on `how` to determine what should be considered unresolved. - pub fn has_unresolved_conflicts(&self, how: UnresolvedConflict) -> bool { + pub fn has_unresolved_conflicts(&self, how: TreatAsUnresolved) -> bool { self.conflicts.iter().any(|c| c.is_unresolved(how)) } + + /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a + /// conflict should be considered unresolved. + /// It's important that `index` is at the state of [`Self::tree`]. + /// + /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`. + /// Also, the unconflicted stage of such entries will be removed merely by setting a flag, so the + /// in-memory entry is still present. + pub fn index_changed_after_applying_conflicts(&self, index: &mut gix_index::State, how: TreatAsUnresolved) -> bool { + apply_index_entries(&self.conflicts, how, index) + } } /// A description of a conflict (i.e. merge issue without an auto-resolution) as seen during a [tree-merge](crate::tree()). @@ -81,11 +110,45 @@ pub struct Conflict { pub ours: Change, /// The change representing *their* side. pub theirs: Change, + /// An array to store an entry for each stage of the conflict. + /// + /// * `entries[0]` => Base + /// * `entries[1]` => Ours + /// * `entries[2]` => Theirs + /// + /// Note that ours and theirs might be swapped, so one should access it through [`Self::entries()`] to compensate for that. + pub entries: [Option; 3], /// Determine how to interpret the `ours` and `theirs` fields. This is used to implement [`Self::changes_in_resolution()`] /// and [`Self::into_parts_by_resolution()`]. map: ConflictMapping, } +/// A conflicting entry for insertion into the index. +/// It will always be either on stage 1 (ancestor/base), 2 (ours) or 3 (theirs) +#[derive(Debug, Clone, Copy)] +pub struct ConflictIndexEntry { + /// The kind of object at this stage. + /// Note that it's possible that this is a directory, for instance if a directory was replaced with a file. + pub mode: gix_object::tree::EntryMode, + /// The id defining the state of the object. + pub id: gix_hash::ObjectId, + /// Hidden, maybe one day we can do without? + path_hint: Option, +} + +/// A hint for [`apply_index_entries()`] to know which paths to use for an entry. +/// This is only used when necessary. +#[derive(Debug, Clone, Copy)] +enum ConflictIndexEntryPathHint { + /// Use the previous path, i.e. rename source. + Source, + /// Use the current path as it is in the tree. + Current, + /// Use the path of the final destination, or *their* name. + /// It's definitely finicky, as we don't store the actual path and instead refer to it. + RenamedOrTheirs, +} + /// A utility to help define which side is what in the [`Conflict`] type. #[derive(Debug, Clone, Copy)] enum ConflictMapping { @@ -109,20 +172,33 @@ impl ConflictMapping { impl Conflict { /// Return `true` if this instance is considered unresolved based on the criterion specified by `how`. - pub fn is_unresolved(&self, how: UnresolvedConflict) -> bool { + pub fn is_unresolved(&self, how: TreatAsUnresolved) -> bool { + use crate::blob; + let content_merge_matches = |info: &ContentMerge| match how { + TreatAsUnresolved::ConflictMarkers | TreatAsUnresolved::Renames => { + matches!(info.resolution, blob::Resolution::Conflict) + } + TreatAsUnresolved::RenamesAndAutoResolvedContent | TreatAsUnresolved::ConflictMarkersAndAutoResolved => { + matches!( + info.resolution, + blob::Resolution::Conflict | blob::Resolution::CompleteWithAutoResolvedConflict + ) + } + }; match how { - UnresolvedConflict::ConflictMarkers => { - self.resolution.is_err() - || self.content_merge().map_or(false, |info| { - matches!(info.resolution, crate::blob::Resolution::Conflict) - }) + TreatAsUnresolved::ConflictMarkers | TreatAsUnresolved::ConflictMarkersAndAutoResolved => { + self.resolution.is_err() || self.content_merge().map_or(false, |info| content_merge_matches(&info)) } - UnresolvedConflict::Renames => match &self.resolution { + TreatAsUnresolved::Renames | TreatAsUnresolved::RenamesAndAutoResolvedContent => match &self.resolution { Ok(success) => match success { - Resolution::SourceLocationAffectedByRename { .. } - | Resolution::OursModifiedTheirsRenamedAndChangedThenRename { .. } => true, + Resolution::SourceLocationAffectedByRename { .. } => false, + Resolution::OursModifiedTheirsRenamedAndChangedThenRename { + merged_blob, + final_location, + .. + } => final_location.is_some() || merged_blob.as_ref().map_or(false, content_merge_matches), Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => { - matches!(merged_blob.resolution, crate::blob::Resolution::Conflict) + content_merge_matches(merged_blob) } }, Err(_failure) => true, @@ -151,6 +227,14 @@ impl Conflict { } } + /// Return the index entries for insertion into the index, to match with what's returned by [`Self::changes_in_resolution()`]. + pub fn entries(&self) -> [Option; 3] { + match self.map { + ConflictMapping::Original => self.entries, + ConflictMapping::Swapped => [self.entries[0], self.entries[2], self.entries[1]], + } + } + /// Return information about the content merge if it was performed. pub fn content_merge(&self) -> Option { match &self.resolution { @@ -264,7 +348,7 @@ pub struct Options { /// If `Some(what-is-unresolved)`, the first unresolved conflict will cause the entire merge to stop. /// This is useful to see if there is any conflict, without performing the whole operation, something /// that can be very relevant during merges that would cause a lot of blob-diffs. - pub fail_on_conflict: Option, + pub fail_on_conflict: Option, /// This value also affects the size of merge-conflict markers, to allow differentiating /// merge conflicts on each level, for any value greater than 0, with values `N` causing `N*2` /// markers to be added to the configured value. @@ -281,3 +365,136 @@ pub struct Options { pub(super) mod function; mod utils; +pub mod apply_index_entries { + + pub(super) mod function { + use crate::tree::{Conflict, ConflictIndexEntryPathHint, Resolution, ResolutionFailure, TreatAsUnresolved}; + use bstr::{BStr, ByteSlice}; + use std::collections::{hash_map, HashMap}; + + /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a + /// conflict should be considered unresolved. + /// Once a stage of a path conflicts, the unconflicting stage is removed even though it might be the one + /// that is currently checked out. + /// This removal, however, is only done by flagging it with [gix_index::entry::Flags::REMOVE], which means + /// these entries won't be written back to disk but will still be present in the index. + /// It's important that `index` matches the tree that was produced as part of the merge that also + /// brought about `conflicts`, or else this function will fail if it cannot find the path matching + /// the conflicting entries. + /// + /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`. + /// Errors can only occour if `index` isn't the one created from the merged tree that produced the `conflicts`. + pub fn apply_index_entries( + conflicts: &[Conflict], + how: TreatAsUnresolved, + index: &mut gix_index::State, + ) -> bool { + if index.is_sparse() { + gix_trace::error!("Refusing to apply index entries to sparse index - it's not tested yet"); + return false; + } + let len = index.entries().len(); + let mut idx_by_path_stage = HashMap::<(gix_index::entry::Stage, &BStr), usize>::default(); + for conflict in conflicts.iter().filter(|c| c.is_unresolved(how)) { + let (renamed_path, current_path): (Option<&BStr>, &BStr) = match &conflict.resolution { + Ok(success) => match success { + Resolution::SourceLocationAffectedByRename { final_location } => { + (Some(final_location.as_bstr()), final_location.as_bstr()) + } + Resolution::OursModifiedTheirsRenamedAndChangedThenRename { final_location, .. } => ( + final_location.as_ref().map(|p| p.as_bstr()), + conflict.changes_in_resolution().1.location(), + ), + Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { .. } => { + (None, conflict.ours.location()) + } + }, + Err(failure) => match failure { + ResolutionFailure::OursRenamedTheirsRenamedDifferently { .. } => { + (Some(conflict.theirs.location()), conflict.ours.location()) + } + ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch + | ResolutionFailure::OursDeletedTheirsRenamed + | ResolutionFailure::OursModifiedTheirsDeleted + | ResolutionFailure::Unknown => (None, conflict.ours.location()), + ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { + renamed_unique_path_to_modified_blob, + } => ( + Some(renamed_unique_path_to_modified_blob.as_bstr()), + conflict.ours.location(), + ), + ResolutionFailure::OursAddedTheirsAddedTypeMismatch { their_unique_location } => { + (Some(their_unique_location.as_bstr()), conflict.ours.location()) + } + }, + }; + let source_path = conflict.ours.source_location(); + + let entries_with_stage = conflict.entries().into_iter().enumerate().filter_map(|(idx, entry)| { + entry.filter(|e| e.mode.is_no_tree()).map(|e| { + ( + match idx { + 0 => gix_index::entry::Stage::Base, + 1 => gix_index::entry::Stage::Ours, + 2 => gix_index::entry::Stage::Theirs, + _ => unreachable!("fixed size array with three items"), + }, + match e.path_hint { + None => renamed_path.unwrap_or(current_path), + Some(ConflictIndexEntryPathHint::Source) => source_path, + Some(ConflictIndexEntryPathHint::Current) => current_path, + Some(ConflictIndexEntryPathHint::RenamedOrTheirs) => { + renamed_path.unwrap_or_else(|| conflict.changes_in_resolution().1.location()) + } + }, + e, + ) + }) + }); + + if !entries_with_stage.clone().any(|(_, path, _)| { + index + .entry_index_by_path_and_stage_bounded(path, gix_index::entry::Stage::Unconflicted, len) + .is_some() + }) { + continue; + } + + for (stage, path, entry) in entries_with_stage { + if let Some(pos) = + index.entry_index_by_path_and_stage_bounded(path, gix_index::entry::Stage::Unconflicted, len) + { + index.entries_mut()[pos].flags.insert(gix_index::entry::Flags::REMOVE); + }; + match idx_by_path_stage.entry((stage, path)) { + hash_map::Entry::Occupied(map_entry) => { + // This can happen due to the way the algorithm works. + // The same happens in Git, but it stores the index-related data as part of its deduplicating tree. + // We store each conflict we encounter, which also may duplicate their index entries, sometimes, but + // with different values. The most recent value wins. + // Instead of trying to deduplicate the index entries when the merge runs, we put the cost + // to the tree-assembly - there is no way around it. + let index_entry = &mut index.entries_mut()[*map_entry.get()]; + index_entry.mode = entry.mode.into(); + index_entry.id = entry.id; + } + hash_map::Entry::Vacant(map_entry) => { + map_entry.insert(index.entries().len()); + index.dangerously_push_entry( + Default::default(), + entry.id, + stage.into(), + entry.mode.into(), + path, + ); + } + }; + } + } + + index.sort_entries(); + index.entries().len() != len + } + } +} +pub use apply_index_entries::function::apply_index_entries; diff --git a/gix-merge/src/tree/utils.rs b/gix-merge/src/tree/utils.rs index cd37f809e1e..318916ff981 100644 --- a/gix-merge/src/tree/utils.rs +++ b/gix-merge/src/tree/utils.rs @@ -7,7 +7,10 @@ //! contribute to finding a fix faster. use crate::blob::builtin_driver::binary::Pick; use crate::blob::ResourceKind; -use crate::tree::{Conflict, ConflictMapping, Error, Options, Resolution, ResolutionFailure}; +use crate::tree::{ + Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, Error, Options, Resolution, + ResolutionFailure, +}; use bstr::ByteSlice; use bstr::{BStr, BString, ByteVec}; use gix_diff::tree_with_rewrites::{Change, ChangeRef}; @@ -98,6 +101,14 @@ pub fn perform_blob_merge( where E: Into>, { + if our_id == their_id { + // This can happen if the merge modes are different. + debug_assert_ne!( + our_mode, their_mode, + "BUG: we must think anything has to be merged if the modes and the ids are the same" + ); + return Ok((their_id, crate::blob::Resolution::Complete)); + } if matches!(our_mode.kind(), EntryKind::Link) && matches!(their_mode.kind(), EntryKind::Link) { let (pick, resolution) = crate::blob::builtin_driver::binary(options.symlink_conflicts); let (our_id, their_id) = match outer_side { @@ -544,29 +555,57 @@ impl Conflict { pub(super) fn without_resolution( resolution: ResolutionFailure, changes: (&Change, &Change, ConflictMapping, ConflictMapping), + entries: [Option; 3], ) -> Self { - Conflict::maybe_resolved(Err(resolution), changes) + Conflict::maybe_resolved(Err(resolution), changes, entries) } pub(super) fn with_resolution( resolution: Resolution, changes: (&Change, &Change, ConflictMapping, ConflictMapping), + entries: [Option; 3], ) -> Self { - Conflict::maybe_resolved(Ok(resolution), changes) + Conflict::maybe_resolved(Ok(resolution), changes, entries) } - pub(super) fn maybe_resolved( + fn maybe_resolved( resolution: Result, (ours, theirs, map, outer_map): (&Change, &Change, ConflictMapping, ConflictMapping), + entries: [Option; 3], ) -> Self { Conflict { resolution, ours: ours.clone(), theirs: theirs.clone(), + entries, map: match outer_map { ConflictMapping::Original => map, ConflictMapping::Swapped => map.swapped(), }, } } + + pub(super) fn unknown(changes: (&Change, &Change, ConflictMapping, ConflictMapping)) -> Self { + let (source_mode, source_id) = changes.0.source_entry_mode_and_id(); + let (our_mode, our_id) = changes.0.entry_mode_and_id(); + let (their_mode, their_id) = changes.1.entry_mode_and_id(); + let entries = [ + Some(ConflictIndexEntry { + mode: source_mode, + id: source_id.into(), + path_hint: Some(ConflictIndexEntryPathHint::Source), + }), + Some(ConflictIndexEntry { + mode: our_mode, + id: our_id.into(), + path_hint: Some(ConflictIndexEntryPathHint::Current), + }), + Some(ConflictIndexEntry { + mode: their_mode, + id: their_id.into(), + path_hint: Some(ConflictIndexEntryPathHint::RenamedOrTheirs), + }), + ]; + Conflict::maybe_resolved(Err(ResolutionFailure::Unknown), changes, entries) + } } diff --git a/gix-merge/tests/fixtures/generated-archives/tree-baseline.tar b/gix-merge/tests/fixtures/generated-archives/tree-baseline.tar index bd50e785648..325533a59a8 100644 Binary files a/gix-merge/tests/fixtures/generated-archives/tree-baseline.tar and b/gix-merge/tests/fixtures/generated-archives/tree-baseline.tar differ diff --git a/gix-merge/tests/fixtures/tree-baseline.sh b/gix-merge/tests/fixtures/tree-baseline.sh index 69b103ae986..70394d1d1e2 100644 --- a/gix-merge/tests/fixtures/tree-baseline.sh +++ b/gix-merge/tests/fixtures/tree-baseline.sh @@ -31,6 +31,11 @@ function seq () { done } +function make_conflict_index() { + local identifier=${1:?The first argument is the name of the parent directory along with the output name} + cp .git/index .git/"${identifier}".index +} + function baseline () ( local dir=${1:?the directory to enter} local output_name=${2:?the basename of the output of the merge} @@ -172,6 +177,29 @@ git init rename-add git commit -m "rename foo to bar" ) +git init rename-add-exe-bit-conflict +(cd rename-add-exe-bit-conflict + touch a b + chmod +x a + git add --chmod=+x a + git add b + git commit -m "original" + + git branch A + git branch B + + git checkout A + chmod -x a + git update-index --chmod=-x a + git commit -m "-x a" + + git checkout B + git mv --force b a + chmod +x a + git update-index --chmod=+x a + git commit -m "mv b a; chmod +x a" +) + git init rename-add-symlink (cd rename-add-symlink write_lines original 1 2 3 4 5 >foo @@ -317,6 +345,34 @@ git init super-2 git add foo git mv foo olddir/bar git commit -m "Modify foo & rename foo -> olddir/bar" + + rm .git/index + git update-index --index-info <a/sub/y.f git mv a a-different git commit -am "changed all content, renamed a -> a-different" + +# Git only sees the files with content changes as conflicting, and somehow misses to add the +# bases of the files without content changes. After all, these also have been renamed into +# different places which must be a conflict just as much. + rm .git/index + git update-index --index-info <a/sub/y.f git mv a/sub a/sub-different git commit -am "changed all content, renamed a/sub -> a/sub-different" + +# Here it's the same as above, i.e. Git doesn't list files as conflicting if +# they didn't change, even though they have a conflicting rename. + rm .git/index + git update-index --index-info <a/x.f git mv a a-renamed git commit -am "Git, when branches are reversed, doesn't keep the +x flag on a/w so we specify our own expectation" + # Git sets +x and adds it as conflict, even though the merge is perfect, i.e. one side adds +x on top, perfectly additive. + make_conflict_index same-rename-different-mode-A-B + make_conflict_index same-rename-different-mode-A-B-reversed +) + +git init remove-executable-mode +(cd remove-executable-mode + touch w + chmod +x w + git add --chmod=+x w + git add . && git commit -m "original" + + git branch A + git branch B + + git checkout A + chmod -x w + git update-index --chmod=-x w + git commit -am "remove executable bit from w" + + git checkout B + write_lines 1 2 3 4 5 >w + git commit -am "unrelated change to w" ) git init renamed-symlink-with-conflict @@ -614,6 +787,23 @@ git init submodule-both-modify git add sub tick git commit -m b + + # We cannot handle submodules yet and thus mark them as conflicted, always if they mismatch at least. + rm .git/index + git update-index --index-info <a/x.f git add . && git commit -m "original" @@ -678,8 +868,8 @@ git init big-file-merge git branch B git checkout A - seq 30 >a/x.f - git commit -am "turn normal file into big one (81 bytes)" + seq 37 >a/x.f + git commit -am "turn normal file into big one (102 bytes)" git branch expected git checkout B @@ -803,6 +993,9 @@ git init type-change-to-symlink +# TODO: Git does not detect the conflict (one turns exe off, the other turns it on), and we do exactly the same. +baseline rename-add-exe-bit-conflict A-B A B +baseline remove-executable-mode A-B A B baseline simple side-1-3-without-conflict side1 side3 baseline simple fast-forward side1 main baseline simple no-change main main @@ -838,7 +1031,6 @@ baseline conflicting-rename-2 A-B A B 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" 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" -baseline same-rename-different-mode A-B-diff3 A B "Git works for the A/B case, but for B/A it forgets to set the executable bit" baseline renamed-symlink-with-conflict A-B A B baseline added-file-changed-content-and-mode A-B A B "We improve on executable bit handling, but loose on diff quality as we are definitely missing some tweaks" diff --git a/gix-merge/tests/merge/blob/builtin_driver.rs b/gix-merge/tests/merge/blob/builtin_driver.rs index fd08c0f8140..07b357c6a84 100644 --- a/gix-merge/tests/merge/blob/builtin_driver.rs +++ b/gix-merge/tests/merge/blob/builtin_driver.rs @@ -25,7 +25,7 @@ fn binary() { mod text { use bstr::ByteSlice; - use gix_merge::blob::builtin_driver::text::Conflict; + use gix_merge::blob::builtin_driver::text::{Conflict, ConflictStyle}; use gix_merge::blob::{builtin_driver, Resolution}; use pretty_assertions::assert_str_eq; @@ -160,10 +160,17 @@ mod text { if is_case_diverging(&case, diverging) { num_diverging += 1; } else { - let expected_resolution = if case.expected.contains_str("<<<<<<<") { - Resolution::Conflict + if case.expected.contains_str("<<<<<<<") { + assert_eq!(actual, Resolution::Conflict, "{}: resolution mismatch", case.name,); } else { - Resolution::Complete + assert!( + matches!( + actual, + Resolution::Complete | Resolution::CompleteWithAutoResolvedConflict + ), + "{}: resolution mismatch", + case.name + ); }; assert_str_eq!( out.as_bstr().to_str_lossy(), @@ -173,7 +180,6 @@ mod text { out.as_bstr() ); assert_eq!(out.as_bstr(), case.expected); - assert_eq!(actual, expected_resolution, "{}: resolution mismatch", case.name,); } } @@ -191,6 +197,100 @@ mod text { Ok(()) } + #[test] + fn both_sides_same_changes_are_conflict_free() { + for conflict in [ + builtin_driver::text::Conflict::Keep { + style: ConflictStyle::Merge, + marker_size: 7.try_into().unwrap(), + }, + builtin_driver::text::Conflict::Keep { + style: ConflictStyle::Diff3, + marker_size: 7.try_into().unwrap(), + }, + builtin_driver::text::Conflict::Keep { + style: ConflictStyle::ZealousDiff3, + marker_size: 7.try_into().unwrap(), + }, + builtin_driver::text::Conflict::ResolveWithOurs, + builtin_driver::text::Conflict::ResolveWithTheirs, + builtin_driver::text::Conflict::ResolveWithUnion, + ] { + let options = builtin_driver::text::Options { + conflict, + ..Default::default() + }; + let mut input = imara_diff::intern::InternedInput::default(); + let mut out = Vec::new(); + let actual = builtin_driver::text( + &mut out, + &mut input, + Default::default(), + b"1\n3\nother", + b"1\n2\n3", + b"1\n3\nother", + options, + ); + assert_eq!(actual, Resolution::Complete, "{conflict:?}"); + } + } + + #[test] + fn both_differ_partially_resolution_is_conflicting() { + for (conflict, expected) in [ + ( + builtin_driver::text::Conflict::Keep { + style: ConflictStyle::Merge, + marker_size: 7.try_into().unwrap(), + }, + Resolution::Conflict, + ), + ( + builtin_driver::text::Conflict::Keep { + style: ConflictStyle::Diff3, + marker_size: 7.try_into().unwrap(), + }, + Resolution::Conflict, + ), + ( + builtin_driver::text::Conflict::Keep { + style: ConflictStyle::ZealousDiff3, + marker_size: 7.try_into().unwrap(), + }, + Resolution::Conflict, + ), + ( + builtin_driver::text::Conflict::ResolveWithOurs, + Resolution::CompleteWithAutoResolvedConflict, + ), + ( + builtin_driver::text::Conflict::ResolveWithTheirs, + Resolution::CompleteWithAutoResolvedConflict, + ), + ( + builtin_driver::text::Conflict::ResolveWithUnion, + Resolution::CompleteWithAutoResolvedConflict, + ), + ] { + let options = builtin_driver::text::Options { + conflict, + ..Default::default() + }; + let mut input = imara_diff::intern::InternedInput::default(); + let mut out = Vec::new(); + let actual = builtin_driver::text( + &mut out, + &mut input, + Default::default(), + b"1\n3\nours", + b"1\n2\n3", + b"1\n3\ntheirs", + options, + ); + assert_eq!(actual, expected, "{conflict:?}"); + } + } + mod baseline { use bstr::BString; use gix_merge::blob::builtin_driver::text::{Conflict, ConflictStyle}; diff --git a/gix-merge/tests/merge/blob/platform.rs b/gix-merge/tests/merge/blob/platform.rs index b9df80a8f21..c0d5050240c 100644 --- a/gix-merge/tests/merge/blob/platform.rs +++ b/gix-merge/tests/merge/blob/platform.rs @@ -125,25 +125,25 @@ theirs let res = platform_ref.merge(&mut buf, default_labels(), &Default::default())?; assert_eq!( res, - (Pick::Buffer, Resolution::Complete), - "it's actually unclear now if there ever was a conflict, but we *could* compute it" + (Pick::Buffer, Resolution::CompleteWithAutoResolvedConflict), + "we can determine that there was a conflict, despite the resolution being complete" ); assert_eq!(buf.as_bstr(), "ours"); platform_ref.options.text.conflict = builtin_driver::text::Conflict::ResolveWithTheirs; let res = platform_ref.merge(&mut buf, default_labels(), &Default::default())?; - assert_eq!(res, (Pick::Buffer, Resolution::Complete)); + assert_eq!(res, (Pick::Buffer, Resolution::CompleteWithAutoResolvedConflict)); assert_eq!(buf.as_bstr(), "theirs"); platform_ref.options.text.conflict = builtin_driver::text::Conflict::ResolveWithUnion; let res = platform_ref.merge(&mut buf, default_labels(), &Default::default())?; - assert_eq!(res, (Pick::Buffer, Resolution::Complete)); + assert_eq!(res, (Pick::Buffer, Resolution::CompleteWithAutoResolvedConflict)); assert_eq!(buf.as_bstr(), "ours\ntheirs"); platform_ref.driver = DriverChoice::BuiltIn(BuiltinDriver::Union); platform_ref.options.text.conflict = builtin_driver::text::Conflict::default(); let res = platform_ref.merge(&mut buf, default_labels(), &Default::default())?; - assert_eq!(res, (Pick::Buffer, Resolution::Complete)); + assert_eq!(res, (Pick::Buffer, Resolution::CompleteWithAutoResolvedConflict)); assert_eq!(buf.as_bstr(), "ours\ntheirs"); platform_ref.driver = DriverChoice::BuiltIn(BuiltinDriver::Binary); diff --git a/gix-merge/tests/merge/tree/baseline.rs b/gix-merge/tests/merge/tree/baseline.rs index 18ba8aa0f06..1eddf0211a7 100644 --- a/gix-merge/tests/merge/tree/baseline.rs +++ b/gix-merge/tests/merge/tree/baseline.rs @@ -6,7 +6,7 @@ use gix_object::FindExt; use std::path::{Path, PathBuf}; /// An entry in the conflict -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub struct Entry { /// The relative path in the repository pub location: String, @@ -17,7 +17,7 @@ pub struct Entry { } /// Keep track of all the sides of a conflict. Some might not be set to indicate removal, including the ancestor. -#[derive(Default, Debug)] +#[derive(Default, Debug, Eq, PartialEq)] pub struct Conflict { pub ancestor: Option, pub ours: Option, @@ -129,7 +129,7 @@ impl Iterator for Expectations<'_> { let mut tokens = line.split(' '); let ( Some(subdir), - Some(conflict_style), + Some(conflict_style_name), Some(our_commit_id), Some(our_side_name), Some(their_commit_id), @@ -160,7 +160,7 @@ impl Iterator for Expectations<'_> { }); let subdir_path = self.root.join(subdir); - let conflict_style = match conflict_style { + let conflict_style = match conflict_style_name { "merge" => ConflictStyle::Merge, "diff3" => ConflictStyle::Diff3, unknown => unreachable!("Unknown conflict style: '{unknown}'"), @@ -221,6 +221,10 @@ fn parse_merge_info(content: String) -> MergeInfo { *field = Some(entry); } + if conflict.any_location().is_some() && conflicts.last() != Some(&conflict) { + conflicts.push(conflict); + } + while lines.peek().is_some() { out.information .push(parse_info(&mut lines).expect("if there are lines, it should be valid info")); @@ -285,6 +289,30 @@ fn parse_info<'a>(mut lines: impl Iterator) -> Option { + path: &'a BStr, + id: gix_hash::ObjectId, + mode: gix_index::entry::Mode, + stage: gix_index::entry::Stage, +} + +pub fn clear_entries(state: &gix_index::State) -> Vec> { + state + .entries() + .iter() + .map(|entry| { + let path = entry.path(state); + DebugIndexEntry { + path, + id: entry.id, + mode: entry.mode, + stage: entry.stage(), + } + }) + .collect() +} + pub fn visualize_tree( id: &gix_hash::oid, odb: &impl gix_object::Find, @@ -342,3 +370,35 @@ pub fn show_diff_and_fail( expected.information ); } + +pub(crate) fn apply_git_index_entries(conflicts: &[Conflict], state: &mut gix_index::State) { + let len = state.entries().len(); + for Conflict { ours, theirs, ancestor } in conflicts { + for (entry, stage) in [ + ancestor.as_ref().map(|e| (e, gix_index::entry::Stage::Base)), + ours.as_ref().map(|e| (e, gix_index::entry::Stage::Ours)), + theirs.as_ref().map(|e| (e, gix_index::entry::Stage::Theirs)), + ] + .into_iter() + .flatten() + { + if let Some(pos) = state.entry_index_by_path_and_stage_bounded( + entry.location.as_str().into(), + gix_index::entry::Stage::Unconflicted, + len, + ) { + state.entries_mut()[pos].flags.insert(gix_index::entry::Flags::REMOVE); + } + + state.dangerously_push_entry( + Default::default(), + entry.id, + stage.into(), + entry.mode.into(), + entry.location.as_str().into(), + ); + } + } + state.sort_entries(); + state.remove_entries(|_, _, e| e.flags.contains(gix_index::entry::Flags::REMOVE)); +} diff --git a/gix-merge/tests/merge/tree/mod.rs b/gix-merge/tests/merge/tree/mod.rs index 833166bc61a..662bcc67998 100644 --- a/gix-merge/tests/merge/tree/mod.rs +++ b/gix-merge/tests/merge/tree/mod.rs @@ -1,6 +1,7 @@ use crate::tree::baseline::Deviation; use gix_diff::Rewrites; use gix_merge::commit::Options; +use gix_merge::tree::TreatAsUnresolved; use gix_object::Write; use gix_worktree::stack::state::attributes; use std::path::Path; @@ -20,7 +21,7 @@ fn run_baseline() -> crate::Result { let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?; let cases = std::fs::read_to_string(root.join("baseline.cases"))?; let mut actual_cases = 0; - // let new_test = Some("simple-fast-forward"); + // let new_test = Some("rename-add-symlink-A-B"); let new_test = None; for baseline::Expectation { root, @@ -98,10 +99,58 @@ fn run_baseline() -> crate::Result { ); } } + + let mut actual_index = gix_index::State::from_tree(&actual_id, &odb, Default::default())?; + let expected_index = { + let derivative_index_path = root.join(".git").join(format!("{case_name}.index")); + if derivative_index_path.exists() { + gix_index::File::at( + derivative_index_path, + odb.store().object_hash(), + true, + Default::default(), + )? + .into() + } else { + let mut index = actual_index.clone(); + if let Some(conflicts) = &merge_info.conflicts { + baseline::apply_git_index_entries(conflicts, &mut index); + } + index + } + }; + let conflicts_like_in_git = TreatAsUnresolved::Renames; + let did_change = actual.index_changed_after_applying_conflicts(&mut actual_index, conflicts_like_in_git); + actual_index.remove_entries(|_, _, e| e.flags.contains(gix_index::entry::Flags::REMOVE)); + + pretty_assertions::assert_eq!( + baseline::clear_entries(&actual_index), + baseline::clear_entries(&expected_index), + "{case_name}: index mismatch\n{:#?}\n{:#?}", + actual.conflicts, + merge_info.conflicts + ); + // if case_name.starts_with("submodule-both-modify-A-B") { + if false { + assert!( + !did_change, + "{case_name}: We can't handle submodules, so there is no index change" + ); + assert!( + actual.has_unresolved_conflicts(conflicts_like_in_git), + "{case_name}: submodules currently result in an unresolved (unknown) conflict" + ); + } else { + assert_eq!( + did_change, + actual.has_unresolved_conflicts(conflicts_like_in_git), + "{case_name}: If there is any kind of conflict, the index should have been changed" + ); + } } assert_eq!( - actual_cases, 105, + actual_cases, 107, "BUG: update this number, and don't forget to remove a filter in the end" ); diff --git a/gix-object/src/tree/mod.rs b/gix-object/src/tree/mod.rs index 1fcb1004a2d..f5889b2ea9a 100644 --- a/gix-object/src/tree/mod.rs +++ b/gix-object/src/tree/mod.rs @@ -42,10 +42,16 @@ pub struct Editor<'a> { /// /// Note that even though it can be created from any `u16`, it should be preferable to /// create it by converting [`EntryKind`] into `EntryMode`. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Ord, PartialOrd, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct EntryMode(pub u16); +impl std::fmt::Debug for EntryMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "EntryMode({:#o})", self.0) + } +} + /// A discretized version of ideal and valid values for entry modes. /// /// Note that even though it can represent every valid [mode](EntryMode), it might diff --git a/gix/src/diff.rs b/gix/src/diff.rs index 01361b1bbdd..f1789b1f816 100644 --- a/gix/src/diff.rs +++ b/gix/src/diff.rs @@ -175,7 +175,7 @@ pub(crate) mod utils { new_rewrites_inner(config, lenient, &Diff::RENAMES, &Diff::RENAME_LIMIT) } - pub fn new_rewrites_inner( + pub(crate) fn new_rewrites_inner( config: &gix_config::File<'static>, lenient: bool, renames: &'static crate::config::tree::diff::Renames, diff --git a/gix/src/merge.rs b/gix/src/merge.rs index 978ce0a8003..d7bd1d4996d 100644 --- a/gix/src/merge.rs +++ b/gix/src/merge.rs @@ -107,7 +107,7 @@ pub mod commit { /// pub mod tree { use gix_merge::blob::builtin_driver; - pub use gix_merge::tree::{Conflict, ContentMerge, Resolution, ResolutionFailure, UnresolvedConflict}; + pub use gix_merge::tree::{Conflict, ContentMerge, Resolution, ResolutionFailure, TreatAsUnresolved}; /// The outcome produced by [`Repository::merge_trees()`](crate::Repository::merge_trees()). #[derive(Clone)] @@ -130,9 +130,29 @@ pub mod tree { impl Outcome<'_> { /// Return `true` if there is any conflict that would still need to be resolved as they would yield undesirable trees. /// This is based on `how` to determine what should be considered unresolved. - pub fn has_unresolved_conflicts(&self, how: UnresolvedConflict) -> bool { + pub fn has_unresolved_conflicts(&self, how: TreatAsUnresolved) -> bool { self.conflicts.iter().any(|c| c.is_unresolved(how)) } + + /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a + /// conflict should be considered unresolved. + /// It's important that `index` is at the state of [`Self::tree`]. + /// + /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`. + /// + /// ### Important + /// + /// Also, the unconflicted stage of such entries will be removed merely by setting a flag, so the + /// in-memory entry is still present. + /// One can prune `index` [in-memory](gix_index::State::remove_entries()) or write it to disk, which will + /// cause entries marked for removal not to be persisted. + pub fn index_changed_after_applying_conflicts( + &self, + index: &mut gix_index::State, + how: TreatAsUnresolved, + ) -> bool { + gix_merge::tree::apply_index_entries(&self.conflicts, how, index) + } } /// A way to configure [`Repository::merge_trees()`](crate::Repository::merge_trees()). @@ -206,7 +226,7 @@ pub mod tree { /// If `Some(what-is-unresolved)`, the first unresolved conflict will cause the entire merge to stop. /// This is useful to see if there is any conflict, without performing the whole operation, something /// that can be very relevant during merges that would cause a lot of blob-diffs. - pub fn with_fail_on_conflict(mut self, fail_on_conflict: Option) -> Self { + pub fn with_fail_on_conflict(mut self, fail_on_conflict: Option) -> Self { self.inner.fail_on_conflict = fail_on_conflict; self } diff --git a/gix/src/repository/merge.rs b/gix/src/repository/merge.rs index d501359cacb..4756ac2433b 100644 --- a/gix/src/repository/merge.rs +++ b/gix/src/repository/merge.rs @@ -89,12 +89,20 @@ impl Repository { /// Read all relevant configuration options to instantiate options for use in [`merge_trees()`](Self::merge_trees). pub fn tree_merge_options(&self) -> Result { Ok(gix_merge::tree::Options { - rewrites: crate::diff::utils::new_rewrites_inner( - &self.config.resolved, - self.config.lenient_config, - &tree::Merge::RENAMES, - &tree::Merge::RENAME_LIMIT, - )?, + rewrites: Some( + crate::diff::utils::new_rewrites_inner( + &self.config.resolved, + self.config.lenient_config, + &tree::Merge::RENAMES, + &tree::Merge::RENAME_LIMIT, + )? + .map(Ok) + .or_else(|| { + crate::diff::utils::new_rewrites(&self.config.resolved, self.config.lenient_config).transpose() + }) + .transpose()? + .unwrap_or_default(), + ), blob_merge: self.blob_merge_options()?, blob_merge_command_ctx: self.command_context()?, fail_on_conflict: None, diff --git a/gix/tests/gix/object/tree/diff.rs b/gix/tests/gix/object/tree/diff.rs index cf44645d7cd..92ca7a6a65e 100644 --- a/gix/tests/gix/object/tree/diff.rs +++ b/gix/tests/gix/object/tree/diff.rs @@ -79,35 +79,23 @@ fn changes_against_tree_modified() -> crate::Result { [ Modification { location: "a", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(78981922613b2afb6025042ff6bd878ac1994e85), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(b4f17b61de71d9b2e54ac9e62b1629ae2d97a6a7), }, Modification { location: "dir", - previous_entry_mode: EntryMode( - 16384, - ), + previous_entry_mode: EntryMode(0o40000), previous_id: Sha1(e5c63aefe4327cb1c780c71966b678ce8e4225da), - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(c7ac5f82f536976f3561c9999b5f11e5893358be), }, Modification { location: "dir/c", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(6695780ceb14b05e076a99bbd2babf34723b3464), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), }, ] @@ -227,9 +215,7 @@ mod track_rewrites { [ Rewrite { source_location: "cli/src/commands/cat.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(f09e8b0e6bf963d8d6d5b578fea48ff4c9b723fb), diff: Some( @@ -241,9 +227,7 @@ mod track_rewrites { similarity: 0.900421, }, ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(081093be2ba0d2be62d14363f43859355bee2aa2), location: "cli/src/commands/file/print.rs", relation: Some( @@ -255,9 +239,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_cat_command.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(8c80364c37b7fc364778efb4214575536e6a1df4), diff: Some( @@ -269,9 +251,7 @@ mod track_rewrites { similarity: 0.77923656, }, ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(45bb2cf6b7fa96a39c95301f619ca3e4cc3eb0f3), location: "cli/tests/test_file_print_command.rs", relation: None, @@ -279,9 +259,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_chmod_command.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(c24ae8e04f53b84e09838d232b3e8c0167ccc010), diff: Some( @@ -293,9 +271,7 @@ mod track_rewrites { similarity: 0.88720536, }, ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(8defe631bc82bf35a53cd25083f85664516f412f), location: "cli/tests/test_file_chmod_command.rs", relation: None, @@ -303,9 +279,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/src/commands/chmod.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Sha1(8f55dec5b81779d23539fa7146d713cc42df70f4), diff: Some( @@ -317,9 +291,7 @@ mod track_rewrites { similarity: 0.9060576, }, ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(94f78deb408d181ccea9da574d0e45ac32a98092), location: "cli/src/commands/file/chmod.rs", relation: Some( @@ -331,13 +303,9 @@ mod track_rewrites { }, Modification { location: "CHANGELOG.md", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(f4cb24f79ec2549a3a8a5028d4c43d953f74137d), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(5a052b7fb0919218b2ecddffbb341277bd443a5c), }, Addition { @@ -347,141 +315,91 @@ mod track_rewrites { 1, ), ), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(d67f782327ea286136b8532eaf9a509806a87e83), }, Modification { location: "cli/src/commands/mod.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(e7e8c4f00412aa9bc9898f396ef9a7597aa64756), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e3a9ec4524d27aa7035a38fd7c5db414809623c4), }, Modification { location: "cli/tests/cli-reference@.md.snap", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(5c1985fc3c89a8d0edaedc23f76feb7f5c4cc962), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(92853cde19b20cadd74113ea3566c87d4def591b), }, Modification { location: "cli/tests/runner.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(a008cb19a57bd44a5a054fced38384b09c9243fc), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(5253f0ff160e8b7001a7bd271ca4a07968ff81a3), }, Modification { location: "cli/tests/test_acls.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(e7e8f15d7f4c0c50aad13b0f82a632e3d55c33c6), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(f644e4c8dd0be6fbe5493b172ce10839bcd9e25c), }, Modification { location: "cli/tests/test_diffedit_command.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(85e7db4f01d8be8faa7a020647273399f815f597), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(fd57f61e92d4d49b4920c08c3522c066cb03ecd2), }, Modification { location: "cli/tests/test_fix_command.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(16ab056981c9ca40cdd4d298feb70510cc3ced37), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e0baefc79038fed0bcf56f2d8c3588a26d5bf985), }, Modification { location: "cli/tests/test_global_opts.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(44f49aec05b7dc920cf1f1a554016e74b06ee1c8), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(a0c0340e495fa759c0b705dd46cee322aa0d80c8), }, Modification { location: "cli/tests/test_immutable_commits.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(ba61cefef4328f126283f25935aab2d04ae2016e), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(3d7598b4e4c570eef701f40853ef3e3b0fb224f7), }, Modification { location: "cli/tests/test_move_command.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(cbd36dbc76760ed41c968f369b470b45c176dabe), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(ac9ad5761637cd731abe1bf4a075fedda7bfc61f), }, Modification { location: "cli/tests/test_new_command.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(3e03295d9b4654adccb6cd625376c36d4d38fb3d), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(a03b50a8a9c23c68d641b51b7c887ea088cd0d2b), }, Modification { location: "cli/tests/test_squash_command.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(f921d5bc423586194bd73419f9814ff072212faa), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(ff1c247d4312adb5b372c6d9ff93fa71846ca527), }, Modification { location: "cli/tests/test_unsquash_command.rs", - previous_entry_mode: EntryMode( - 33188, - ), + previous_entry_mode: EntryMode(0o100644), previous_id: Sha1(0dcc138981223171df13d35444c7aaee4b502c6f), - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(b8b29cc0ca0176fafaa97c7421a10ed116bcba8a), }, ] @@ -512,9 +430,7 @@ mod track_rewrites { [ Rewrite { source_location: "cli", - source_entry_mode: EntryMode( - 16384, - ), + source_entry_mode: EntryMode(0o40000), source_relation: Some( Parent( 2, @@ -522,9 +438,7 @@ mod track_rewrites { ), source_id: Sha1(f203064a6a81df47498fb415a2064a8ec568ed67), diff: None, - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(f203064a6a81df47498fb415a2064a8ec568ed67), location: "c", relation: Some( @@ -536,9 +450,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/src/commands/file/print.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -546,9 +458,7 @@ mod track_rewrites { ), source_id: Sha1(081093be2ba0d2be62d14363f43859355bee2aa2), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(081093be2ba0d2be62d14363f43859355bee2aa2), location: "c/src/commands/file/print.rs", relation: Some( @@ -560,9 +470,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/src/commands/file", - source_entry_mode: EntryMode( - 16384, - ), + source_entry_mode: EntryMode(0o40000), source_relation: Some( ChildOfParent( 2, @@ -570,9 +478,7 @@ mod track_rewrites { ), source_id: Sha1(0f3bc154b577b84fb5ce31383e25acc99c2f24a5), diff: None, - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(0f3bc154b577b84fb5ce31383e25acc99c2f24a5), location: "c/src/commands/file", relation: Some( @@ -584,9 +490,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests", - source_entry_mode: EntryMode( - 16384, - ), + source_entry_mode: EntryMode(0o40000), source_relation: Some( ChildOfParent( 2, @@ -594,9 +498,7 @@ mod track_rewrites { ), source_id: Sha1(17be3b367831653883a36a2f2a8dea418b8d96b7), diff: None, - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(17be3b367831653883a36a2f2a8dea418b8d96b7), location: "c/tests", relation: Some( @@ -608,9 +510,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_immutable_commits.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -618,9 +518,7 @@ mod track_rewrites { ), source_id: Sha1(3d7598b4e4c570eef701f40853ef3e3b0fb224f7), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(3d7598b4e4c570eef701f40853ef3e3b0fb224f7), location: "c/tests/test_immutable_commits.rs", relation: Some( @@ -632,9 +530,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_file_print_command.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -642,9 +538,7 @@ mod track_rewrites { ), source_id: Sha1(45bb2cf6b7fa96a39c95301f619ca3e4cc3eb0f3), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(45bb2cf6b7fa96a39c95301f619ca3e4cc3eb0f3), location: "c/tests/test_file_print_command.rs", relation: Some( @@ -656,9 +550,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/runner.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -666,9 +558,7 @@ mod track_rewrites { ), source_id: Sha1(5253f0ff160e8b7001a7bd271ca4a07968ff81a3), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(5253f0ff160e8b7001a7bd271ca4a07968ff81a3), location: "c/tests/runner.rs", relation: Some( @@ -680,9 +570,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/src", - source_entry_mode: EntryMode( - 16384, - ), + source_entry_mode: EntryMode(0o40000), source_relation: Some( ChildOfParent( 2, @@ -690,9 +578,7 @@ mod track_rewrites { ), source_id: Sha1(80e5b08f25f75c2050afbcb794e8434f4cf082f1), diff: None, - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(80e5b08f25f75c2050afbcb794e8434f4cf082f1), location: "c/src", relation: Some( @@ -704,9 +590,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_file_chmod_command.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -714,9 +598,7 @@ mod track_rewrites { ), source_id: Sha1(8defe631bc82bf35a53cd25083f85664516f412f), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(8defe631bc82bf35a53cd25083f85664516f412f), location: "c/tests/test_file_chmod_command.rs", relation: Some( @@ -728,9 +610,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/cli-reference@.md.snap", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -738,9 +618,7 @@ mod track_rewrites { ), source_id: Sha1(92853cde19b20cadd74113ea3566c87d4def591b), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(92853cde19b20cadd74113ea3566c87d4def591b), location: "c/tests/cli-reference@.md.snap", relation: Some( @@ -752,9 +630,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/src/commands/file/chmod.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -762,9 +638,7 @@ mod track_rewrites { ), source_id: Sha1(94f78deb408d181ccea9da574d0e45ac32a98092), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(94f78deb408d181ccea9da574d0e45ac32a98092), location: "c/src/commands/file/chmod.rs", relation: Some( @@ -776,9 +650,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_new_command.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -786,9 +658,7 @@ mod track_rewrites { ), source_id: Sha1(a03b50a8a9c23c68d641b51b7c887ea088cd0d2b), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(a03b50a8a9c23c68d641b51b7c887ea088cd0d2b), location: "c/tests/test_new_command.rs", relation: Some( @@ -800,9 +670,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_global_opts.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -810,9 +678,7 @@ mod track_rewrites { ), source_id: Sha1(a0c0340e495fa759c0b705dd46cee322aa0d80c8), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(a0c0340e495fa759c0b705dd46cee322aa0d80c8), location: "c/tests/test_global_opts.rs", relation: Some( @@ -824,9 +690,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_move_command.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -834,9 +698,7 @@ mod track_rewrites { ), source_id: Sha1(ac9ad5761637cd731abe1bf4a075fedda7bfc61f), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(ac9ad5761637cd731abe1bf4a075fedda7bfc61f), location: "c/tests/test_move_command.rs", relation: Some( @@ -848,9 +710,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_unsquash_command.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -858,9 +718,7 @@ mod track_rewrites { ), source_id: Sha1(b8b29cc0ca0176fafaa97c7421a10ed116bcba8a), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(b8b29cc0ca0176fafaa97c7421a10ed116bcba8a), location: "c/tests/test_unsquash_command.rs", relation: Some( @@ -872,9 +730,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/src/commands/file/mod.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -882,9 +738,7 @@ mod track_rewrites { ), source_id: Sha1(d67f782327ea286136b8532eaf9a509806a87e83), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(d67f782327ea286136b8532eaf9a509806a87e83), location: "c/src/commands/file/mod.rs", relation: Some( @@ -896,9 +750,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_fix_command.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -906,9 +758,7 @@ mod track_rewrites { ), source_id: Sha1(e0baefc79038fed0bcf56f2d8c3588a26d5bf985), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e0baefc79038fed0bcf56f2d8c3588a26d5bf985), location: "c/tests/test_fix_command.rs", relation: Some( @@ -920,9 +770,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/src/commands/mod.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -930,9 +778,7 @@ mod track_rewrites { ), source_id: Sha1(e3a9ec4524d27aa7035a38fd7c5db414809623c4), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(e3a9ec4524d27aa7035a38fd7c5db414809623c4), location: "c/src/commands/mod.rs", relation: Some( @@ -944,9 +790,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/src/commands", - source_entry_mode: EntryMode( - 16384, - ), + source_entry_mode: EntryMode(0o40000), source_relation: Some( ChildOfParent( 2, @@ -954,9 +798,7 @@ mod track_rewrites { ), source_id: Sha1(f414de88468352d59c129d0e7686fb1e1f387929), diff: None, - entry_mode: EntryMode( - 16384, - ), + entry_mode: EntryMode(0o40000), id: Sha1(f414de88468352d59c129d0e7686fb1e1f387929), location: "c/src/commands", relation: Some( @@ -968,9 +810,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_acls.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -978,9 +818,7 @@ mod track_rewrites { ), source_id: Sha1(f644e4c8dd0be6fbe5493b172ce10839bcd9e25c), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(f644e4c8dd0be6fbe5493b172ce10839bcd9e25c), location: "c/tests/test_acls.rs", relation: Some( @@ -992,9 +830,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_diffedit_command.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -1002,9 +838,7 @@ mod track_rewrites { ), source_id: Sha1(fd57f61e92d4d49b4920c08c3522c066cb03ecd2), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(fd57f61e92d4d49b4920c08c3522c066cb03ecd2), location: "c/tests/test_diffedit_command.rs", relation: Some( @@ -1016,9 +850,7 @@ mod track_rewrites { }, Rewrite { source_location: "cli/tests/test_squash_command.rs", - source_entry_mode: EntryMode( - 33188, - ), + source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, @@ -1026,9 +858,7 @@ mod track_rewrites { ), source_id: Sha1(ff1c247d4312adb5b372c6d9ff93fa71846ca527), diff: None, - entry_mode: EntryMode( - 33188, - ), + entry_mode: EntryMode(0o100644), id: Sha1(ff1c247d4312adb5b372c6d9ff93fa71846ca527), location: "c/tests/test_squash_command.rs", relation: Some( diff --git a/gix/tests/gix/repository/merge.rs b/gix/tests/gix/repository/merge.rs new file mode 100644 index 00000000000..9aa117c0126 --- /dev/null +++ b/gix/tests/gix/repository/merge.rs @@ -0,0 +1,13 @@ +use crate::util::named_repo; + +#[test] +fn tree_merge_options() -> crate::Result { + let repo = named_repo("make_basic_repo.sh")?; + let opts: gix::merge::plumbing::tree::Options = repo.tree_merge_options()?.into(); + assert_eq!( + opts.rewrites, + Some(gix::diff::Rewrites::default()), + "If merge options aren't set, it defaults to diff options, and these default to doing rename tracking" + ); + Ok(()) +} diff --git a/gix/tests/gix/repository/mod.rs b/gix/tests/gix/repository/mod.rs index e9d1df4e4ab..37a6bc73fcd 100644 --- a/gix/tests/gix/repository/mod.rs +++ b/gix/tests/gix/repository/mod.rs @@ -5,6 +5,8 @@ mod config; mod excludes; #[cfg(feature = "attributes")] mod filter; +#[cfg(feature = "merge")] +mod merge; mod object; mod open; #[cfg(feature = "attributes")] diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 409ea6a10b8..046de6e76da 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -174,6 +174,7 @@ pub fn main() -> Result<()> { merge::SubCommands::Tree { in_memory, file_favor, + debug, ours, base, theirs, @@ -196,6 +197,7 @@ pub fn main() -> Result<()> { format, file_favor: file_favor.map(Into::into), in_memory, + debug, }, ) }, @@ -203,6 +205,7 @@ pub fn main() -> Result<()> { merge::SubCommands::Commit { in_memory, file_favor, + debug, ours, theirs, } => prepare_and_run( @@ -223,6 +226,7 @@ pub fn main() -> Result<()> { format, file_favor: file_favor.map(Into::into), in_memory, + debug, }, ) }, diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index 546b02c1622..d2cd78850cf 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -420,6 +420,9 @@ pub mod merge { /// Decide how to resolve content conflicts in files. If unset, write conflict markers and fail. #[clap(long, short = 'f')] file_favor: Option, + /// Print additional information about conflicts for debugging. + #[clap(long, short = 'd')] + debug: bool, /// A revspec to our treeish. #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)] @@ -441,6 +444,9 @@ pub mod merge { /// Decide how to resolve content conflicts in files. If unset, write conflict markers and fail. #[clap(long, short = 'f')] file_favor: Option, + /// Print additional information about conflicts for debugging. + #[clap(long, short = 'd')] + debug: bool, /// A revspec to our committish. #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]