From 09213bc1b2aa725af1571dff040415772e844c3a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 8 Nov 2024 13:14:55 +0100 Subject: [PATCH 01/12] feat: when blob-merging, clarify if something would have conflicted. --- .../src/blob/builtin_driver/text/function.rs | 19 ++- gix-merge/src/blob/mod.rs | 10 +- gix-merge/tests/merge/blob/builtin_driver.rs | 110 +++++++++++++++++- gix-merge/tests/merge/blob/platform.rs | 10 +- 4 files changed, 129 insertions(+), 20 deletions(-) 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/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); From aff76f291a52fc6806944d72d249a8bd1b804c39 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 8 Nov 2024 15:08:12 +0100 Subject: [PATCH 02/12] feat!: Add more modes for checking for unresolved conflicts. They aim at making it possible to know if a conflict happened that was automatically resolved. --- gix-merge/src/commit/virtual_merge_base.rs | 4 +- gix-merge/src/tree/mod.rs | 59 ++++++++++++++++------ 2 files changed, 45 insertions(+), 18 deletions(-) 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/mod.rs b/gix-merge/src/tree/mod.rs index f1f52146481..37a6453ea82 100644 --- a/gix-merge/src/tree/mod.rs +++ b/gix-merge/src/tree/mod.rs @@ -47,20 +47,38 @@ 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)) } } @@ -109,20 +127,29 @@ 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 { .. } => true, Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => { - matches!(merged_blob.resolution, crate::blob::Resolution::Conflict) + content_merge_matches(merged_blob) } }, Err(_failure) => true, @@ -264,7 +291,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. From 47110d600c5b02f82b4aad69ec838865e1887c0a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 8 Nov 2024 15:18:16 +0100 Subject: [PATCH 03/12] adapt to changes in `gix-merge`. --- gitoxide-core/src/repository/merge/commit.rs | 4 ++-- gitoxide-core/src/repository/merge/tree.rs | 4 ++-- gix/src/merge.rs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gitoxide-core/src/repository/merge/commit.rs b/gitoxide-core/src/repository/merge/commit.rs index a50876ac3f4..59b1aae7615 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; @@ -48,7 +48,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; diff --git a/gitoxide-core/src/repository/merge/tree.rs b/gitoxide-core/src/repository/merge/tree.rs index edc7820819f..0948465d826 100644 --- a/gitoxide-core/src/repository/merge/tree.rs +++ b/gitoxide-core/src/repository/merge/tree.rs @@ -12,7 +12,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; @@ -62,7 +62,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; diff --git a/gix/src/merge.rs b/gix/src/merge.rs index 978ce0a8003..f1c7f8c9521 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,7 +130,7 @@ 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)) } } @@ -206,7 +206,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 } From 36aaf1731a0b818494298f4a67f5b8eae14d0dbe Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 11 Nov 2024 07:00:43 +0100 Subject: [PATCH 04/12] add a note about remote-helpers in crate-status (#1666) `gix-protocol` should support it one day, probably with a crate implementing the protocol, which is then used from the functionsin the crate. Ideally abstracting it fully. --- crate-status.md | 1 + 1 file changed, 1 insertion(+) diff --git a/crate-status.md b/crate-status.md index b0316d7bf5c..9dcf98f1248 100644 --- a/crate-status.md +++ b/crate-status.md @@ -462,6 +462,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 From 63978f6e54cf48e7b5afe65dede851890a163ebd Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 19 Nov 2024 11:27:36 +0100 Subject: [PATCH 05/12] feat: `entry::Stage` can now be converted into `entry::Flags`. This makes it easier to change the stage of an entry. --- gix-index/src/decode/entries.rs | 4 +--- gix-index/src/entry/flags.rs | 11 +++++++++++ gix-index/tests/index/entry/mod.rs | 13 +++++++++++++ gix-index/tests/index/entry/mode.rs | 15 +++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) 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" + ); +} From e00e454ed6d6152d7926e4bd23e1bc2fcc628de0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 20 Nov 2024 20:59:32 +0100 Subject: [PATCH 06/12] fix: `tree()` diffing now also recognizes merge mode changes as modification. --- gix-diff/src/tree/function.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From cd61c25369d3e39b6160bac4b332b177dabddf4b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 22 Nov 2024 09:32:19 +0100 Subject: [PATCH 07/12] feat: octal Debug representation of `tree::EntryMode`. This makes it easier to reason about. --- gix-diff/tests/diff/tree_with_rewrites.rs | 532 ++++++---------------- gix-object/src/tree/mod.rs | 8 +- 2 files changed, 140 insertions(+), 400 deletions(-) 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-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 From 0a5993f4ed6e3815949b3cd398d38d69417d21c8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 23 Nov 2024 16:08:54 +0100 Subject: [PATCH 08/12] feat: add `gix merge commit --debug` This yields additional debugging information with details about detected conflicts. --- gitoxide-core/src/repository/merge/commit.rs | 4 ++++ gitoxide-core/src/repository/merge/tree.rs | 5 +++++ src/plumbing/main.rs | 4 ++++ src/plumbing/options/mod.rs | 6 ++++++ 4 files changed, 19 insertions(+) diff --git a/gitoxide-core/src/repository/merge/commit.rs b/gitoxide-core/src/repository/merge/commit.rs index 59b1aae7615..e95d4803a3c 100644 --- a/gitoxide-core/src/repository/merge/commit.rs +++ b/gitoxide-core/src/repository/merge/commit.rs @@ -18,6 +18,7 @@ pub fn commit( format, file_favor, in_memory, + debug, }: Options, ) -> anyhow::Result<()> { if format != OutputFormat::Human { @@ -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 0948465d826..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 { @@ -29,6 +30,7 @@ pub(super) mod function { format, file_favor, in_memory, + debug, }: Options, ) -> anyhow::Result<()> { if format != OutputFormat::Human { @@ -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/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)] From b57be7189df216110adb52aa51c50a0f38692f2a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 23 Nov 2024 16:27:28 +0100 Subject: [PATCH 09/12] fix: `Repository::tree_merge_options()` now comes with rewrite tracking. This is the way Git acts, as it's either configured, or defaults to the value coming from the `diff.renames` configuration. --- gix/src/diff.rs | 2 +- gix/src/repository/merge.rs | 20 +- gix/tests/gix/object/tree/diff.rs | 340 ++++++++---------------------- gix/tests/gix/repository/merge.rs | 13 ++ gix/tests/gix/repository/mod.rs | 2 + 5 files changed, 115 insertions(+), 262 deletions(-) create mode 100644 gix/tests/gix/repository/merge.rs 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/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")] From 5db2d2d8eef8570a42f9c040a7cdb341a71f7192 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 23 Nov 2024 20:51:50 +0100 Subject: [PATCH 10/12] feat: add `tree_with_rewrites::Change(Ref)::source_mode_and_id|mode_and_id()` --- gix-diff/src/tree_with_rewrites/change.rs | 56 +++++++++++++++++++++++ 1 file changed, 56 insertions(+) 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 { From 3ee8b62dd025d6fdb0d9929dec7a561fa576f545 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 19 Nov 2024 11:42:32 +0100 Subject: [PATCH 11/12] feat: provide a way to record and apply index changes. These changes will then be applicable to an index that is created from the written tree editor. --- Cargo.lock | 1 + crate-status.md | 5 +- gix-merge/Cargo.toml | 1 + gix-merge/src/tree/function.rs | 212 +++++++++++++++--- gix-merge/src/tree/mod.rs | 192 +++++++++++++++- gix-merge/src/tree/utils.rs | 47 +++- .../generated-archives/tree-baseline.tar | Bin 2789376 -> 2840576 bytes gix-merge/tests/fixtures/tree-baseline.sh | 200 ++++++++++++++++- gix-merge/tests/merge/tree/baseline.rs | 68 +++++- gix-merge/tests/merge/tree/mod.rs | 53 ++++- 10 files changed, 729 insertions(+), 50 deletions(-) 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 9dcf98f1248..0774895b67a 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,8 +351,7 @@ 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*) * [x] **commits** - with handling of multiple merge bases by recursive merge-base merge * [x] API documentation 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/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 37a6453ea82..f6a534e916e 100644 --- a/gix-merge/src/tree/mod.rs +++ b/gix-merge/src/tree/mod.rs @@ -81,6 +81,17 @@ impl Outcome<'_> { 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()). @@ -99,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 { @@ -147,7 +192,11 @@ impl Conflict { TreatAsUnresolved::Renames | TreatAsUnresolved::RenamesAndAutoResolvedContent => match &self.resolution { Ok(success) => match success { Resolution::SourceLocationAffectedByRename { .. } => false, - Resolution::OursModifiedTheirsRenamedAndChangedThenRename { .. } => true, + Resolution::OursModifiedTheirsRenamedAndChangedThenRename { + merged_blob, + final_location, + .. + } => final_location.is_some() || merged_blob.as_ref().map_or(false, content_merge_matches), Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => { content_merge_matches(merged_blob) } @@ -178,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 { @@ -308,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 bd50e785648cce991953eb04acf30de922a6d063..325533a59a83b71145d73e245f284b601995841b 100644 GIT binary patch delta 50125 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 delta 101573 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

6eVpgZ)5vatHDhI7Z-`QtGE4nuI`EYjt16YM zQZ=Y5m8wz|%qmb-s#ekMdGH}srBYQYRi(PyDwQ|hhR|vYdJSYmww;Z$* z6RV70K}?K56+xo41c{JxIoPf$4!CrB{0bst3itvW6UyZkZ|N4?fsD}Wf&9_Im7;VC_IRXcMqreZt$-3Gi1Ems!CVY^27^XDo)SkqQjswS!!VnNXaq7z z2YaMDX^Tg#7nVypXr*!9i3c|my#p24Z%8P9eRIOIC{ACMa0A?>H~}t*%-N84PaO?U zSgH@e)zBcrsZcP;4nqQyy>?p>S;U`yVtrx)YTP>p(J`c+4jxOkn?E+gAJBT$Y=Xz5Stna>n7`1<38whJ zBU#;{ES*M{Yt5sH8Y72(XXSU)R~Jgg`>OM;Ay~S%n9;Nu7SSDy0xm#rRrR&&ZX*H zs?MeAT&m8c>RfkK=jxi~Ns*jeLx5*nG(b%(!Js8UtL(p$wU6_ybt1!1#JI+IHd3~S z5LRN0%i>wa85eIU`TKPrEGjPr1e7wzIUiUjy6=$lfzNGya2ApuZ0z-R&l;ng(BSC_ z^tONeVk<DsyJScH#>%ajXnZ&^?P2!qo zF%IqWn3O;yeuxtU&tgHg-SmLaB%Jn`(Dg<1!1FkyVl`#|F3(;eU_0c|vGmpe<{WGj zJ=^X98%o>j0mT`pVWC*bwEi^ad5kH!>?{T;Y$j`ICR^Af%UPflKSZ4!VaT0Pm7tZn zx#Vfl%Q3oatxh%{&Jqxm6r2627Iz>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/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" ); From 71b0ceaf02e022e83e6c24cfd0bdc26299dc95a0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 24 Nov 2024 09:05:51 +0100 Subject: [PATCH 12/12] feat: Add support for `index` application in merge results via `merge::tree::Outcome::index_changed_after_applying_conflicts()` --- crate-status.md | 1 + gix/src/merge.rs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/crate-status.md b/crate-status.md index 0774895b67a..9027fb8240d 100644 --- a/crate-status.md +++ b/crate-status.md @@ -353,6 +353,7 @@ Check out the [performance discussion][gix-diff-performance] as well. * [x] **tree**-diff-heuristics match Git for its test-cases - [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 diff --git a/gix/src/merge.rs b/gix/src/merge.rs index f1c7f8c9521..d7bd1d4996d 100644 --- a/gix/src/merge.rs +++ b/gix/src/merge.rs @@ -133,6 +133,26 @@ pub mod tree { 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()).