From b25fe4d052250ddace9a118b2247537b2a7fa09e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 1 Oct 2024 11:12:25 +0200 Subject: [PATCH 01/13] Add some performance traces for blob-merges --- gix-merge/src/blob/platform/merge.rs | 4 ++++ gix-merge/src/blob/platform/set_resource.rs | 1 + 2 files changed, 5 insertions(+) diff --git a/gix-merge/src/blob/platform/merge.rs b/gix-merge/src/blob/platform/merge.rs index 1c2ec1a48b4..cd803006268 100644 --- a/gix-merge/src/blob/platform/merge.rs +++ b/gix-merge/src/blob/platform/merge.rs @@ -376,6 +376,10 @@ impl<'parent> PlatformRef<'parent> { labels: builtin_driver::text::Labels<'_>, context: gix_command::Context, ) -> Result<(inner::builtin_merge::Pick, Resolution), Error> { + let _span = gix_trace::coarse!( + "gix_merge::blob::PlatformRef::merge()", + current_rela_path = %self.current.rela_path + ); match self.configured_driver() { Ok(driver) => { let mut cmd = self.prepare_external_driver(driver.command.clone(), labels, context)?; diff --git a/gix-merge/src/blob/platform/set_resource.rs b/gix-merge/src/blob/platform/set_resource.rs index 377642e2355..c3dae8ffd4d 100644 --- a/gix-merge/src/blob/platform/set_resource.rs +++ b/gix-merge/src/blob/platform/set_resource.rs @@ -46,6 +46,7 @@ impl Platform { kind: ResourceKind, objects: &impl gix_object::FindObjectOrHeader, ) -> Result<(), Error> { + gix_trace::detail!("gix_merge::blob::Platform::set_resource()", %id, %rela_path); if !matches!( mode, gix_object::tree::EntryKind::Blob | gix_object::tree::EntryKind::BlobExecutable From cebef0bbfb6b64ce9c1a3ea3b8fd36e6121b1f1c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 2 Oct 2024 09:13:00 +0200 Subject: [PATCH 02/13] feat!: better handling and tracking of directory renames Deletations that are caused by deletions higher up will now carry IDs to associate them with each other. This allows to bundle them at a later stage to re-obtain directory deletions. --- gix-diff/src/tree/changes.rs | 182 +++++++++---- gix-diff/src/tree/mod.rs | 30 ++- gix-diff/src/tree/recorder.rs | 23 +- gix-diff/src/tree/visit.rs | 61 +++-- .../generated-archives/make_diff_repo.tar | Bin 140288 -> 150016 bytes .../generated-archives/make_diff_repo_a.tar | Bin 169984 -> 180224 bytes gix-diff/tests/fixtures/make_diff_repo.sh | 7 +- gix-diff/tests/tree/mod.rs | 251 +++++++++++++----- 8 files changed, 409 insertions(+), 145 deletions(-) diff --git a/gix-diff/src/tree/changes.rs b/gix-diff/src/tree/changes.rs index 4391f4fe0cd..f7c545526ef 100644 --- a/gix-diff/src/tree/changes.rs +++ b/gix-diff/src/tree/changes.rs @@ -2,9 +2,10 @@ use std::{borrow::BorrowMut, collections::VecDeque}; use gix_object::{tree::EntryRef, FindExt}; +use crate::tree::visit::{ChangeId, Relation}; use crate::{ tree, - tree::{visit::Change, TreeInfoPair}, + tree::{visit::Change, TreeInfoTuple}, }; /// The error returned by [`tree::Changes::needed_to_obtain()`]. @@ -43,21 +44,21 @@ impl<'a> tree::Changes<'a> { /// /// [git_cmp_c]: https://github.com/git/git/blob/311531c9de557d25ac087c1637818bd2aad6eb3a/tree-diff.c#L49:L65 /// [git_cmp_rs]: https://github.com/Byron/gitoxide/blob/a4d5f99c8dc99bf814790928a3bf9649cd99486b/gix-object/src/mutable/tree.rs#L52-L55 - pub fn needed_to_obtain( + pub fn needed_to_obtain( mut self, other: gix_object::TreeRefIter<'_>, mut state: StateMut, objects: impl gix_object::Find, - delegate: &mut R, + delegate: &mut impl tree::Visit, ) -> Result<(), Error> where - R: tree::Visit, StateMut: BorrowMut, { let state = state.borrow_mut(); state.clear(); let mut lhs_entries = peekable(self.0.take().unwrap_or_default()); let mut rhs_entries = peekable(other); + let mut relation = None; let mut pop_path = false; loop { @@ -69,20 +70,23 @@ impl<'a> tree::Changes<'a> { match (lhs_entries.next(), rhs_entries.next()) { (None, None) => { match state.trees.pop_front() { - Some((None, Some(rhs))) => { + Some((None, Some(rhs), relation_to_propagate)) => { delegate.pop_front_tracked_path_and_set_current(); + relation = relation_to_propagate; rhs_entries = peekable(objects.find_tree_iter(&rhs, &mut state.buf2)?); } - Some((Some(lhs), Some(rhs))) => { + Some((Some(lhs), Some(rhs), relation_to_propagate)) => { delegate.pop_front_tracked_path_and_set_current(); lhs_entries = peekable(objects.find_tree_iter(&lhs, &mut state.buf1)?); rhs_entries = peekable(objects.find_tree_iter(&rhs, &mut state.buf2)?); + relation = relation_to_propagate; } - Some((Some(lhs), None)) => { + Some((Some(lhs), None, relation_to_propagate)) => { delegate.pop_front_tracked_path_and_set_current(); lhs_entries = peekable(objects.find_tree_iter(&lhs, &mut state.buf1)?); + relation = relation_to_propagate; } - Some((None, None)) => unreachable!("BUG: it makes no sense to fill the stack with empties"), + Some((None, None, _)) => unreachable!("BUG: it makes no sense to fill the stack with empties"), None => return Ok(()), }; pop_path = false; @@ -91,18 +95,41 @@ impl<'a> tree::Changes<'a> { use std::cmp::Ordering::*; let (lhs, rhs) = (lhs?, rhs?); match compare(&lhs, &rhs) { - Equal => handle_lhs_and_rhs_with_equal_filenames(lhs, rhs, &mut state.trees, delegate)?, - Less => catchup_lhs_with_rhs(&mut lhs_entries, lhs, rhs, &mut state.trees, delegate)?, - Greater => catchup_rhs_with_lhs(&mut rhs_entries, lhs, rhs, &mut state.trees, delegate)?, + Equal => handle_lhs_and_rhs_with_equal_filenames( + lhs, + rhs, + &mut state.trees, + &mut state.change_id, + relation, + delegate, + )?, + Less => catchup_lhs_with_rhs( + &mut lhs_entries, + lhs, + rhs, + &mut state.trees, + &mut state.change_id, + relation, + delegate, + )?, + Greater => catchup_rhs_with_lhs( + &mut rhs_entries, + lhs, + rhs, + &mut state.trees, + &mut state.change_id, + relation, + delegate, + )?, } } (Some(lhs), None) => { let lhs = lhs?; - delete_entry_schedule_recursion(lhs, &mut state.trees, delegate)?; + delete_entry_schedule_recursion(lhs, &mut state.trees, &mut state.change_id, relation, delegate)?; } (None, Some(rhs)) => { let rhs = rhs?; - add_entry_schedule_recursion(rhs, &mut state.trees, delegate)?; + add_entry_schedule_recursion(rhs, &mut state.trees, &mut state.change_id, relation, delegate)?; } } } @@ -118,39 +145,57 @@ fn compare(a: &EntryRef<'_>, b: &EntryRef<'_>) -> std::cmp::Ordering { }) } -fn delete_entry_schedule_recursion( +fn delete_entry_schedule_recursion( entry: EntryRef<'_>, - queue: &mut VecDeque, - delegate: &mut R, + queue: &mut VecDeque, + change_id: &mut ChangeId, + relation_to_propagate: Option, + delegate: &mut impl tree::Visit, ) -> Result<(), Error> { delegate.push_path_component(entry.filename); - if delegate + let relation = relation_to_propagate.or_else(|| { + entry.mode.is_tree().then(|| { + *change_id += 1; + Relation::Parent(*change_id) + }) + }); + let is_cancelled = delegate .visit(Change::Deletion { entry_mode: entry.mode, oid: entry.oid.to_owned(), + relation, }) - .cancelled() - { + .cancelled(); + if is_cancelled { return Err(Error::Cancelled); } if entry.mode.is_tree() { delegate.pop_path_component(); delegate.push_back_tracked_path_component(entry.filename); - queue.push_back((Some(entry.oid.to_owned()), None)); + queue.push_back((Some(entry.oid.to_owned()), None, to_child(relation))); } Ok(()) } -fn add_entry_schedule_recursion( +fn add_entry_schedule_recursion( entry: EntryRef<'_>, - queue: &mut VecDeque, - delegate: &mut R, + queue: &mut VecDeque, + change_id: &mut ChangeId, + relation_to_propagate: Option, + delegate: &mut impl tree::Visit, ) -> Result<(), Error> { delegate.push_path_component(entry.filename); + let relation = relation_to_propagate.or_else(|| { + entry.mode.is_tree().then(|| { + *change_id += 1; + Relation::Parent(*change_id) + }) + }); if delegate .visit(Change::Addition { entry_mode: entry.mode, oid: entry.oid.to_owned(), + relation, }) .cancelled() { @@ -159,43 +204,53 @@ fn add_entry_schedule_recursion( if entry.mode.is_tree() { delegate.pop_path_component(); delegate.push_back_tracked_path_component(entry.filename); - queue.push_back((None, Some(entry.oid.to_owned()))); + queue.push_back((None, Some(entry.oid.to_owned()), to_child(relation))); } Ok(()) } -fn catchup_rhs_with_lhs( + +fn catchup_rhs_with_lhs( rhs_entries: &mut IteratorType>, lhs: EntryRef<'_>, rhs: EntryRef<'_>, - queue: &mut VecDeque, - delegate: &mut R, + queue: &mut VecDeque, + change_id: &mut ChangeId, + relation_to_propagate: Option, + delegate: &mut impl tree::Visit, ) -> Result<(), Error> { use std::cmp::Ordering::*; - add_entry_schedule_recursion(rhs, queue, delegate)?; + add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?; loop { match rhs_entries.peek() { Some(Ok(rhs)) => match compare(&lhs, rhs) { Equal => { let rhs = rhs_entries.next().transpose()?.expect("the peeked item to be present"); delegate.pop_path_component(); - handle_lhs_and_rhs_with_equal_filenames(lhs, rhs, queue, delegate)?; + handle_lhs_and_rhs_with_equal_filenames( + lhs, + rhs, + queue, + change_id, + relation_to_propagate, + delegate, + )?; break; } Greater => { let rhs = rhs_entries.next().transpose()?.expect("the peeked item to be present"); delegate.pop_path_component(); - add_entry_schedule_recursion(rhs, queue, delegate)?; + add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?; } Less => { delegate.pop_path_component(); - delete_entry_schedule_recursion(lhs, queue, delegate)?; + delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?; break; } }, Some(Err(err)) => return Err(Error::EntriesDecode(err.to_owned())), None => { delegate.pop_path_component(); - delete_entry_schedule_recursion(lhs, queue, delegate)?; + delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?; break; } } @@ -203,39 +258,48 @@ fn catchup_rhs_with_lhs( Ok(()) } -fn catchup_lhs_with_rhs( +fn catchup_lhs_with_rhs( lhs_entries: &mut IteratorType>, lhs: EntryRef<'_>, rhs: EntryRef<'_>, - queue: &mut VecDeque, - delegate: &mut R, + queue: &mut VecDeque, + change_id: &mut ChangeId, + relation_to_propagate: Option, + delegate: &mut impl tree::Visit, ) -> Result<(), Error> { use std::cmp::Ordering::*; - delete_entry_schedule_recursion(lhs, queue, delegate)?; + delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?; loop { match lhs_entries.peek() { Some(Ok(lhs)) => match compare(lhs, &rhs) { Equal => { let lhs = lhs_entries.next().expect("the peeked item to be present")?; delegate.pop_path_component(); - handle_lhs_and_rhs_with_equal_filenames(lhs, rhs, queue, delegate)?; + handle_lhs_and_rhs_with_equal_filenames( + lhs, + rhs, + queue, + change_id, + relation_to_propagate, + delegate, + )?; break; } Less => { let lhs = lhs_entries.next().expect("the peeked item to be present")?; delegate.pop_path_component(); - delete_entry_schedule_recursion(lhs, queue, delegate)?; + delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?; } Greater => { delegate.pop_path_component(); - add_entry_schedule_recursion(rhs, queue, delegate)?; + add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?; break; } }, Some(Err(err)) => return Err(Error::EntriesDecode(err.to_owned())), None => { delegate.pop_path_component(); - add_entry_schedule_recursion(rhs, queue, delegate)?; + add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?; break; } } @@ -243,11 +307,13 @@ fn catchup_lhs_with_rhs( Ok(()) } -fn handle_lhs_and_rhs_with_equal_filenames( +fn handle_lhs_and_rhs_with_equal_filenames( lhs: EntryRef<'_>, rhs: EntryRef<'_>, - queue: &mut VecDeque, - delegate: &mut R, + queue: &mut VecDeque, + change_id: &mut ChangeId, + relation_to_propagate: Option, + delegate: &mut impl tree::Visit, ) -> Result<(), Error> { match (lhs.mode.is_tree(), rhs.mode.is_tree()) { (true, true) => { @@ -264,7 +330,11 @@ fn handle_lhs_and_rhs_with_equal_filenames( { return Err(Error::Cancelled); } - queue.push_back((Some(lhs.oid.to_owned()), Some(rhs.oid.to_owned()))); + queue.push_back(( + Some(lhs.oid.to_owned()), + Some(rhs.oid.to_owned()), + relation_to_propagate, + )); } (_, true) => { delegate.push_back_tracked_path_component(lhs.filename); @@ -272,28 +342,40 @@ fn handle_lhs_and_rhs_with_equal_filenames( .visit(Change::Deletion { entry_mode: lhs.mode, oid: lhs.oid.to_owned(), + relation: None, }) .cancelled() { return Err(Error::Cancelled); }; + + let relation = relation_to_propagate.or_else(|| { + *change_id += 1; + Some(Relation::Parent(*change_id)) + }); if delegate .visit(Change::Addition { entry_mode: rhs.mode, oid: rhs.oid.to_owned(), + relation, }) .cancelled() { return Err(Error::Cancelled); }; - queue.push_back((None, Some(rhs.oid.to_owned()))); + queue.push_back((None, Some(rhs.oid.to_owned()), to_child(relation))); } (true, _) => { delegate.push_back_tracked_path_component(lhs.filename); + let relation = relation_to_propagate.or_else(|| { + *change_id += 1; + Some(Relation::Parent(*change_id)) + }); if delegate .visit(Change::Deletion { entry_mode: lhs.mode, oid: lhs.oid.to_owned(), + relation, }) .cancelled() { @@ -303,12 +385,13 @@ fn handle_lhs_and_rhs_with_equal_filenames( .visit(Change::Addition { entry_mode: rhs.mode, oid: rhs.oid.to_owned(), + relation: None, }) .cancelled() { return Err(Error::Cancelled); }; - queue.push_back((Some(lhs.oid.to_owned()), None)); + queue.push_back((Some(lhs.oid.to_owned()), None, to_child(relation))); } (false, false) => { delegate.push_path_component(lhs.filename); @@ -332,6 +415,13 @@ fn handle_lhs_and_rhs_with_equal_filenames( type IteratorType = std::mem::ManuallyDrop>; +fn to_child(r: Option) -> Option { + r.map(|r| match r { + Relation::Parent(id) => Relation::ChildOfParent(id), + Relation::ChildOfParent(id) => Relation::ChildOfParent(id), + }) +} + fn peekable(iter: I) -> IteratorType { std::mem::ManuallyDrop::new(iter.peekable()) } diff --git a/gix-diff/src/tree/mod.rs b/gix-diff/src/tree/mod.rs index 77e125e910e..ab0b4c143cf 100644 --- a/gix-diff/src/tree/mod.rs +++ b/gix-diff/src/tree/mod.rs @@ -1,23 +1,43 @@ -use std::collections::VecDeque; - +use crate::tree::visit::Relation; +use bstr::BStr; use gix_hash::ObjectId; use gix_object::{bstr::BString, TreeRefIter}; +use std::collections::VecDeque; + +/// A trait to allow responding to a traversal designed to figure out the [changes](visit::Change) +/// to turn tree A into tree B. +pub trait Visit { + /// Sets the full path in front of the queue so future calls to push and pop components affect it instead. + fn pop_front_tracked_path_and_set_current(&mut self); + /// Append a `component` to the end of a path, which may be empty. + fn push_back_tracked_path_component(&mut self, component: &BStr); + /// Append a `component` to the end of a path, which may be empty. + fn push_path_component(&mut self, component: &BStr); + /// Removes the last component from the path, which may leave it empty. + fn pop_path_component(&mut self); + /// Record a `change` and return an instruction whether to continue or not. + /// + /// The implementation may use the current path to lean where in the tree the change is located. + fn visit(&mut self, change: visit::Change) -> visit::Action; +} /// The state required to visit [Changes] to be instantiated with `State::default()`. #[derive(Default, Clone)] pub struct State { buf1: Vec, buf2: Vec, - trees: VecDeque, + trees: VecDeque, + change_id: visit::ChangeId, } -type TreeInfoPair = (Option, Option); +type TreeInfoTuple = (Option, Option, Option); impl State { fn clear(&mut self) { self.trees.clear(); self.buf1.clear(); self.buf2.clear(); + self.change_id = 0; } } @@ -38,8 +58,6 @@ pub mod changes; /// pub mod visit; -#[doc(inline)] -pub use visit::Visit; /// A [Visit] implementation to record every observed change and keep track of the changed paths. #[derive(Clone, Debug)] diff --git a/gix-diff/src/tree/recorder.rs b/gix-diff/src/tree/recorder.rs index e1c4d51264f..69055ba16e7 100644 --- a/gix-diff/src/tree/recorder.rs +++ b/gix-diff/src/tree/recorder.rs @@ -4,7 +4,8 @@ use gix_object::{ tree, }; -use crate::tree::{visit, Recorder}; +use crate::tree::visit::Relation; +use crate::tree::{visit, Recorder, Visit}; /// Describe how to track the location of a change. #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -17,7 +18,7 @@ pub enum Location { FileName, } -/// A Change as observed by a call to [`visit(…)`][visit::Visit::visit()], enhanced with the path affected by the change. +/// A Change as observed by a call to [`visit(…)`](Visit::visit()), enhanced with the path affected by the change. /// Its similar to [`visit::Change`] but includes the path that changed. #[derive(Clone, Debug, PartialEq, Eq)] #[allow(missing_docs)] @@ -26,11 +27,13 @@ pub enum Change { entry_mode: tree::EntryMode, oid: ObjectId, path: BString, + relation: Option, }, Deletion { entry_mode: tree::EntryMode, oid: ObjectId, path: BString, + relation: Option, }, Modification { previous_entry_mode: tree::EntryMode, @@ -93,7 +96,7 @@ impl Recorder { } } -impl visit::Visit for Recorder { +impl Visit for Recorder { fn pop_front_tracked_path_and_set_current(&mut self) { if let Some(Location::Path) = self.location { self.path = self.path_deque.pop_front().expect("every parent is set only once"); @@ -136,15 +139,25 @@ impl visit::Visit for Recorder { fn visit(&mut self, change: visit::Change) -> visit::Action { use visit::Change::*; self.records.push(match change { - Deletion { entry_mode, oid } => Change::Deletion { + Deletion { + entry_mode, + oid, + relation, + } => Change::Deletion { entry_mode, oid, path: self.path_clone(), + relation, }, - Addition { entry_mode, oid } => Change::Addition { + Addition { + entry_mode, + oid, + relation, + } => Change::Addition { entry_mode, oid, path: self.path_clone(), + relation, }, Modification { previous_entry_mode, diff --git a/gix-diff/src/tree/visit.rs b/gix-diff/src/tree/visit.rs index 8ab3512be9e..f9feb0eeba8 100644 --- a/gix-diff/src/tree/visit.rs +++ b/gix-diff/src/tree/visit.rs @@ -1,5 +1,23 @@ use gix_hash::ObjectId; -use gix_object::{bstr::BStr, tree, tree::EntryMode}; +use gix_object::{tree, tree::EntryMode}; + +/// A way to recognize and associate different [`Change`] instances. +/// +/// These are unique only within one diff operation. +pub type ChangeId = u32; + +/// Identifies a relationship between this instance and another one. +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Hash)] +pub enum Relation { + /// This is a parent with the given ID, which will always have at least one child + /// assuming that empty directories are not allowed in valid trees. + /// It's also always a tree which is the start of a recursive deletion or addition. + /// + /// The change with this relation is always emitted first. + Parent(ChangeId), + /// This is a direct or indirect child, tree or not tree, of the parent with the given ID. + ChildOfParent(ChangeId), +} /// Represents any possible change in order to turn one tree into another. #[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq, Hash)] @@ -10,6 +28,8 @@ pub enum Change { entry_mode: tree::EntryMode, /// The object id of the added entry. oid: ObjectId, + /// Possibly associate this change with another for hierarchical rename tracking. + relation: Option, }, /// An entry was deleted, like the deletion of a file or directory. Deletion { @@ -17,6 +37,8 @@ pub enum Change { entry_mode: tree::EntryMode, /// The object id of the deleted entry. oid: ObjectId, + /// Possibly associate this change with another for hierarchical rename tracking. + relation: Option, }, /// An entry was modified, e.g. changing the contents of a file adjusts its object id and turning /// a file into a symbolic link adjusts its mode. @@ -51,20 +73,28 @@ impl Change { /// Return the current object id and tree entry mode of a change. pub fn oid_and_entry_mode(&self) -> (&gix_hash::oid, EntryMode) { match self { - Change::Addition { oid, entry_mode } - | Change::Deletion { oid, entry_mode } + Change::Addition { + oid, + entry_mode, + relation: _, + } + | Change::Deletion { + oid, + entry_mode, + relation: _, + } | Change::Modification { oid, entry_mode, .. } => (oid, *entry_mode), } } } -/// What to do after a [Change] was [recorded][Visit::visit()]. +/// What to do after a [Change] was [recorded](super::Visit::visit()). #[derive(Default, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)] pub enum Action { /// Continue the traversal of changes. #[default] Continue, - /// Stop the traversal of changes, making this the last call to [visit(…)][Visit::visit()]. + /// Stop the traversal of changes, making this the last call to [visit(…)](super::Visit::visit()). Cancel, } @@ -75,23 +105,6 @@ impl Action { } } -/// A trait to allow responding to a traversal designed to figure out the [changes][Change] -/// to turn tree A into tree B. -pub trait Visit { - /// Sets the full path path in front of the queue so future calls to push and pop components affect it instead. - fn pop_front_tracked_path_and_set_current(&mut self); - /// Append a `component` to the end of a path, which may be empty. - fn push_back_tracked_path_component(&mut self, component: &BStr); - /// Append a `component` to the end of a path, which may be empty. - fn push_path_component(&mut self, component: &BStr); - /// Removes the last component from the path, which may leave it empty. - fn pop_path_component(&mut self); - /// Record a `change` and return an instruction whether to continue or not. - /// - /// The implementation may use the current path to lean where in the tree the change is located. - fn visit(&mut self, change: Change) -> Action; -} - #[cfg(feature = "blob")] mod change_impls { use gix_hash::oid; @@ -140,8 +153,8 @@ mod tests { fn size_of_change() { let actual = std::mem::size_of::(); assert!( - actual <= 46, - "{actual} <= 46: this type shouldn't grow without us knowing" + actual <= 48, + "{actual} <= 48: this type shouldn't grow without us knowing" ); } } diff --git a/gix-diff/tests/fixtures/generated-archives/make_diff_repo.tar b/gix-diff/tests/fixtures/generated-archives/make_diff_repo.tar index 9327d317f3a0460f04a3aafe8eabbc9beebf92a7..e661f5f1abd9466bb70e034c0d41f1e2dd0865e4 100644 GIT binary patch delta 2844 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 at_O|;N9N|9qLEl#K`10hYd|!2+UXxLsV~9+ diff --git a/gix-diff/tests/fixtures/generated-archives/make_diff_repo_a.tar b/gix-diff/tests/fixtures/generated-archives/make_diff_repo_a.tar index 0a6bbf5031bf87081f4059b9045735e6d17e3830..e0ece399d6b1d7c029c0dd7c0936107ec0b9835c 100644 GIT binary patch delta 3132 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 XHYqnZM>eHq6(mB6@kF0MdISFfHOzNQ diff --git a/gix-diff/tests/fixtures/make_diff_repo.sh b/gix-diff/tests/fixtures/make_diff_repo.sh index 64955c3bfe1..983383e9157 100755 --- a/gix-diff/tests/fixtures/make_diff_repo.sh +++ b/gix-diff/tests/fixtures/make_diff_repo.sh @@ -99,6 +99,9 @@ git commit -qam 'rm g/aa, add g/a' rm -Rf ./* && mkdir git-sec gix && touch a git-sec/2 git-sequencer h gix/5 && git add . git commit -am "clear slate" -git mv git-sec gix-sec && git commit -m "interesting rename 1" +mkdir git-sec/subdir && touch git-sec/subdir/6 git-sec/7 +git add . && git commit -m "add files to git-sec" -git mv gix-sec git-sec && git commit -m "interesting rename 2" +git mv git-sec gix-sec && git commit -m "rename git-sec to gix-sec" + +git mv gix-sec git-sec && git commit -m "rename gix-sec to git-sec" diff --git a/gix-diff/tests/tree/mod.rs b/gix-diff/tests/tree/mod.rs index 54e09f33016..f54610816cd 100644 --- a/gix-diff/tests/tree/mod.rs +++ b/gix-diff/tests/tree/mod.rs @@ -2,6 +2,7 @@ mod changes { mod to_obtain_tree { use std::collections::HashMap; + use gix_diff::tree::visit::Relation; use gix_diff::tree::{ recorder, recorder::{Change::*, Location}, @@ -161,7 +162,8 @@ mod changes { vec![Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "f".into() + path: "f".into(), + relation: None }], ":000000 100644 0000000000000000000000000000000000000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 A f" ); @@ -183,7 +185,8 @@ mod changes { vec![Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242"), - path: "f".into() + path: "f".into(), + relation: None }], ":100644 000000 28ce6a8b26aa170e1de65536fe8abe1832bd3242 0000000000000000000000000000000000000000 D f " @@ -195,17 +198,20 @@ mod changes { Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242"), - path: "f".into() + path: "f".into(), + relation: None }, Addition { entry_mode: EntryKind::Tree.into(), oid: hex_to_id("10f2f4b82222d2b5c31985130979a91fd87410f7"), - path: "f".into() + path: "f".into(), + relation: Some(Relation::Parent(1)), }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242"), - path: "f/f".into() + path: "f/f".into(), + relation: Some(Relation::ChildOfParent(1)), } ], ":100644 000000 28ce6a8b26aa170e1de65536fe8abe1832bd3242 0000000000000000000000000000000000000000 D f @@ -218,12 +224,14 @@ mod changes { Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "a".into() + path: "a".into(), + relation: None }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "b".into() + path: "b".into(), + relation: None } ], "simple rename, same level @@ -283,27 +291,32 @@ mod changes { Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "f".into() + path: "f".into(), + relation: None }, Deletion { entry_mode: EntryKind::Tree.into(), oid: tree_with_link_id, - path: "f".into() + path: "f".into(), + relation: Some(Relation::Parent(1)) }, Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "f/a".into() + path: "f/a".into(), + relation: Some(Relation::ChildOfParent(1)) }, Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "f/b".into() + path: "f/b".into(), + relation: Some(Relation::ChildOfParent(1)) }, Deletion { entry_mode: link_entry_mode.into(), oid: link_entry_oid, - path: "f/f".into() + path: "f/f".into(), + relation: Some(Relation::ChildOfParent(1)) }, ], ":000000 100644 0000000000000000000000000000000000000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 A f @@ -317,12 +330,14 @@ mod changes { Deletion { entry_mode: EntryKind::Tree.into(), oid: hex_to_id("3d5a503f4062d198b443db5065ca727f8354e7df"), - path: "d".into() + path: "d".into(), + relation: Some(Relation::Parent(1)) }, Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "d/f".into() + path: "d/f".into(), + relation: Some(Relation::ChildOfParent(1)) }, ], ":100644 000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0000000000000000000000000000000000000000 D d/f" @@ -333,17 +348,20 @@ mod changes { Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "c".into() + path: "c".into(), + relation: None, }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "d".into() + path: "d".into(), + relation: None, }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "e".into() + path: "e".into(), + relation: None, }, ], ":000000 100644 0000000000000000000000000000000000000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 A c @@ -356,12 +374,14 @@ mod changes { Addition { entry_mode: EntryKind::Tree.into(), oid: hex_to_id("496d6428b9cf92981dc9495211e6e1120fb6f2ba"), - path: "g".into() + path: "g".into(), + relation: Some(Relation::Parent(1)) }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "g/a".into() + path: "g/a".into(), + relation: Some(Relation::ChildOfParent(1)) }, ], ":000000 100644 0000000000000000000000000000000000000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 A g/a" @@ -372,17 +392,20 @@ mod changes { Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "c".into() + path: "c".into(), + relation: None }, Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "d".into() + path: "d".into(), + relation: None }, Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "e".into() + path: "e".into(), + relation: None }, ], ":100644 000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0000000000000000000000000000000000000000 D c @@ -395,12 +418,14 @@ mod changes { Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "f".into() + path: "f".into(), + relation: None }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "ff".into() + path: "ff".into(), + relation: None }, ], ":100644 000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0000000000000000000000000000000000000000 D f @@ -419,12 +444,14 @@ mod changes { Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "g/a".into() + path: "g/a".into(), + relation: None }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "g/aa".into() + path: "g/aa".into(), + relation: None }, ], ":100644 000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0000000000000000000000000000000000000000 D g/a @@ -436,12 +463,14 @@ mod changes { Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "f".into() + path: "f".into(), + relation: None, }, Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "ff".into() + path: "ff".into(), + relation: None, }, ], ":100644 000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0000000000000000000000000000000000000000 D f @@ -460,12 +489,14 @@ mod changes { Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "g/a".into() + path: "g/a".into(), + relation: None, }, Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "g/aa".into() + path: "g/aa".into(), + relation: None, }, ], ":000000 100644 0000000000000000000000000000000000000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 A g/a @@ -485,12 +516,14 @@ mod changes { Addition { entry_mode: EntryKind::Tree.into(), oid: hex_to_id("3d5a503f4062d198b443db5065ca727f8354e7df"), - path: "a".into() + path: "a".into(), + relation: Some(Relation::Parent(1)), }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "a/f".into() + path: "a/f".into(), + relation: Some(Relation::ChildOfParent(1)), } ], ":000000 100644 0000000000000000000000000000000000000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 A a/f" @@ -538,17 +571,20 @@ mod changes { Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "".into() + path: "".into(), + relation: None }, Addition { entry_mode: EntryKind::Tree.into(), oid: hex_to_id("496d6428b9cf92981dc9495211e6e1120fb6f2ba"), - path: "".into() + path: "".into(), + relation: Some(Relation::Parent(1)) }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "".into() + path: "".into(), + relation: Some(Relation::ChildOfParent(1)) } ] ); @@ -558,17 +594,20 @@ mod changes { Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "b".into() + path: "b".into(), + relation: None }, Deletion { entry_mode: EntryKind::Tree.into(), oid: hex_to_id("496d6428b9cf92981dc9495211e6e1120fb6f2ba"), - path: "g".into() + path: "g".into(), + relation: Some(Relation::Parent(1)) }, Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "a".into() + path: "a".into(), + relation: Some(Relation::ChildOfParent(1)) } ] ); @@ -586,42 +625,50 @@ mod changes { Addition { entry_mode: EntryKind::Tree.into(), oid: hex_to_id("0df4d0ed769eacd0a231e7512fca25d3cabdeca4"), - path: "a".into() + path: "a".into(), + relation: Some(Relation::Parent(1)), }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "a/b".into() + path: "a/b".into(), + relation: Some(Relation::ChildOfParent(1)), }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "a/c".into() + path: "a/c".into(), + relation: Some(Relation::ChildOfParent(1)), }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "a/d".into() + path: "a/d".into(), + relation: Some(Relation::ChildOfParent(1)), }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "a/e".into() + path: "a/e".into(), + relation: Some(Relation::ChildOfParent(1)), }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "a/f".into() + path: "a/f".into(), + relation: Some(Relation::ChildOfParent(1)), }, Addition { entry_mode: EntryKind::Tree.into(), oid: hex_to_id("496d6428b9cf92981dc9495211e6e1120fb6f2ba"), - path: "a/g".into() + path: "a/g".into(), + relation: Some(Relation::ChildOfParent(1)), }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "a/g/a".into() + path: "a/g/a".into(), + relation: Some(Relation::ChildOfParent(1)), } ] ); @@ -629,32 +676,72 @@ mod changes { } #[test] - fn interesting_rename() -> crate::Result { + fn directory_rename() -> crate::Result { let db = db(None)?; let all_commits = all_commits(&db); assert_eq!( - diff_with_previous_commit_from(&db, &all_commits["interesting rename 1"])?, + diff_with_previous_commit_from(&db, &all_commits["rename git-sec to gix-sec"])?, vec![ Deletion { entry_mode: EntryKind::Tree.into(), - oid: hex_to_id("f84fc275158a2973cb4a79b1618b79ec7f573a95"), - path: "git-sec".into() + oid: hex_to_id("d8c30fb72173778ed57fac5813c5e37038a8746c"), + path: "git-sec".into(), + relation: Some(Relation::Parent(1)), + }, + Addition { + entry_mode: EntryKind::Tree.into(), + oid: hex_to_id("d8c30fb72173778ed57fac5813c5e37038a8746c"), + path: "gix-sec".into(), + relation: Some(Relation::Parent(2)), + }, + Deletion { + entry_mode: EntryKind::Blob.into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), + path: "git-sec/2".into(), + relation: Some(Relation::ChildOfParent(1)), + }, + Deletion { + entry_mode: EntryKind::Blob.into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), + path: "git-sec/7".into(), + relation: Some(Relation::ChildOfParent(1)), + }, + Deletion { + entry_mode: EntryKind::Tree.into(), + oid: hex_to_id("fd7938a0c18f993c89eda3f40a6d06fa6785833c"), + path: "git-sec/subdir".into(), + relation: Some(Relation::ChildOfParent(1)), + }, + Addition { + entry_mode: EntryKind::Blob.into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), + path: "gix-sec/2".into(), + relation: Some(Relation::ChildOfParent(2)), + }, + Addition { + entry_mode: EntryKind::Blob.into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), + path: "gix-sec/7".into(), + relation: Some(Relation::ChildOfParent(2)), }, Addition { entry_mode: EntryKind::Tree.into(), - oid: hex_to_id("f84fc275158a2973cb4a79b1618b79ec7f573a95"), - path: "gix-sec".into() + oid: hex_to_id("fd7938a0c18f993c89eda3f40a6d06fa6785833c"), + path: "gix-sec/subdir".into(), + relation: Some(Relation::ChildOfParent(2)), }, Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "git-sec/2".into() + path: "git-sec/subdir/6".into(), + relation: Some(Relation::ChildOfParent(1)), }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "gix-sec/2".into() + path: "gix-sec/subdir/6".into(), + relation: Some(Relation::ChildOfParent(2)), } ] ); @@ -662,32 +749,72 @@ mod changes { } #[test] - fn interesting_rename_2() -> crate::Result { + fn reverse_directory_rename() -> crate::Result { let db = db(None)?; let all_commits = all_commits(&db); assert_eq!( - diff_with_previous_commit_from(&db, &all_commits["interesting rename 2"])?, + diff_with_previous_commit_from(&db, &all_commits["rename gix-sec to git-sec"])?, vec![ Addition { entry_mode: EntryKind::Tree.into(), - oid: hex_to_id("f84fc275158a2973cb4a79b1618b79ec7f573a95"), - path: "git-sec".into() + oid: hex_to_id("d8c30fb72173778ed57fac5813c5e37038a8746c"), + path: "git-sec".into(), + relation: Some(Relation::Parent(1)), + }, + Deletion { + entry_mode: EntryKind::Tree.into(), + oid: hex_to_id("d8c30fb72173778ed57fac5813c5e37038a8746c"), + path: "gix-sec".into(), + relation: Some(Relation::Parent(2)), + }, + Addition { + entry_mode: EntryKind::Blob.into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), + path: "git-sec/2".into(), + relation: Some(Relation::ChildOfParent(1)), + }, + Addition { + entry_mode: EntryKind::Blob.into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), + path: "git-sec/7".into(), + relation: Some(Relation::ChildOfParent(1)), + }, + Addition { + entry_mode: EntryKind::Tree.into(), + oid: hex_to_id("fd7938a0c18f993c89eda3f40a6d06fa6785833c"), + path: "git-sec/subdir".into(), + relation: Some(Relation::ChildOfParent(1)), + }, + Deletion { + entry_mode: EntryKind::Blob.into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), + path: "gix-sec/2".into(), + relation: Some(Relation::ChildOfParent(2)), + }, + Deletion { + entry_mode: EntryKind::Blob.into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), + path: "gix-sec/7".into(), + relation: Some(Relation::ChildOfParent(2)), }, Deletion { entry_mode: EntryKind::Tree.into(), - oid: hex_to_id("f84fc275158a2973cb4a79b1618b79ec7f573a95"), - path: "gix-sec".into() + oid: hex_to_id("fd7938a0c18f993c89eda3f40a6d06fa6785833c"), + path: "gix-sec/subdir".into(), + relation: Some(Relation::ChildOfParent(2)), }, Addition { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "git-sec/2".into() + path: "git-sec/subdir/6".into(), + relation: Some(Relation::ChildOfParent(1)), }, Deletion { entry_mode: EntryKind::Blob.into(), oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), - path: "gix-sec/2".into() + path: "gix-sec/subdir/6".into(), + relation: Some(Relation::ChildOfParent(2)), } ] ); From 5c1f010851e40bf5b284ec82e6cd27cd41ab70bf Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 2 Oct 2024 13:23:04 +0200 Subject: [PATCH 03/13] adapt to chagnes in `gix-diff` --- gix-pack/src/data/output/count/objects/tree.rs | 7 ++++++- gix/src/object/tree/diff/for_each.rs | 12 ++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/gix-pack/src/data/output/count/objects/tree.rs b/gix-pack/src/data/output/count/objects/tree.rs index 824b48062ce..27cb161c9a9 100644 --- a/gix-pack/src/data/output/count/objects/tree.rs +++ b/gix-pack/src/data/output/count/objects/tree.rs @@ -42,7 +42,12 @@ pub mod changes { fn visit(&mut self, change: Change) -> Action { match change { - Change::Addition { oid, entry_mode } | Change::Modification { oid, entry_mode, .. } => { + Change::Addition { + oid, + entry_mode, + relation: _, + } + | Change::Modification { oid, entry_mode, .. } => { if entry_mode.is_commit() { return Action::Continue; } diff --git a/gix/src/object/tree/diff/for_each.rs b/gix/src/object/tree/diff/for_each.rs index 0f186429116..e19156a0485 100644 --- a/gix/src/object/tree/diff/for_each.rs +++ b/gix/src/object/tree/diff/for_each.rs @@ -139,11 +139,19 @@ where ) -> gix_diff::tree::visit::Action { use gix_diff::tree::visit::Change::*; let event = match change { - Addition { entry_mode, oid } => change::Event::Addition { + Addition { + entry_mode, + oid, + relation: _, + } => change::Event::Addition { entry_mode, id: oid.attach(other_repo), }, - Deletion { entry_mode, oid } => change::Event::Deletion { + Deletion { + entry_mode, + oid, + relation: _, + } => change::Event::Deletion { entry_mode, id: oid.attach(repo), }, From 7be142d087a736339af54f2cb894edc7c36cdc90 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 2 Oct 2024 13:30:57 +0200 Subject: [PATCH 04/13] feat!: optional rename tracking for directories. Depending on the source of the rename-information, items that are children of renamed parents may be provided to enable rename tracking based on these containers, instead of always resorting to tracking leaf nodes (i.e. blobs). --- crate-status.md | 9 +- gix-diff/src/rewrites/tracker.rs | 161 ++++++++++++++++----- gix-diff/src/tree/visit.rs | 8 ++ gix-diff/tests/rewrites/mod.rs | 47 ++++++- gix-diff/tests/rewrites/tracker.rs | 217 ++++++++++++++++++++++++++++- gix/tests/object/tree/diff.rs | 4 +- 6 files changed, 396 insertions(+), 50 deletions(-) diff --git a/crate-status.md b/crate-status.md index c0e24adabf2..35b960a55a1 100644 --- a/crate-status.md +++ b/crate-status.md @@ -311,10 +311,13 @@ Check out the [performance discussion][gix-diff-performance] as well. * **lines** * [x] Simple line-by-line diffs powered by the `imara-diff` crate. * **generic rename tracker to find renames and copies** - * [x] find by exact match - * [x] find by similarity check + * [x] find blobs by exact match + * [x] find blobs by similarity check * [ ] heuristics to find best candidate - * [ ] find by basename to help detecting simple moves + * [ ] find by basename to support similarity check + * [x] directory tracking + - [x] by identity + - [ ] by similarity * **blob** * [x] a choice of to-worktree, to-git and to-worktree-if-needed conversions * [x] `textconv` filters diff --git a/gix-diff/src/rewrites/tracker.rs b/gix-diff/src/rewrites/tracker.rs index 5720ec793d2..678a646acf5 100644 --- a/gix-diff/src/rewrites/tracker.rs +++ b/gix-diff/src/rewrites/tracker.rs @@ -4,11 +4,13 @@ //! //! - it's less sophisticated and doesn't use any ranking of candidates. Instead, it picks the first possible match. //! - the set used for copy-detection is probably smaller by default. + use std::ops::Range; -use bstr::BStr; +use bstr::{BStr, ByteSlice}; use gix_object::tree::{EntryKind, EntryMode}; +use crate::tree::visit::{Action, ChangeId, Relation}; use crate::{ blob::{platform::prepare_diff::Operation, DiffLineStats, ResourceKind}, rewrites::{CopySource, Outcome, Tracker}, @@ -28,11 +30,22 @@ pub enum ChangeKind { /// A trait providing all functionality to abstract over the concept of a change, as seen by the [`Tracker`]. pub trait Change: Clone { - /// Return the hash of this change for identification. + /// Return the hash of the object behind this change for identification. /// /// Note that this is the id of the object as stored in `git`, i.e. it must have gone through workspace - /// conversions. + /// conversions. What matters is that the IDs are comparable. fn id(&self) -> &gix_hash::oid; + /// Return the relation that this change may have with other changes. + /// + /// It allows to associate a directory with its children that are added or removed at the same moment. + /// Note that this is ignored for modifications. + /// + /// If rename-tracking should always be on leaf-level, this should be set to `None` consistently. + /// Note that trees will never be looked up by their `id` as their children are assumed to be passed in + /// with the respective relationship. + /// + /// Also note that the tracker only sees what's given to it, it will not lookup trees or match paths itself. + fn relation(&self) -> Option; /// Return the kind of this change. fn kind(&self) -> ChangeKind; /// Return more information about the kind of entry affected by this change. @@ -55,11 +68,11 @@ impl Item { fn location<'a>(&self, backing: &'a [u8]) -> &'a BStr { backing[self.path.clone()].as_ref() } - fn entry_mode_compatible(&self, mode: EntryMode) -> bool { + fn entry_mode_compatible(&self, other: EntryMode) -> bool { use EntryKind::*; matches!( - (mode.kind(), self.change.entry_mode().kind()), - (Blob | BlobExecutable, Blob | BlobExecutable) | (Link, Link) + (other.kind(), self.change.entry_mode().kind()), + (Blob | BlobExecutable, Blob | BlobExecutable) | (Link, Link) | (Tree, Tree) ) } @@ -108,7 +121,7 @@ pub mod visit { } /// A change along with a location. - #[derive(Clone)] + #[derive(Debug, Clone)] pub struct Destination<'a, T: Clone> { /// The change at the given `location`. pub change: T, @@ -150,23 +163,25 @@ impl Tracker { impl Tracker { /// We may refuse the push if that information isn't needed for what we have to track. pub fn try_push_change(&mut self, change: T, location: &BStr) -> Option { - if !change.entry_mode().is_blob_or_symlink() { + let change_kind = change.kind(); + if let (None, ChangeKind::Modification { .. }) = (self.rewrites.copies, change_kind) { return Some(change); - } - let keep = match (self.rewrites.copies, change.kind()) { - (Some(_find_copies), _) => true, - (None, ChangeKind::Modification { .. }) => false, - (None, _) => true, }; - if !keep { + let relation = change + .relation() + .filter(|_| matches!(change_kind, ChangeKind::Addition | ChangeKind::Deletion)); + let entry_kind = change.entry_mode().kind(); + if let (None | Some(Relation::ChildOfParent(_)), EntryKind::Commit | EntryKind::Tree) = (relation, entry_kind) { return Some(change); - } + }; let start = self.path_backing.len(); self.path_backing.extend_from_slice(location); + let path = start..self.path_backing.len(); + self.items.push(Item { - path: start..self.path_backing.len(), + path, change, emitted: false, }); @@ -178,6 +193,8 @@ impl Tracker { /// `cb(destination, source)` is called for each item, either with `Some(source)` if it's /// the destination of a copy or rename, or with `None` for source if no relation to other /// items in the tracked set exist, which is like saying 'no rename or rewrite or copy' happened. + /// Note that directories with [relation](Relation) will be emitted if there is a match, along with all their matching + /// child-items which are similarly bundled as rename. /// /// `objects` is used to access blob data for similarity checks if required and is taken directly from the object database. /// Worktree filters and text conversions will be applied afterwards automatically. Note that object-caching *should not* @@ -195,7 +212,7 @@ impl Tracker { /// will panic if `change` is not a modification, and it's valid to not call `push` at all. pub fn emit( &mut self, - mut cb: impl FnMut(visit::Destination<'_, T>, Option>) -> crate::tree::visit::Action, + mut cb: impl FnMut(visit::Destination<'_, T>, Option>) -> Action, diff_cache: &mut crate::blob::Platform, objects: &impl gix_object::FindObjectOrHeader, mut push_source_tree: PushSourceTreeFn, @@ -272,7 +289,7 @@ impl Tracker { change: item.change, }, None, - ) == crate::tree::visit::Action::Cancel + ) == Action::Cancel { break; } @@ -285,7 +302,7 @@ impl Tracker { fn match_pairs_of_kind( &mut self, kind: visit::SourceKind, - cb: &mut impl FnMut(visit::Destination<'_, T>, Option>) -> crate::tree::visit::Action, + cb: &mut impl FnMut(visit::Destination<'_, T>, Option>) -> Action, percentage: Option, out: &mut Outcome, diff_cache: &mut crate::blob::Platform, @@ -293,9 +310,7 @@ impl Tracker { ) -> Result<(), emit::Error> { // we try to cheaply reduce the set of possibilities first, before possibly looking more exhaustively. let needs_second_pass = !needs_exact_match(percentage); - if self.match_pairs(cb, None /* by identity */, kind, out, diff_cache, objects)? - == crate::tree::visit::Action::Cancel - { + if self.match_pairs(cb, None /* by identity */, kind, out, diff_cache, objects)? == Action::Cancel { return Ok(()); } if needs_second_pass { @@ -328,13 +343,13 @@ impl Tracker { fn match_pairs( &mut self, - cb: &mut impl FnMut(visit::Destination<'_, T>, Option>) -> crate::tree::visit::Action, + cb: &mut impl FnMut(visit::Destination<'_, T>, Option>) -> Action, percentage: Option, kind: visit::SourceKind, stats: &mut Outcome, diff_cache: &mut crate::blob::Platform, objects: &impl gix_object::FindObjectOrHeader, - ) -> Result { + ) -> Result { let mut dest_ofs = 0; while let Some((mut dest_idx, dest)) = self.items[dest_ofs..].iter().enumerate().find_map(|(idx, item)| { (!item.emitted && matches!(item.change.kind(), ChangeKind::Addition)).then_some((idx, item)) @@ -368,28 +383,102 @@ impl Tracker { src_idx, ) }); - if src.is_none() { + let Some((src, src_idx)) = src else { continue; - } + }; let location = dest.location(&self.path_backing); let change = dest.change.clone(); let dest = visit::Destination { change, location }; - let src_idx = src.as_ref().map(|t| t.1); - let res = cb(dest, src.map(|t| t.0)); + let relations = if percentage.is_none() { + src.change.relation().zip(dest.change.relation()) + } else { + None + }; + let res = cb(dest, Some(src)); self.items[dest_idx].emitted = true; - if let Some(src_idx) = src_idx { - self.items[src_idx].emitted = true; + self.items[src_idx].emitted = true; + + if res == Action::Cancel { + return Ok(Action::Cancel); + } + + if let Some((Relation::Parent(src), Relation::Parent(dst))) = relations { + let res = self.emit_child_renames_matching_identity(cb, kind, src, dst)?; + if res == Action::Cancel { + return Ok(Action::Cancel); + } } + } + Ok(Action::Continue) + } - if res == crate::tree::visit::Action::Cancel { - return Ok(crate::tree::visit::Action::Cancel); + /// Emit the children of `src_parent_id` and `dst_parent_id` as pairs of exact matches, which are assumed + /// as `src` and `dst` were an exact match (so all children have to match exactly). + fn emit_child_renames_matching_identity( + &mut self, + cb: &mut impl FnMut(visit::Destination<'_, T>, Option>) -> Action, + kind: visit::SourceKind, + src_parent_id: ChangeId, + dst_parent_id: ChangeId, + ) -> Result { + debug_assert_ne!( + src_parent_id, dst_parent_id, + "src and destination directories must be distinct" + ); + let (mut src_items, mut dst_items) = (Vec::with_capacity(1), Vec::with_capacity(1)); + for item in self.items.iter_mut().filter(|item| !item.emitted) { + match item.change.relation() { + Some(Relation::ChildOfParent(id)) if id == src_parent_id => { + src_items.push((item.change.id().to_owned(), item)); + } + Some(Relation::ChildOfParent(id)) if id == dst_parent_id => { + dst_items.push((item.change.id().to_owned(), item)); + } + _ => continue, + }; + } + + for ((src_id, src_item), (dst_id, dst_item)) in src_items.into_iter().zip(dst_items) { + // Since the parent items are already identical by ID, we know that the children will also match, we just + // double-check to still have a chance to be correct in case some of that goes wrong. + if src_id == dst_id + && filename(src_item.location(&self.path_backing)) == filename(dst_item.location(&self.path_backing)) + { + let entry_mode = src_item.change.entry_mode(); + let location = src_item.location(&self.path_backing); + let src = visit::Source { + entry_mode, + id: src_id, + kind, + location, + change: &src_item.change, + diff: None, + }; + let location = dst_item.location(&self.path_backing); + let change = dst_item.change.clone(); + let dst = visit::Destination { change, location }; + let res = cb(dst, Some(src)); + + src_item.emitted = true; + dst_item.emitted = true; + + if res == Action::Cancel { + return Ok(res); + } + } else { + gix_trace::warn!("Children of parents with change-id {src_parent_id} and {dst_parent_id} were not equal, even though their parents claimed to be"); + break; } } - Ok(crate::tree::visit::Action::Continue) + Ok(Action::Continue) } } +fn filename(path: &BStr) -> &BStr { + path.rfind_byte(b'/').map_or(path, |idx| path[idx + 1..].as_bstr()) +} + /// Returns the amount of viable sources and destinations for `items` as eligible for the given `kind` of operation. fn estimate_involved_items( items: impl IntoIterator, @@ -473,13 +562,9 @@ fn find_match<'a, T: Change>( if let Some(src) = res { return Ok(Some(src)); } - } else { + } else if item_mode.is_blob() { let mut has_new = false; let percentage = percentage.expect("it's set to something below 1.0 and we assured this"); - debug_assert!( - item_mode.is_blob(), - "symlinks are matched exactly, and trees aren't used here" - ); for (can_idx, src) in items .iter() diff --git a/gix-diff/src/tree/visit.rs b/gix-diff/src/tree/visit.rs index f9feb0eeba8..cf8030e748c 100644 --- a/gix-diff/src/tree/visit.rs +++ b/gix-diff/src/tree/visit.rs @@ -110,6 +110,7 @@ mod change_impls { use gix_hash::oid; use gix_object::tree::EntryMode; + use crate::tree::visit::Relation; use crate::{rewrites::tracker::ChangeKind, tree::visit::Change}; impl crate::rewrites::tracker::Change for crate::tree::visit::Change { @@ -119,6 +120,13 @@ mod change_impls { } } + fn relation(&self) -> Option { + match self { + Change::Addition { relation, .. } | Change::Deletion { relation, .. } => *relation, + Change::Modification { .. } => None, + } + } + fn kind(&self) -> ChangeKind { match self { Change::Addition { .. } => ChangeKind::Addition, diff --git a/gix-diff/tests/rewrites/mod.rs b/gix-diff/tests/rewrites/mod.rs index ddcb12dfc86..c5b152c3245 100644 --- a/gix-diff/tests/rewrites/mod.rs +++ b/gix-diff/tests/rewrites/mod.rs @@ -1,14 +1,16 @@ use gix_diff::rewrites::tracker::ChangeKind; +use gix_diff::tree::visit::{ChangeId, Relation}; use gix_hash::{oid, ObjectId}; use gix_object::tree::{EntryKind, EntryMode}; mod tracker; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Change { id: ObjectId, kind: ChangeKind, mode: EntryMode, + relation: Option, } impl gix_diff::rewrites::tracker::Change for Change { @@ -16,6 +18,10 @@ impl gix_diff::rewrites::tracker::Change for Change { &self.id } + fn relation(&self) -> Option { + self.relation + } + fn kind(&self) -> ChangeKind { self.kind } @@ -37,6 +43,7 @@ impl Change { id: NULL_ID, kind: ChangeKind::Modification, mode: EntryKind::Blob.into(), + relation: None, } } fn deletion() -> Self { @@ -44,6 +51,7 @@ impl Change { id: NULL_ID, kind: ChangeKind::Deletion, mode: EntryKind::Blob.into(), + relation: None, } } fn addition() -> Self { @@ -51,6 +59,43 @@ impl Change { id: NULL_ID, kind: ChangeKind::Addition, mode: EntryKind::Blob.into(), + relation: None, + } + } + + fn addition_in_tree(id: ChangeId) -> Self { + Change { + id: NULL_ID, + kind: ChangeKind::Addition, + mode: EntryKind::Blob.into(), + relation: Some(Relation::ChildOfParent(id)), + } + } + + fn deletion_in_tree(id: ChangeId) -> Self { + Change { + id: NULL_ID, + kind: ChangeKind::Deletion, + mode: EntryKind::Blob.into(), + relation: Some(Relation::ChildOfParent(id)), + } + } + + fn tree_addition(id: ChangeId) -> Self { + Change { + id: NULL_ID, + kind: ChangeKind::Addition, + mode: EntryKind::Tree.into(), + relation: Some(Relation::Parent(id)), + } + } + + fn tree_deletion(id: ChangeId) -> Self { + Change { + id: NULL_ID, + kind: ChangeKind::Deletion, + mode: EntryKind::Tree.into(), + relation: Some(Relation::Parent(id)), } } } diff --git a/gix-diff/tests/rewrites/tracker.rs b/gix-diff/tests/rewrites/tracker.rs index 77d3a0f6a6f..1aeb0f442c4 100644 --- a/gix-diff/tests/rewrites/tracker.rs +++ b/gix-diff/tests/rewrites/tracker.rs @@ -1,3 +1,8 @@ +use crate::{ + hex_to_id, + rewrites::{Change, NULL_ID}, +}; +use gix_diff::tree::visit::Relation; use gix_diff::{ blob::DiffLineStats, rewrites, @@ -14,11 +19,6 @@ use gix_diff::{ use gix_object::tree::EntryKind; use pretty_assertions::assert_eq; -use crate::{ - hex_to_id, - rewrites::{Change, NULL_ID}, -}; - #[test] fn rename_by_id() -> crate::Result { // Limits are only applied when doing rewrite-checks @@ -522,6 +522,211 @@ fn rename_by_50_percent_similarity() -> crate::Result { Ok(()) } +#[test] +fn directories_without_relation_are_ignored() -> crate::Result { + let mut track = util::new_tracker(Default::default()); + let tree_without_relation = Change { + id: NULL_ID, + kind: ChangeKind::Deletion, + mode: EntryKind::Tree.into(), + relation: None, + }; + assert_eq!( + track.try_push_change(tree_without_relation, "dir".into()), + Some(tree_without_relation), + "trees, or non-blobs, are ignored, particularly when they have no relation" + ); + Ok(()) +} + +#[test] +fn directory_renames_by_id_can_fail_gracefully() -> crate::Result { + let rename_by_similarity = Rewrites { + copies: None, + percentage: Some(0.5), + limit: 0, + }; + let mut track = util::new_tracker(rename_by_similarity); + let tree_dst_id = 1; + let tree_id = hex_to_id("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + assert!(track + .try_push_change( + Change { + id: tree_id, + kind: ChangeKind::Addition, + mode: EntryKind::Tree.into(), + relation: Some(Relation::Parent(tree_dst_id)), + }, + "d-renamed".into() + ) + .is_none()); + + let tree_src_id = 3; + let tree_id = hex_to_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + assert!(track + .try_push_change( + Change { + id: tree_id, + kind: ChangeKind::Addition, + mode: EntryKind::Tree.into(), + relation: Some(Relation::Parent(tree_src_id)), + }, + "d".into() + ) + .is_none()); + let odb = util::add_retained_blobs( + &mut track, + [ + (Change::deletion_in_tree(tree_src_id), "d/a", "a"), + (Change::deletion_in_tree(tree_src_id), "d/c", "c"), + (Change::deletion_in_tree(tree_src_id), "d/subdir/d", "d"), + (Change::addition_in_tree(tree_dst_id), "d-renamed/a", "a"), + (Change::addition_in_tree(tree_dst_id), "d-renamed/subdir/c", "c"), + (Change::deletion(), "a", "first\nsecond\n"), + (Change::addition(), "b", "firt\nsecond\n"), + ], + ); + let mut calls = 0; + let out = util::assert_emit_with_objects( + &mut track, + |dst, src| { + match calls { + 0..=2 => { + let src = src.unwrap(); + let (expected_src, expected_dst) = + &[("d/a", "d-renamed/a"), ("d/c", "d-renamed/subdir/c"), ("a", "b")][calls]; + assert_eq!(src.location, expected_src); + assert_eq!(dst.location, expected_dst); + } + 3..=6 => { + assert_eq!(src, None); + let expected_dst = ["d", "d-renamed", "d/subdir/d"][calls - 3]; + assert_eq!(dst.location, expected_dst); + } + _ => unreachable!("Should have expected emission call {calls}"), + } + calls += 1; + Action::Continue + }, + &odb, + ); + assert_eq!( + out, + rewrites::Outcome { + options: rename_by_similarity, + num_similarity_checks: 1, + ..Default::default() + } + ); + assert_eq!(calls, 6, "Should not have too few calls"); + Ok(()) +} + +#[test] +fn simple_directory_rename_by_id() -> crate::Result { + let renames_by_identity = Rewrites { + copies: None, + percentage: None, + limit: 0, + }; + let mut track = util::new_tracker(renames_by_identity); + let tree_dst_id = 1; + assert!(track + .try_push_change(Change::tree_addition(tree_dst_id), "d-renamed".into()) + .is_none()); + let tree_src_id = 3; + assert!(track + .try_push_change(Change::tree_deletion(tree_src_id), "d".into()) + .is_none()); + let tree_id = hex_to_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + assert!( + track + .try_push_change( + Change { + id: tree_id, /* does not matter for trees */ + kind: ChangeKind::Deletion, + mode: EntryKind::Tree.into(), + relation: Some(Relation::ChildOfParent(tree_src_id)), + }, + "d/subdir".into(), + ) + .is_some(), + "trees that are children are simply ignored. It's easier to compare views of worktrees (`gix-dirwalk`) \ + and trees/indices that way." + ); + assert!(track + .try_push_change( + Change { + id: tree_id, + kind: ChangeKind::Addition, + mode: EntryKind::Tree.into(), + relation: Some(Relation::ChildOfParent(tree_dst_id)), + }, + "d-renamed/subdir".into(), + ) + .is_some()); + let _odb = util::add_retained_blobs( + &mut track, + [ + (Change::deletion_in_tree(tree_src_id), "d/a", "a"), + (Change::deletion_in_tree(tree_src_id), "d/b", "b"), + (Change::deletion_in_tree(tree_src_id), "d/c", "c"), + (Change::deletion_in_tree(tree_src_id), "d/subdir/d", "d"), + (Change::addition_in_tree(tree_dst_id), "d-renamed/a", "a"), + (Change::addition_in_tree(tree_dst_id), "d-renamed/b", "b"), + (Change::addition_in_tree(tree_dst_id), "d-renamed/c", "c"), + (Change::addition_in_tree(tree_dst_id), "d-renamed/subdir/d", "d"), + (Change::deletion(), "a", "first\nsecond\n"), + (Change::addition(), "b", "firt\nsecond\n"), + ], + ); + let mut calls = 0; + let out = util::assert_emit(&mut track, |dst, src| { + match calls { + 0 => { + let src = src.unwrap(); + assert_eq!(src.location, "d"); + assert_eq!(src.entry_mode.kind(), EntryKind::Tree); + assert_eq!(src.change.relation, Some(Relation::Parent(3))); + assert_eq!(dst.location, "d-renamed", "it found the renamed directory"); + assert_eq!(dst.change.relation, Some(Relation::Parent(1))); + assert_eq!(dst.change.mode.kind(), EntryKind::Tree); + } + 1..=4 => { + let src = src.unwrap(); + let (expected_src, expected_dst) = &[ + ("d/a", "d-renamed/a"), + ("d/c", "d-renamed/c"), + ("d/b", "d-renamed/b"), + ("d/subdir/d", "d-renamed/subdir/d"), + ][calls - 1]; + assert_eq!(src.location, expected_src); + assert_eq!(dst.location, expected_dst); + } + 5 => { + assert_eq!(src, None); + assert_eq!(dst.location, "a"); + } + 6 => { + assert_eq!(src, None); + assert_eq!(dst.location, "b"); + } + _ => unreachable!("Should have expected emission call {calls}"), + } + calls += 1; + Action::Continue + }); + assert_eq!( + out, + rewrites::Outcome { + options: renames_by_identity, + ..Default::default() + } + ); + assert_eq!(calls, 7, "Should not have too few calls"); + Ok(()) +} + #[test] fn remove_only() -> crate::Result { let mut track = util::new_tracker(Default::default()); @@ -554,7 +759,7 @@ fn add_only() -> crate::Result { let out = util::assert_emit(&mut track, |dst, src| { assert!(!called); called = true; - assert!(src.is_none(), "there is just a single deletion, no pair"); + assert!(src.is_none(), "there is just a single addition, no pair"); assert_eq!(dst.location, "a"); assert_eq!(dst.change.kind, ChangeKind::Addition); Action::Continue diff --git a/gix/tests/object/tree/diff.rs b/gix/tests/object/tree/diff.rs index e39e6cf3a67..149967c8357 100644 --- a/gix/tests/object/tree/diff.rs +++ b/gix/tests/object/tree/diff.rs @@ -1270,10 +1270,10 @@ rename to gix-sec/tests/sec.rs "gix-sec/src/permission.rs", "git-sec/src/trust.rs", "gix-sec/src/trust.rs", + "git-sec/tests/identity/mod.rs", + "gix-sec/tests/identity/mod.rs", "git-sec/tests/sec.rs", "gix-sec/tests/sec.rs", - "git-sec/tests/identity/mod.rs", - "gix-sec/tests/identity/mod.rs" ] ); From 2bf1e5f15a60ef57c7c15279124d3eb227e585e0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 2 Oct 2024 21:11:16 +0200 Subject: [PATCH 05/13] adapt to changes in `gix-diff` --- gix-status/src/index_as_worktree_with_renames/mod.rs | 7 +++++++ gix/tests/object/tree/diff.rs | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/gix-status/src/index_as_worktree_with_renames/mod.rs b/gix-status/src/index_as_worktree_with_renames/mod.rs index 6479877ebaf..257879c392c 100644 --- a/gix-status/src/index_as_worktree_with_renames/mod.rs +++ b/gix-status/src/index_as_worktree_with_renames/mod.rs @@ -404,6 +404,7 @@ pub(super) mod function { use crate::index_as_worktree_with_renames::{Entry, Error}; use bstr::BStr; use gix_diff::rewrites::tracker::ChangeKind; + use gix_diff::tree::visit::Relation; use gix_dir::entry::Kind; use gix_filter::pipeline::convert::ToGitOutcome; use gix_hash::oid; @@ -437,6 +438,12 @@ pub(super) mod function { } } + fn relation(&self) -> Option { + // TODO: figure out if index or worktree can provide containerization - worktree should be possible. + // index would take some processing. + None + } + fn kind(&self) -> ChangeKind { match self { ModificationOrDirwalkEntry::Modification(m) => match &m.status { diff --git a/gix/tests/object/tree/diff.rs b/gix/tests/object/tree/diff.rs index 149967c8357..0fc92644d99 100644 --- a/gix/tests/object/tree/diff.rs +++ b/gix/tests/object/tree/diff.rs @@ -1270,10 +1270,10 @@ rename to gix-sec/tests/sec.rs "gix-sec/src/permission.rs", "git-sec/src/trust.rs", "gix-sec/src/trust.rs", - "git-sec/tests/identity/mod.rs", - "gix-sec/tests/identity/mod.rs", "git-sec/tests/sec.rs", "gix-sec/tests/sec.rs", + "git-sec/tests/identity/mod.rs", + "gix-sec/tests/identity/mod.rs", ] ); From af0383254422b70d53f27572c415eea2e4154447 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 4 Oct 2024 16:27:38 +0200 Subject: [PATCH 06/13] thanks clippy --- Cargo.lock | 4 +-- gitoxide-core/src/hours/util.rs | 4 +-- gitoxide-core/src/query/engine/update.rs | 2 +- .../src/repository/revision/explain.rs | 8 +++--- gitoxide-core/src/repository/tree.rs | 2 +- gix-actor/src/identity.rs | 2 +- gix-actor/src/signature/mod.rs | 2 +- gix-attributes/src/name.rs | 2 +- gix-attributes/src/state.rs | 2 +- gix-commitgraph/src/file/commit.rs | 8 +++--- gix-config-value/src/path.rs | 6 ++-- gix-config/src/file/access/comfort.rs | 2 +- gix-config/src/file/mutable/multi_value.rs | 2 +- gix-config/src/file/mutable/section.rs | 2 +- gix-config/src/file/mutable/value.rs | 2 +- gix-config/src/file/section/body.rs | 2 +- gix-diff/src/rewrites/tracker.rs | 2 +- gix-diff/src/tree/changes.rs | 2 +- gix-dir/tests/walk_utils/mod.rs | 2 +- gix-features/src/interrupt.rs | 8 +++--- gix-features/src/parallel/serial.rs | 2 +- gix-filter/src/driver/process/client.rs | 2 +- gix-filter/src/pipeline/convert.rs | 6 ++-- gix-fs/src/dir/create.rs | 4 +-- gix-hash/src/oid.rs | 2 +- gix-ignore/src/parse.rs | 2 +- gix-index/src/access/mod.rs | 2 +- gix-negotiate/tests/baseline/mod.rs | 2 +- gix-object/src/blob.rs | 4 +-- gix-object/src/commit/message/body.rs | 4 +-- gix-object/src/commit/ref_iter.rs | 6 ++-- gix-object/src/commit/write.rs | 2 +- gix-object/src/object/convert.rs | 2 +- gix-object/src/object/mod.rs | 2 +- gix-object/src/tag/ref_iter.rs | 2 +- gix-object/src/tag/write.rs | 2 +- gix-object/src/tree/editor.rs | 4 +-- gix-object/src/tree/mod.rs | 4 +-- gix-object/src/tree/write.rs | 2 +- gix-odb/src/store_impls/dynamic/handle.rs | 2 +- gix-odb/src/store_impls/dynamic/load_index.rs | 2 +- gix-pack/src/data/input/bytes_to_entries.rs | 4 +-- .../src/data/output/count/objects/tree.rs | 4 +-- gix-pack/src/index/traverse/reduce.rs | 2 +- .../src/line/blocking_io.rs | 10 +++---- .../src/read/sidebands/async_io.rs | 12 ++++---- .../src/read/sidebands/blocking_io.rs | 6 ++-- gix-packetline/src/line/blocking_io.rs | 10 +++---- gix-packetline/src/read/sidebands/async_io.rs | 12 ++++---- .../src/read/sidebands/blocking_io.rs | 6 ++-- gix-prompt/src/unix.rs | 8 +++--- gix-prompt/tests/prompt.rs | 5 ++-- gix-protocol/src/handshake/refs/tests.rs | 12 ++++---- gix-protocol/src/remote_progress.rs | 2 +- gix-ref/src/name.rs | 2 +- gix-ref/src/store/file/log/iter.rs | 4 +-- gix-ref/src/store/file/log/line.rs | 4 +-- gix-ref/src/store/file/overlay_iter.rs | 6 ++-- gix-ref/src/store/file/transaction/commit.rs | 2 +- gix-ref/src/store/file/transaction/mod.rs | 2 +- gix-ref/src/store/file/transaction/prepare.rs | 4 +-- gix-ref/src/store/packed/mod.rs | 2 +- gix-ref/src/target.rs | 2 +- gix-refspec/src/match_group/validate.rs | 2 +- gix-revision/src/describe.rs | 6 ++-- gix-revision/src/spec/parse/function.rs | 8 +++--- gix-revwalk/src/graph/commit.rs | 2 +- gix-revwalk/src/graph/mod.rs | 14 +++++----- gix-status/src/index_as_worktree/function.rs | 4 +-- gix-status/src/index_as_worktree/traits.rs | 2 +- .../src/index_as_worktree_with_renames/mod.rs | 4 +-- .../src/client/async_io/bufread_ext.rs | 2 +- gix-transport/src/client/async_io/request.rs | 2 +- .../src/client/blocking_io/bufread_ext.rs | 2 +- .../src/client/blocking_io/request.rs | 2 +- gix-utils/src/btoi.rs | 28 +++++++++---------- gix-utils/src/buffers.rs | 2 +- gix-worktree-state/src/checkout/chunk.rs | 2 +- gix-worktree-stream/src/entry.rs | 2 +- gix-worktree/src/stack/delegate.rs | 2 +- gix-worktree/src/stack/platform.rs | 2 +- gix/src/attribute_stack.rs | 2 +- gix/src/commit.rs | 2 +- gix/src/config/snapshot/access.rs | 2 +- gix/src/create.rs | 8 +++--- gix/src/ext/tree.rs | 4 +-- gix/src/filter.rs | 2 +- gix/src/id.rs | 16 +++++------ gix/src/object/blob.rs | 2 +- gix/src/object/commit.rs | 4 +-- gix/src/object/impls.rs | 4 +-- gix/src/object/mod.rs | 2 +- gix/src/object/tree/diff/change.rs | 4 +-- gix/src/object/tree/diff/for_each.rs | 6 ++-- gix/src/object/tree/diff/mod.rs | 4 +-- gix/src/object/tree/iter.rs | 2 +- gix/src/object/tree/mod.rs | 2 +- gix/src/object/tree/traverse.rs | 4 +-- gix/src/reference/edits.rs | 4 +-- gix/src/reference/iter.rs | 4 +-- gix/src/reference/log.rs | 2 +- gix/src/reference/mod.rs | 2 +- gix/src/remote/connection/access.rs | 22 +++++++-------- gix/src/remote/connection/fetch/mod.rs | 6 ++-- .../remote/connection/fetch/receive_pack.rs | 2 +- gix/src/remote/connection/ref_map.rs | 2 +- gix/src/remote/name.rs | 2 +- gix/src/repository/remote.rs | 2 +- gix/src/revision/spec/mod.rs | 8 +++--- gix/src/revision/spec/parse/delegate/mod.rs | 6 ++-- .../revision/spec/parse/delegate/navigate.rs | 2 +- .../revision/spec/parse/delegate/revision.rs | 2 +- gix/src/revision/walk.rs | 2 +- gix/src/status/index_worktree.rs | 2 +- gix/src/status/platform.rs | 2 +- gix/src/submodule/mod.rs | 4 +-- gix/src/types.rs | 10 +++---- gix/src/worktree/mod.rs | 4 +-- gix/src/worktree/proxy.rs | 2 +- gix/tests/repository/worktree.rs | 4 +-- tests/tools/src/lib.rs | 2 +- 121 files changed, 252 insertions(+), 251 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea9ef059fba..802b90fb6b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,9 +314,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", diff --git a/gitoxide-core/src/hours/util.rs b/gitoxide-core/src/hours/util.rs index 2ee1b559bcf..1d7ef7c273f 100644 --- a/gitoxide-core/src/hours/util.rs +++ b/gitoxide-core/src/hours/util.rs @@ -14,8 +14,8 @@ pub struct WorkByPerson { pub lines: LineStats, } -impl<'a> WorkByPerson { - pub fn merge(&mut self, other: &'a WorkByEmail) { +impl WorkByPerson { + pub fn merge(&mut self, other: &WorkByEmail) { if !self.name.contains(&other.name) { self.name.push(other.name); } diff --git a/gitoxide-core/src/query/engine/update.rs b/gitoxide-core/src/query/engine/update.rs index f3f395593e1..da332ea1b17 100644 --- a/gitoxide-core/src/query/engine/update.rs +++ b/gitoxide-core/src/query/engine/update.rs @@ -369,7 +369,7 @@ pub fn update( } } - impl<'a, Find> gix::prelude::Find for Db<'a, Find> + impl gix::prelude::Find for Db<'_, Find> where Find: gix::prelude::Find + Clone, { diff --git a/gitoxide-core/src/repository/revision/explain.rs b/gitoxide-core/src/repository/revision/explain.rs index ef1c79adf8e..07bee89cb89 100644 --- a/gitoxide-core/src/repository/revision/explain.rs +++ b/gitoxide-core/src/repository/revision/explain.rs @@ -55,7 +55,7 @@ impl<'a> Explain<'a> { } } -impl<'a> delegate::Revision for Explain<'a> { +impl delegate::Revision for Explain<'_> { fn find_ref(&mut self, name: &BStr) -> Option<()> { self.prefix()?; self.ref_name = Some(name.into()); @@ -121,7 +121,7 @@ impl<'a> delegate::Revision for Explain<'a> { } } -impl<'a> delegate::Navigate for Explain<'a> { +impl delegate::Navigate for Explain<'_> { fn traverse(&mut self, kind: Traversal) -> Option<()> { self.prefix()?; let name = self.revision_name(); @@ -194,7 +194,7 @@ impl<'a> delegate::Navigate for Explain<'a> { } } -impl<'a> delegate::Kind for Explain<'a> { +impl delegate::Kind for Explain<'_> { fn kind(&mut self, kind: spec::Kind) -> Option<()> { self.prefix()?; self.call = 0; @@ -215,7 +215,7 @@ impl<'a> delegate::Kind for Explain<'a> { } } -impl<'a> Delegate for Explain<'a> { +impl Delegate for Explain<'_> { fn done(&mut self) { if !self.has_implicit_anchor && self.ref_name.is_none() && self.oid_prefix.is_none() { self.err = Some("Incomplete specification lacks its anchor, like a reference or object name".into()); diff --git a/gitoxide-core/src/repository/tree.rs b/gitoxide-core/src/repository/tree.rs index 77379d29956..04881317737 100644 --- a/gitoxide-core/src/repository/tree.rs +++ b/gitoxide-core/src/repository/tree.rs @@ -65,7 +65,7 @@ mod entries { } } - impl<'repo, 'a> gix::traverse::tree::Visit for Traverse<'repo, 'a> { + impl gix::traverse::tree::Visit for Traverse<'_, '_> { fn pop_front_tracked_path_and_set_current(&mut self) { self.path = self.path_deque.pop_front().expect("every parent is set only once"); } diff --git a/gix-actor/src/identity.rs b/gix-actor/src/identity.rs index e6d8fcb520f..792405b5077 100644 --- a/gix-actor/src/identity.rs +++ b/gix-actor/src/identity.rs @@ -40,7 +40,7 @@ mod write { } } - impl<'a> IdentityRef<'a> { + impl IdentityRef<'_> { /// Serialize this instance to `out` in the git serialization format for signatures (but without timestamp). pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> { out.write_all(validated_token(self.name)?)?; diff --git a/gix-actor/src/signature/mod.rs b/gix-actor/src/signature/mod.rs index 057d4293e7d..64a48739493 100644 --- a/gix-actor/src/signature/mod.rs +++ b/gix-actor/src/signature/mod.rs @@ -104,7 +104,7 @@ pub(crate) mod write { } } - impl<'a> SignatureRef<'a> { + impl SignatureRef<'_> { /// Serialize this instance to `out` in the git serialization format for actors. pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> { out.write_all(validated_token(self.name)?)?; diff --git a/gix-attributes/src/name.rs b/gix-attributes/src/name.rs index bed73388e78..c8d50743a1e 100644 --- a/gix-attributes/src/name.rs +++ b/gix-attributes/src/name.rs @@ -2,7 +2,7 @@ use crate::{Name, NameRef}; use bstr::{BStr, BString, ByteSlice}; use kstring::KStringRef; -impl<'a> NameRef<'a> { +impl NameRef<'_> { /// Turn this ref into its owned counterpart. pub fn to_owned(self) -> Name { Name(self.0.into()) diff --git a/gix-attributes/src/state.rs b/gix-attributes/src/state.rs index 0e9b41a2d75..21e51faedf1 100644 --- a/gix-attributes/src/state.rs +++ b/gix-attributes/src/state.rs @@ -96,7 +96,7 @@ impl<'a> StateRef<'a> { } /// Access -impl<'a> StateRef<'a> { +impl StateRef<'_> { /// Turn ourselves into our owned counterpart. pub fn to_owned(self) -> State { self.into() diff --git a/gix-commitgraph/src/file/commit.rs b/gix-commitgraph/src/file/commit.rs index db47cfcead9..f735615c2ea 100644 --- a/gix-commitgraph/src/file/commit.rs +++ b/gix-commitgraph/src/file/commit.rs @@ -106,7 +106,7 @@ impl<'a> Commit<'a> { } } -impl<'a> Debug for Commit<'a> { +impl Debug for Commit<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, @@ -121,9 +121,9 @@ impl<'a> Debug for Commit<'a> { } } -impl<'a> Eq for Commit<'a> {} +impl Eq for Commit<'_> {} -impl<'a> PartialEq for Commit<'a> { +impl PartialEq for Commit<'_> { fn eq(&self, other: &Self) -> bool { std::ptr::eq(self.file, other.file) && self.pos == other.pos } @@ -135,7 +135,7 @@ pub struct Parents<'a> { state: ParentIteratorState<'a>, } -impl<'a> Iterator for Parents<'a> { +impl Iterator for Parents<'_> { type Item = Result; fn next(&mut self) -> Option { diff --git a/gix-config-value/src/path.rs b/gix-config-value/src/path.rs index 703c4db332e..4d5d08e017e 100644 --- a/gix-config-value/src/path.rs +++ b/gix-config-value/src/path.rs @@ -86,7 +86,7 @@ pub mod interpolate { } } -impl<'a> std::ops::Deref for Path<'a> { +impl std::ops::Deref for Path<'_> { type Target = BStr; fn deref(&self) -> &Self::Target { @@ -94,13 +94,13 @@ impl<'a> std::ops::Deref for Path<'a> { } } -impl<'a> AsRef<[u8]> for Path<'a> { +impl AsRef<[u8]> for Path<'_> { fn as_ref(&self) -> &[u8] { self.value.as_ref() } } -impl<'a> AsRef for Path<'a> { +impl AsRef for Path<'_> { fn as_ref(&self) -> &BStr { self.value.as_ref() } diff --git a/gix-config/src/file/access/comfort.rs b/gix-config/src/file/access/comfort.rs index 80661f0ee8e..12753891ec0 100644 --- a/gix-config/src/file/access/comfort.rs +++ b/gix-config/src/file/access/comfort.rs @@ -5,7 +5,7 @@ use bstr::BStr; use crate::{file::MetadataFilter, value, AsKey, File}; /// Comfortable API for accessing values -impl<'event> File<'event> { +impl File<'_> { /// Like [`string_by()`](File::string_by()), but suitable for statically known `key`s like `remote.origin.url`. pub fn string(&self, key: impl AsKey) -> Option> { self.string_filter(key, &mut |_| true) diff --git a/gix-config/src/file/mutable/multi_value.rs b/gix-config/src/file/mutable/multi_value.rs index a1761108a24..b01f9b02182 100644 --- a/gix-config/src/file/mutable/multi_value.rs +++ b/gix-config/src/file/mutable/multi_value.rs @@ -35,7 +35,7 @@ pub struct MultiValueMut<'borrow, 'lookup, 'event> { pub(crate) offsets: HashMap>, } -impl<'borrow, 'lookup, 'event> MultiValueMut<'borrow, 'lookup, 'event> { +impl<'lookup, 'event> MultiValueMut<'_, 'lookup, 'event> { /// Returns the actual values. pub fn get(&self) -> Result>, lookup::existing::Error> { let mut expect_value = false; diff --git a/gix-config/src/file/mutable/section.rs b/gix-config/src/file/mutable/section.rs index d94c226e49d..e81eb8569e9 100644 --- a/gix-config/src/file/mutable/section.rs +++ b/gix-config/src/file/mutable/section.rs @@ -27,7 +27,7 @@ pub struct SectionMut<'a, 'event> { } /// Mutating methods. -impl<'a, 'event> SectionMut<'a, 'event> { +impl<'event> SectionMut<'_, 'event> { /// Adds an entry to the end of this section name `value_name` and `value`. If `value` is `None`, no equal sign will be written leaving /// just the key. This is useful for boolean values which are true if merely the key exists. pub fn push<'b>(&mut self, value_name: ValueName<'event>, value: Option<&'b BStr>) -> &mut Self { diff --git a/gix-config/src/file/mutable/value.rs b/gix-config/src/file/mutable/value.rs index 0e42e2cc0f4..f2e8c99f4e9 100644 --- a/gix-config/src/file/mutable/value.rs +++ b/gix-config/src/file/mutable/value.rs @@ -18,7 +18,7 @@ pub struct ValueMut<'borrow, 'lookup, 'event> { pub(crate) size: Size, } -impl<'borrow, 'lookup, 'event> ValueMut<'borrow, 'lookup, 'event> { +impl<'borrow, 'event> ValueMut<'borrow, '_, 'event> { /// Returns the actual value. This is computed each time this is called /// requiring an allocation for multi-line values. pub fn get(&self) -> Result, lookup::existing::Error> { diff --git a/gix-config/src/file/section/body.rs b/gix-config/src/file/section/body.rs index 45546aa135e..54f26303feb 100644 --- a/gix-config/src/file/section/body.rs +++ b/gix-config/src/file/section/body.rs @@ -120,7 +120,7 @@ impl<'event> Body<'event> { } } -impl<'event> Body<'event> { +impl Body<'_> { pub(crate) fn as_ref(&self) -> &[Event<'_>] { &self.0 } diff --git a/gix-diff/src/rewrites/tracker.rs b/gix-diff/src/rewrites/tracker.rs index 678a646acf5..bd10edf4ffd 100644 --- a/gix-diff/src/rewrites/tracker.rs +++ b/gix-diff/src/rewrites/tracker.rs @@ -641,7 +641,7 @@ mod diff { pub input: &'a crate::blob::intern::InternedInput<&'data [u8]>, } - impl<'a, 'data> crate::blob::Sink for Statistics<'a, 'data> { + impl crate::blob::Sink for Statistics<'_, '_> { type Out = usize; fn process_change(&mut self, before: Range, _after: Range) { diff --git a/gix-diff/src/tree/changes.rs b/gix-diff/src/tree/changes.rs index f7c545526ef..cdabefd549f 100644 --- a/gix-diff/src/tree/changes.rs +++ b/gix-diff/src/tree/changes.rs @@ -20,7 +20,7 @@ pub enum Error { EntriesDecode(#[from] gix_object::decode::Error), } -impl<'a> tree::Changes<'a> { +impl tree::Changes<'_> { /// Calculate the changes that would need to be applied to `self` to get `other` using `objects` to obtain objects as needed for traversal. /// /// * The `state` maybe owned or mutably borrowed to allow reuses allocated data structures through multiple runs. diff --git a/gix-dir/tests/walk_utils/mod.rs b/gix-dir/tests/walk_utils/mod.rs index 3a8bb72947c..37b5d12a80d 100644 --- a/gix-dir/tests/walk_utils/mod.rs +++ b/gix-dir/tests/walk_utils/mod.rs @@ -374,7 +374,7 @@ impl<'a> Options<'a> { } } -impl<'a> Default for Options<'a> { +impl Default for Options<'_> { fn default() -> Self { Options { fresh_index: true, diff --git a/gix-features/src/interrupt.rs b/gix-features/src/interrupt.rs index 2f972d5c2ce..187dcde7425 100644 --- a/gix-features/src/interrupt.rs +++ b/gix-features/src/interrupt.rs @@ -29,7 +29,7 @@ where } } -impl<'a, I> Iterator for Iter<'a, I> +impl Iterator for Iter<'_, I> where I: Iterator, { @@ -67,7 +67,7 @@ where } } -impl<'a, I, EFN, E> Iterator for IterWithErr<'a, I, EFN> +impl Iterator for IterWithErr<'_, I, EFN> where I: Iterator, EFN: FnOnce() -> E, @@ -99,7 +99,7 @@ pub struct Read<'a, R> { pub should_interrupt: &'a AtomicBool, } -impl<'a, R> io::Read for Read<'a, R> +impl io::Read for Read<'_, R> where R: io::Read, { @@ -111,7 +111,7 @@ where } } -impl<'a, R> io::BufRead for Read<'a, R> +impl io::BufRead for Read<'_, R> where R: io::BufRead, { diff --git a/gix-features/src/parallel/serial.rs b/gix-features/src/parallel/serial.rs index 9f91de6cdd6..67c170f7a4d 100644 --- a/gix-features/src/parallel/serial.rs +++ b/gix-features/src/parallel/serial.rs @@ -42,7 +42,7 @@ mod not_parallel { } } - impl<'scope, 'env> Scope<'scope, 'env> { + impl<'scope> Scope<'scope, '_> { /// Provided with this scope, let `f` start new threads that live within it. pub fn spawn(&'scope self, f: F) -> ScopedJoinHandle<'scope, T> where diff --git a/gix-filter/src/driver/process/client.rs b/gix-filter/src/driver/process/client.rs index 912b1629091..57e59cc3e87 100644 --- a/gix-filter/src/driver/process/client.rs +++ b/gix-filter/src/driver/process/client.rs @@ -256,7 +256,7 @@ struct ReadProcessOutputAndStatus<'a> { inner: PacketlineReader<'a>, } -impl<'a> std::io::Read for ReadProcessOutputAndStatus<'a> { +impl std::io::Read for ReadProcessOutputAndStatus<'_> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let num_read = self.inner.read(buf)?; if num_read == 0 { diff --git a/gix-filter/src/pipeline/convert.rs b/gix-filter/src/pipeline/convert.rs index 4962296656d..049ccdfbbc7 100644 --- a/gix-filter/src/pipeline/convert.rs +++ b/gix-filter/src/pipeline/convert.rs @@ -260,7 +260,7 @@ pub enum ToWorktreeOutcome<'input, 'pipeline> { Process(driver::apply::MaybeDelayed<'pipeline>), } -impl<'input, 'pipeline> ToWorktreeOutcome<'input, 'pipeline> { +impl ToWorktreeOutcome<'_, '_> { /// Return true if this outcome is delayed. In that case, one isn't allowed to use [`Read`] or cause a panic. pub fn is_delayed(&self) -> bool { matches!( @@ -297,7 +297,7 @@ impl<'input, 'pipeline> ToWorktreeOutcome<'input, 'pipeline> { } } -impl<'input, 'pipeline> std::io::Read for ToWorktreeOutcome<'input, 'pipeline> { +impl std::io::Read for ToWorktreeOutcome<'_, '_> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { match self { ToWorktreeOutcome::Unchanged(b) => b.read(buf), @@ -310,7 +310,7 @@ impl<'input, 'pipeline> std::io::Read for ToWorktreeOutcome<'input, 'pipeline> { } } -impl<'pipeline, R> std::io::Read for ToGitOutcome<'pipeline, R> +impl std::io::Read for ToGitOutcome<'_, R> where R: std::io::Read, { diff --git a/gix-fs/src/dir/create.rs b/gix-fs/src/dir/create.rs index 642629bfd1d..93dd5badbef 100644 --- a/gix-fs/src/dir/create.rs +++ b/gix-fs/src/dir/create.rs @@ -47,7 +47,7 @@ mod error { }, } - impl<'a> fmt::Display for Error<'a> { + impl fmt::Display for Error<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::Intermediate { dir, kind } => write!( @@ -69,7 +69,7 @@ mod error { } } - impl<'a> std::error::Error for Error<'a> { + impl std::error::Error for Error<'_> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Error::Permanent { err, .. } => Some(err), diff --git a/gix-hash/src/oid.rs b/gix-hash/src/oid.rs index 8d3bb89d932..66fdf3f5568 100644 --- a/gix-hash/src/oid.rs +++ b/gix-hash/src/oid.rs @@ -41,7 +41,7 @@ pub struct HexDisplay<'a> { hex_len: usize, } -impl<'a> std::fmt::Display for HexDisplay<'a> { +impl std::fmt::Display for HexDisplay<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut hex = Kind::hex_buf(); let max_len = self.inner.hex_to_buf(hex.as_mut()); diff --git a/gix-ignore/src/parse.rs b/gix-ignore/src/parse.rs index 4c21b82a285..cb53c456d94 100644 --- a/gix-ignore/src/parse.rs +++ b/gix-ignore/src/parse.rs @@ -17,7 +17,7 @@ impl<'a> Lines<'a> { } } -impl<'a> Iterator for Lines<'a> { +impl Iterator for Lines<'_> { type Item = (gix_glob::Pattern, usize, crate::Kind); fn next(&mut self) -> Option { diff --git a/gix-index/src/access/mod.rs b/gix-index/src/access/mod.rs index 36b80ad69e4..855a7e97dc7 100644 --- a/gix-index/src/access/mod.rs +++ b/gix-index/src/access/mod.rs @@ -389,7 +389,7 @@ impl State { } } -impl<'a> AccelerateLookup<'a> { +impl AccelerateLookup<'_> { fn with_capacity(cap: usize) -> Self { let ratio_of_entries_to_dirs_in_webkit = 20; // 400k entries and 20k dirs Self { diff --git a/gix-negotiate/tests/baseline/mod.rs b/gix-negotiate/tests/baseline/mod.rs index f149c9918d7..aefbd313f8b 100644 --- a/gix-negotiate/tests/baseline/mod.rs +++ b/gix-negotiate/tests/baseline/mod.rs @@ -154,7 +154,7 @@ impl<'a> ParseRounds<'a> { } } -impl<'a> Iterator for ParseRounds<'a> { +impl Iterator for ParseRounds<'_> { type Item = Round; fn next(&mut self) -> Option { diff --git a/gix-object/src/blob.rs b/gix-object/src/blob.rs index 57db54758f6..455155ce685 100644 --- a/gix-object/src/blob.rs +++ b/gix-object/src/blob.rs @@ -2,7 +2,7 @@ use std::{convert::Infallible, io}; use crate::{Blob, BlobRef, Kind}; -impl<'a> crate::WriteTo for BlobRef<'a> { +impl crate::WriteTo for BlobRef<'_> { /// Write the blobs data to `out` verbatim. fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { out.write_all(self.data) @@ -39,7 +39,7 @@ impl Blob { } } -impl<'a> BlobRef<'a> { +impl BlobRef<'_> { /// Instantiate a `Blob` from the given `data`, which is used as-is. pub fn from_bytes(data: &[u8]) -> Result, Infallible> { Ok(BlobRef { data }) diff --git a/gix-object/src/commit/message/body.rs b/gix-object/src/commit/message/body.rs index 4831e3b4050..e7b8bccd970 100644 --- a/gix-object/src/commit/message/body.rs +++ b/gix-object/src/commit/message/body.rs @@ -101,13 +101,13 @@ impl<'a> BodyRef<'a> { } } -impl<'a> AsRef for BodyRef<'a> { +impl AsRef for BodyRef<'_> { fn as_ref(&self) -> &BStr { self.body_without_trailer } } -impl<'a> Deref for BodyRef<'a> { +impl Deref for BodyRef<'_> { type Target = BStr; fn deref(&self) -> &Self::Target { diff --git a/gix-object/src/commit/ref_iter.rs b/gix-object/src/commit/ref_iter.rs index 6f5604f2c2b..d9d59f7498e 100644 --- a/gix-object/src/commit/ref_iter.rs +++ b/gix-object/src/commit/ref_iter.rs @@ -338,8 +338,8 @@ pub enum Token<'a> { Message(&'a BStr), } -impl<'a> Token<'a> { - /// Return the object id of this token if its a [tree][Token::Tree] or a [parent commit][Token::Parent]. +impl Token<'_> { + /// Return the object id of this token if it's a [tree][Token::Tree] or a [parent commit][Token::Parent]. pub fn id(&self) -> Option<&oid> { match self { Token::Tree { id } | Token::Parent { id } => Some(id.as_ref()), @@ -347,7 +347,7 @@ impl<'a> Token<'a> { } } - /// Return the owned object id of this token if its a [tree][Token::Tree] or a [parent commit][Token::Parent]. + /// Return the owned object id of this token if it's a [tree][Token::Tree] or a [parent commit][Token::Parent]. pub fn try_into_id(self) -> Option { match self { Token::Tree { id } | Token::Parent { id } => Some(id), diff --git a/gix-object/src/commit/write.rs b/gix-object/src/commit/write.rs index aca0dd25aa6..8f7041370d6 100644 --- a/gix-object/src/commit/write.rs +++ b/gix-object/src/commit/write.rs @@ -50,7 +50,7 @@ impl crate::WriteTo for Commit { } } -impl<'a> crate::WriteTo for CommitRef<'a> { +impl crate::WriteTo for CommitRef<'_> { /// Serializes this instance to `out` in the git serialization format. fn write_to(&self, mut out: &mut dyn io::Write) -> io::Result<()> { encode::trusted_header_id(b"tree", &self.tree(), &mut out)?; diff --git a/gix-object/src/object/convert.rs b/gix-object/src/object/convert.rs index 87ee35b1c48..99046b6e2d3 100644 --- a/gix-object/src/object/convert.rs +++ b/gix-object/src/object/convert.rs @@ -78,7 +78,7 @@ impl From> for tree::Entry { } } -impl<'a> From> for Object { +impl From> for Object { fn from(v: ObjectRef<'_>) -> Self { match v { ObjectRef::Tree(v) => Object::Tree(v.into()), diff --git a/gix-object/src/object/mod.rs b/gix-object/src/object/mod.rs index e7f775cd47b..853d62bb521 100644 --- a/gix-object/src/object/mod.rs +++ b/gix-object/src/object/mod.rs @@ -8,7 +8,7 @@ mod write { use crate::{Kind, Object, ObjectRef, WriteTo}; /// Serialization - impl<'a> WriteTo for ObjectRef<'a> { + impl WriteTo for ObjectRef<'_> { /// Write the contained object to `out` in the git serialization format. fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { use crate::ObjectRef::*; diff --git a/gix-object/src/tag/ref_iter.rs b/gix-object/src/tag/ref_iter.rs index f9acc9fc936..9c32fc7fb81 100644 --- a/gix-object/src/tag/ref_iter.rs +++ b/gix-object/src/tag/ref_iter.rs @@ -153,7 +153,7 @@ pub enum Token<'a> { }, } -impl<'a> Token<'a> { +impl Token<'_> { /// Return the object id of this token if its a [Target][Token::Target]. pub fn id(&self) -> Option<&oid> { match self { diff --git a/gix-object/src/tag/write.rs b/gix-object/src/tag/write.rs index dc8fd1ba336..b8ea340d190 100644 --- a/gix-object/src/tag/write.rs +++ b/gix-object/src/tag/write.rs @@ -57,7 +57,7 @@ impl crate::WriteTo for Tag { } } -impl<'a> crate::WriteTo for TagRef<'a> { +impl crate::WriteTo for TagRef<'_> { fn write_to(&self, mut out: &mut dyn io::Write) -> io::Result<()> { encode::trusted_header_field(b"object", self.target, &mut out)?; encode::trusted_header_field(b"type", self.target_kind.as_bytes(), &mut out)?; diff --git a/gix-object/src/tree/editor.rs b/gix-object/src/tree/editor.rs index d7632b7728d..4edf723b434 100644 --- a/gix-object/src/tree/editor.rs +++ b/gix-object/src/tree/editor.rs @@ -53,7 +53,7 @@ impl<'a> Editor<'a> { } /// Operations -impl<'a> Editor<'a> { +impl Editor<'_> { /// Write the entire in-memory state of all changed trees (and only changed trees) to `out`. /// Note that the returned object id *can* be the empty tree if everything was removed or if nothing /// was added to the tree. @@ -362,7 +362,7 @@ mod cursor { } } - impl<'a, 'find> Cursor<'a, 'find> { + impl Cursor<'_, '_> { /// Like [`Editor::upsert()`], but with the constraint of only editing in this cursor's tree. pub fn upsert(&mut self, rela_path: I, kind: EntryKind, id: ObjectId) -> Result<&mut Self, super::Error> where diff --git a/gix-object/src/tree/mod.rs b/gix-object/src/tree/mod.rs index 1d3c0838df0..688af26dd57 100644 --- a/gix-object/src/tree/mod.rs +++ b/gix-object/src/tree/mod.rs @@ -224,13 +224,13 @@ pub struct EntryRef<'a> { pub oid: &'a gix_hash::oid, } -impl<'a> PartialOrd for EntryRef<'a> { +impl PartialOrd for EntryRef<'_> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl<'a> Ord for EntryRef<'a> { +impl Ord for EntryRef<'_> { fn cmp(&self, b: &Self) -> Ordering { let a = self; let common = a.filename.len().min(b.filename.len()); diff --git a/gix-object/src/tree/write.rs b/gix-object/src/tree/write.rs index 831a65a236f..929620f0e04 100644 --- a/gix-object/src/tree/write.rs +++ b/gix-object/src/tree/write.rs @@ -70,7 +70,7 @@ impl crate::WriteTo for Tree { } /// Serialization -impl<'a> crate::WriteTo for TreeRef<'a> { +impl crate::WriteTo for TreeRef<'_> { /// Serialize this tree to `out` in the git internal format. fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { debug_assert_eq!( diff --git a/gix-odb/src/store_impls/dynamic/handle.rs b/gix-odb/src/store_impls/dynamic/handle.rs index 4084ac4c95e..945a8c23fb4 100644 --- a/gix-odb/src/store_impls/dynamic/handle.rs +++ b/gix-odb/src/store_impls/dynamic/handle.rs @@ -32,7 +32,7 @@ pub(crate) enum IntraPackLookup<'a> { }, } -impl<'a> IntraPackLookup<'a> { +impl IntraPackLookup<'_> { pub(crate) fn pack_offset_by_id(&self, id: &oid) -> Option { match self { IntraPackLookup::Single(index) => index diff --git a/gix-odb/src/store_impls/dynamic/load_index.rs b/gix-odb/src/store_impls/dynamic/load_index.rs index df5b6624668..a83381976cb 100644 --- a/gix-odb/src/store_impls/dynamic/load_index.rs +++ b/gix-odb/src/store_impls/dynamic/load_index.rs @@ -683,7 +683,7 @@ impl<'a> IncOnNewAndDecOnDrop<'a> { Self(v) } } -impl<'a> Drop for IncOnNewAndDecOnDrop<'a> { +impl Drop for IncOnNewAndDecOnDrop<'_> { fn drop(&mut self) { self.0.fetch_sub(1, Ordering::SeqCst); } diff --git a/gix-pack/src/data/input/bytes_to_entries.rs b/gix-pack/src/data/input/bytes_to_entries.rs index ed5902d60b9..f8f7aa926bd 100644 --- a/gix-pack/src/data/input/bytes_to_entries.rs +++ b/gix-pack/src/data/input/bytes_to_entries.rs @@ -291,7 +291,7 @@ pub struct DecompressRead<'a, R> { pub decompressor: &'a mut Decompress, } -impl<'a, R> io::Read for DecompressRead<'a, R> +impl io::Read for DecompressRead<'_, R> where R: io::BufRead, { @@ -308,7 +308,7 @@ pub struct HashWrite<'a, T> { pub inner: T, } -impl<'a, T> std::io::Write for HashWrite<'a, T> +impl std::io::Write for HashWrite<'_, T> where T: std::io::Write, { diff --git a/gix-pack/src/data/output/count/objects/tree.rs b/gix-pack/src/data/output/count/objects/tree.rs index 27cb161c9a9..3f464217b8b 100644 --- a/gix-pack/src/data/output/count/objects/tree.rs +++ b/gix-pack/src/data/output/count/objects/tree.rs @@ -28,7 +28,7 @@ pub mod changes { } } - impl<'a, H> Visit for AllNew<'a, H> + impl Visit for AllNew<'_, H> where H: InsertImmutable, { @@ -90,7 +90,7 @@ pub mod traverse { } } - impl<'a, H> Visit for AllUnseen<'a, H> + impl Visit for AllUnseen<'_, H> where H: InsertImmutable, { diff --git a/gix-pack/src/index/traverse/reduce.rs b/gix-pack/src/index/traverse/reduce.rs index 1c77ec327c1..2f8d548b9d5 100644 --- a/gix-pack/src/index/traverse/reduce.rs +++ b/gix-pack/src/index/traverse/reduce.rs @@ -63,7 +63,7 @@ where } } -impl<'a, P, E> parallel::Reduce for Reducer<'a, P, E> +impl parallel::Reduce for Reducer<'_, P, E> where P: Progress, E: std::error::Error + Send + Sync + 'static, diff --git a/gix-packetline-blocking/src/line/blocking_io.rs b/gix-packetline-blocking/src/line/blocking_io.rs index 0354e3de77a..749ccfce765 100644 --- a/gix-packetline-blocking/src/line/blocking_io.rs +++ b/gix-packetline-blocking/src/line/blocking_io.rs @@ -4,7 +4,7 @@ use std::io; use crate::{encode, BandRef, Channel, ErrorRef, PacketLineRef, TextRef}; -impl<'a> BandRef<'a> { +impl BandRef<'_> { /// Serialize this instance to `out`, returning the amount of bytes written. /// /// The data written to `out` can be decoded with [`Borrowed::decode_band()]`. @@ -17,23 +17,23 @@ impl<'a> BandRef<'a> { } } -impl<'a> TextRef<'a> { +impl TextRef<'_> { /// Serialize this instance to `out`, appending a newline if there is none, returning the amount of bytes written. pub fn write_to(&self, out: impl io::Write) -> io::Result { encode::text_to_write(self.0, out) } } -impl<'a> ErrorRef<'a> { +impl ErrorRef<'_> { /// Serialize this line as error to `out`. /// - /// This includes a marker to allow decoding it outside of a side-band channel, returning the amount of bytes written. + /// This includes a marker to allow decoding it outside a side-band channel, returning the amount of bytes written. pub fn write_to(&self, out: impl io::Write) -> io::Result { encode::error_to_write(self.0, out) } } -impl<'a> PacketLineRef<'a> { +impl PacketLineRef<'_> { /// Serialize this instance to `out` in git `packetline` format, returning the amount of bytes written to `out`. pub fn write_to(&self, out: impl io::Write) -> io::Result { match self { diff --git a/gix-packetline-blocking/src/read/sidebands/async_io.rs b/gix-packetline-blocking/src/read/sidebands/async_io.rs index 848ab23b140..9a9ddc9dff6 100644 --- a/gix-packetline-blocking/src/read/sidebands/async_io.rs +++ b/gix-packetline-blocking/src/read/sidebands/async_io.rs @@ -25,7 +25,7 @@ where cap: usize, } -impl<'a, T, F> Drop for WithSidebands<'a, T, F> +impl Drop for WithSidebands<'_, T, F> where T: AsyncRead, { @@ -72,7 +72,7 @@ enum State<'a, T> { /// to a thread possibly. // TODO: Is it possible to declare it as it should be? #[allow(unsafe_code, clippy::non_send_fields_in_send_ty)] -unsafe impl<'a, T> Send for State<'a, T> where T: Send {} +unsafe impl Send for State<'_, T> where T: Send {} impl<'a, T, F> WithSidebands<'a, T, F> where @@ -182,7 +182,7 @@ pub struct ReadDataLineFuture<'a, 'b, T: AsyncRead, F> { buf: &'b mut Vec, } -impl<'a, 'b, T, F> Future for ReadDataLineFuture<'a, 'b, T, F> +impl Future for ReadDataLineFuture<'_, '_, T, F> where T: AsyncRead + Unpin, F: FnMut(bool, &[u8]) -> ProgressAction + Unpin, @@ -209,7 +209,7 @@ pub struct ReadLineFuture<'a, 'b, T: AsyncRead, F> { buf: &'b mut String, } -impl<'a, 'b, T, F> Future for ReadLineFuture<'a, 'b, T, F> +impl Future for ReadLineFuture<'_, '_, T, F> where T: AsyncRead + Unpin, F: FnMut(bool, &[u8]) -> ProgressAction + Unpin, @@ -232,7 +232,7 @@ where } } -impl<'a, T, F> AsyncBufRead for WithSidebands<'a, T, F> +impl AsyncBufRead for WithSidebands<'_, T, F> where T: AsyncRead + Unpin, F: FnMut(bool, &[u8]) -> ProgressAction + Unpin, @@ -350,7 +350,7 @@ where } } -impl<'a, T, F> AsyncRead for WithSidebands<'a, T, F> +impl AsyncRead for WithSidebands<'_, T, F> where T: AsyncRead + Unpin, F: FnMut(bool, &[u8]) -> ProgressAction + Unpin, diff --git a/gix-packetline-blocking/src/read/sidebands/blocking_io.rs b/gix-packetline-blocking/src/read/sidebands/blocking_io.rs index 3a33082e3b8..347993e57db 100644 --- a/gix-packetline-blocking/src/read/sidebands/blocking_io.rs +++ b/gix-packetline-blocking/src/read/sidebands/blocking_io.rs @@ -17,7 +17,7 @@ where cap: usize, } -impl<'a, T, F> Drop for WithSidebands<'a, T, F> +impl Drop for WithSidebands<'_, T, F> where T: io::Read, { @@ -129,7 +129,7 @@ where } } -impl<'a, T, F> BufRead for WithSidebands<'a, T, F> +impl BufRead for WithSidebands<'_, T, F> where T: io::Read, F: FnMut(bool, &[u8]) -> ProgressAction, @@ -204,7 +204,7 @@ where } } -impl<'a, T, F> io::Read for WithSidebands<'a, T, F> +impl io::Read for WithSidebands<'_, T, F> where T: io::Read, F: FnMut(bool, &[u8]) -> ProgressAction, diff --git a/gix-packetline/src/line/blocking_io.rs b/gix-packetline/src/line/blocking_io.rs index 9e4ede311da..fa57d5ad797 100644 --- a/gix-packetline/src/line/blocking_io.rs +++ b/gix-packetline/src/line/blocking_io.rs @@ -2,7 +2,7 @@ use std::io; use crate::{encode, BandRef, Channel, ErrorRef, PacketLineRef, TextRef}; -impl<'a> BandRef<'a> { +impl BandRef<'_> { /// Serialize this instance to `out`, returning the amount of bytes written. /// /// The data written to `out` can be decoded with [`Borrowed::decode_band()]`. @@ -15,23 +15,23 @@ impl<'a> BandRef<'a> { } } -impl<'a> TextRef<'a> { +impl TextRef<'_> { /// Serialize this instance to `out`, appending a newline if there is none, returning the amount of bytes written. pub fn write_to(&self, out: impl io::Write) -> io::Result { encode::text_to_write(self.0, out) } } -impl<'a> ErrorRef<'a> { +impl ErrorRef<'_> { /// Serialize this line as error to `out`. /// - /// This includes a marker to allow decoding it outside of a side-band channel, returning the amount of bytes written. + /// This includes a marker to allow decoding it outside a side-band channel, returning the amount of bytes written. pub fn write_to(&self, out: impl io::Write) -> io::Result { encode::error_to_write(self.0, out) } } -impl<'a> PacketLineRef<'a> { +impl PacketLineRef<'_> { /// Serialize this instance to `out` in git `packetline` format, returning the amount of bytes written to `out`. pub fn write_to(&self, out: impl io::Write) -> io::Result { match self { diff --git a/gix-packetline/src/read/sidebands/async_io.rs b/gix-packetline/src/read/sidebands/async_io.rs index 974bac9597b..cd563bfc601 100644 --- a/gix-packetline/src/read/sidebands/async_io.rs +++ b/gix-packetline/src/read/sidebands/async_io.rs @@ -23,7 +23,7 @@ where cap: usize, } -impl<'a, T, F> Drop for WithSidebands<'a, T, F> +impl Drop for WithSidebands<'_, T, F> where T: AsyncRead, { @@ -70,7 +70,7 @@ enum State<'a, T> { /// to a thread possibly. // TODO: Is it possible to declare it as it should be? #[allow(unsafe_code, clippy::non_send_fields_in_send_ty)] -unsafe impl<'a, T> Send for State<'a, T> where T: Send {} +unsafe impl Send for State<'_, T> where T: Send {} impl<'a, T, F> WithSidebands<'a, T, F> where @@ -180,7 +180,7 @@ pub struct ReadDataLineFuture<'a, 'b, T: AsyncRead, F> { buf: &'b mut Vec, } -impl<'a, 'b, T, F> Future for ReadDataLineFuture<'a, 'b, T, F> +impl Future for ReadDataLineFuture<'_, '_, T, F> where T: AsyncRead + Unpin, F: FnMut(bool, &[u8]) -> ProgressAction + Unpin, @@ -207,7 +207,7 @@ pub struct ReadLineFuture<'a, 'b, T: AsyncRead, F> { buf: &'b mut String, } -impl<'a, 'b, T, F> Future for ReadLineFuture<'a, 'b, T, F> +impl Future for ReadLineFuture<'_, '_, T, F> where T: AsyncRead + Unpin, F: FnMut(bool, &[u8]) -> ProgressAction + Unpin, @@ -230,7 +230,7 @@ where } } -impl<'a, T, F> AsyncBufRead for WithSidebands<'a, T, F> +impl AsyncBufRead for WithSidebands<'_, T, F> where T: AsyncRead + Unpin, F: FnMut(bool, &[u8]) -> ProgressAction + Unpin, @@ -348,7 +348,7 @@ where } } -impl<'a, T, F> AsyncRead for WithSidebands<'a, T, F> +impl AsyncRead for WithSidebands<'_, T, F> where T: AsyncRead + Unpin, F: FnMut(bool, &[u8]) -> ProgressAction + Unpin, diff --git a/gix-packetline/src/read/sidebands/blocking_io.rs b/gix-packetline/src/read/sidebands/blocking_io.rs index 50742688d86..ac7dfbd982d 100644 --- a/gix-packetline/src/read/sidebands/blocking_io.rs +++ b/gix-packetline/src/read/sidebands/blocking_io.rs @@ -15,7 +15,7 @@ where cap: usize, } -impl<'a, T, F> Drop for WithSidebands<'a, T, F> +impl Drop for WithSidebands<'_, T, F> where T: io::Read, { @@ -127,7 +127,7 @@ where } } -impl<'a, T, F> BufRead for WithSidebands<'a, T, F> +impl BufRead for WithSidebands<'_, T, F> where T: io::Read, F: FnMut(bool, &[u8]) -> ProgressAction, @@ -202,7 +202,7 @@ where } } -impl<'a, T, F> io::Read for WithSidebands<'a, T, F> +impl io::Read for WithSidebands<'_, T, F> where T: io::Read, F: FnMut(bool, &[u8]) -> ProgressAction, diff --git a/gix-prompt/src/unix.rs b/gix-prompt/src/unix.rs index 2d9a4b071e8..c5b437e6bee 100644 --- a/gix-prompt/src/unix.rs +++ b/gix-prompt/src/unix.rs @@ -58,7 +58,7 @@ pub(crate) mod imp { fd: File, } - impl<'a> Read for RestoreTerminalStateOnDrop<'a> { + impl Read for RestoreTerminalStateOnDrop<'_> { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.fd.read(buf) } @@ -68,7 +68,7 @@ pub(crate) mod imp { } } - impl<'a> Write for RestoreTerminalStateOnDrop<'a> { + impl Write for RestoreTerminalStateOnDrop<'_> { #[inline(always)] fn write(&mut self, buf: &[u8]) -> io::Result { self.fd.write(buf) @@ -85,7 +85,7 @@ pub(crate) mod imp { } } - impl<'a> RestoreTerminalStateOnDrop<'a> { + impl RestoreTerminalStateOnDrop<'_> { fn restore_term_state(mut self) -> Result<(), Error> { let state = self.state.take().expect("BUG: we exist only if something is saved"); termios::tcsetattr(&self.fd, termios::OptionalActions::Flush, &state)?; @@ -93,7 +93,7 @@ pub(crate) mod imp { } } - impl<'a> Drop for RestoreTerminalStateOnDrop<'a> { + impl Drop for RestoreTerminalStateOnDrop<'_> { fn drop(&mut self) { if let Some(state) = self.state.take() { termios::tcsetattr(&self.fd, termios::OptionalActions::Flush, &state).ok(); diff --git a/gix-prompt/tests/prompt.rs b/gix-prompt/tests/prompt.rs index 8ab31532517..3853162e1ef 100644 --- a/gix-prompt/tests/prompt.rs +++ b/gix-prompt/tests/prompt.rs @@ -7,7 +7,7 @@ mod ask { /// execution environment. This is necessary because certain environment variables and /// configuration options can change its location (e.g. CARGO_TARGET_DIR). fn evaluate_target_dir() -> String { - let manifest_proc = std::process::Command::new(env!("CARGO")) + let mut manifest_proc = std::process::Command::new(env!("CARGO")) .args(["metadata", "--format-version", "1"]) .stdout(std::process::Stdio::piped()) .spawn() @@ -15,7 +15,7 @@ mod ask { let jq_proc = std::process::Command::new("jq") .args(["-r", ".target_directory"]) // -r makes it output raw strings - .stdin(manifest_proc.stdout.unwrap()) + .stdin(manifest_proc.stdout.take().unwrap()) .stdout(std::process::Stdio::piped()) .spawn() .expect("jq utility is available in PATH"); @@ -23,6 +23,7 @@ mod ask { let output = jq_proc .wait_with_output() .expect(".target_directory is a valid search path for manifest format version 1"); + manifest_proc.wait().unwrap(); output .stdout diff --git a/gix-protocol/src/handshake/refs/tests.rs b/gix-protocol/src/handshake/refs/tests.rs index 31f04df52d6..69ea5872790 100644 --- a/gix-protocol/src/handshake/refs/tests.rs +++ b/gix-protocol/src/handshake/refs/tests.rs @@ -212,14 +212,14 @@ fn extract_symbolic_references_from_capabilities() -> Result<(), client::Error> struct Fixture<'a>(&'a [u8]); #[cfg(feature = "blocking-client")] -impl<'a> std::io::Read for Fixture<'a> { +impl std::io::Read for Fixture<'_> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.0.read(buf) } } #[cfg(feature = "blocking-client")] -impl<'a> std::io::BufRead for Fixture<'a> { +impl std::io::BufRead for Fixture<'_> { fn fill_buf(&mut self) -> std::io::Result<&[u8]> { self.0.fill_buf() } @@ -230,7 +230,7 @@ impl<'a> std::io::BufRead for Fixture<'a> { } #[cfg(feature = "blocking-client")] -impl<'a> gix_transport::client::ReadlineBufRead for Fixture<'a> { +impl gix_transport::client::ReadlineBufRead for Fixture<'_> { fn readline( &mut self, ) -> Option, gix_packetline::decode::Error>>> { @@ -268,7 +268,7 @@ impl<'a> Fixture<'a> { } #[cfg(feature = "async-client")] -impl<'a> futures_io::AsyncRead for Fixture<'a> { +impl futures_io::AsyncRead for Fixture<'_> { fn poll_read( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, @@ -279,7 +279,7 @@ impl<'a> futures_io::AsyncRead for Fixture<'a> { } #[cfg(feature = "async-client")] -impl<'a> futures_io::AsyncBufRead for Fixture<'a> { +impl futures_io::AsyncBufRead for Fixture<'_> { fn poll_fill_buf( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, @@ -294,7 +294,7 @@ impl<'a> futures_io::AsyncBufRead for Fixture<'a> { #[cfg(feature = "async-client")] #[async_trait::async_trait(?Send)] -impl<'a> gix_transport::client::ReadlineBufRead for Fixture<'a> { +impl gix_transport::client::ReadlineBufRead for Fixture<'_> { async fn readline( &mut self, ) -> Option, gix_packetline::decode::Error>>> { diff --git a/gix-protocol/src/remote_progress.rs b/gix-protocol/src/remote_progress.rs index deba3e6749d..d144144b464 100644 --- a/gix-protocol/src/remote_progress.rs +++ b/gix-protocol/src/remote_progress.rs @@ -21,7 +21,7 @@ pub struct RemoteProgress<'a> { pub max: Option, } -impl<'a> RemoteProgress<'a> { +impl RemoteProgress<'_> { /// Parse the progress from a typical git progress `line` as sent by the remote. pub fn from_bytes(mut line: &[u8]) -> Option> { parse_progress(&mut line).ok().and_then(|r| { diff --git a/gix-ref/src/name.rs b/gix-ref/src/name.rs index 35332853729..27522758af9 100644 --- a/gix-ref/src/name.rs +++ b/gix-ref/src/name.rs @@ -7,7 +7,7 @@ use crate::{Category, FullName, FullNameRef, PartialName, PartialNameRef}; /// The error used in the [`PartialNameRef`]`::try_from`(…) implementations. pub type Error = gix_validate::reference::name::Error; -impl<'a> Category<'a> { +impl Category<'_> { /// Return the prefix that would contain all references of our kind, or an empty string if the reference would /// be directly inside of the [`git_dir()`][crate::file::Store::git_dir()]. pub fn prefix(&self) -> &BStr { diff --git a/gix-ref/src/store/file/log/iter.rs b/gix-ref/src/store/file/log/iter.rs index d62df680014..ea1c87d1d3b 100644 --- a/gix-ref/src/store/file/log/iter.rs +++ b/gix-ref/src/store/file/log/iter.rs @@ -88,7 +88,7 @@ pub struct Platform<'a, 's> { pub buf: Vec, } -impl<'a, 's> Platform<'a, 's> { +impl Platform<'_, '_> { /// Return a forward iterator over all log-lines, most recent to oldest. pub fn rev(&mut self) -> std::io::Result>> { self.buf.clear(); @@ -158,7 +158,7 @@ pub mod reverse { } } -impl<'a, F> Iterator for Reverse<'a, F> +impl Iterator for Reverse<'_, F> where F: std::io::Read + std::io::Seek, { diff --git a/gix-ref/src/store/file/log/line.rs b/gix-ref/src/store/file/log/line.rs index 435478bdbbf..ab53314bfbd 100644 --- a/gix-ref/src/store/file/log/line.rs +++ b/gix-ref/src/store/file/log/line.rs @@ -2,7 +2,7 @@ use gix_hash::ObjectId; use crate::{log::Line, store_impl::file::log::LineRef}; -impl<'a> LineRef<'a> { +impl LineRef<'_> { /// Convert this instance into its mutable counterpart pub fn to_owned(&self) -> Line { (*self).into() @@ -48,7 +48,7 @@ mod write { } } -impl<'a> LineRef<'a> { +impl LineRef<'_> { /// The previous object id of the ref. It will be a null hash if there was no previous id as /// this ref is being created. pub fn previous_oid(&self) -> ObjectId { diff --git a/gix-ref/src/store/file/overlay_iter.rs b/gix-ref/src/store/file/overlay_iter.rs index a97d817fcf2..7bda0ae9c72 100644 --- a/gix-ref/src/store/file/overlay_iter.rs +++ b/gix-ref/src/store/file/overlay_iter.rs @@ -40,7 +40,7 @@ pub struct Platform<'s> { packed: Option, } -impl<'p, 's> LooseThenPacked<'p, 's> { +impl<'p> LooseThenPacked<'p, '_> { fn strip_namespace(&self, mut r: Reference) -> Reference { if let Some(namespace) = &self.namespace { r.strip_namespace(namespace); @@ -112,7 +112,7 @@ impl<'p, 's> LooseThenPacked<'p, 's> { } } -impl<'p, 's> Iterator for LooseThenPacked<'p, 's> { +impl Iterator for LooseThenPacked<'_, '_> { type Item = Result; fn next(&mut self) -> Option { @@ -187,7 +187,7 @@ impl<'p, 's> Iterator for LooseThenPacked<'p, 's> { } } -impl<'s> Platform<'s> { +impl Platform<'_> { /// Return an iterator over all references, loose or `packed`, sorted by their name. /// /// Errors are returned similarly to what would happen when loose and packed refs where iterated by themselves. diff --git a/gix-ref/src/store/file/transaction/commit.rs b/gix-ref/src/store/file/transaction/commit.rs index e55ed3770a4..89894f475bf 100644 --- a/gix-ref/src/store/file/transaction/commit.rs +++ b/gix-ref/src/store/file/transaction/commit.rs @@ -4,7 +4,7 @@ use crate::{ Target, }; -impl<'s, 'p> Transaction<'s, 'p> { +impl Transaction<'_, '_> { /// Make all [prepared][Transaction::prepare()] permanent and return the performed edits which represent the current /// state of the affected refs in the ref store in that instant. Please note that the obtained edits may have been /// adjusted to contain more dependent edits or additional information. diff --git a/gix-ref/src/store/file/transaction/mod.rs b/gix-ref/src/store/file/transaction/mod.rs index f22493ad86e..6d1550ccb1c 100644 --- a/gix-ref/src/store/file/transaction/mod.rs +++ b/gix-ref/src/store/file/transaction/mod.rs @@ -77,7 +77,7 @@ impl file::Store { } } -impl<'s, 'p> Transaction<'s, 'p> { +impl<'p> Transaction<'_, 'p> { /// Configure the way packed refs are handled during the transaction pub fn packed_refs(mut self, packed_refs: PackedRefs<'p>) -> Self { self.packed_refs = packed_refs; diff --git a/gix-ref/src/store/file/transaction/prepare.rs b/gix-ref/src/store/file/transaction/prepare.rs index 4b17b71b2b9..988f8a3446e 100644 --- a/gix-ref/src/store/file/transaction/prepare.rs +++ b/gix-ref/src/store/file/transaction/prepare.rs @@ -13,7 +13,7 @@ use crate::{ FullName, FullNameRef, Reference, Target, }; -impl<'s, 'p> Transaction<'s, 'p> { +impl Transaction<'_, '_> { fn lock_ref_and_apply_change( store: &file::Store, lock_fail_mode: gix_lock::acquire::Fail, @@ -196,7 +196,7 @@ impl<'s, 'p> Transaction<'s, 'p> { } } -impl<'s, 'p> Transaction<'s, 'p> { +impl Transaction<'_, '_> { /// Prepare for calling [`commit(…)`][Transaction::commit()] in a way that can be rolled back perfectly. /// /// If the operation succeeds, the transaction can be committed or dropped to cause a rollback automatically. diff --git a/gix-ref/src/store/packed/mod.rs b/gix-ref/src/store/packed/mod.rs index 12aee9f815f..f6f71ac7982 100644 --- a/gix-ref/src/store/packed/mod.rs +++ b/gix-ref/src/store/packed/mod.rs @@ -55,7 +55,7 @@ pub struct Reference<'a> { pub object: Option<&'a BStr>, } -impl<'a> Reference<'a> { +impl Reference<'_> { /// Decode the target as object pub fn target(&self) -> ObjectId { gix_hash::ObjectId::from_hex(self.target).expect("parser validation") diff --git a/gix-ref/src/target.rs b/gix-ref/src/target.rs index 46967f969bd..87b21799770 100644 --- a/gix-ref/src/target.rs +++ b/gix-ref/src/target.rs @@ -4,7 +4,7 @@ use gix_hash::{oid, ObjectId}; use crate::{FullName, FullNameRef, Kind, Target, TargetRef}; -impl<'a> TargetRef<'a> { +impl TargetRef<'_> { /// Returns the kind of the target the ref is pointing to. pub fn kind(&self) -> Kind { match self { diff --git a/gix-refspec/src/match_group/validate.rs b/gix-refspec/src/match_group/validate.rs index 3c9b8c4bcef..6a72a72fdfa 100644 --- a/gix-refspec/src/match_group/validate.rs +++ b/gix-refspec/src/match_group/validate.rs @@ -88,7 +88,7 @@ impl std::fmt::Display for Error { impl std::error::Error for Error {} -impl<'spec, 'item> Outcome<'spec, 'item> { +impl Outcome<'_, '_> { /// Validate all mappings or dissolve them into an error stating the discovered issues. /// Return `(modified self, issues)` providing a fixed-up set of mappings in `self` with the fixed `issues` /// provided as part of it. diff --git a/gix-revision/src/describe.rs b/gix-revision/src/describe.rs index 735b743a416..cb92dec099f 100644 --- a/gix-revision/src/describe.rs +++ b/gix-revision/src/describe.rs @@ -59,7 +59,7 @@ pub struct Format<'a> { pub dirty_suffix: Option, } -impl<'a> Format<'a> { +impl Format<'_> { /// Return true if the `name` is directly associated with `id`, i.e. there are no commits between them. pub fn is_exact_match(&self) -> bool { self.depth == 0 @@ -75,7 +75,7 @@ impl<'a> Format<'a> { } } -impl<'a> Display for Format<'a> { +impl Display for Format<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if let Some(name) = self.name.as_deref() { if !self.long && self.is_exact_match() { @@ -116,7 +116,7 @@ pub struct Options<'name> { pub first_parent: bool, } -impl<'name> Default for Options<'name> { +impl Default for Options<'_> { fn default() -> Self { Options { max_candidates: 10, // the same number as git uses, otherwise we perform worse by default on big repos diff --git a/gix-revision/src/spec/parse/function.rs b/gix-revision/src/spec/parse/function.rs index 819f3c7b90c..d9740a3e036 100644 --- a/gix-revision/src/spec/parse/function.rs +++ b/gix-revision/src/spec/parse/function.rs @@ -117,7 +117,7 @@ mod intercept { } } - impl<'a, T> Delegate for InterceptRev<'a, T> + impl Delegate for InterceptRev<'_, T> where T: Delegate, { @@ -127,7 +127,7 @@ mod intercept { } } - impl<'a, T> delegate::Revision for InterceptRev<'a, T> + impl delegate::Revision for InterceptRev<'_, T> where T: Delegate, { @@ -158,7 +158,7 @@ mod intercept { } } - impl<'a, T> delegate::Navigate for InterceptRev<'a, T> + impl delegate::Navigate for InterceptRev<'_, T> where T: Delegate, { @@ -179,7 +179,7 @@ mod intercept { } } - impl<'a, T> delegate::Kind for InterceptRev<'a, T> + impl delegate::Kind for InterceptRev<'_, T> where T: Delegate, { diff --git a/gix-revwalk/src/graph/commit.rs b/gix-revwalk/src/graph/commit.rs index 8c0171f6aeb..874585a1253 100644 --- a/gix-revwalk/src/graph/commit.rs +++ b/gix-revwalk/src/graph/commit.rs @@ -119,7 +119,7 @@ pub struct Parents<'graph, 'cache> { >, } -impl<'graph, 'cache> Iterator for Parents<'graph, 'cache> { +impl Iterator for Parents<'_, '_> { type Item = Result; fn next(&mut self) -> Option { diff --git a/gix-revwalk/src/graph/mod.rs b/gix-revwalk/src/graph/mod.rs index faa51579885..4fc1bdd77d4 100644 --- a/gix-revwalk/src/graph/mod.rs +++ b/gix-revwalk/src/graph/mod.rs @@ -53,13 +53,13 @@ use gix_date::SecondsSinceUnixEpoch; /// This number is only available natively if there is a commit-graph. pub type Generation = u32; -impl<'find, 'cache, T: std::fmt::Debug> std::fmt::Debug for Graph<'find, 'cache, T> { +impl std::fmt::Debug for Graph<'_, '_, T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(&self.map, f) } } -impl<'find, 'cache, T: Default> Graph<'find, 'cache, T> { +impl<'cache, T: Default> Graph<'_, 'cache, T> { /// Lookup `id` without failing if the commit doesn't exist, and assure that `id` is inserted into our set. /// If it wasn't, associate it with the default value. Assure `update_data(data)` gets run. /// Return the commit when done. @@ -74,7 +74,7 @@ impl<'find, 'cache, T: Default> Graph<'find, 'cache, T> { } /// Access and mutation -impl<'find, 'cache, T> Graph<'find, 'cache, T> { +impl<'cache, T> Graph<'_, 'cache, T> { /// Returns the amount of entries in the graph. pub fn len(&self) -> usize { self.map.len() @@ -226,7 +226,7 @@ impl<'find, 'cache, T> Graph<'find, 'cache, T> { } /// Commit based methods -impl<'find, 'cache, T> Graph<'find, 'cache, Commit> { +impl Graph<'_, '_, Commit> { /// Lookup `id` in the graph, but insert it if it's not yet present by looking it up without failing if the commit doesn't exist. /// Call `new_data()` to obtain data for a newly inserted commit. /// `update_data(data)` gets run either on existing or on new data. @@ -263,7 +263,7 @@ impl<'find, 'cache, T> Graph<'find, 'cache, Commit> { } /// Commit based methods -impl<'find, 'cache, T: Default> Graph<'find, 'cache, Commit> { +impl Graph<'_, '_, Commit> { /// Lookup `id` in the graph, but insert it if it's not yet present by looking it up without failing if the commit doesn't exist. /// Newly inserted commits are populated with default data. /// `update_data(data)` gets run either on existing or on new data. @@ -309,7 +309,7 @@ impl<'find, 'cache, T: Default> Graph<'find, 'cache, Commit> { } /// Lazy commit access -impl<'find, 'cache, T> Graph<'find, 'cache, T> { +impl<'cache, T> Graph<'_, 'cache, T> { /// Lookup `id` without failing if the commit doesn't exist or `id` isn't a commit, /// and assure that `id` is inserted into our set /// with a `default` value assigned to it. @@ -390,7 +390,7 @@ fn try_lookup<'graph, 'cache>( ) } -impl<'a, 'find, 'cache, T> Index<&'a gix_hash::oid> for Graph<'find, 'cache, T> { +impl<'a, T> Index<&'a gix_hash::oid> for Graph<'_, '_, T> { type Output = T; fn index(&self, index: &'a oid) -> &Self::Output { diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index c757bae7c06..515da2568e1 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -327,7 +327,7 @@ impl<'index> State<'_, 'index> { /// isn't really a race-condition to worry about. This also explains why removing the `return` here doesn't have an apparent effect. /// This entire branch here is just the optimization of "don't even look at index entries where the stat hasn't changed". /// If we don't have this optimization the result shouldn't change, our status implementation will just be super slow :D - + /// /// We calculate whether this change is `racy_clean`, so if the last `timestamp` is before or the same as the `mtime` of the entry /// which is what `new_stat.is_racy(..)` does in the branch, and only if we are sure that there is no race condition /// do we `return` early. Since we don't `return` early we just do a full content comparison below, @@ -335,7 +335,7 @@ impl<'index> State<'_, 'index> { /// /// If a file showed up as racily clean and didn't change then we don't need to do anything. After this status check is /// complete and the file won't show up as racily clean anymore, since it's mtime is now before the new timestamp. - /// However if the file did actually change then we really ran into one of those rare race conditions in that case we, + /// However, if the file did actually change then we really ran into one of those rare race conditions in that case we, /// and git does the same, set the size of the file in the index to 0. This will always make the file show up as changed. /// This adds the need to treat all files of size 0 in the index as changed. This is not quite right of course because 0 sized files /// could be entirely valid and unchanged. Therefore this only applies if the oid doesn't match the oid of an empty file, diff --git a/gix-status/src/index_as_worktree/traits.rs b/gix-status/src/index_as_worktree/traits.rs index 4282a216f6f..e5224f38609 100644 --- a/gix-status/src/index_as_worktree/traits.rs +++ b/gix-status/src/index_as_worktree/traits.rs @@ -84,7 +84,7 @@ pub mod read_data { } } - impl<'a> std::io::Read for Stream<'a> { + impl std::io::Read for Stream<'_> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let n = self.inner.read(buf)?; if let Some(bytes) = self.bytes { diff --git a/gix-status/src/index_as_worktree_with_renames/mod.rs b/gix-status/src/index_as_worktree_with_renames/mod.rs index 257879c392c..f2202ea0869 100644 --- a/gix-status/src/index_as_worktree_with_renames/mod.rs +++ b/gix-status/src/index_as_worktree_with_renames/mod.rs @@ -385,7 +385,7 @@ pub(super) mod function { pub(super) should_interrupt: &'a AtomicBool, } - impl<'index, 'a, T, U> gix_dir::walk::Delegate for Delegate<'index, 'a, T, U> { + impl gix_dir::walk::Delegate for Delegate<'_, '_, T, U> { fn emit(&mut self, entry: EntryRef<'_>, collapsed_directory_status: Option) -> Action { let entry = entry.to_owned(); self.tx.send(Event::DirEntry(entry, collapsed_directory_status)).ok(); @@ -426,7 +426,7 @@ pub(super) mod function { }, } - impl<'index, T, U> gix_diff::rewrites::tracker::Change for ModificationOrDirwalkEntry<'index, T, U> + impl gix_diff::rewrites::tracker::Change for ModificationOrDirwalkEntry<'_, T, U> where T: Clone, U: Clone, diff --git a/gix-transport/src/client/async_io/bufread_ext.rs b/gix-transport/src/client/async_io/bufread_ext.rs index 0d32c4005a0..00389811ebe 100644 --- a/gix-transport/src/client/async_io/bufread_ext.rs +++ b/gix-transport/src/client/async_io/bufread_ext.rs @@ -60,7 +60,7 @@ pub trait ExtendedBufRead<'a>: ReadlineBufRead { } #[async_trait(?Send)] -impl<'a, T: ReadlineBufRead + ?Sized + 'a + Unpin> ReadlineBufRead for Box { +impl ReadlineBufRead for Box { async fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { self.deref_mut().readline().await } diff --git a/gix-transport/src/client/async_io/request.rs b/gix-transport/src/client/async_io/request.rs index 938ebe09bb7..6c6dedf9e3c 100644 --- a/gix-transport/src/client/async_io/request.rs +++ b/gix-transport/src/client/async_io/request.rs @@ -21,7 +21,7 @@ pin_project! { trace: bool, } } -impl<'a> futures_io::AsyncWrite for RequestWriter<'a> { +impl futures_io::AsyncWrite for RequestWriter<'_> { fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { self.project().writer.poll_write(cx, buf) } diff --git a/gix-transport/src/client/blocking_io/bufread_ext.rs b/gix-transport/src/client/blocking_io/bufread_ext.rs index 403f6581453..9054fb54290 100644 --- a/gix-transport/src/client/blocking_io/bufread_ext.rs +++ b/gix-transport/src/client/blocking_io/bufread_ext.rs @@ -52,7 +52,7 @@ pub trait ExtendedBufRead<'a>: ReadlineBufRead { fn stopped_at(&self) -> Option; } -impl<'a, T: ReadlineBufRead + ?Sized + 'a> ReadlineBufRead for Box { +impl ReadlineBufRead for Box { fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { ReadlineBufRead::readline(self.deref_mut()) } diff --git a/gix-transport/src/client/blocking_io/request.rs b/gix-transport/src/client/blocking_io/request.rs index 06704c0586e..f4b3a77a329 100644 --- a/gix-transport/src/client/blocking_io/request.rs +++ b/gix-transport/src/client/blocking_io/request.rs @@ -12,7 +12,7 @@ pub struct RequestWriter<'a> { trace: bool, } -impl<'a> io::Write for RequestWriter<'a> { +impl io::Write for RequestWriter<'_> { fn write(&mut self, buf: &[u8]) -> io::Result { #[allow(unused_imports)] if self.trace { diff --git a/gix-utils/src/btoi.rs b/gix-utils/src/btoi.rs index 08dff105a0d..d00421f48b7 100644 --- a/gix-utils/src/btoi.rs +++ b/gix-utils/src/btoi.rs @@ -1,17 +1,17 @@ -/// A module with utilities to turn byte slices with decimal numbers back into their -/// binary representation. -/// -/// ### Credits -/// -/// This module was ported from version 0.4.3 -/// see for how it came to be in order -/// to save 2.2 seconds of per-core compile time by not compiling the `num-traits` crate -/// anymore. -/// -/// Licensed with compatible licenses [MIT] and [Apache] -/// -/// [MIT]: https://github.com/niklasf/rust-btoi/blob/master/LICENSE-MIT -/// [Apache]: https://github.com/niklasf/rust-btoi/blob/master/LICENSE-APACHE +//! A module with utilities to turn byte slices with decimal numbers back into their +//! binary representation. +//! +//! ### Credits +//! +//! This module was ported from version 0.4.3 +//! see for how it came to be in order +//! to save 2.2 seconds of per-core compile time by not compiling the `num-traits` crate +//! anymore. +//! +//! Licensed with compatible licenses [MIT] and [Apache] +//! +//! [MIT]: https://github.com/niklasf/rust-btoi/blob/master/LICENSE-MIT +//! [Apache]: https://github.com/niklasf/rust-btoi/blob/master/LICENSE-APACHE /// An error that can occur when parsing an integer. /// diff --git a/gix-utils/src/buffers.rs b/gix-utils/src/buffers.rs index 2c0544ea3da..a99edbec5d3 100644 --- a/gix-utils/src/buffers.rs +++ b/gix-utils/src/buffers.rs @@ -37,7 +37,7 @@ pub struct WithForeignSource<'src, 'bufs> { dest: &'bufs mut Vec, } -impl<'bufs> WithForeignSource<'_, 'bufs> { +impl WithForeignSource<'_, '_> { /// Must be called after every change (i.e. when it's known that `dest` was written. pub fn swap(&mut self) { self.ro_src.take(); diff --git a/gix-worktree-state/src/checkout/chunk.rs b/gix-worktree-state/src/checkout/chunk.rs index 3783da61989..7f23722f59e 100644 --- a/gix-worktree-state/src/checkout/chunk.rs +++ b/gix-worktree-state/src/checkout/chunk.rs @@ -263,7 +263,7 @@ pub struct WriteWithProgress<'a, T> { pub progress: &'a AtomicUsize, } -impl<'a, T> std::io::Write for WriteWithProgress<'a, T> +impl std::io::Write for WriteWithProgress<'_, T> where T: std::io::Write, { diff --git a/gix-worktree-stream/src/entry.rs b/gix-worktree-stream/src/entry.rs index b8ef2c5afcc..c6c8aeb4029 100644 --- a/gix-worktree-stream/src/entry.rs +++ b/gix-worktree-stream/src/entry.rs @@ -112,7 +112,7 @@ impl Entry<'_> { } } -impl<'a> Drop for Entry<'a> { +impl Drop for Entry<'_> { fn drop(&mut self) { if self.remaining == Some(0) { self.parent.path_buf = self.path_buf.take(); diff --git a/gix-worktree/src/stack/delegate.rs b/gix-worktree/src/stack/delegate.rs index 6b74c2aa098..01a84611056 100644 --- a/gix-worktree/src/stack/delegate.rs +++ b/gix-worktree/src/stack/delegate.rs @@ -27,7 +27,7 @@ pub(crate) struct StackDelegate<'a, 'find> { pub statistics: &'a mut super::Statistics, } -impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { +impl gix_fs::stack::Delegate for StackDelegate<'_, '_> { fn push_directory(&mut self, stack: &gix_fs::Stack) -> std::io::Result<()> { self.statistics.delegate.push_directory += 1; let rela_dir_bstr = gix_path::into_bstr(stack.current_relative()); diff --git a/gix-worktree/src/stack/platform.rs b/gix-worktree/src/stack/platform.rs index 0b270516d24..5f8f5431b25 100644 --- a/gix-worktree/src/stack/platform.rs +++ b/gix-worktree/src/stack/platform.rs @@ -64,7 +64,7 @@ impl<'a> Platform<'a> { } } -impl<'a> std::fmt::Debug for Platform<'a> { +impl std::fmt::Debug for Platform<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(&self.path(), f) } diff --git a/gix/src/attribute_stack.rs b/gix/src/attribute_stack.rs index bf9a1cafb18..d9921f58af3 100644 --- a/gix/src/attribute_stack.rs +++ b/gix/src/attribute_stack.rs @@ -32,7 +32,7 @@ impl DerefMut for AttributeStack<'_> { } /// Platform retrieval -impl<'repo> AttributeStack<'repo> { +impl AttributeStack<'_> { /// Append the `relative` path to the root directory of the cache and load all attribute or ignore files on the way as needed. /// Use `mode` to specify what kind of item lives at `relative` - directories may match against rules specifically. /// If `mode` is `None`, the item at `relative` is assumed to be a file. diff --git a/gix/src/commit.rs b/gix/src/commit.rs index 6849da4fd44..f5dcb65a587 100644 --- a/gix/src/commit.rs +++ b/gix/src/commit.rs @@ -40,7 +40,7 @@ pub mod describe { pub id: crate::Id<'repo>, } - impl<'repo> Resolution<'repo> { + impl Resolution<'_> { /// Turn this instance into something displayable. pub fn format(self) -> Result, Error> { let prefix = self.id.shorten()?; diff --git a/gix/src/config/snapshot/access.rs b/gix/src/config/snapshot/access.rs index 4cbbf73600c..218417bc23a 100644 --- a/gix/src/config/snapshot/access.rs +++ b/gix/src/config/snapshot/access.rs @@ -79,7 +79,7 @@ impl<'repo> Snapshot<'repo> { } /// Utilities and additional access -impl<'repo> Snapshot<'repo> { +impl Snapshot<'_> { /// Returns the underlying configuration implementation for a complete API, despite being a little less convenient. /// /// It's expected that more functionality will move up depending on demand. diff --git a/gix/src/create.rs b/gix/src/create.rs index a12b8921d97..8963315f5b4 100644 --- a/gix/src/create.rs +++ b/gix/src/create.rs @@ -53,14 +53,14 @@ struct PathCursor<'a>(&'a mut PathBuf); struct NewDir<'a>(&'a mut PathBuf); -impl<'a> PathCursor<'a> { +impl PathCursor<'_> { fn at(&mut self, component: &str) -> &Path { self.0.push(component); self.0.as_path() } } -impl<'a> NewDir<'a> { +impl NewDir<'_> { fn at(self, component: &str) -> Result { self.0.push(component); create_dir(self.0)?; @@ -71,13 +71,13 @@ impl<'a> NewDir<'a> { } } -impl<'a> Drop for NewDir<'a> { +impl Drop for NewDir<'_> { fn drop(&mut self) { self.0.pop(); } } -impl<'a> Drop for PathCursor<'a> { +impl Drop for PathCursor<'_> { fn drop(&mut self) { self.0.pop(); } diff --git a/gix/src/ext/tree.rs b/gix/src/ext/tree.rs index 9aacc9d5808..72922e31b20 100644 --- a/gix/src/ext/tree.rs +++ b/gix/src/ext/tree.rs @@ -24,9 +24,9 @@ pub trait TreeIterExt: Sealed { V: gix_traverse::tree::Visit; } -impl<'d> Sealed for TreeRefIter<'d> {} +impl Sealed for TreeRefIter<'_> {} -impl<'d> TreeIterExt for TreeRefIter<'d> { +impl TreeIterExt for TreeRefIter<'_> { fn traverse( &self, state: StateMut, diff --git a/gix/src/filter.rs b/gix/src/filter.rs index db840f94ec6..92eafcc2b5e 100644 --- a/gix/src/filter.rs +++ b/gix/src/filter.rs @@ -128,7 +128,7 @@ impl<'repo> Pipeline<'repo> { } /// Conversions -impl<'repo> Pipeline<'repo> { +impl Pipeline<'_> { /// Convert a `src` stream (to be found at `rela_path`, a repo-relative path) to a representation suitable for storage in `git` /// by using all attributes at `rela_path` and configuration of the repository to know exactly which filters apply. /// `index` is used in particularly rare cases where the CRLF filter in auto-mode tries to determine whether to apply itself, diff --git a/gix/src/id.rs b/gix/src/id.rs index 5de45020b99..90aed00db05 100644 --- a/gix/src/id.rs +++ b/gix/src/id.rs @@ -83,7 +83,7 @@ pub mod shorten { } } -impl<'repo> Deref for Id<'repo> { +impl Deref for Id<'_> { type Target = oid; fn deref(&self) -> &Self::Target { @@ -118,7 +118,7 @@ mod impls { // Eq, Hash, Ord, PartialOrd, - impl<'a> std::hash::Hash for Id<'a> { + impl std::hash::Hash for Id<'_> { fn hash(&self, state: &mut H) { self.inner.hash(state); } @@ -136,7 +136,7 @@ mod impls { } } - impl<'repo> PartialEq for Id<'repo> { + impl PartialEq for Id<'_> { fn eq(&self, other: &ObjectId) -> bool { &self.inner == other } @@ -148,7 +148,7 @@ mod impls { } } - impl<'repo> PartialEq for Id<'repo> { + impl PartialEq for Id<'_> { fn eq(&self, other: &oid) -> bool { self.inner == other } @@ -160,25 +160,25 @@ mod impls { } } - impl<'repo> PartialEq for Id<'repo> { + impl PartialEq for Id<'_> { fn eq(&self, other: &ObjectDetached) -> bool { self.inner == other.id } } - impl<'repo> std::fmt::Debug for Id<'repo> { + impl std::fmt::Debug for Id<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.inner.fmt(f) } } - impl<'repo> std::fmt::Display for Id<'repo> { + impl std::fmt::Display for Id<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.inner.fmt(f) } } - impl<'repo> AsRef for Id<'repo> { + impl AsRef for Id<'_> { fn as_ref(&self) -> &oid { &self.inner } diff --git a/gix/src/object/blob.rs b/gix/src/object/blob.rs index 59629727f3a..8d189a3bd3e 100644 --- a/gix/src/object/blob.rs +++ b/gix/src/object/blob.rs @@ -153,7 +153,7 @@ pub mod diff { } } - impl<'a> Platform<'a> { + impl Platform<'_> { /// Perform a diff on lines between the old and the new version of a blob, passing each hunk of lines to `process_hunk`. /// The diffing algorithm is determined by the `diff.algorithm` configuration, or individual diff drivers. /// Note that `process_hunk` is not called if one of the involved resources are binary, but that can be determined diff --git a/gix/src/object/commit.rs b/gix/src/object/commit.rs index bb2e3a99eff..b5da82b95a4 100644 --- a/gix/src/object/commit.rs +++ b/gix/src/object/commit.rs @@ -21,7 +21,7 @@ mod error { pub use error::Error; /// Remove Lifetime -impl<'repo> Commit<'repo> { +impl Commit<'_> { /// Create an owned instance of this object, copying our data in the process. pub fn detached(&self) -> ObjectDetached { ObjectDetached { @@ -167,7 +167,7 @@ impl<'repo> Commit<'repo> { } } -impl<'r> std::fmt::Debug for Commit<'r> { +impl std::fmt::Debug for Commit<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Commit({})", self.id) } diff --git a/gix/src/object/impls.rs b/gix/src/object/impls.rs index 58e068e402d..941b42dacb6 100644 --- a/gix/src/object/impls.rs +++ b/gix/src/object/impls.rs @@ -61,7 +61,7 @@ impl<'repo> From> for Object<'repo> { } } -impl<'repo> AsRef<[u8]> for Object<'repo> { +impl AsRef<[u8]> for Object<'_> { fn as_ref(&self) -> &[u8] { &self.data } @@ -137,7 +137,7 @@ impl<'repo> TryFrom> for Blob<'repo> { } } -impl<'r> std::fmt::Debug for Object<'r> { +impl std::fmt::Debug for Object<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use gix_object::Kind::*; let type_name = match self.kind { diff --git a/gix/src/object/mod.rs b/gix/src/object/mod.rs index 0c84e134eec..d640e737d9b 100644 --- a/gix/src/object/mod.rs +++ b/gix/src/object/mod.rs @@ -142,7 +142,7 @@ impl<'repo> Object<'repo> { } } -impl<'repo> Object<'repo> { +impl Object<'_> { /// Create an owned instance of this object, copying our data in the process. pub fn detached(&self) -> ObjectDetached { ObjectDetached { diff --git a/gix/src/object/tree/diff/change.rs b/gix/src/object/tree/diff/change.rs index abfeac8b24d..9d8265ea7a4 100644 --- a/gix/src/object/tree/diff/change.rs +++ b/gix/src/object/tree/diff/change.rs @@ -163,7 +163,7 @@ impl ChangeDetached { } } -impl<'a, 'old, 'new> super::Change<'a, 'old, 'new> { +impl super::Change<'_, '_, '_> { /// Produce a platform for performing a line-diff no matter whether the underlying [Event] is an addition, modification, /// deletion or rewrite. /// Use `resource_cache` to store the diffable data and possibly reuse previously stored data, usually obtained with @@ -271,7 +271,7 @@ impl EventDetached { } } -impl<'a, 'old, 'new> Event<'a, 'old, 'new> { +impl Event<'_, '_, '_> { /// Return the current mode of this instance. pub fn entry_mode(&self) -> gix_object::tree::EntryMode { match self { diff --git a/gix/src/object/tree/diff/for_each.rs b/gix/src/object/tree/diff/for_each.rs index e19156a0485..6c493e6e83c 100644 --- a/gix/src/object/tree/diff/for_each.rs +++ b/gix/src/object/tree/diff/for_each.rs @@ -31,7 +31,7 @@ pub struct Outcome { } /// Add the item to compare to. -impl<'a, 'old> Platform<'a, 'old> { +impl<'old> Platform<'_, 'old> { /// Call `for_each` repeatedly with all changes that are needed to convert the source of the diff to the tree to `other`. /// /// `other` could also be created with the [`empty_tree()`][crate::Repository::empty_tree()] method to handle the first commit @@ -123,7 +123,7 @@ struct Delegate<'a, 'old, 'new, VisitFn, E> { err: Option, } -impl<'a, 'old, 'new, VisitFn, E> Delegate<'a, 'old, 'new, VisitFn, E> +impl<'old, 'new, VisitFn, E> Delegate<'_, 'old, 'new, VisitFn, E> where VisitFn: for<'delegate> FnMut(Change<'delegate, 'old, 'new>) -> Result, E: Into>, @@ -245,7 +245,7 @@ where } } -impl<'a, 'old, 'new, VisitFn, E> gix_diff::tree::Visit for Delegate<'a, 'old, 'new, VisitFn, E> +impl<'old, 'new, VisitFn, E> gix_diff::tree::Visit for Delegate<'_, 'old, 'new, VisitFn, E> where VisitFn: for<'delegate> FnMut(Change<'delegate, 'old, 'new>) -> Result, E: Into>, diff --git a/gix/src/object/tree/diff/mod.rs b/gix/src/object/tree/diff/mod.rs index 54c96392c78..354315cbbca 100644 --- a/gix/src/object/tree/diff/mod.rs +++ b/gix/src/object/tree/diff/mod.rs @@ -74,7 +74,7 @@ pub struct Platform<'a, 'repo> { } /// Configuration -impl<'a, 'repo> Platform<'a, 'repo> { +impl Platform<'_, '_> { /// Keep track of file-names, which makes the [`location`][Change::location] field usable with the filename of the changed item. pub fn track_filename(&mut self) -> &mut Self { self.tracking = Some(Location::FileName); @@ -128,7 +128,7 @@ pub mod stats { } /// Convenience -impl<'a, 'repo> Platform<'a, 'repo> { +impl Platform<'_, '_> { /// Calculate statistics about the lines of the diff between our current and the `other` tree. /// /// ### Performance Notes diff --git a/gix/src/object/tree/iter.rs b/gix/src/object/tree/iter.rs index cc9c4bd2890..8ba3ca58694 100644 --- a/gix/src/object/tree/iter.rs +++ b/gix/src/object/tree/iter.rs @@ -46,7 +46,7 @@ impl<'repo, 'a> EntryRef<'repo, 'a> { } } -impl<'repo, 'a> std::fmt::Display for EntryRef<'repo, 'a> { +impl std::fmt::Display for EntryRef<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, diff --git a/gix/src/object/tree/mod.rs b/gix/src/object/tree/mod.rs index 647030fa769..29f9fe07592 100644 --- a/gix/src/object/tree/mod.rs +++ b/gix/src/object/tree/mod.rs @@ -186,7 +186,7 @@ pub mod traverse; mod iter; pub use iter::EntryRef; -impl<'r> std::fmt::Debug for Tree<'r> { +impl std::fmt::Debug for Tree<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Tree({})", self.id) } diff --git a/gix/src/object/tree/traverse.rs b/gix/src/object/tree/traverse.rs index f2f3ff817f8..78159016abd 100644 --- a/gix/src/object/tree/traverse.rs +++ b/gix/src/object/tree/traverse.rs @@ -24,7 +24,7 @@ pub struct BreadthFirstPresets<'a, 'repo> { root: &'a Tree<'repo>, } -impl<'a, 'repo> BreadthFirstPresets<'a, 'repo> { +impl BreadthFirstPresets<'_, '_> { /// Returns all entries and their file paths, recursively, as reachable from this tree. pub fn files(&self) -> Result, gix_traverse::tree::breadthfirst::Error> { let mut recorder = gix_traverse::tree::Recorder::default(); @@ -37,7 +37,7 @@ impl<'a, 'repo> BreadthFirstPresets<'a, 'repo> { } } -impl<'a, 'repo> Platform<'a, 'repo> { +impl Platform<'_, '_> { /// Start a breadth-first, recursive traversal using `delegate`, for which a [`Recorder`][gix_traverse::tree::Recorder] can be used to get started. /// /// # Note diff --git a/gix/src/reference/edits.rs b/gix/src/reference/edits.rs index d640bab528f..5c834153184 100644 --- a/gix/src/reference/edits.rs +++ b/gix/src/reference/edits.rs @@ -19,7 +19,7 @@ pub mod set_target_id { } pub use error::Error; - impl<'repo> Reference<'repo> { + impl Reference<'_> { /// Set the id of this direct reference to `id` and use `reflog_message` for the reflog (if enabled in the repository). /// /// Note that the operation will fail on symbolic references, to change their type use the lower level reference database, @@ -56,7 +56,7 @@ pub mod delete { use crate::Reference; - impl<'repo> Reference<'repo> { + impl Reference<'_> { /// Delete this reference or fail if it was changed since last observed. /// Note that this instance remains available in memory but probably shouldn't be used anymore. pub fn delete(&self) -> Result<(), crate::reference::edit::Error> { diff --git a/gix/src/reference/iter.rs b/gix/src/reference/iter.rs index 3f9dbdaa74d..3ec775a19ea 100644 --- a/gix/src/reference/iter.rs +++ b/gix/src/reference/iter.rs @@ -30,7 +30,7 @@ impl<'r> Iter<'r> { } } -impl<'r> Platform<'r> { +impl Platform<'_> { /// Return an iterator over all references in the repository. /// /// Even broken or otherwise unparsable or inaccessible references are returned and have to be handled by the caller on a @@ -73,7 +73,7 @@ impl<'r> Platform<'r> { } } -impl<'r> Iter<'r> { +impl Iter<'_> { /// Automatically peel references before yielding them during iteration. /// /// This has the same effect as using `iter.map(|r| {r.peel_to_id_in_place(); r})`. diff --git a/gix/src/reference/log.rs b/gix/src/reference/log.rs index 275eaf86850..e9956809d0a 100644 --- a/gix/src/reference/log.rs +++ b/gix/src/reference/log.rs @@ -8,7 +8,7 @@ use crate::{ Reference, }; -impl<'repo> Reference<'repo> { +impl Reference<'_> { /// Return a platform for obtaining iterators over reference logs. pub fn log_iter(&self) -> gix_ref::file::log::iter::Platform<'_, '_> { self.inner.log_iter(&self.repo.refs) diff --git a/gix/src/reference/mod.rs b/gix/src/reference/mod.rs index 5cd6f951101..68308f433ba 100644 --- a/gix/src/reference/mod.rs +++ b/gix/src/reference/mod.rs @@ -50,7 +50,7 @@ impl<'repo> Reference<'repo> { } } -impl<'repo> std::fmt::Debug for Reference<'repo> { +impl std::fmt::Debug for Reference<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(&self.inner, f) } diff --git a/gix/src/remote/connection/access.rs b/gix/src/remote/connection/access.rs index 932fdb09d43..2b9c3f1dadf 100644 --- a/gix/src/remote/connection/access.rs +++ b/gix/src/remote/connection/access.rs @@ -4,16 +4,16 @@ use crate::{ }; /// Builder -impl<'a, 'repo, T> Connection<'a, 'repo, T> { +impl<'a, T> Connection<'a, '_, T> { /// Set a custom credentials callback to provide credentials if the remotes require authentication. /// - /// Otherwise we will use the git configuration to perform the same task as the `git credential` helper program, + /// Otherwise, we will use the git configuration to perform the same task as the `git credential` helper program, /// which is calling other helper programs in succession while resorting to a prompt to obtain credentials from the /// user. /// /// A custom function may also be used to prevent accessing resources with authentication. /// - /// Use the [`configured_credentials()`][Connection::configured_credentials()] method to obtain the implementation + /// Use the [`configured_credentials()`](Connection::configured_credentials()) method to obtain the implementation /// that would otherwise be used, which can be useful to proxy the default configuration and obtain information about the /// URLs to authenticate with. pub fn with_credentials( @@ -25,7 +25,7 @@ impl<'a, 'repo, T> Connection<'a, 'repo, T> { } /// Provide configuration to be used before the first handshake is conducted. - /// It's typically created by initializing it with [`Repository::transport_options()`][crate::Repository::transport_options()], which + /// It's typically created by initializing it with [`Repository::transport_options()`](crate::Repository::transport_options()), which /// is also the default if this isn't set explicitly. Note that all of the default configuration is created from `git` /// configuration, which can also be manipulated through overrides to affect the default configuration. /// @@ -38,8 +38,8 @@ impl<'a, 'repo, T> Connection<'a, 'repo, T> { } /// Mutation -impl<'a, 'repo, T> Connection<'a, 'repo, T> { - /// Like [`with_credentials()`][Self::with_credentials()], but without consuming the connection. +impl<'a, T> Connection<'a, '_, T> { + /// Like [`with_credentials()`](Self::with_credentials()), but without consuming the connection. pub fn set_credentials( &mut self, helper: impl FnMut(gix_credentials::helper::Action) -> gix_credentials::protocol::Result + 'a, @@ -48,7 +48,7 @@ impl<'a, 'repo, T> Connection<'a, 'repo, T> { self } - /// Like [`with_transport_options()`][Self::with_transport_options()], but without consuming the connection. + /// Like [`with_transport_options()`](Self::with_transport_options()), but without consuming the connection. pub fn set_transport_options(&mut self, config: Box) -> &mut Self { self.transport_options = Some(config); self @@ -56,11 +56,11 @@ impl<'a, 'repo, T> Connection<'a, 'repo, T> { } /// Access -impl<'a, 'repo, T> Connection<'a, 'repo, T> { +impl<'repo, T> Connection<'_, 'repo, T> { /// A utility to return a function that will use this repository's configuration to obtain credentials, similar to /// what `git credential` is doing. /// - /// It's meant to be used by users of the [`with_credentials()`][Self::with_credentials()] builder to gain access to the + /// It's meant to be used by users of the [`with_credentials()`](Self::with_credentials()) builder to gain access to the /// default way of handling credentials, which they can call as fallback. pub fn configured_credentials( &self, @@ -76,9 +76,9 @@ impl<'a, 'repo, T> Connection<'a, 'repo, T> { } /// Provide a mutable transport to allow interacting with it according to its actual type. - /// Note that the caller _should not_ call [`configure()`][gix_protocol::transport::client::TransportWithoutIO::configure()] + /// Note that the caller _should not_ call [`configure()`](gix_protocol::transport::client::TransportWithoutIO::configure()) /// as we will call it automatically before performing the handshake. Instead, to bring in custom configuration, - /// call [`with_transport_options()`][Connection::with_transport_options()]. + /// call [`with_transport_options()`](Connection::with_transport_options()). pub fn transport_mut(&mut self) -> &mut T { &mut self.transport } diff --git a/gix/src/remote/connection/fetch/mod.rs b/gix/src/remote/connection/fetch/mod.rs index 8dd842c7dae..e0017e6ed39 100644 --- a/gix/src/remote/connection/fetch/mod.rs +++ b/gix/src/remote/connection/fetch/mod.rs @@ -197,7 +197,7 @@ where } } -impl<'remote, 'repo, T> Prepare<'remote, 'repo, T> +impl Prepare<'_, '_, T> where T: Transport, { @@ -227,7 +227,7 @@ where } /// Builder -impl<'remote, 'repo, T> Prepare<'remote, 'repo, T> +impl Prepare<'_, '_, T> where T: Transport, { @@ -267,7 +267,7 @@ where } } -impl<'remote, 'repo, T> Drop for Prepare<'remote, 'repo, T> +impl Drop for Prepare<'_, '_, T> where T: Transport, { diff --git a/gix/src/remote/connection/fetch/receive_pack.rs b/gix/src/remote/connection/fetch/receive_pack.rs index d0d2eefcbc1..b8caf153114 100644 --- a/gix/src/remote/connection/fetch/receive_pack.rs +++ b/gix/src/remote/connection/fetch/receive_pack.rs @@ -26,7 +26,7 @@ use crate::{ Repository, }; -impl<'remote, 'repo, T> Prepare<'remote, 'repo, T> +impl Prepare<'_, '_, T> where T: Transport, { diff --git a/gix/src/remote/connection/ref_map.rs b/gix/src/remote/connection/ref_map.rs index 264984ecb8a..4752a54290b 100644 --- a/gix/src/remote/connection/ref_map.rs +++ b/gix/src/remote/connection/ref_map.rs @@ -71,7 +71,7 @@ impl Default for Options { } } -impl<'remote, 'repo, T> Connection<'remote, 'repo, T> +impl Connection<'_, '_, T> where T: Transport, { diff --git a/gix/src/remote/name.rs b/gix/src/remote/name.rs index 67af8e19ca6..e2e68cc5e29 100644 --- a/gix/src/remote/name.rs +++ b/gix/src/remote/name.rs @@ -85,7 +85,7 @@ impl From for Name<'static> { } } -impl<'a> AsRef for Name<'a> { +impl AsRef for Name<'_> { fn as_ref(&self) -> &BStr { self.as_bstr() } diff --git a/gix/src/repository/remote.rs b/gix/src/repository/remote.rs index 1314a9ebeac..809cf3b7e1e 100644 --- a/gix/src/repository/remote.rs +++ b/gix/src/repository/remote.rs @@ -89,7 +89,7 @@ impl crate::Repository { .transpose()? .map(Ok) .or_else(|| self.find_default_remote(remote::Direction::Fetch)) - .ok_or_else(|| find::for_fetch::Error::ExactlyOneRemoteNotAvailable)??, + .ok_or(find::for_fetch::Error::ExactlyOneRemoteNotAvailable)??, }) } diff --git a/gix/src/revision/spec/mod.rs b/gix/src/revision/spec/mod.rs index af58ecdff51..fe85c9151d3 100644 --- a/gix/src/revision/spec/mod.rs +++ b/gix/src/revision/spec/mod.rs @@ -9,7 +9,7 @@ mod impls { use crate::revision::Spec; - impl<'repo> Deref for Spec<'repo> { + impl Deref for Spec<'_> { type Target = gix_revision::Spec; fn deref(&self) -> &Self::Target { @@ -17,19 +17,19 @@ mod impls { } } - impl<'repo> DerefMut for Spec<'repo> { + impl DerefMut for Spec<'_> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } } - impl<'repo> PartialEq for Spec<'repo> { + impl PartialEq for Spec<'_> { fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } - impl<'repo> Eq for Spec<'repo> {} + impl Eq for Spec<'_> {} } /// Initialization diff --git a/gix/src/revision/spec/parse/delegate/mod.rs b/gix/src/revision/spec/parse/delegate/mod.rs index 8dd313358c6..74aeaa1b361 100644 --- a/gix/src/revision/spec/parse/delegate/mod.rs +++ b/gix/src/revision/spec/parse/delegate/mod.rs @@ -110,7 +110,7 @@ impl<'repo> Delegate<'repo> { } } -impl<'repo> parse::Delegate for Delegate<'repo> { +impl parse::Delegate for Delegate<'_> { fn done(&mut self) { self.follow_refs_to_objects_if_needed(); self.disambiguate_objects_by_fallback_hint( @@ -121,7 +121,7 @@ impl<'repo> parse::Delegate for Delegate<'repo> { } } -impl<'repo> delegate::Kind for Delegate<'repo> { +impl delegate::Kind for Delegate<'_> { fn kind(&mut self, kind: gix_revision::spec::Kind) -> Option<()> { use gix_revision::spec::Kind::*; self.kind = Some(kind); @@ -137,7 +137,7 @@ impl<'repo> delegate::Kind for Delegate<'repo> { } } -impl<'repo> Delegate<'repo> { +impl Delegate<'_> { fn kind_implies_committish(&self) -> bool { self.kind.unwrap_or(gix_revision::spec::Kind::IncludeReachable) != gix_revision::spec::Kind::IncludeReachable } diff --git a/gix/src/revision/spec/parse/delegate/navigate.rs b/gix/src/revision/spec/parse/delegate/navigate.rs index d0837c625d9..ec463a72500 100644 --- a/gix/src/revision/spec/parse/delegate/navigate.rs +++ b/gix/src/revision/spec/parse/delegate/navigate.rs @@ -18,7 +18,7 @@ use crate::{ Object, }; -impl<'repo> delegate::Navigate for Delegate<'repo> { +impl delegate::Navigate for Delegate<'_> { fn traverse(&mut self, kind: Traversal) -> Option<()> { self.unset_disambiguate_call(); self.follow_refs_to_objects_if_needed()?; diff --git a/gix/src/revision/spec/parse/delegate/revision.rs b/gix/src/revision/spec/parse/delegate/revision.rs index 3e37ab99e5a..d92a51ac225 100644 --- a/gix/src/revision/spec/parse/delegate/revision.rs +++ b/gix/src/revision/spec/parse/delegate/revision.rs @@ -13,7 +13,7 @@ use crate::{ revision::spec::parse::{Delegate, Error, RefsHint}, }; -impl<'repo> delegate::Revision for Delegate<'repo> { +impl delegate::Revision for Delegate<'_> { fn find_ref(&mut self, name: &BStr) -> Option<()> { self.unset_disambiguate_call(); if !self.err.is_empty() && self.refs[self.idx].is_some() { diff --git a/gix/src/revision/walk.rs b/gix/src/revision/walk.rs index ccaca901c98..51216ffb076 100644 --- a/gix/src/revision/walk.rs +++ b/gix/src/revision/walk.rs @@ -176,7 +176,7 @@ impl<'repo> Platform<'repo> { } /// Create-time builder methods -impl<'repo> Platform<'repo> { +impl Platform<'_> { /// Set the sort mode for commits to the given value. The default is to order topologically breadth-first. pub fn sorting(mut self, sorting: Sorting) -> Self { self.sorting = sorting; diff --git a/gix/src/status/index_worktree.rs b/gix/src/status/index_worktree.rs index adae2443014..de20c5fd119 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -610,7 +610,7 @@ pub mod iter { } /// Lifecycle - impl<'repo, Progress> Platform<'repo, Progress> + impl Platform<'_, Progress> where Progress: gix_features::progress::Progress, { diff --git a/gix/src/status/platform.rs b/gix/src/status/platform.rs index 2b73ae172a3..43c5fbdeabf 100644 --- a/gix/src/status/platform.rs +++ b/gix/src/status/platform.rs @@ -2,7 +2,7 @@ use crate::status::{index_worktree, OwnedOrStaticAtomicBool, Platform, Submodule use std::sync::atomic::AtomicBool; /// Builder -impl<'repo, Progress> Platform<'repo, Progress> +impl Platform<'_, Progress> where Progress: gix_features::progress::Progress, { diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 259a79770b0..7d630addf2b 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -83,7 +83,7 @@ struct IsActiveState { } ///Access -impl<'repo> Submodule<'repo> { +impl Submodule<'_> { /// Return the submodule's name. pub fn name(&self) -> &BStr { self.name.as_ref() @@ -304,7 +304,7 @@ pub mod status { IndexWorktreeStatus(#[from] crate::status::index_worktree::Error), } - impl<'repo> Submodule<'repo> { + impl Submodule<'_> { /// Return the status of the submodule. /// /// Use `ignore` to control the portion of the submodule status to ignore. It can be obtained from diff --git a/gix/src/types.rs b/gix/src/types.rs index 20d240e99bb..8993ebc03b8 100644 --- a/gix/src/types.rs +++ b/gix/src/types.rs @@ -42,7 +42,7 @@ pub struct Object<'repo> { pub(crate) repo: &'repo Repository, } -impl<'a> Drop for Object<'a> { +impl Drop for Object<'_> { fn drop(&mut self) { self.repo.reuse_buffer(&mut self.data); } @@ -58,7 +58,7 @@ pub struct Blob<'repo> { pub(crate) repo: &'repo Repository, } -impl<'a> Drop for Blob<'a> { +impl Drop for Blob<'_> { fn drop(&mut self) { self.repo.reuse_buffer(&mut self.data); } @@ -74,7 +74,7 @@ pub struct Tree<'repo> { pub(crate) repo: &'repo Repository, } -impl<'a> Drop for Tree<'a> { +impl Drop for Tree<'_> { fn drop(&mut self) { self.repo.reuse_buffer(&mut self.data); } @@ -90,7 +90,7 @@ pub struct Tag<'repo> { pub(crate) repo: &'repo Repository, } -impl<'a> Drop for Tag<'a> { +impl Drop for Tag<'_> { fn drop(&mut self) { self.repo.reuse_buffer(&mut self.data); } @@ -106,7 +106,7 @@ pub struct Commit<'repo> { pub(crate) repo: &'repo Repository, } -impl<'a> Drop for Commit<'a> { +impl Drop for Commit<'_> { fn drop(&mut self) { self.repo.reuse_buffer(&mut self.data); } diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index 890f140a0d7..b56ccd3fbc1 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -128,7 +128,7 @@ pub mod open_index { IndexCorrupt(#[from] gix_index::file::verify::Error), } - impl<'repo> crate::Worktree<'repo> { + impl crate::Worktree<'_> { /// A shortcut to [`crate::Repository::open_index()`]. pub fn open_index(&self) -> Result { self.parent.open_index() @@ -156,7 +156,7 @@ pub mod excludes { CreateCache(#[from] crate::config::exclude_stack::Error), } - impl<'repo> crate::Worktree<'repo> { + impl crate::Worktree<'_> { /// Configure a file-system cache checking if files below the repository are excluded. /// /// This takes into consideration all the usual repository configuration, namely: diff --git a/gix/src/worktree/proxy.rs b/gix/src/worktree/proxy.rs index 8a77db815ea..e4b6672774c 100644 --- a/gix/src/worktree/proxy.rs +++ b/gix/src/worktree/proxy.rs @@ -33,7 +33,7 @@ impl<'repo> Proxy<'repo> { } } -impl<'repo> Proxy<'repo> { +impl Proxy<'_> { /// Read the location of the checkout, the base of the work tree. /// Note that the location might not exist. pub fn base(&self) -> std::io::Result { diff --git a/gix/tests/repository/worktree.rs b/gix/tests/repository/worktree.rs index fe55f4c69c3..a77eb357e86 100644 --- a/gix/tests/repository/worktree.rs +++ b/gix/tests/repository/worktree.rs @@ -159,7 +159,7 @@ mod baseline { use super::Baseline; - impl<'a> Baseline<'a> { + impl Baseline<'_> { pub fn collect(dir: impl AsRef) -> std::io::Result> { let content = std::fs::read(dir.as_ref().join("worktree-list.baseline"))?; Ok(Baseline { lines: content.lines() }.collect()) @@ -179,7 +179,7 @@ mod baseline { pub prunable: Option, } - impl<'a> Iterator for Baseline<'a> { + impl Iterator for Baseline<'_> { type Item = Worktree; fn next(&mut self) -> Option { diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs index 091d899a6ab..a17c1aab8e8 100644 --- a/tests/tools/src/lib.rs +++ b/tests/tools/src/lib.rs @@ -871,7 +871,7 @@ impl<'a> Env<'a> { } } -impl<'a> Drop for Env<'a> { +impl Drop for Env<'_> { fn drop(&mut self) { for (var, prev_value) in self.altered_vars.iter().rev() { match prev_value { From 989276fa12cac8682fbcbdf52a945d41cb922a3f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 5 Oct 2024 15:34:50 +0200 Subject: [PATCH 07/13] refactor!: add `tree()` function to diff a tree. Remove `tree::Changes()` and the ceremony required to diff a tree. --- gix-diff/src/lib.rs | 1 + gix-diff/src/tree/{changes.rs => function.rs} | 247 ++++++++---------- gix-diff/src/tree/mod.rs | 31 ++- gix-diff/tests/Cargo.toml | 2 +- gix-diff/tests/{ => diff}/blob/mod.rs | 0 gix-diff/tests/{ => diff}/blob/pipeline.rs | 0 gix-diff/tests/{ => diff}/blob/platform.rs | 0 gix-diff/tests/{diff.rs => diff/main.rs} | 0 gix-diff/tests/{ => diff}/rewrites/mod.rs | 0 gix-diff/tests/{ => diff}/rewrites/tracker.rs | 0 gix-diff/tests/{tree/mod.rs => diff/tree.rs} | 6 +- 11 files changed, 135 insertions(+), 152 deletions(-) rename gix-diff/src/tree/{changes.rs => function.rs} (61%) rename gix-diff/tests/{ => diff}/blob/mod.rs (100%) rename gix-diff/tests/{ => diff}/blob/pipeline.rs (100%) rename gix-diff/tests/{ => diff}/blob/platform.rs (100%) rename gix-diff/tests/{diff.rs => diff/main.rs} (100%) rename gix-diff/tests/{ => diff}/rewrites/mod.rs (100%) rename gix-diff/tests/{ => diff}/rewrites/tracker.rs (100%) rename gix-diff/tests/{tree/mod.rs => diff/tree.rs} (99%) diff --git a/gix-diff/src/lib.rs b/gix-diff/src/lib.rs index 1fe8d2e6bb4..14c67e45080 100644 --- a/gix-diff/src/lib.rs +++ b/gix-diff/src/lib.rs @@ -45,6 +45,7 @@ pub mod rewrites; /// pub mod tree; +pub use tree::function::diff as tree; /// #[cfg(feature = "blob")] diff --git a/gix-diff/src/tree/changes.rs b/gix-diff/src/tree/function.rs similarity index 61% rename from gix-diff/src/tree/changes.rs rename to gix-diff/src/tree/function.rs index cdabefd549f..ba371e5789e 100644 --- a/gix-diff/src/tree/changes.rs +++ b/gix-diff/src/tree/function.rs @@ -1,136 +1,117 @@ use std::{borrow::BorrowMut, collections::VecDeque}; -use gix_object::{tree::EntryRef, FindExt}; +use gix_object::{tree::EntryRef, FindExt, TreeRefIter}; use crate::tree::visit::{ChangeId, Relation}; -use crate::{ - tree, - tree::{visit::Change, TreeInfoTuple}, -}; +use crate::tree::{visit::Change, Error, State, TreeInfoTuple, Visit}; -/// The error returned by [`tree::Changes::needed_to_obtain()`]. -#[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] -pub enum Error { - #[error(transparent)] - Find(#[from] gix_object::find::existing_iter::Error), - #[error("The delegate cancelled the operation")] - Cancelled, - #[error(transparent)] - EntriesDecode(#[from] gix_object::decode::Error), -} - -impl tree::Changes<'_> { - /// Calculate the changes that would need to be applied to `self` to get `other` using `objects` to obtain objects as needed for traversal. - /// - /// * The `state` maybe owned or mutably borrowed to allow reuses allocated data structures through multiple runs. - /// * `locate` is a function `f(object_id, &mut buffer) -> Option` to return a `TreeIter` for the given object id backing - /// its data in the given buffer. Returning `None` is unexpected as these trees are obtained during iteration, and in a typical - /// database errors are not expected either which is why the error case is omitted. To allow proper error reporting, [`Error::Find`] - /// should be converted into a more telling error. - /// * `delegate` will receive the computed changes, see the [`Visit`][`tree::Visit`] trait for more information on what to expect. - /// - /// # Notes - /// - /// * To obtain progress, implement it within the `delegate`. - /// * Tree entries are expected to be ordered using [`tree-entry-comparison`][git_cmp_c] (the same [in Rust][git_cmp_rs]) - /// * it does a breadth first iteration as buffer space only fits two trees, the current one on the one we compare with. - /// * does not do rename tracking but attempts to reduce allocations to zero (so performance is mostly determined - /// by the delegate implementation which should be as specific as possible. Rename tracking can be computed on top of the changes - /// received by the `delegate`. - /// * cycle checking is not performed, but can be performed in the delegate which can return [`tree::visit::Action::Cancel`] to stop the traversal. - /// * [`std::mem::ManuallyDrop`] is used because `Peekable` is needed. When using it as wrapper around our no-drop iterators, all of the sudden - /// borrowcheck complains as Drop is present (even though it's not) - /// - /// [git_cmp_c]: https://github.com/git/git/blob/311531c9de557d25ac087c1637818bd2aad6eb3a/tree-diff.c#L49:L65 - /// [git_cmp_rs]: https://github.com/Byron/gitoxide/blob/a4d5f99c8dc99bf814790928a3bf9649cd99486b/gix-object/src/mutable/tree.rs#L52-L55 - pub fn needed_to_obtain( - mut self, - other: gix_object::TreeRefIter<'_>, - mut state: StateMut, - objects: impl gix_object::Find, - delegate: &mut impl tree::Visit, - ) -> Result<(), Error> - where - StateMut: BorrowMut, - { - let state = state.borrow_mut(); - state.clear(); - let mut lhs_entries = peekable(self.0.take().unwrap_or_default()); - let mut rhs_entries = peekable(other); - let mut relation = None; - let mut pop_path = false; +/// Calculate the changes that would need to be applied to `lhs` to get `rhs` using `objects` to obtain objects as needed for traversal. +/// `state` can be used between multiple calls to re-use memory. +/// +/// * The `state` maybe owned or mutably borrowed to allow reuses allocated data structures through multiple runs. +/// * `delegate` will receive the computed changes, see the [`Visit`] trait for more information on what to expect. +/// +/// # Notes +/// +/// * `lhs` can be an empty tree to simulate what would happen if the left-hand side didn't exist. +/// * To obtain progress, implement it within the `delegate`. +/// * Tree entries are expected to be ordered using [`tree-entry-comparison`][git_cmp_c] (the same [in Rust][git_cmp_rs]) +/// * it does a breadth first iteration as buffer space only fits two trees, the current one on the one we compare with. +/// * does not do rename tracking but attempts to reduce allocations to zero (so performance is mostly determined +/// by the delegate implementation which should be as specific as possible. Rename tracking can be computed on top of the changes +/// received by the `delegate`. +/// * cycle checking is not performed, but can be performed in the delegate which can return +/// [`tree::visit::Action::Cancel`](crate::tree::visit::Action::Cancel) to stop the traversal. +/// +/// [git_cmp_c]: https://github.com/git/git/blob/311531c9de557d25ac087c1637818bd2aad6eb3a/tree-diff.c#L49:L65 +/// [git_cmp_rs]: https://github.com/Byron/gitoxide/blob/a4d5f99c8dc99bf814790928a3bf9649cd99486b/gix-object/src/mutable/tree.rs#L52-L55 +#[doc(alias = "diff_tree_to_tree", alias = "git2")] +pub fn diff( + lhs: TreeRefIter<'_>, + rhs: TreeRefIter<'_>, + mut state: StateMut, + objects: impl gix_object::Find, + delegate: &mut impl Visit, +) -> Result<(), Error> +where + StateMut: BorrowMut, +{ + let state = state.borrow_mut(); + state.clear(); + let mut lhs_entries = peekable(lhs); + let mut rhs_entries = peekable(rhs); + let mut relation = None; + let mut pop_path = false; - loop { - if pop_path { - delegate.pop_path_component(); - } - pop_path = true; + loop { + if pop_path { + delegate.pop_path_component(); + } + pop_path = true; - match (lhs_entries.next(), rhs_entries.next()) { - (None, None) => { - match state.trees.pop_front() { - Some((None, Some(rhs), relation_to_propagate)) => { - delegate.pop_front_tracked_path_and_set_current(); - relation = relation_to_propagate; - rhs_entries = peekable(objects.find_tree_iter(&rhs, &mut state.buf2)?); - } - Some((Some(lhs), Some(rhs), relation_to_propagate)) => { - delegate.pop_front_tracked_path_and_set_current(); - lhs_entries = peekable(objects.find_tree_iter(&lhs, &mut state.buf1)?); - rhs_entries = peekable(objects.find_tree_iter(&rhs, &mut state.buf2)?); - relation = relation_to_propagate; - } - Some((Some(lhs), None, relation_to_propagate)) => { - delegate.pop_front_tracked_path_and_set_current(); - lhs_entries = peekable(objects.find_tree_iter(&lhs, &mut state.buf1)?); - relation = relation_to_propagate; - } - Some((None, None, _)) => unreachable!("BUG: it makes no sense to fill the stack with empties"), - None => return Ok(()), - }; - pop_path = false; - } - (Some(lhs), Some(rhs)) => { - use std::cmp::Ordering::*; - let (lhs, rhs) = (lhs?, rhs?); - match compare(&lhs, &rhs) { - Equal => handle_lhs_and_rhs_with_equal_filenames( - lhs, - rhs, - &mut state.trees, - &mut state.change_id, - relation, - delegate, - )?, - Less => catchup_lhs_with_rhs( - &mut lhs_entries, - lhs, - rhs, - &mut state.trees, - &mut state.change_id, - relation, - delegate, - )?, - Greater => catchup_rhs_with_lhs( - &mut rhs_entries, - lhs, - rhs, - &mut state.trees, - &mut state.change_id, - relation, - delegate, - )?, + match (lhs_entries.next(), rhs_entries.next()) { + (None, None) => { + match state.trees.pop_front() { + Some((None, Some(rhs), relation_to_propagate)) => { + delegate.pop_front_tracked_path_and_set_current(); + relation = relation_to_propagate; + rhs_entries = peekable(objects.find_tree_iter(&rhs, &mut state.buf2)?); } + Some((Some(lhs), Some(rhs), relation_to_propagate)) => { + delegate.pop_front_tracked_path_and_set_current(); + lhs_entries = peekable(objects.find_tree_iter(&lhs, &mut state.buf1)?); + rhs_entries = peekable(objects.find_tree_iter(&rhs, &mut state.buf2)?); + relation = relation_to_propagate; + } + Some((Some(lhs), None, relation_to_propagate)) => { + delegate.pop_front_tracked_path_and_set_current(); + lhs_entries = peekable(objects.find_tree_iter(&lhs, &mut state.buf1)?); + relation = relation_to_propagate; + } + Some((None, None, _)) => unreachable!("BUG: it makes no sense to fill the stack with empties"), + None => return Ok(()), + }; + pop_path = false; + } + (Some(lhs), Some(rhs)) => { + use std::cmp::Ordering::*; + let (lhs, rhs) = (lhs?, rhs?); + match compare(&lhs, &rhs) { + Equal => handle_lhs_and_rhs_with_equal_filenames( + lhs, + rhs, + &mut state.trees, + &mut state.change_id, + relation, + delegate, + )?, + Less => catchup_lhs_with_rhs( + &mut lhs_entries, + lhs, + rhs, + &mut state.trees, + &mut state.change_id, + relation, + delegate, + )?, + Greater => catchup_rhs_with_lhs( + &mut rhs_entries, + lhs, + rhs, + &mut state.trees, + &mut state.change_id, + relation, + delegate, + )?, } - (Some(lhs), None) => { - let lhs = lhs?; - delete_entry_schedule_recursion(lhs, &mut state.trees, &mut state.change_id, relation, delegate)?; - } - (None, Some(rhs)) => { - let rhs = rhs?; - add_entry_schedule_recursion(rhs, &mut state.trees, &mut state.change_id, relation, delegate)?; - } + } + (Some(lhs), None) => { + let lhs = lhs?; + delete_entry_schedule_recursion(lhs, &mut state.trees, &mut state.change_id, relation, delegate)?; + } + (None, Some(rhs)) => { + let rhs = rhs?; + add_entry_schedule_recursion(rhs, &mut state.trees, &mut state.change_id, relation, delegate)?; } } } @@ -150,7 +131,7 @@ fn delete_entry_schedule_recursion( queue: &mut VecDeque, change_id: &mut ChangeId, relation_to_propagate: Option, - delegate: &mut impl tree::Visit, + delegate: &mut impl Visit, ) -> Result<(), Error> { delegate.push_path_component(entry.filename); let relation = relation_to_propagate.or_else(|| { @@ -182,7 +163,7 @@ fn add_entry_schedule_recursion( queue: &mut VecDeque, change_id: &mut ChangeId, relation_to_propagate: Option, - delegate: &mut impl tree::Visit, + delegate: &mut impl Visit, ) -> Result<(), Error> { delegate.push_path_component(entry.filename); let relation = relation_to_propagate.or_else(|| { @@ -210,13 +191,13 @@ fn add_entry_schedule_recursion( } fn catchup_rhs_with_lhs( - rhs_entries: &mut IteratorType>, + rhs_entries: &mut IteratorType>, lhs: EntryRef<'_>, rhs: EntryRef<'_>, queue: &mut VecDeque, change_id: &mut ChangeId, relation_to_propagate: Option, - delegate: &mut impl tree::Visit, + delegate: &mut impl Visit, ) -> Result<(), Error> { use std::cmp::Ordering::*; add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?; @@ -259,13 +240,13 @@ fn catchup_rhs_with_lhs( } fn catchup_lhs_with_rhs( - lhs_entries: &mut IteratorType>, + lhs_entries: &mut IteratorType>, lhs: EntryRef<'_>, rhs: EntryRef<'_>, queue: &mut VecDeque, change_id: &mut ChangeId, relation_to_propagate: Option, - delegate: &mut impl tree::Visit, + delegate: &mut impl Visit, ) -> Result<(), Error> { use std::cmp::Ordering::*; delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?; @@ -313,7 +294,7 @@ fn handle_lhs_and_rhs_with_equal_filenames( queue: &mut VecDeque, change_id: &mut ChangeId, relation_to_propagate: Option, - delegate: &mut impl tree::Visit, + delegate: &mut impl Visit, ) -> Result<(), Error> { match (lhs.mode.is_tree(), rhs.mode.is_tree()) { (true, true) => { @@ -413,7 +394,7 @@ fn handle_lhs_and_rhs_with_equal_filenames( Ok(()) } -type IteratorType = std::mem::ManuallyDrop>; +type IteratorType = std::iter::Peekable; fn to_child(r: Option) -> Option { r.map(|r| match r { @@ -423,7 +404,7 @@ fn to_child(r: Option) -> Option { } fn peekable(iter: I) -> IteratorType { - std::mem::ManuallyDrop::new(iter.peekable()) + iter.peekable() } #[cfg(test)] diff --git a/gix-diff/src/tree/mod.rs b/gix-diff/src/tree/mod.rs index ab0b4c143cf..7ef2b277a4d 100644 --- a/gix-diff/src/tree/mod.rs +++ b/gix-diff/src/tree/mod.rs @@ -1,9 +1,21 @@ use crate::tree::visit::Relation; use bstr::BStr; use gix_hash::ObjectId; -use gix_object::{bstr::BString, TreeRefIter}; +use gix_object::bstr::BString; use std::collections::VecDeque; +/// The error returned by [`tree()`](super::tree()). +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + Find(#[from] gix_object::find::existing_iter::Error), + #[error("The delegate cancelled the operation")] + Cancelled, + #[error(transparent)] + EntriesDecode(#[from] gix_object::decode::Error), +} + /// A trait to allow responding to a traversal designed to figure out the [changes](visit::Change) /// to turn tree A into tree B. pub trait Visit { @@ -21,7 +33,7 @@ pub trait Visit { fn visit(&mut self, change: visit::Change) -> visit::Action; } -/// The state required to visit [Changes] to be instantiated with `State::default()`. +/// The state required to run [tree-diffs](super::tree()). #[derive(Default, Clone)] pub struct State { buf1: Vec, @@ -41,20 +53,7 @@ impl State { } } -/// An iterator over changes of a tree, instantiated using `Changes::from(…)`. -pub struct Changes<'a>(Option>); - -impl<'a, T> From for Changes<'a> -where - T: Into>>, -{ - fn from(v: T) -> Self { - Changes(v.into()) - } -} - -/// -pub mod changes; +pub(super) mod function; /// pub mod visit; diff --git a/gix-diff/tests/Cargo.toml b/gix-diff/tests/Cargo.toml index 86aa0d446e5..76f6bae506f 100644 --- a/gix-diff/tests/Cargo.toml +++ b/gix-diff/tests/Cargo.toml @@ -14,7 +14,7 @@ rust-version = "1.65" [[test]] doctest = false name = "diff" -path = "diff.rs" +path = "diff/main.rs" [dev-dependencies] gix-diff = { path = ".." } diff --git a/gix-diff/tests/blob/mod.rs b/gix-diff/tests/diff/blob/mod.rs similarity index 100% rename from gix-diff/tests/blob/mod.rs rename to gix-diff/tests/diff/blob/mod.rs diff --git a/gix-diff/tests/blob/pipeline.rs b/gix-diff/tests/diff/blob/pipeline.rs similarity index 100% rename from gix-diff/tests/blob/pipeline.rs rename to gix-diff/tests/diff/blob/pipeline.rs diff --git a/gix-diff/tests/blob/platform.rs b/gix-diff/tests/diff/blob/platform.rs similarity index 100% rename from gix-diff/tests/blob/platform.rs rename to gix-diff/tests/diff/blob/platform.rs diff --git a/gix-diff/tests/diff.rs b/gix-diff/tests/diff/main.rs similarity index 100% rename from gix-diff/tests/diff.rs rename to gix-diff/tests/diff/main.rs diff --git a/gix-diff/tests/rewrites/mod.rs b/gix-diff/tests/diff/rewrites/mod.rs similarity index 100% rename from gix-diff/tests/rewrites/mod.rs rename to gix-diff/tests/diff/rewrites/mod.rs diff --git a/gix-diff/tests/rewrites/tracker.rs b/gix-diff/tests/diff/rewrites/tracker.rs similarity index 100% rename from gix-diff/tests/rewrites/tracker.rs rename to gix-diff/tests/diff/rewrites/tracker.rs diff --git a/gix-diff/tests/tree/mod.rs b/gix-diff/tests/diff/tree.rs similarity index 99% rename from gix-diff/tests/tree/mod.rs rename to gix-diff/tests/diff/tree.rs index f54610816cd..19ba1632387 100644 --- a/gix-diff/tests/tree/mod.rs +++ b/gix-diff/tests/diff/tree.rs @@ -59,7 +59,8 @@ mod changes { let mut buf2 = Vec::new(); let rhs_tree = locate_tree_by_commit(db, rhs, &mut buf2)?; let mut recorder = gix_diff::tree::Recorder::default().track_location(location); - gix_diff::tree::Changes::from(lhs_tree).needed_to_obtain( + gix_diff::tree( + lhs_tree.unwrap_or_default(), rhs_tree, gix_diff::tree::State::default(), db, @@ -102,7 +103,8 @@ mod changes { }; let mut recorder = gix_diff::tree::Recorder::default(); - gix_diff::tree::Changes::from(previous_tree).needed_to_obtain( + gix_diff::tree( + previous_tree.unwrap_or_default(), current_tree, &mut gix_diff::tree::State::default(), db, From 3fd9fabe0c0e74d54b5153c4f572eff76293f334 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 5 Oct 2024 15:59:30 +0200 Subject: [PATCH 08/13] adapt to changes in `gix-diff` --- crate-status.md | 19 ++++++++++++------- gix-pack/src/data/output/count/objects/mod.rs | 16 ++++++++-------- .../src/data/output/count/objects/types.rs | 2 +- gix/src/object/tree/diff/for_each.rs | 9 +++++---- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/crate-status.md b/crate-status.md index 35b960a55a1..5be7acd9e66 100644 --- a/crate-status.md +++ b/crate-status.md @@ -304,12 +304,16 @@ Check out the [performance discussion][gix-diff-performance] as well. * **tree** * [x] changes needed to obtain _other tree_ -* **patches** - * There are various ways to generate a patch from two blobs. - * [ ] text - * [ ] binary -* **lines** - * [x] Simple line-by-line diffs powered by the `imara-diff` crate. +* **blobs** + * **patches** + * There are various ways to generate a patch from two blobs. + * [ ] text + * [ ] binary + * [ ] `git-apply` compatibility + * [ ] merge hunks that are close enough based on line-setting (`interhunk-lines`) + * [ ] white-space related settings + * **lines** + * [x] Simple line-by-line diffs powered by the `imara-diff` crate. * **generic rename tracker to find renames and copies** * [x] find blobs by exact match * [x] find blobs by similarity check @@ -335,12 +339,13 @@ Check out the [performance discussion][gix-diff-performance] as well. ### gix-merge -* [x] three-way merge analysis of blobs with choice of how to resolve conflicts +* [x] three-way merge analysis of **blobs** with choice of how to resolve conflicts - [ ] choose how to resolve conflicts on the data-structure - [ ] produce a new blob based on data-structure containing possible resolutions - [x] `merge` style - [x] `diff3` style - [x] `zdiff` style + - [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same) * [ ] diff-heuristics match Git perfectly * [x] API documentation * [ ] Examples diff --git a/gix-pack/src/data/output/count/objects/mod.rs b/gix-pack/src/data/output/count/objects/mod.rs index 7da95b3382e..17e5c68d266 100644 --- a/gix-pack/src/data/output/count/objects/mod.rs +++ b/gix-pack/src/data/output/count/objects/mod.rs @@ -247,14 +247,14 @@ mod expand { changes_delegate.clear(); let objects = CountingObjects::new(db); - gix_diff::tree::Changes::from(Some(parent_tree)) - .needed_to_obtain( - current_tree_iter, - &mut tree_diff_state, - &objects, - &mut changes_delegate, - ) - .map_err(Error::TreeChanges)?; + gix_diff::tree( + parent_tree, + current_tree_iter, + &mut tree_diff_state, + &objects, + &mut changes_delegate, + ) + .map_err(Error::TreeChanges)?; stats.decoded_objects += objects.into_count(); } &changes_delegate.objects diff --git a/gix-pack/src/data/output/count/objects/types.rs b/gix-pack/src/data/output/count/objects/types.rs index 15e39501828..4b0bad8d0d9 100644 --- a/gix-pack/src/data/output/count/objects/types.rs +++ b/gix-pack/src/data/output/count/objects/types.rs @@ -90,7 +90,7 @@ pub enum Error { #[error(transparent)] TreeTraverse(gix_traverse::tree::breadthfirst::Error), #[error(transparent)] - TreeChanges(gix_diff::tree::changes::Error), + TreeChanges(gix_diff::tree::Error), #[error("Operation interrupted")] Interrupted, } diff --git a/gix/src/object/tree/diff/for_each.rs b/gix/src/object/tree/diff/for_each.rs index 6c493e6e83c..2582f765768 100644 --- a/gix/src/object/tree/diff/for_each.rs +++ b/gix/src/object/tree/diff/for_each.rs @@ -14,7 +14,7 @@ use crate::{ #[allow(missing_docs)] pub enum Error { #[error(transparent)] - Diff(#[from] gix_diff::tree::changes::Error), + Diff(#[from] gix_diff::tree::Error), #[error("The user-provided callback failed")] ForEach(#[source] Box), #[error(transparent)] @@ -88,7 +88,8 @@ impl<'old> Platform<'_, 'old> { tracked: self.rewrites.map(rewrites::Tracker::new), err: None, }; - match gix_diff::tree::Changes::from(TreeRefIter::from_bytes(&self.lhs.data)).needed_to_obtain( + match gix_diff::tree( + TreeRefIter::from_bytes(&self.lhs.data), TreeRefIter::from_bytes(&other.data), &mut self.state, &repo.objects, @@ -103,9 +104,9 @@ impl<'old> Platform<'_, 'old> { None => Ok(outcome), } } - Err(gix_diff::tree::changes::Error::Cancelled) => delegate + Err(gix_diff::tree::Error::Cancelled) => delegate .err - .map_or(Err(Error::Diff(gix_diff::tree::changes::Error::Cancelled)), |err| { + .map_or(Err(Error::Diff(gix_diff::tree::Error::Cancelled)), |err| { Err(Error::ForEach(err.into())) }), Err(err) => Err(err.into()), From f2e813d8712f245f7f93ecae2a3da768dd6dc2db Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 4 Oct 2024 10:47:28 +0200 Subject: [PATCH 09/13] feat: provide facilities to perform rewrite-tracking for tree-diffs in plumbing. This effectively pulls down a higher-level implementation in `gix` to the plumbing level, to allow it to be used there as well. --- Cargo.lock | 44 + Cargo.toml | 3 +- gix-diff/Cargo.toml | 3 +- gix-diff/src/lib.rs | 6 + gix-diff/src/tree_with_rewrites/change.rs | 480 ++++ gix-diff/src/tree_with_rewrites/function.rs | 277 +++ gix-diff/src/tree_with_rewrites/mod.rs | 39 + gix-diff/tests/Cargo.toml | 1 + gix-diff/tests/diff/main.rs | 1 + gix-diff/tests/diff/tree_with_rewrites.rs | 1989 +++++++++++++++++ .../make_diff_for_rewrites_repo.tar | Bin 0 -> 335360 bytes .../fixtures/make_diff_for_rewrites_repo.sh | 797 +++++++ 12 files changed, 3638 insertions(+), 2 deletions(-) create mode 100644 gix-diff/src/tree_with_rewrites/change.rs create mode 100644 gix-diff/src/tree_with_rewrites/function.rs create mode 100644 gix-diff/src/tree_with_rewrites/mod.rs create mode 100644 gix-diff/tests/diff/tree_with_rewrites.rs create mode 100644 gix-diff/tests/fixtures/generated-archives/make_diff_for_rewrites_repo.tar create mode 100644 gix-diff/tests/fixtures/make_diff_for_rewrites_repo.sh diff --git a/Cargo.lock b/Cargo.lock index 802b90fb6b2..8f6df147b05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,6 +601,18 @@ dependencies = [ "windows 0.44.0", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -919,6 +931,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -1650,6 +1668,7 @@ dependencies = [ "gix-path 0.10.11", "gix-tempfile 14.0.2", "gix-trace 0.1.10", + "gix-traverse 0.41.0", "gix-worktree 0.36.0", "imara-diff", "serde", @@ -1669,6 +1688,7 @@ dependencies = [ "gix-testtools", "gix-traverse 0.41.0", "gix-worktree 0.36.0", + "insta", "pretty_assertions", "shell-words", ] @@ -3127,6 +3147,18 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "insta" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6593a41c7a73841868772495db7dc1e8ecab43bb5c0b6da2059246c4b506ab60" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", +] + [[package]] name = "instant" version = "0.1.13" @@ -3382,6 +3414,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -4496,6 +4534,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index 6e5b2dfe1bf..81fcf5cc74c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -194,8 +194,9 @@ serde_derive = ">=1.0.185" once_cell = "1.18.0" document-features = { version = "0.2.0", optional = true } - [profile.dev.package] +insta.opt-level = 3 +similar.opt-level = 3 gix-object = { opt-level = 3 } gix-ref = { opt-level = 3 } #gix-pack = { opt-level = 3 } diff --git a/gix-diff/Cargo.toml b/gix-diff/Cargo.toml index 5a034512d14..512d13f3545 100644 --- a/gix-diff/Cargo.toml +++ b/gix-diff/Cargo.toml @@ -15,7 +15,7 @@ autotests = false [features] default = ["blob"] ## Enable diffing of blobs using imara-diff, which also allows for a generic rewrite tracking implementation. -blob = ["dep:imara-diff", "dep:gix-filter", "dep:gix-worktree", "dep:gix-path", "dep:gix-fs", "dep:gix-command", "dep:gix-tempfile", "dep:gix-trace"] +blob = ["dep:imara-diff", "dep:gix-filter", "dep:gix-worktree", "dep:gix-path", "dep:gix-fs", "dep:gix-command", "dep:gix-tempfile", "dep:gix-trace", "dep:gix-traverse"] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = ["dep:serde", "gix-hash/serde", "gix-object/serde"] ## Make it possible to compile to the `wasm32-unknown-unknown` target. @@ -34,6 +34,7 @@ gix-path = { version = "^0.10.11", path = "../gix-path", optional = true } gix-fs = { version = "^0.11.3", path = "../gix-fs", optional = true } gix-tempfile = { version = "^14.0.0", path = "../gix-tempfile", optional = true } gix-trace = { version = "^0.1.10", path = "../gix-trace", optional = true } +gix-traverse = { version = "^0.41.0", path = "../gix-traverse", optional = true } thiserror = "1.0.32" imara-diff = { version = "0.1.7", optional = true } diff --git a/gix-diff/src/lib.rs b/gix-diff/src/lib.rs index 14c67e45080..1aa2895a84c 100644 --- a/gix-diff/src/lib.rs +++ b/gix-diff/src/lib.rs @@ -47,6 +47,12 @@ pub mod rewrites; pub mod tree; pub use tree::function::diff as tree; +/// +#[cfg(feature = "blob")] +pub mod tree_with_rewrites; +#[cfg(feature = "blob")] +pub use tree_with_rewrites::function::diff as tree_with_rewrites; + /// #[cfg(feature = "blob")] pub mod blob; diff --git a/gix-diff/src/tree_with_rewrites/change.rs b/gix-diff/src/tree_with_rewrites/change.rs new file mode 100644 index 00000000000..196a4b977b6 --- /dev/null +++ b/gix-diff/src/tree_with_rewrites/change.rs @@ -0,0 +1,480 @@ +use crate::blob::{DiffLineStats, ResourceKind}; +use crate::tree; +use bstr::BString; +use bstr::{BStr, ByteSlice}; + +/// Represents any possible change in order to turn one tree into another, which references data owned by its producer. +#[derive(Debug, Clone, Copy)] +pub enum ChangeRef<'a> { + /// An entry was added, like the addition of a file or directory. + Addition { + /// The location of the file or directory. + /// + /// It may be empty if [file names](super::Options::location) is `None`. + location: &'a BStr, + /// The mode of the added entry. + entry_mode: gix_object::tree::EntryMode, + /// Identifies a relationship between this instance and another one, + /// making it easy to reconstruct the top-level of directory changes. + relation: Option, + /// The object id of the added entry. + id: gix_hash::ObjectId, + }, + /// An entry was deleted, like the deletion of a file or directory. + Deletion { + /// The location of the file or directory. + /// + /// It may be empty if [file names](super::Options::location) is `None`. + /// are tracked. + location: &'a BStr, + /// The mode of the deleted entry. + entry_mode: gix_object::tree::EntryMode, + /// Identifies a relationship between this instance and another one, + /// making it easy to reconstruct the top-level of directory changes. + relation: Option, + /// The object id of the deleted entry. + id: gix_hash::ObjectId, + }, + /// An entry was modified, e.g. changing the contents of a file adjusts its object id and turning + /// a file into a symbolic link adjusts its mode. + Modification { + /// The location of the file or directory. + /// + /// It may be empty if [file names](super::Options::location) is `None`. + /// are tracked. + location: &'a BStr, + /// The mode of the entry before the modification. + previous_entry_mode: gix_object::tree::EntryMode, + /// The object id of the entry before the modification. + previous_id: gix_hash::ObjectId, + + /// The mode of the entry after the modification. + entry_mode: gix_object::tree::EntryMode, + /// The object id after the modification. + id: gix_hash::ObjectId, + }, + /// Entries are considered rewritten if they are not trees and they, according to some understanding of identity, were renamed + /// or copied. + /// In case of renames, this means they originally appeared as [`Deletion`](ChangeRef::Deletion) signalling their source as well as an + /// [`Addition`](ChangeRef::Addition) acting as destination. + /// + /// In case of copies, the `copy` flag is true and typically represents a perfect copy of a source was made. + /// + /// This variant can only be encountered if [rewrite tracking](super::Options::rewrites) is enabled. + /// + /// Note that mode changes may have occurred as well, i.e. changes from executable to non-executable or vice-versa. + Rewrite { + /// The location of the source of the rename or copy operation. + /// + /// It may be empty if [file names](super::Options::location) is `None`. + /// are tracked. + source_location: &'a BStr, + /// The mode of the entry before the rename. + source_entry_mode: gix_object::tree::EntryMode, + /// Identifies a relationship between the source and another source, + /// making it easy to reconstruct the top-level of directory changes. + source_relation: Option, + /// The object id of the entry before the rename. + /// + /// Note that this is the same as `id` if we require the [similarity to be 100%](super::Rewrites::percentage), but may + /// be different otherwise. + source_id: gix_hash::ObjectId, + /// Information about the diff we performed to detect similarity and match the `source_id` with the current state at `id`. + /// It's `None` if `source_id` is equal to `id`, as identity made an actual diff computation unnecessary. + diff: Option, + /// The mode of the entry after the rename. + /// It could differ but still be considered a rename as we are concerned only about content. + entry_mode: gix_object::tree::EntryMode, + /// The object id after the rename. + id: gix_hash::ObjectId, + /// The location after the rename or copy operation. + /// + /// It may be empty if [file names](super::Options::location) is `None`. + location: &'a BStr, + /// Identifies a relationship between this destination and another destination, + /// making it easy to reconstruct the top-level of directory changes. + relation: Option, + /// If true, this rewrite is created by copy, and `source_id` is pointing to its source. Otherwise, it's a rename, and `source_id` + /// points to a deleted object, as renames are tracked as deletions and additions of the same or similar content. + copy: bool, + }, +} + +/// Represents any possible change in order to turn one tree into another, with fully-owned data. +#[derive(Debug, Clone)] +pub enum Change { + /// An entry was added, like the addition of a file or directory. + Addition { + /// The location of the file or directory. + /// + /// It may be empty if [file names](super::Options::location) is `None`. + location: BString, + /// Identifies a relationship between this instance and another one, + /// making it easy to reconstruct the top-level of directory changes. + relation: Option, + /// The mode of the added entry. + entry_mode: gix_object::tree::EntryMode, + /// The object id of the added entry. + id: gix_hash::ObjectId, + }, + /// An entry was deleted, like the deletion of a file or directory. + Deletion { + /// The location of the file or directory. + /// + /// It may be empty if [file names](super::Options::location) is `None`. + location: BString, + /// Identifies a relationship between this instance and another one, + /// making it easy to reconstruct the top-level of directory changes. + relation: Option, + /// The mode of the deleted entry. + entry_mode: gix_object::tree::EntryMode, + /// The object id of the deleted entry. + id: gix_hash::ObjectId, + }, + /// An entry was modified, e.g. changing the contents of a file adjusts its object id and turning + /// a file into a symbolic link adjusts its mode. + Modification { + /// The location of the file or directory. + /// + /// It may be empty if [file names](super::Options::location) is `None`. + location: BString, + /// The mode of the entry before the modification. + previous_entry_mode: gix_object::tree::EntryMode, + /// The object id of the entry before the modification. + previous_id: gix_hash::ObjectId, + + /// The mode of the entry after the modification. + entry_mode: gix_object::tree::EntryMode, + /// The object id after the modification. + id: gix_hash::ObjectId, + }, + /// Entries are considered rewritten if they are not trees and they, according to some understanding of identity, were renamed + /// or copied. + /// In case of renames, this means they originally appeared as [`Deletion`](ChangeRef::Deletion) signalling their source as well as an + /// [`Addition`](ChangeRef::Addition) acting as destination. + /// + /// In case of copies, the `copy` flag is true and typically represents a perfect copy of a source was made. + /// + /// This variant can only be encountered if [rewrite tracking](super::Options::rewrites) is enabled. + /// + /// Note that mode changes may have occurred as well, i.e. changes from executable to non-executable or vice-versa. + Rewrite { + /// The location of the source of the rename operation. + /// + /// It may be empty if [file names](super::Options::location) is `None`. + source_location: BString, + /// The mode of the entry before the rename. + source_entry_mode: gix_object::tree::EntryMode, + /// Identifies a relationship between the source and another source, + /// making it easy to reconstruct the top-level of directory changes. + source_relation: Option, + /// The object id of the entry before the rename. + /// + /// Note that this is the same as `id` if we require the [similarity to be 100%](super::Rewrites::percentage), but may + /// be different otherwise. + source_id: gix_hash::ObjectId, + /// Information about the diff we performed to detect similarity and match the `source_id` with the current state at `id`. + /// It's `None` if `source_id` is equal to `id`, as identity made an actual diff computation unnecessary. + diff: Option, + /// The mode of the entry after the rename. + /// It could differ but still be considered a rename as we are concerned only about content. + entry_mode: gix_object::tree::EntryMode, + /// The object id after the rename. + id: gix_hash::ObjectId, + /// The location after the rename or copy operation. + /// + /// It may be empty if [file names](super::Options::location) is `None`. + location: BString, + /// Identifies a relationship between this destination and another destination, + /// making it easy to reconstruct the top-level of directory changes. + relation: Option, + /// If true, this rewrite is created by copy, and `source_id` is pointing to its source. Otherwise, it's a rename, and `source_id` + /// points to a deleted object, as renames are tracked as deletions and additions of the same or similar content. + copy: bool, + }, +} + +/// Lifecycle +impl ChangeRef<'_> { + /// Copy this instance into a fully-owned version + pub fn into_owned(self) -> Change { + match self { + ChangeRef::Addition { + location, + entry_mode, + id, + relation, + } => Change::Addition { + location: location.to_owned(), + entry_mode, + id, + relation, + }, + ChangeRef::Deletion { + location, + entry_mode, + id, + relation, + } => Change::Deletion { + location: location.to_owned(), + entry_mode, + id, + relation, + }, + ChangeRef::Modification { + location, + previous_entry_mode, + previous_id, + entry_mode, + id, + } => Change::Modification { + location: location.to_owned(), + previous_entry_mode, + previous_id, + entry_mode, + id, + }, + ChangeRef::Rewrite { + source_location, + source_relation, + source_entry_mode, + source_id, + diff, + entry_mode, + id, + location, + relation, + copy, + } => Change::Rewrite { + source_location: source_location.to_owned(), + source_relation, + source_entry_mode, + source_id, + diff, + entry_mode, + id, + location: location.to_owned(), + relation, + copy, + }, + } + } +} + +/// Lifecycle +impl Change { + /// Return an attached version of this instance that uses `old_repo` for previous values and `new_repo` for current values. + pub fn to_ref(&self) -> ChangeRef<'_> { + match self { + Change::Addition { + location, + relation, + entry_mode, + id, + } => ChangeRef::Addition { + location: location.as_bstr(), + entry_mode: *entry_mode, + id: *id, + relation: *relation, + }, + Change::Deletion { + location, + relation, + entry_mode, + id, + } => ChangeRef::Deletion { + location: location.as_bstr(), + entry_mode: *entry_mode, + id: *id, + relation: *relation, + }, + Change::Modification { + location, + previous_entry_mode, + previous_id, + entry_mode, + id, + } => ChangeRef::Modification { + location: location.as_bstr(), + previous_entry_mode: *previous_entry_mode, + previous_id: *previous_id, + entry_mode: *entry_mode, + id: *id, + }, + Change::Rewrite { + source_location, + source_relation, + source_entry_mode, + source_id, + diff, + entry_mode, + id, + location, + relation, + copy, + } => ChangeRef::Rewrite { + source_location: source_location.as_ref(), + source_relation: *source_relation, + source_entry_mode: *source_entry_mode, + source_id: *source_id, + diff: *diff, + entry_mode: *entry_mode, + id: *id, + location: location.as_bstr(), + relation: *relation, + copy: *copy, + }, + } + } +} + +impl crate::blob::Platform { + /// Set ourselves up to produces blob-diffs from `change`, so this platform can be used to produce diffs easily. + /// `objects` are used to fetch object data as needed. + /// + /// ### Warning about Memory Consumption + /// + /// This instance only grows, so one should call [`crate::blob::Platform::clear_resource_cache`] occasionally. + pub fn set_resource_by_change( + &mut self, + change: ChangeRef<'_>, + objects: &impl gix_object::FindObjectOrHeader, + ) -> Result<&mut Self, crate::blob::platform::set_resource::Error> { + match change { + ChangeRef::Addition { + location, + relation: _, + entry_mode, + id, + } => { + self.set_resource( + id.kind().null(), + entry_mode.kind(), + location, + ResourceKind::OldOrSource, + objects, + )?; + self.set_resource(id, entry_mode.kind(), location, ResourceKind::NewOrDestination, objects)?; + } + ChangeRef::Deletion { + location, + relation: _, + entry_mode, + id, + } => { + self.set_resource(id, entry_mode.kind(), location, ResourceKind::OldOrSource, objects)?; + self.set_resource( + id.kind().null(), + entry_mode.kind(), + location, + ResourceKind::NewOrDestination, + objects, + )?; + } + ChangeRef::Modification { + location, + previous_entry_mode, + previous_id, + entry_mode, + id, + } => { + self.set_resource( + previous_id, + previous_entry_mode.kind(), + location, + ResourceKind::OldOrSource, + objects, + )?; + self.set_resource(id, entry_mode.kind(), location, ResourceKind::NewOrDestination, objects)?; + } + ChangeRef::Rewrite { + source_location, + source_relation: _, + source_entry_mode, + source_id, + entry_mode, + id, + location, + relation: _, + diff: _, + copy: _, + } => { + self.set_resource( + source_id, + source_entry_mode.kind(), + source_location, + ResourceKind::OldOrSource, + objects, + )?; + self.set_resource(id, entry_mode.kind(), location, ResourceKind::NewOrDestination, objects)?; + } + } + Ok(self) + } +} + +impl<'a> ChangeRef<'a> { + /// Return the relation this instance may have to other changes. + pub fn relation(&self) -> Option { + match self { + ChangeRef::Addition { relation, .. } + | ChangeRef::Deletion { relation, .. } + | ChangeRef::Rewrite { relation, .. } => *relation, + ChangeRef::Modification { .. } => None, + } + } + + /// Return the current mode of this instance. + pub fn entry_mode(&self) -> gix_object::tree::EntryMode { + match self { + ChangeRef::Addition { entry_mode, .. } + | ChangeRef::Deletion { entry_mode, .. } + | ChangeRef::Modification { entry_mode, .. } + | ChangeRef::Rewrite { entry_mode, .. } => *entry_mode, + } + } + + /// 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 { + match self { + ChangeRef::Addition { location, .. } + | ChangeRef::Deletion { location, .. } + | ChangeRef::Modification { location, .. } + | ChangeRef::Rewrite { location, .. } => location, + } + } +} + +impl Change { + /// Return the relation this instance may have to other changes. + pub fn relation(&self) -> Option { + match self { + Change::Addition { relation, .. } + | Change::Deletion { relation, .. } + | Change::Rewrite { relation, .. } => *relation, + Change::Modification { .. } => None, + } + } + + /// Return the current mode of this instance. + pub fn entry_mode(&self) -> gix_object::tree::EntryMode { + match self { + Change::Addition { entry_mode, .. } + | Change::Deletion { entry_mode, .. } + | Change::Modification { entry_mode, .. } + | Change::Rewrite { entry_mode, .. } => *entry_mode, + } + } + + /// 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 { + match self { + Change::Addition { location, .. } + | Change::Deletion { location, .. } + | Change::Modification { location, .. } + | Change::Rewrite { location, .. } => location.as_bstr(), + } + } +} diff --git a/gix-diff/src/tree_with_rewrites/function.rs b/gix-diff/src/tree_with_rewrites/function.rs new file mode 100644 index 00000000000..b37856a3379 --- /dev/null +++ b/gix-diff/src/tree_with_rewrites/function.rs @@ -0,0 +1,277 @@ +use bstr::BStr; +use gix_object::TreeRefIter; + +use super::{Action, ChangeRef, Error, Options}; +use crate::rewrites; +use crate::rewrites::tracker; + +/// Call `for_each` repeatedly with all changes that are needed to convert `lhs` to `rhs`. +/// Provide a `resource_cache` to speed up obtaining blobs for similarity checks. +/// `tree_diff_state` can be used to re-use tree-diff memory between calls. +/// `objects` are used to lookup trees while performing the diff. +/// Use `options` to further configure how the rename tracking is performed. +/// +/// Reusing `resource_cache` between multiple invocations saves a lot of IOps as it avoids the creation +/// of a temporary `resource_cache` that triggers reading or checking for multiple gitattribute files. +/// Note that it's recommended to call [`clear_resource_cache()`](`crate::blob::Platform::clear_resource_cache()`) +/// between the calls to avoid runaway memory usage, as the cache isn't limited. +/// +/// Note that to do rename tracking like `git` does, one has to configure the `resource_cache` with +/// a conversion pipeline that uses [`crate::blob::pipeline::Mode::ToGit`]. +/// +/// `rhs` or `lhs` can be empty to indicate deletion or addition of an entire tree. +/// +/// Note that the rewrite outcome is only available if [rewrite-tracking was enabled](Options::rewrites). +pub fn diff( + lhs: TreeRefIter<'_>, + rhs: TreeRefIter<'_>, + resource_cache: &mut crate::blob::Platform, + tree_diff_state: &mut crate::tree::State, + objects: &impl gix_object::FindObjectOrHeader, + for_each: impl FnMut(ChangeRef<'_>) -> Result, + options: Options, +) -> Result, Error> +where + E: Into>, +{ + let mut delegate = Delegate { + src_tree: lhs, + recorder: crate::tree::Recorder::default().track_location(options.location), + visit: for_each, + location: options.location, + objects, + tracked: options.rewrites.map(rewrites::Tracker::new), + err: None, + }; + match crate::tree(lhs, rhs, tree_diff_state, objects, &mut delegate) { + Ok(()) => { + let outcome = delegate.process_tracked_changes(resource_cache)?; + match delegate.err { + Some(err) => Err(Error::ForEach(err.into())), + None => Ok(outcome), + } + } + Err(crate::tree::Error::Cancelled) => delegate + .err + .map_or(Err(Error::Diff(crate::tree::Error::Cancelled)), |err| { + Err(Error::ForEach(err.into())) + }), + Err(err) => Err(err.into()), + } +} + +struct Delegate<'a, 'old, VisitFn, E, Objects> { + src_tree: TreeRefIter<'old>, + recorder: crate::tree::Recorder, + objects: &'a Objects, + visit: VisitFn, + tracked: Option>, + location: Option, + err: Option, +} + +impl Delegate<'_, '_, VisitFn, E, Objects> +where + Objects: gix_object::FindObjectOrHeader, + VisitFn: for<'delegate> FnMut(ChangeRef<'_>) -> Result, + E: Into>, +{ + /// Call `visit` on an attached version of `change`. + fn emit_change( + change: crate::tree::visit::Change, + location: &BStr, + visit: &mut VisitFn, + stored_err: &mut Option, + ) -> crate::tree::visit::Action { + use crate::tree::visit::Change::*; + let change = match change { + Addition { + entry_mode, + oid, + relation, + } => ChangeRef::Addition { + location, + relation, + entry_mode, + id: oid, + }, + Deletion { + entry_mode, + oid, + relation, + } => ChangeRef::Deletion { + entry_mode, + location, + relation, + id: oid, + }, + Modification { + previous_entry_mode, + previous_oid, + entry_mode, + oid, + } => ChangeRef::Modification { + location, + previous_entry_mode, + entry_mode, + previous_id: previous_oid, + id: oid, + }, + }; + match visit(change) { + Ok(Action::Cancel) => crate::tree::visit::Action::Cancel, + Ok(Action::Continue) => crate::tree::visit::Action::Continue, + Err(err) => { + *stored_err = Some(err); + crate::tree::visit::Action::Cancel + } + } + } + + fn process_tracked_changes( + &mut self, + diff_cache: &mut crate::blob::Platform, + ) -> Result, Error> { + use crate::rewrites::tracker::Change as _; + let tracked = match self.tracked.as_mut() { + Some(t) => t, + None => return Ok(None), + }; + + let outcome = tracked.emit( + |dest, source| match source { + Some(source) => { + let (oid, mode) = dest.change.oid_and_entry_mode(); + let change = ChangeRef::Rewrite { + source_location: source.location, + source_entry_mode: source.entry_mode, + source_id: source.id, + source_relation: source.change.relation(), + entry_mode: mode, + id: oid.to_owned(), + relation: dest.change.relation(), + diff: source.diff, + location: dest.location, + copy: match source.kind { + tracker::visit::SourceKind::Rename => false, + tracker::visit::SourceKind::Copy => true, + }, + }; + match (self.visit)(change) { + Ok(Action::Cancel) => crate::tree::visit::Action::Cancel, + Ok(Action::Continue) => crate::tree::visit::Action::Continue, + Err(err) => { + self.err = Some(err); + crate::tree::visit::Action::Cancel + } + } + } + None => Self::emit_change(dest.change, dest.location, &mut self.visit, &mut self.err), + }, + diff_cache, + self.objects, + |push| { + let mut delegate = tree_to_changes::Delegate::new(push, self.location); + let state = gix_traverse::tree::breadthfirst::State::default(); + gix_traverse::tree::breadthfirst(self.src_tree, state, self.objects, &mut delegate) + }, + )?; + Ok(Some(outcome)) + } +} + +impl crate::tree::Visit for Delegate<'_, '_, VisitFn, E, Objects> +where + Objects: gix_object::FindObjectOrHeader, + VisitFn: for<'delegate> FnMut(ChangeRef<'_>) -> Result, + E: Into>, +{ + fn pop_front_tracked_path_and_set_current(&mut self) { + self.recorder.pop_front_tracked_path_and_set_current(); + } + + fn push_back_tracked_path_component(&mut self, component: &BStr) { + self.recorder.push_back_tracked_path_component(component); + } + + fn push_path_component(&mut self, component: &BStr) { + self.recorder.push_path_component(component); + } + + fn pop_path_component(&mut self) { + self.recorder.pop_path_component(); + } + + fn visit(&mut self, change: crate::tree::visit::Change) -> crate::tree::visit::Action { + match self.tracked.as_mut() { + Some(tracked) => tracked + .try_push_change(change, self.recorder.path()) + .map_or(crate::tree::visit::Action::Continue, |change| { + Self::emit_change(change, self.recorder.path(), &mut self.visit, &mut self.err) + }), + None => Self::emit_change(change, self.recorder.path(), &mut self.visit, &mut self.err), + } + } +} + +mod tree_to_changes { + use crate::tree::visit::Change; + use gix_object::tree::EntryRef; + + use bstr::BStr; + + pub struct Delegate<'a> { + push: &'a mut dyn FnMut(Change, &BStr), + recorder: gix_traverse::tree::Recorder, + } + + impl<'a> Delegate<'a> { + pub fn new(push: &'a mut dyn FnMut(Change, &BStr), location: Option) -> Self { + let location = location.map(|t| match t { + crate::tree::recorder::Location::FileName => gix_traverse::tree::recorder::Location::FileName, + crate::tree::recorder::Location::Path => gix_traverse::tree::recorder::Location::Path, + }); + Self { + push, + recorder: gix_traverse::tree::Recorder::default().track_location(location), + } + } + } + + impl gix_traverse::tree::Visit for Delegate<'_> { + fn pop_front_tracked_path_and_set_current(&mut self) { + self.recorder.pop_front_tracked_path_and_set_current(); + } + + fn push_back_tracked_path_component(&mut self, component: &BStr) { + self.recorder.push_back_tracked_path_component(component); + } + + fn push_path_component(&mut self, component: &BStr) { + self.recorder.push_path_component(component); + } + + fn pop_path_component(&mut self) { + self.recorder.pop_path_component(); + } + + fn visit_tree(&mut self, _entry: &EntryRef<'_>) -> gix_traverse::tree::visit::Action { + gix_traverse::tree::visit::Action::Continue + } + + fn visit_nontree(&mut self, entry: &EntryRef<'_>) -> gix_traverse::tree::visit::Action { + if entry.mode.is_blob() { + (self.push)( + Change::Modification { + previous_entry_mode: entry.mode, + previous_oid: gix_hash::ObjectId::null(entry.oid.kind()), + entry_mode: entry.mode, + oid: entry.oid.to_owned(), + }, + self.recorder.path(), + ); + } + gix_traverse::tree::visit::Action::Continue + } + } +} diff --git a/gix-diff/src/tree_with_rewrites/mod.rs b/gix-diff/src/tree_with_rewrites/mod.rs new file mode 100644 index 00000000000..855eb082e39 --- /dev/null +++ b/gix-diff/src/tree_with_rewrites/mod.rs @@ -0,0 +1,39 @@ +use crate::tree::recorder::Location; +use crate::Rewrites; + +mod change; +pub use change::{Change, ChangeRef}; + +/// The error returned by [`tree_with_rewrites()`](super::tree_with_rewrites()). +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + Diff(#[from] crate::tree::Error), + #[error("The user-provided callback failed")] + ForEach(#[source] Box), + #[error("Failure during rename tracking")] + RenameTracking(#[from] crate::rewrites::tracker::emit::Error), +} + +/// Returned by the [`tree_with_rewrites()`](super::tree_with_rewrites()) function to control flow. +#[derive(Default, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)] +pub enum Action { + /// Continue the traversal of changes. + #[default] + Continue, + /// Stop the traversal of changes and stop calling the function that returned it. + Cancel, +} + +/// Options for use in [`tree_with_rewrites()`](super::tree_with_rewrites()). +#[derive(Default, Clone, Debug)] +pub struct Options { + /// Determine how locations of changes, i.e. their repository-relative path, should be tracked. + /// If `None`, locations will always be empty. + pub location: Option, + /// If not `None`, rename tracking will be performed accordingly. + pub rewrites: Option, +} + +pub(super) mod function; diff --git a/gix-diff/tests/Cargo.toml b/gix-diff/tests/Cargo.toml index 76f6bae506f..eb977e24eee 100644 --- a/gix-diff/tests/Cargo.toml +++ b/gix-diff/tests/Cargo.toml @@ -17,6 +17,7 @@ name = "diff" path = "diff/main.rs" [dev-dependencies] +insta = "1.40.0" gix-diff = { path = ".." } gix-hash = { path = "../../gix-hash" } gix-fs = { path = "../../gix-fs" } diff --git a/gix-diff/tests/diff/main.rs b/gix-diff/tests/diff/main.rs index 6a4201e2051..2163b5d3b01 100644 --- a/gix-diff/tests/diff/main.rs +++ b/gix-diff/tests/diff/main.rs @@ -7,6 +7,7 @@ fn hex_to_id(hex: &str) -> gix_hash::ObjectId { mod blob; mod rewrites; mod tree; +mod tree_with_rewrites; mod util { use std::collections::HashMap; diff --git a/gix-diff/tests/diff/tree_with_rewrites.rs b/gix-diff/tests/diff/tree_with_rewrites.rs new file mode 100644 index 00000000000..f0c137b6abe --- /dev/null +++ b/gix-diff/tests/diff/tree_with_rewrites.rs @@ -0,0 +1,1989 @@ +use gix_diff::rewrites::{Copies, CopySource}; +use gix_diff::tree::recorder::Location; +use gix_diff::tree::visit::Relation; +use gix_diff::tree_with_rewrites::{Change, Options}; +use gix_diff::Rewrites; +use gix_object::bstr::BStr; +use gix_object::TreeRefIter; + +#[test] +fn empty_to_new_tree_without_rename_tracking() -> crate::Result { + let (changes, _out) = collect_changes(None, "c1 - initial").expect("full path tracking is the default"); + insta::assert_debug_snapshot!(changes, @r#" + [ + Addition { + location: "a", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "b", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "d", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "dir", + relation: Some( + Parent( + 1, + ), + ), + entry_mode: EntryMode( + 16384, + ), + id: Sha1(587ff082e0b98914788500eae5dd6a33f04883c9), + }, + Addition { + location: "dir/c", + relation: Some( + ChildOfParent( + 1, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + ] + "#); + + let (changes, _out) = collect_changes_opts( + None, + "c1 - initial", + Options { + location: Some(Location::FileName), + ..Default::default() + }, + ) + .expect("the path-options are respected - we only see the filename here"); + insta::assert_debug_snapshot!(changes, @r#" + [ + Addition { + location: "a", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "b", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "d", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "dir", + relation: Some( + Parent( + 1, + ), + ), + entry_mode: EntryMode( + 16384, + ), + id: Sha1(587ff082e0b98914788500eae5dd6a33f04883c9), + }, + Addition { + location: "c", + relation: Some( + ChildOfParent( + 1, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + ] + "#); + + { + let (lhs, rhs, mut cache, odb) = repo_with_trees(None, "c1 - initial")?; + let err = gix_diff::tree_with_rewrites( + TreeRefIter::from_bytes(&lhs), + TreeRefIter::from_bytes(&rhs), + &mut cache, + &mut Default::default(), + &odb, + |_change| Err(std::io::Error::new(std::io::ErrorKind::Other, "custom error")), + Options::default(), + ) + .unwrap_err(); + assert_eq!( + err.to_string(), + "The user-provided callback failed", + "custom errors made visible and not squelched" + ); + } + Ok(()) +} + +#[test] +fn changes_against_modified_tree_with_filename_tracking() -> crate::Result { + let (changes, _out) = collect_changes("c2", "c3-modification")?; + + insta::assert_debug_snapshot!(changes, @r#" + [ + Modification { + location: "a", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(78981922613b2afb6025042ff6bd878ac1994e85), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(b4f17b61de71d9b2e54ac9e62b1629ae2d97a6a7), + }, + Modification { + location: "dir", + previous_entry_mode: EntryMode( + 16384, + ), + previous_id: Sha1(e5c63aefe4327cb1c780c71966b678ce8e4225da), + entry_mode: EntryMode( + 16384, + ), + id: Sha1(c7ac5f82f536976f3561c9999b5f11e5893358be), + }, + Modification { + location: "dir/c", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(6695780ceb14b05e076a99bbd2babf34723b3464), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), + }, + ] + "#); + let (changes, _out) = collect_changes_opts( + "c2", + "c3-modification", + Options { + location: Some(Location::FileName), + ..Default::default() + }, + )?; + insta::assert_debug_snapshot!(changes, @r#" + [ + Modification { + location: "a", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(78981922613b2afb6025042ff6bd878ac1994e85), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(b4f17b61de71d9b2e54ac9e62b1629ae2d97a6a7), + }, + Modification { + location: "dir", + previous_entry_mode: EntryMode( + 16384, + ), + previous_id: Sha1(e5c63aefe4327cb1c780c71966b678ce8e4225da), + entry_mode: EntryMode( + 16384, + ), + id: Sha1(c7ac5f82f536976f3561c9999b5f11e5893358be), + }, + Modification { + location: "c", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(6695780ceb14b05e076a99bbd2babf34723b3464), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), + }, + ] + "#); + Ok(()) +} + +#[test] +fn renames_by_identity() -> crate::Result { + for (from, to, expected, assert_msg) in [ + ( + "c3-modification", + "r1-identity", + vec![BStr::new("a"), "dir/a-moved".into()], + "one rename and nothing else", + ), + ( + "c4 - add identical files", + "r2-ambiguous", + vec![ + "s1".into(), + "b1".into(), + "s2".into(), + "b2".into(), + "s3".into(), + "z".into(), + ], + "multiple possible sources decide by ordering everything lexicographically", + ), + ( + "c5 - add links", + "r4-symlinks", + vec!["link-1".into(), "renamed-link-1".into()], + "symlinks are only tracked by identity", + ), + ( + "r1-identity", + "c4 - add identical files", + vec![], + "not having any renames is OK as well", + ), + ( + "tc1-identity", + "tc1-identity", + vec![], + "copy tracking is off by default", + ), + ] { + for percentage in [None, Some(0.5)] { + let (changes, out) = collect_changes_opts( + from, + to, + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + percentage, + ..Default::default() + }), + }, + )?; + let actual: Vec<_> = changes + .into_iter() + .filter(|c| !c.entry_mode().is_tree()) + .flat_map(|c| match c { + Change::Rewrite { + source_location, + location, + copy, + .. + } => { + assert!(!copy); + vec![source_location, location] + } + _ => vec![], + }) + .collect(); + + assert_eq!(actual, expected, "{assert_msg}"); + #[cfg(not(windows))] + assert_eq!( + out.expect("present as rewrites are configured").num_similarity_checks, + 0, + "there are no fuzzy checks in if everything was resolved by identity only" + ); + } + } + Ok(()) +} + +#[test] +fn rename_by_similarity() -> crate::Result { + insta::allow_duplicates! { + for percentage in [ + None, + Some(0.76), /*cutoff point where git stops seeing it as equal */ + ] { + let (changes, out) = collect_changes_opts( + "r2-ambiguous", + "r3-simple", + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + percentage, + ..Default::default() + }), + }, + ).expect("errors can only happen with IO or ODB access fails"); + insta::assert_debug_snapshot!( + changes, + @r#" + [ + Modification { + location: "b", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(61780798228d17af2d34fce4cfbdf35556832472), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(54781fa52cf133fa9d0bf59cfe2ef2621b5ad29f), + }, + Modification { + location: "dir", + previous_entry_mode: EntryMode( + 16384, + ), + previous_id: Sha1(d1622e275dbb2cb3215a0bdcd2fc77273891f360), + entry_mode: EntryMode( + 16384, + ), + id: Sha1(6602e61ea053525e4907e155c0b3da3a269e1385), + }, + Deletion { + location: "dir/c", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), + }, + Addition { + location: "dir/c-moved", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(f01e8ddf5adc56985b9a1cda6d7c7ef9e3abe034), + }, + ] + "# + ); + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, if percentage.is_some() { 1 } else { 0 }); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + } + } + + let (changes, out) = collect_changes_opts( + "r2-ambiguous", + "r3-simple", + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + percentage: Some(0.6), + limit: 1, // has no effect as it's just one item here. + ..Default::default() + }), + }, + ) + .expect("it found all items at the cut-off point, similar to git"); + + insta::assert_debug_snapshot!(changes, @r#" + [ + Modification { + location: "b", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(61780798228d17af2d34fce4cfbdf35556832472), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(54781fa52cf133fa9d0bf59cfe2ef2621b5ad29f), + }, + Modification { + location: "dir", + previous_entry_mode: EntryMode( + 16384, + ), + previous_id: Sha1(d1622e275dbb2cb3215a0bdcd2fc77273891f360), + entry_mode: EntryMode( + 16384, + ), + id: Sha1(6602e61ea053525e4907e155c0b3da3a269e1385), + }, + Rewrite { + source_location: "dir/c", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), + diff: Some( + DiffLineStats { + removals: 0, + insertions: 1, + before: 2, + after: 3, + similarity: 0.65, + }, + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(f01e8ddf5adc56985b9a1cda6d7c7ef9e3abe034), + location: "dir/c-moved", + relation: None, + copy: false, + }, + ] + "#); + + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 1); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + Ok(()) +} + +#[test] +fn renames_by_similarity_with_limit() -> crate::Result { + let (changes, out) = collect_changes_opts( + "c6", + "r5", + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + limit: 1, // prevent fuzzy tracking from happening + ..Default::default() + }), + }, + )?; + assert_eq!( + changes.iter().filter(|c| matches!(c, Change::Rewrite { .. })).count(), + 0, + "fuzzy tracking is effectively disabled due to limit" + ); + let actual: Vec<_> = changes.iter().map(Change::location).collect(); + assert_eq!(actual, ["f1", "f1-renamed", "f2", "f2-renamed"],); + + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 0); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 4); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn copies_by_identity() -> crate::Result { + let (changes, out) = collect_changes_opts( + "c7", + "tc1-identity", + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + copies: Some(Copies { + source: CopySource::FromSetOfModifiedFiles, + percentage: None, + }), + limit: 1, // the limit isn't actually used for identity based checks + ..Default::default() + }), + }, + )?; + insta::assert_debug_snapshot!(changes, @r#" + [ + Modification { + location: "dir", + previous_entry_mode: EntryMode( + 16384, + ), + previous_id: Sha1(6602e61ea053525e4907e155c0b3da3a269e1385), + entry_mode: EntryMode( + 16384, + ), + id: Sha1(f01fd5b4d733a4ae749cbb58a828cdb3f342f298), + }, + Rewrite { + source_location: "base", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(f00c965d8307308469e537302baa73048488f162), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(f00c965d8307308469e537302baa73048488f162), + location: "c1", + relation: None, + copy: true, + }, + Rewrite { + source_location: "base", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(f00c965d8307308469e537302baa73048488f162), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(f00c965d8307308469e537302baa73048488f162), + location: "c2", + relation: None, + copy: true, + }, + Rewrite { + source_location: "base", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(f00c965d8307308469e537302baa73048488f162), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(f00c965d8307308469e537302baa73048488f162), + location: "dir/c3", + relation: None, + copy: true, + }, + ] + "#); + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 0); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn copies_by_similarity() -> crate::Result { + let (changes, out) = collect_changes_opts( + "tc1-identity", + "tc2-similarity", + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + copies: Some(Copies::default()), + ..Default::default() + }), + }, + )?; + insta::assert_debug_snapshot!(changes, @r#" + [ + Modification { + location: "dir", + previous_entry_mode: EntryMode( + 16384, + ), + previous_id: Sha1(f01fd5b4d733a4ae749cbb58a828cdb3f342f298), + entry_mode: EntryMode( + 16384, + ), + id: Sha1(1d7e20e07562a54af0408fd2669b0c56a6faa6f0), + }, + Rewrite { + source_location: "base", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + location: "c4", + relation: None, + copy: true, + }, + Rewrite { + source_location: "base", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + diff: Some( + DiffLineStats { + removals: 0, + insertions: 1, + before: 11, + after: 12, + similarity: 0.8888889, + }, + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(08fe19ca4d2f79624f35333157d610811efc1aed), + location: "c5", + relation: None, + copy: true, + }, + Rewrite { + source_location: "base", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + diff: Some( + DiffLineStats { + removals: 0, + insertions: 1, + before: 11, + after: 12, + similarity: 0.8888889, + }, + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), + location: "dir/c6", + relation: None, + copy: true, + }, + ] + "#); + + let out = out.expect("tracking enabled"); + assert_eq!( + out.num_similarity_checks, 2, + "two are similar, the other one is identical" + ); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn copies_in_entire_tree_by_similarity() -> crate::Result { + let (changes, out) = collect_changes_opts( + "tc2-similarity", + "tc3-find-harder", + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + copies: Some(Copies::default()), + ..Default::default() + }), + }, + )?; + assert_eq!( + changes.iter().filter(|c| matches!(c, Change::Rewrite { .. })).count(), + 0, + "needs --find-copies-harder to detect rewrites here" + ); + let actual: Vec<_> = changes.iter().map(Change::location).collect(); + assert_eq!(actual, ["b", "c6", "c7", "newly-added"]); + + let out = out.expect("tracking enabled"); + assert_eq!( + out.num_similarity_checks, 3, + "it does have some candidates, probably for rename tracking" + ); + assert_eq!( + out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, + "no limit configured" + ); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + let (changes, out) = collect_changes_opts( + "tc2-similarity", + "tc3-find-harder", + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + copies: Some(Copies { + source: CopySource::FromSetOfModifiedFilesAndAllSources, + ..Default::default() + }), + ..Default::default() + }), + }, + )?; + insta::assert_debug_snapshot!(changes, @r#" + [ + Rewrite { + source_location: "base", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + location: "c6", + relation: None, + copy: true, + }, + Rewrite { + source_location: "dir/c6", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), + location: "c7", + relation: None, + copy: true, + }, + Rewrite { + source_location: "c5", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(08fe19ca4d2f79624f35333157d610811efc1aed), + diff: Some( + DiffLineStats { + removals: 0, + insertions: 3, + before: 12, + after: 15, + similarity: 0.75, + }, + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(97b3d1a5707f8a11fa5fa8bc6c3bd7b3965601fd), + location: "newly-added", + relation: None, + copy: true, + }, + Modification { + location: "b", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(54781fa52cf133fa9d0bf59cfe2ef2621b5ad29f), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(f198d0640214092732566fb00543163845c8252c), + }, + ] + "#); + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 4); + assert_eq!( + out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, + "no limit configured" + ); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn copies_in_entire_tree_by_similarity_with_limit() -> crate::Result { + let (changes, out) = collect_changes_opts( + "tc2-similarity", + "tc3-find-harder", + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + copies: Some(Copies { + source: CopySource::FromSetOfModifiedFilesAndAllSources, + ..Default::default() + }), + limit: 2, // similarity checks can't be made that way + ..Default::default() + }), + }, + )?; + insta::assert_debug_snapshot!(changes, @r#" + [ + Rewrite { + source_location: "base", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + location: "c6", + relation: None, + copy: true, + }, + Rewrite { + source_location: "dir/c6", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), + location: "c7", + relation: None, + copy: true, + }, + Modification { + location: "b", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(54781fa52cf133fa9d0bf59cfe2ef2621b5ad29f), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(f198d0640214092732566fb00543163845c8252c), + }, + Addition { + location: "newly-added", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(97b3d1a5707f8a11fa5fa8bc6c3bd7b3965601fd), + }, + ] + "#); + + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 0, "similarity checks can't run"); + assert_eq!( + out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, + "no limit configured" + ); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 19); + + Ok(()) +} + +#[test] +fn copies_by_similarity_with_limit() -> crate::Result { + let (changes, out) = collect_changes_opts( + "tc1-identity", + "tc2-similarity", + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + copies: Some(Copies::default()), + limit: 1, + ..Default::default() + }), + }, + )?; + + insta::assert_debug_snapshot!(changes, @r#" + [ + Modification { + location: "dir", + previous_entry_mode: EntryMode( + 16384, + ), + previous_id: Sha1(f01fd5b4d733a4ae749cbb58a828cdb3f342f298), + entry_mode: EntryMode( + 16384, + ), + id: Sha1(1d7e20e07562a54af0408fd2669b0c56a6faa6f0), + }, + Rewrite { + source_location: "base", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + location: "c4", + relation: None, + copy: true, + }, + Addition { + location: "c5", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(08fe19ca4d2f79624f35333157d610811efc1aed), + }, + Addition { + location: "dir/c6", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), + }, + ] + "#); + + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 0); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!( + out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 2, + "limit prevents any similarity check from being performed, and identity fails in most places" + ); + + Ok(()) +} + +#[test] +fn realistic_renames_by_identity() -> crate::Result { + let (changes, out) = collect_changes_opts( + "r1-base", + "r1-change", + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + copies: Some(Copies::default()), + limit: 1, + ..Default::default() + }), + }, + )?; + + insta::assert_debug_snapshot!(changes.into_iter().filter(|c| !c.entry_mode().is_tree()).collect::>(), @r#" + [ + Rewrite { + source_location: "git-index/src/file.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "git-index/src/file/mod.rs", + relation: None, + copy: false, + }, + Addition { + location: "git-index/tests/index/file/access.rs", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Modification { + location: "git-index/tests/index/file/mod.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(8ba3a16384aacc37d01564b28401755ce8053f51), + }, + ] + "#); + + #[cfg(not(windows))] + { + let actual = std::fs::read_to_string(repo_workdir()?.join("baseline.with-renames"))?; + let expected = r#"commit 0231f5093bd3d760e7ee82984e0453da80e05c87 +Author: author +Date: Sat Jan 1 00:00:00 2000 +0000 + + r1-change + +diff --git a/git-index/src/file.rs b/git-index/src/file/mod.rs +similarity index 100% +rename from git-index/src/file.rs +rename to git-index/src/file/mod.rs +diff --git a/git-index/tests/index/file/access.rs b/git-index/tests/index/file/access.rs +new file mode 100644 +index 0000000..e69de29 +diff --git a/git-index/tests/index/file/mod.rs b/git-index/tests/index/file/mod.rs +index e69de29..8ba3a16 100644 +--- a/git-index/tests/index/file/mod.rs ++++ b/git-index/tests/index/file/mod.rs +@@ -0,0 +1 @@ ++n +"#; + assert_eq!(actual, expected); + } + + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 1); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn realistic_renames_disabled() -> crate::Result { + let (changes, out) = collect_changes_opts( + "r1-base", + "r1-change", + Options { + location: Some(Location::Path), + rewrites: None, + }, + )?; + + insta::assert_debug_snapshot!(changes.into_iter().filter(|c| !c.entry_mode().is_tree()).collect::>(), @r#" + [ + Deletion { + location: "git-index/src/file.rs", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "git-index/src/file/mod.rs", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "git-index/tests/index/file/access.rs", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Modification { + location: "git-index/tests/index/file/mod.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(8ba3a16384aacc37d01564b28401755ce8053f51), + }, + ] + "#); + + #[cfg(not(windows))] + { + let actual = std::fs::read_to_string(repo_workdir()?.join("baseline.no-renames"))?; + let expected = r#"commit 0231f5093bd3d760e7ee82984e0453da80e05c87 +Author: author +Date: Sat Jan 1 00:00:00 2000 +0000 + + r1-change + +diff --git a/git-index/src/file.rs b/git-index/src/file.rs +deleted file mode 100644 +index e69de29..0000000 +diff --git a/git-index/src/file/mod.rs b/git-index/src/file/mod.rs +new file mode 100644 +index 0000000..e69de29 +diff --git a/git-index/tests/index/file/access.rs b/git-index/tests/index/file/access.rs +new file mode 100644 +index 0000000..e69de29 +diff --git a/git-index/tests/index/file/mod.rs b/git-index/tests/index/file/mod.rs +index e69de29..8ba3a16 100644 +--- a/git-index/tests/index/file/mod.rs ++++ b/git-index/tests/index/file/mod.rs +@@ -0,0 +1 @@ ++n +"#; + assert_eq!(actual, expected); + } + + assert_eq!(out, None, "tracking is disabled completely"); + Ok(()) +} + +#[test] +fn realistic_renames_disabled_2() -> crate::Result { + let (changes, out) = collect_changes_opts( + "r2-base", + "r2-change", + Options { + location: Some(Location::Path), + rewrites: None, + }, + )?; + + // Directories are associated with their children, making a bundling possible. + insta::assert_debug_snapshot!(changes.into_iter() + .filter(|c| !c.entry_mode().is_tree() || + c.relation().map_or(false, |r| matches!(r, Relation::Parent(_))) + ).collect::>(), @r#" + [ + Deletion { + location: "git-sec", + relation: Some( + Parent( + 1, + ), + ), + entry_mode: EntryMode( + 16384, + ), + id: Sha1(0026010e87631065a2739f627622feb14f903fd4), + }, + Addition { + location: "gix-sec", + relation: Some( + Parent( + 2, + ), + ), + entry_mode: EntryMode( + 16384, + ), + id: Sha1(0026010e87631065a2739f627622feb14f903fd4), + }, + Deletion { + location: "git-sec/CHANGELOG.md", + relation: Some( + ChildOfParent( + 1, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/Cargo.toml", + relation: Some( + ChildOfParent( + 1, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/CHANGELOG.md", + relation: Some( + ChildOfParent( + 2, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/Cargo.toml", + relation: Some( + ChildOfParent( + 2, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/src/identity.rs", + relation: Some( + ChildOfParent( + 1, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/src/lib.rs", + relation: Some( + ChildOfParent( + 1, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/src/permission.rs", + relation: Some( + ChildOfParent( + 1, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/src/trust.rs", + relation: Some( + ChildOfParent( + 1, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/tests/sec.rs", + relation: Some( + ChildOfParent( + 1, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/src/identity.rs", + relation: Some( + ChildOfParent( + 2, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/src/lib.rs", + relation: Some( + ChildOfParent( + 2, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/src/permission.rs", + relation: Some( + ChildOfParent( + 2, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/src/trust.rs", + relation: Some( + ChildOfParent( + 2, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/tests/sec.rs", + relation: Some( + ChildOfParent( + 2, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/tests/identity/mod.rs", + relation: Some( + ChildOfParent( + 1, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/tests/identity/mod.rs", + relation: Some( + ChildOfParent( + 2, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + ] + "#); + + #[cfg(not(windows))] + { + let expected = r#"commit d78c63c5ea3149040767e4387e7fc743cda118fd +Author: author +Date: Sat Jan 1 00:00:00 2000 +0000 + + r2-change + +diff --git a/git-sec/CHANGELOG.md b/git-sec/CHANGELOG.md +deleted file mode 100644 +index e69de29..0000000 +diff --git a/git-sec/Cargo.toml b/git-sec/Cargo.toml +deleted file mode 100644 +index e69de29..0000000 +diff --git a/git-sec/src/identity.rs b/git-sec/src/identity.rs +deleted file mode 100644 +index e69de29..0000000 +diff --git a/git-sec/src/lib.rs b/git-sec/src/lib.rs +deleted file mode 100644 +index e69de29..0000000 +diff --git a/git-sec/src/permission.rs b/git-sec/src/permission.rs +deleted file mode 100644 +index e69de29..0000000 +diff --git a/git-sec/src/trust.rs b/git-sec/src/trust.rs +deleted file mode 100644 +index e69de29..0000000 +diff --git a/git-sec/tests/identity/mod.rs b/git-sec/tests/identity/mod.rs +deleted file mode 100644 +index e69de29..0000000 +diff --git a/git-sec/tests/sec.rs b/git-sec/tests/sec.rs +deleted file mode 100644 +index e69de29..0000000 +diff --git a/gix-sec/CHANGELOG.md b/gix-sec/CHANGELOG.md +new file mode 100644 +index 0000000..e69de29 +diff --git a/gix-sec/Cargo.toml b/gix-sec/Cargo.toml +new file mode 100644 +index 0000000..e69de29 +diff --git a/gix-sec/src/identity.rs b/gix-sec/src/identity.rs +new file mode 100644 +index 0000000..e69de29 +diff --git a/gix-sec/src/lib.rs b/gix-sec/src/lib.rs +new file mode 100644 +index 0000000..e69de29 +diff --git a/gix-sec/src/permission.rs b/gix-sec/src/permission.rs +new file mode 100644 +index 0000000..e69de29 +diff --git a/gix-sec/src/trust.rs b/gix-sec/src/trust.rs +new file mode 100644 +index 0000000..e69de29 +diff --git a/gix-sec/tests/identity/mod.rs b/gix-sec/tests/identity/mod.rs +new file mode 100644 +index 0000000..e69de29 +diff --git a/gix-sec/tests/sec.rs b/gix-sec/tests/sec.rs +new file mode 100644 +index 0000000..e69de29 +"#; + assert_eq!( + std::fs::read_to_string(repo_workdir()?.join("baseline-2.no-renames"))?, + expected + ); + } + + assert_eq!(out, None, "tracking is disabled completely"); + Ok(()) +} + +#[test] +fn realistic_renames_disabled_3() -> crate::Result { + let (changes, out) = collect_changes_opts( + "r3-base", + "r3-change", + Options { + location: Some(Location::Path), + rewrites: None, + }, + )?; + + insta::assert_debug_snapshot!(changes.into_iter().filter(|c| !c.entry_mode().is_tree()).collect::>(), @r#" + [ + Addition { + location: "src/ein.rs", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "src/gix.rs", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "src/plumbing-cli.rs", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "src/porcelain-cli.rs", + relation: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + ] + "#); + + #[cfg(not(windows))] + { + let expected = r#"commit 0cf7a4fe3ad6c49ae7beb394a1c1df7cc5173ce4 +Author: author +Date: Sat Jan 1 00:00:00 2000 +0000 + + r3-change + +diff --git a/src/ein.rs b/src/ein.rs +new file mode 100644 +index 0000000..e69de29 +diff --git a/src/gix.rs b/src/gix.rs +new file mode 100644 +index 0000000..e69de29 +diff --git a/src/plumbing-cli.rs b/src/plumbing-cli.rs +deleted file mode 100644 +index e69de29..0000000 +diff --git a/src/porcelain-cli.rs b/src/porcelain-cli.rs +deleted file mode 100644 +index e69de29..0000000 +"#; + + assert_eq!( + std::fs::read_to_string(repo_workdir()?.join("baseline-3.no-renames"))?, + expected + ); + } + + assert_eq!(out, None, "tracking is disabled completely"); + Ok(()) +} + +#[test] +fn realistic_renames_by_identity_3() -> crate::Result { + let (changes, out) = collect_changes_opts( + "r3-base", + "r3-change", + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + copies: Some(Copies::default()), + limit: 1, + ..Default::default() + }), + }, + )?; + + insta::assert_debug_snapshot!(changes.into_iter().filter(|c| !c.entry_mode().is_tree()).collect::>(), @r#" + [ + Rewrite { + source_location: "src/plumbing-cli.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "src/ein.rs", + relation: None, + copy: false, + }, + Rewrite { + source_location: "src/porcelain-cli.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "src/gix.rs", + relation: None, + copy: false, + }, + ] + "#); + + #[cfg(not(windows))] + { + let expected = r#"commit 0cf7a4fe3ad6c49ae7beb394a1c1df7cc5173ce4 +Author: author +Date: Sat Jan 1 00:00:00 2000 +0000 + + r3-change + +diff --git a/src/plumbing-cli.rs b/src/ein.rs +similarity index 100% +rename from src/plumbing-cli.rs +rename to src/ein.rs +diff --git a/src/porcelain-cli.rs b/src/gix.rs +similarity index 100% +rename from src/porcelain-cli.rs +rename to src/gix.rs +"#; + assert_eq!( + std::fs::read_to_string(repo_workdir()?.join("baseline-3.with-renames"))?, + expected + ); + } + + let out = out.expect("tracking enabled"); + assert_eq!( + out.num_similarity_checks, 0, + "similarity checks disabled, and not necessary" + ); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn realistic_renames_2() -> crate::Result { + let (changes, out) = collect_changes_opts( + "r2-base", + "r2-change", + Options { + location: Some(Location::Path), + rewrites: Some(Rewrites { + copies: Some(Copies::default()), + limit: 1, + ..Default::default() + }), + }, + )?; + + // Look how nicely it captures and associates this directory rename. + insta::assert_debug_snapshot!(changes.into_iter() + .filter(|c| !c.entry_mode().is_tree() || + c.relation().map_or(false, |r| matches!(r, Relation::Parent(_))) + ).collect::>(), @r#" + [ + Rewrite { + source_location: "git-sec", + source_entry_mode: EntryMode( + 16384, + ), + source_relation: Some( + Parent( + 1, + ), + ), + source_id: Sha1(0026010e87631065a2739f627622feb14f903fd4), + diff: None, + entry_mode: EntryMode( + 16384, + ), + id: Sha1(0026010e87631065a2739f627622feb14f903fd4), + location: "gix-sec", + relation: Some( + Parent( + 2, + ), + ), + copy: false, + }, + Rewrite { + source_location: "git-sec/CHANGELOG.md", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: Some( + ChildOfParent( + 1, + ), + ), + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "gix-sec/CHANGELOG.md", + relation: Some( + ChildOfParent( + 2, + ), + ), + copy: false, + }, + Rewrite { + source_location: "git-sec/Cargo.toml", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: Some( + ChildOfParent( + 1, + ), + ), + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "gix-sec/Cargo.toml", + relation: Some( + ChildOfParent( + 2, + ), + ), + copy: false, + }, + Rewrite { + source_location: "git-sec/src/identity.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: Some( + ChildOfParent( + 1, + ), + ), + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "gix-sec/src/identity.rs", + relation: Some( + ChildOfParent( + 2, + ), + ), + copy: false, + }, + Rewrite { + source_location: "git-sec/src/lib.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: Some( + ChildOfParent( + 1, + ), + ), + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "gix-sec/src/lib.rs", + relation: Some( + ChildOfParent( + 2, + ), + ), + copy: false, + }, + Rewrite { + source_location: "git-sec/src/permission.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: Some( + ChildOfParent( + 1, + ), + ), + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "gix-sec/src/permission.rs", + relation: Some( + ChildOfParent( + 2, + ), + ), + copy: false, + }, + Rewrite { + source_location: "git-sec/src/trust.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: Some( + ChildOfParent( + 1, + ), + ), + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "gix-sec/src/trust.rs", + relation: Some( + ChildOfParent( + 2, + ), + ), + copy: false, + }, + Rewrite { + source_location: "git-sec/tests/sec.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: Some( + ChildOfParent( + 1, + ), + ), + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "gix-sec/tests/sec.rs", + relation: Some( + ChildOfParent( + 2, + ), + ), + copy: false, + }, + Rewrite { + source_location: "git-sec/tests/identity/mod.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: Some( + ChildOfParent( + 1, + ), + ), + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + diff: None, + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "gix-sec/tests/identity/mod.rs", + relation: Some( + ChildOfParent( + 2, + ), + ), + copy: false, + }, + ] + "#); + + #[cfg(not(windows))] + { + let expected = r#"commit d78c63c5ea3149040767e4387e7fc743cda118fd +Author: author +Date: Sat Jan 1 00:00:00 2000 +0000 + + r2-change + +diff --git a/git-sec/CHANGELOG.md b/gix-sec/CHANGELOG.md +similarity index 100% +rename from git-sec/CHANGELOG.md +rename to gix-sec/CHANGELOG.md +diff --git a/git-sec/Cargo.toml b/gix-sec/Cargo.toml +similarity index 100% +rename from git-sec/Cargo.toml +rename to gix-sec/Cargo.toml +diff --git a/git-sec/src/identity.rs b/gix-sec/src/identity.rs +similarity index 100% +rename from git-sec/src/identity.rs +rename to gix-sec/src/identity.rs +diff --git a/git-sec/src/lib.rs b/gix-sec/src/lib.rs +similarity index 100% +rename from git-sec/src/lib.rs +rename to gix-sec/src/lib.rs +diff --git a/git-sec/src/permission.rs b/gix-sec/src/permission.rs +similarity index 100% +rename from git-sec/src/permission.rs +rename to gix-sec/src/permission.rs +diff --git a/git-sec/src/trust.rs b/gix-sec/src/trust.rs +similarity index 100% +rename from git-sec/src/trust.rs +rename to gix-sec/src/trust.rs +diff --git a/git-sec/tests/identity/mod.rs b/gix-sec/tests/identity/mod.rs +similarity index 100% +rename from git-sec/tests/identity/mod.rs +rename to gix-sec/tests/identity/mod.rs +diff --git a/git-sec/tests/sec.rs b/gix-sec/tests/sec.rs +similarity index 100% +rename from git-sec/tests/sec.rs +rename to gix-sec/tests/sec.rs +"#; + assert_eq!( + std::fs::read_to_string(repo_workdir()?.join("baseline-2.with-renames"))?, + expected + ); + } + + let out = out.expect("tracking enabled"); + assert_eq!( + out.num_similarity_checks, 0, + "similarity checks disabled, and not necessary" + ); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +mod util { + use gix_diff::rewrites; + use gix_object::{FindExt, TreeRefIter}; + use std::convert::Infallible; + use std::path::{Path, PathBuf}; + + pub fn repo_workdir() -> crate::Result { + gix_testtools::scripted_fixture_read_only_standalone("make_diff_for_rewrites_repo.sh") + } + + pub fn repo_with_trees( + lhs: impl Into>, + rhs: impl Into>, + ) -> gix_testtools::Result<(Vec, Vec, gix_diff::blob::Platform, gix_odb::Handle)> { + let root = repo_workdir()?; + let odb = gix_odb::at(root.join(".git/objects"))?; + let lhs = read_tree(&odb, &root, lhs.into())?; + let rhs = read_tree(&odb, &root, rhs.into())?; + + let cache = gix_diff::blob::Platform::new( + Default::default(), + gix_diff::blob::Pipeline::new(Default::default(), Default::default(), Vec::new(), Default::default()), + Default::default(), + gix_worktree::Stack::new( + &root, + gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::default()), + Default::default(), + Vec::new(), + Vec::new(), + ), + ); + Ok((lhs, rhs, cache, odb)) + } + + pub fn collect_changes( + lhs: impl Into>, + rhs: impl Into>, + ) -> gix_testtools::Result<(Vec, Option)> { + let options = gix_diff::tree_with_rewrites::Options { + location: Some(gix_diff::tree::recorder::Location::Path), + rewrites: None, + }; + collect_changes_opts(lhs, rhs, options) + } + + pub fn collect_changes_opts( + lhs: impl Into>, + rhs: impl Into>, + options: gix_diff::tree_with_rewrites::Options, + ) -> gix_testtools::Result<(Vec, Option)> { + let (from, to, mut cache, odb) = repo_with_trees(lhs, rhs)?; + let mut out = Vec::new(); + let rewrites_info = gix_diff::tree_with_rewrites( + TreeRefIter::from_bytes(&from), + TreeRefIter::from_bytes(&to), + &mut cache, + &mut Default::default(), + &odb, + |change| -> Result<_, Infallible> { + out.push(change.into_owned()); + Ok(gix_diff::tree_with_rewrites::Action::Continue) + }, + options, + )?; + Ok((out, rewrites_info)) + } + + fn read_tree(odb: &dyn gix_object::Find, root: &Path, tree: Option<&str>) -> gix_testtools::Result> { + let Some(tree) = tree else { return Ok(Vec::new()) }; + let tree_id_path = root.join(tree).with_extension("tree"); + let hex_id = std::fs::read_to_string(&tree_id_path).map_err(|err| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("Could not read '{}': {}", tree_id_path.display(), err), + ) + })?; + let tree_id = gix_hash::ObjectId::from_hex(hex_id.trim().as_bytes())?; + let mut buf = Vec::new(); + odb.find_tree(&tree_id, &mut buf)?; + Ok(buf) + } +} +use util::{collect_changes, collect_changes_opts, repo_with_trees, repo_workdir}; diff --git a/gix-diff/tests/fixtures/generated-archives/make_diff_for_rewrites_repo.tar b/gix-diff/tests/fixtures/generated-archives/make_diff_for_rewrites_repo.tar new file mode 100644 index 0000000000000000000000000000000000000000..ecc1fadbcc9873da647b45261b8693d0d9cd84ae GIT binary patch literal 335360 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

AFX-L-IN z3re7=v$JzoF4EbP3w1`F5v1ws|C}M15OHp-dmtR(!FF~IN8N<}Txg)=;P6`f-P{6U zHk=mzbqfj=rQzC$a0p5V{o@`U;mqg#yMri&NRg80tZV1|T2Y}8v`7#Rue)%TC{~m( z*4lI*GuEZjSU!^U%OVVoZXNn@bsY0ty(!W z4z*C>6;xpUpIxul|8AJ=-T*p*0BL-^4ur4Dzc>L{e~SGduHruz&V%+=|BEyu0Maih zaX1P|-vj_Z&=P?&v_#7Syo&{qLs3Q+t*-yE{PjPqwX|(Iw(iuSowsiX_krSDEus9m z;M)Ink6d_Qn0sW{z>s%`$@$SbgXrF6L825I859}yubS5luQ+49;X;D=+#|jD(Rzan zlEPmvgu_B}k7aPqdPBngAR;pNX!@V6Gtk>t(OZrgE(>&A%k#W7hlW86@A1uuZ+6gH3|bNE#_@8rT9C1(TX9vLo4 zwi(&T`VWDW6Xp63rNAbr#{b%eAMf^xk_bs+49y~d`T<`FQf62TB7FqFd!t+hJyc=OcuyLf`d*_<=^(t8HX|w)O2ESLXjT3TgByiEMvna#cOM4MOPlxrjmbYm6)D&MI02a8>iggJ&x79o zCzuBOWOe&5p11v1XHOXvGGrk5%LCnnkf1kKFx{_Gkj}{H5D_#mD1-xlJNWZ;enOqs zslLM-54N)$9yZW9k1q7*Bg0;wecP3R?+>DVGeJF;0+n4S6 zYitB~|LatM4d4F8 zfKrUeh`Ivci2DAw#>4jU^${tcZaFR~8<_x&z5nx&e}Dv3*Z+m_v=AarNgNFceh4BU zEGa|U9~6I*C4m10@P7&cz<(4Mc~$-kgByiEM$Y;l`(W#T0GG~d{jcJ`7sk`@UyJ{E z>pFfXEX0JZfG(O)>Fa)HK@N-Y+@}q81vwoU@naj`mE`UzuZiRglRI`$cOSYObg5)59(rv+y~46MRQ;70_?3rzFjDpZ76LCm#1P>B;lRrl zI3#RHgiVAWZpxDPKd#n)DU`~n89RKkqJOfNnI)mEVSPgTMH4JPy z7~-#ijg|-o07&+wD1pTQc2NeDKSba;mc%3h7YL3b35HVl|AoSiLLWpRoB^gm6rJCA zjRY>_HLQhI_W~BkKcTA$q{e^Q{&7GlmSQ3NLkIK6qKLpU0t2O=5WwW)Xpw;Ye^vq@ z05$&0_N@3IBS8M=M;ZT#srZkD@;tC2gnog2kH!Ry3+fh9heJS7AIEIEly{ zPD9!kE0H9Q1M~x;OZdb9|CtpD0)Sv7Qji!`{tJT}g+GQs+eYT!y%P)MAKw3J{@1%S zwAFWs7Z3uY1yUAK0_6pP6$MfhNec7<0KzfwL1~E+RQb0R2R>}S;p9gu|47%r;zf7`R-gN)F(FW9rfhJ*LN??5S% z>)Y1`v(UZ&&9whxB&OQ`ZNQ3LUFPC{PGWJcrWXHTbo~dlJ{9#J2%J*mKP@J>85d$0 z#en*UvaW7WkZZUZSE^0_KTsgI{OkN1Z>zsq=?0C;Kh*kC%D*lqTwVWJi2?8F2Dt64 zul0wy+DCOnc0iFYtBwW;;-+RdSpFaihUpaUoHZlGJg^bF7Zv1~J|Eb!4 z3gh9Ta2oPIdH9H60TNM8gv4(Kli?4GVvNj7kRXoAs{OYxnDGI|2-gF!+RzMSf&2sZ zr276ZjK_h%L=ikdG@yRV6oV2XK{BMwaf~D|9Bd6BI8;Ue1W;WC6b3icF;ohu6i_Kp Hcog_Q5r*PM literal 0 HcmV?d00001 diff --git a/gix-diff/tests/fixtures/make_diff_for_rewrites_repo.sh b/gix-diff/tests/fixtures/make_diff_for_rewrites_repo.sh new file mode 100644 index 00000000000..5d63c067de6 --- /dev/null +++ b/gix-diff/tests/fixtures/make_diff_for_rewrites_repo.sh @@ -0,0 +1,797 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +function store_tree() { + local revspec="${1:?the commit to get the tree for}" + local current_commit + current_commit=$(git rev-parse HEAD) + git rev-parse "${current_commit}^{tree}" > ../"$revspec".tree +} + +git init -q + +cat <>.git/config + +[diff "binary-true"] + binary = true +[diff "binary-false"] + binary = false +[diff ""] + command = "empty is ignored" +[diff] + command = "this is also ignored as sub-section name is missing" + algorithm = histogram +[diff "all-but-binary"] + command = command + textconv = textconv + algorithm = histogram + binary = auto +EOF + +git checkout -b main +mkdir dir +touch a b dir/c d +git add . +git commit -q -m "c1 - initial" +store_tree "c1 - initial" + +echo a >> a +echo b >> b +echo dir/c >> dir/c +echo d >> d +git commit -q -am "c2" +store_tree "c2" + +echo a1 >> a +echo dir/c1 >> dir/c +git commit -q -am "c3-modification" +store_tree "c3-modification" + +git mv a dir/a-moved +git commit -m "r1-identity" +store_tree "r1-identity" + +touch s1 s2 s3 +git add s* && git commit -m "c4 - add identical files" +store_tree "c4 - add identical files" + +git mv s1 z && git mv s2 b2 && git mv s3 b1 +git commit -m "r2-ambiguous" +store_tree "r2-ambiguous" + +git mv dir/c dir/c-moved +echo dir/cn >> dir/c-moved +echo n >> b +git commit -am "r3-simple" # modified rename and normal modification +store_tree "r3-simple" + +touch lt1 lt2 +ln -s lt1 link-1 +echo lt1 > no-link # a file that has content like a link and a similar name +ln -s ../lt2 dir/link-2 +git add . && git commit -m "c5 - add links" +store_tree "c5 - add links" + +git mv link-1 renamed-link-1 +git rm no-link +git rm dir/link-2 && ln -s lt1 z-link-2 && git add . +git commit -m "r4-symlinks" # symlinks are only tracked by identity +store_tree "r4-symlinks" + +seq 10 > f1 +seq 11 > f2 +git add . && git commit -m "c6 - two files with more content" +store_tree "c6" + +echo n >> f1 +echo n >> f2 +git mv f1 f1-renamed +git mv f2 f2-renamed + +git commit -am "r5" # two renames +store_tree "r5" + + +seq 9 > base +git add base +git commit -m "c7" # base has to be added +store_tree "c7" + +echo 10 >> base +cp base c1 +cp base c2 +cp base dir/c3 +git add . && git commit -m "tc1-identity" +store_tree "tc1-identity" + +echo 11 >> base +cp base c4 # can be located by identity +cp base c5 && echo 12 >> c5 +cp base dir/c6 && echo 13 >> dir/c6 +git add . && git commit -m "tc2-similarity" +store_tree "tc2-similarity" + +cp base c6 # can be located by identity, but base needs --find-copies-harder +cp base c7 && echo 13 >> c7 # modified copy, similarity and find copies harder +seq 15 > newly-added +echo nn >> b +git add . +git commit -m "tc3-find-harder" +store_tree "tc3-find-harder" + +rm -Rf ./* +# from 92de081dc9ab5660cb18fa750452345dd63550ea~1 of `gitoxide` +while read -r _ _ _ path; do + mkdir -p ${path%/*} && touch $path +done < git-index/tests/index/file/mod.rs +git add . && git commit -m "r1-change" +store_tree "r1-change" + +rm -Rf ./* +# from d7ad650d3~1 of `gitoxide` +while read -r _ _ _ path; do + mkdir -p ${path%/*} && touch $path +done < baseline-3.no-renames +git -c diff.renames=1 show > baseline-3.with-renames +git -c diff.renames=0 show HEAD~2 > baseline-2.no-renames +git -c diff.renames=1 show HEAD~2 > baseline-2.with-renames +git -c diff.renames=0 show HEAD~4 > baseline.no-renames +git -c diff.renames=1 show HEAD~4 > baseline.with-renames + +mv ../*.tree . \ No newline at end of file From 45b71554f6437fbfe3ead020ff182f77cd57e47f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 7 Oct 2024 11:22:45 +0200 Subject: [PATCH 10/13] refactor!: Use the new `tree_with_rewrites` plumbing implementation. This merges `object::tree::diff::change::Event` into `object::tree::diff::Change` as well. --- gitoxide-core/src/hours/core.rs | 9 +- gitoxide-core/src/query/engine/update.rs | 37 +- gix/src/ext/mod.rs | 2 + gix/src/ext/tree.rs | 20 +- gix/src/object/blob.rs | 104 +- gix/src/object/tree/diff/change.rs | 421 ++++---- gix/src/object/tree/diff/for_each.rs | 313 +----- gix/src/object/tree/diff/mod.rs | 121 ++- gix/tests/object/tree/diff.rs | 1170 +--------------------- 9 files changed, 433 insertions(+), 1764 deletions(-) diff --git a/gitoxide-core/src/hours/core.rs b/gitoxide-core/src/hours/core.rs index 1fa073b3253..7563e64f62d 100644 --- a/gitoxide-core/src/hours/core.rs +++ b/gitoxide-core/src/hours/core.rs @@ -128,19 +128,19 @@ pub fn spawn_tree_delta_threads<'scope>( .track_filename() .track_rewrites(None) .for_each_to_obtain_tree(&to, |change| { - use gix::object::tree::diff::change::Event::*; + use gix::object::tree::diff::Change::*; changes.fetch_add(1, Ordering::Relaxed); - match change.event { + match change { Rewrite { .. } => { unreachable!("we turned that off") } - Addition { entry_mode, id } => { + Addition { entry_mode, id, .. } => { if entry_mode.is_no_tree() { files.added += 1; add_lines(line_stats, &lines_count, &mut lines, id); } } - Deletion { entry_mode, id } => { + Deletion { entry_mode, id, .. } => { if entry_mode.is_no_tree() { files.removed += 1; remove_lines(line_stats, &lines_count, &mut lines, id); @@ -151,6 +151,7 @@ pub fn spawn_tree_delta_threads<'scope>( previous_entry_mode, id, previous_id, + .. } => match (previous_entry_mode.is_blob(), entry_mode.is_blob()) { (false, false) => {} (false, true) => { diff --git a/gitoxide-core/src/query/engine/update.rs b/gitoxide-core/src/query/engine/update.rs index da332ea1b17..060a87f6e19 100644 --- a/gitoxide-core/src/query/engine/update.rs +++ b/gitoxide-core/src/query/engine/update.rs @@ -210,12 +210,17 @@ pub fn update( .track_path() .track_rewrites(Some(rewrites)) .for_each_to_obtain_tree_with_cache(&to, &mut rewrite_cache, |change| { - use gix::object::tree::diff::change::Event::*; + use gix::object::tree::diff::Change::*; change_counter.fetch_add(1, Ordering::SeqCst); - match change.event { - Addition { entry_mode, id } => { + match change { + Addition { + entry_mode, + id, + location, + .. + } => { if entry_mode.is_blob_or_symlink() { - add_lines(&mut out, change.location, &lines_counter, id); + add_lines(&mut out, location, &lines_counter, id); } } Modification { @@ -223,18 +228,14 @@ pub fn update( previous_entry_mode, id, previous_id, + location, } => match (previous_entry_mode.is_blob(), entry_mode.is_blob()) { (false, false) => {} (false, true) => { - add_lines(&mut out, change.location, &lines_counter, id); + add_lines(&mut out, location, &lines_counter, id); } (true, false) => { - add_lines( - &mut out, - change.location, - &lines_counter, - previous_id, - ); + add_lines(&mut out, location, &lines_counter, previous_id); } (true, true) => { if let Ok(cache) = @@ -266,7 +267,7 @@ pub fn update( lines_counter .fetch_add(nl, Ordering::SeqCst); out.push(FileChange { - relpath: change.location.to_owned(), + relpath: location.to_owned(), mode: FileMode::Modified, source_relpath: None, lines: Some(lines), @@ -281,19 +282,25 @@ pub fn update( } } }, - Deletion { entry_mode, id } => { + Deletion { + entry_mode, + id, + location, + .. + } => { if entry_mode.is_blob_or_symlink() { - remove_lines(&mut out, change.location, &lines_counter, id); + remove_lines(&mut out, location, &lines_counter, id); } } Rewrite { source_location, diff, copy, + location, .. } => { out.push(FileChange { - relpath: change.location.to_owned(), + relpath: location.to_owned(), source_relpath: Some(source_location.to_owned()), mode: if copy { FileMode::Copy } else { FileMode::Rename }, lines: diff.map(|d| LineStats { diff --git a/gix/src/ext/mod.rs b/gix/src/ext/mod.rs index ad69fec0715..3cbc40ee01c 100644 --- a/gix/src/ext/mod.rs +++ b/gix/src/ext/mod.rs @@ -2,6 +2,8 @@ pub use object_id::ObjectIdExt; pub use reference::ReferenceExt; #[cfg(feature = "revision")] pub use rev_spec::RevSpecExt; +#[cfg(feature = "blob-diff")] +pub use tree::TreeDiffChangeExt; pub use tree::{TreeEntryExt, TreeEntryRefExt, TreeIterExt}; mod object_id; diff --git a/gix/src/ext/tree.rs b/gix/src/ext/tree.rs index 72922e31b20..dc357c2cc5b 100644 --- a/gix/src/ext/tree.rs +++ b/gix/src/ext/tree.rs @@ -42,9 +42,9 @@ impl TreeIterExt for TreeRefIter<'_> { } } -/// Extensions for [EntryRef][gix_object::tree::EntryRef]. +/// Extensions for [EntryRef](gix_object::tree::EntryRef). pub trait TreeEntryRefExt<'a>: 'a { - /// Attach [`Repository`][crate::Repository] to the given tree entry. It can be detached later with `detach()`. + /// Attach [`repo`](crate::Repository) to the given tree entry. It can be detached later with `detach()`. fn attach<'repo>(self, repo: &'repo crate::Repository) -> crate::object::tree::EntryRef<'repo, 'a>; } @@ -54,9 +54,9 @@ impl<'a> TreeEntryRefExt<'a> for gix_object::tree::EntryRef<'a> { } } -/// Extensions for [Entry][gix_object::tree::Entry]. +/// Extensions for [Entry](gix_object::tree::Entry). pub trait TreeEntryExt { - /// Attach [`Repository`][crate::Repository] to the given tree entry. It can be detached later with `detach()`. + /// Attach [`repo`](crate::Repository) to the given tree entry. It can be detached later with `detach()`. fn attach(self, repo: &crate::Repository) -> crate::object::tree::Entry<'_>; } @@ -65,3 +65,15 @@ impl TreeEntryExt for gix_object::tree::Entry { crate::object::tree::Entry { inner: self, repo } } } + +/// Extensions for [Change](gix_diff::tree_with_rewrites::Change). +#[cfg(feature = "blob-diff")] +pub trait TreeDiffChangeExt { + /// Attach [`old_repo`](crate::Repository) and `new_repo` to current instance. It can be detached later with `detach()`. + /// Note that both repositories are usually the same. + fn attach<'old, 'new>( + &self, + old_repo: &'old crate::Repository, + new_repo: &'new crate::Repository, + ) -> crate::object::tree::diff::Change<'_, 'old, 'new>; +} diff --git a/gix/src/object/blob.rs b/gix/src/object/blob.rs index 8d189a3bd3e..c07fd319832 100644 --- a/gix/src/object/blob.rs +++ b/gix/src/object/blob.rs @@ -5,12 +5,9 @@ use crate::{Blob, ObjectDetached}; pub mod diff { use std::ops::Range; - use gix_diff::blob::{platform::prepare_diff::Operation, ResourceKind}; + use gix_diff::blob::platform::prepare_diff::Operation; - use crate::{ - bstr::ByteSlice, - object::{blob::diff::lines::Change, tree::diff::change::Event}, - }; + use crate::bstr::ByteSlice; /// A platform to keep temporary information to perform line diffs on modified blobs. /// @@ -21,99 +18,10 @@ pub mod diff { /// pub mod init { - /// The error returned by [`Platform::from_tree_change()`][super::Platform::from_tree_change()]. + /// The error returned by [`object::tree::diff::Change::diff`](crate::object::tree::diff::Change::diff()). pub type Error = gix_diff::blob::platform::set_resource::Error; } - impl<'a> Platform<'a> { - /// Produce a platform for performing various diffs after obtaining the data from a single `tree_change`. - pub fn from_tree_change( - tree_change: &crate::object::tree::diff::Change<'_, '_, '_>, - resource_cache: &'a mut gix_diff::blob::Platform, - ) -> Result, init::Error> { - match tree_change.event { - Event::Addition { entry_mode, id } => { - resource_cache.set_resource( - id.repo.object_hash().null(), - entry_mode.kind(), - tree_change.location, - ResourceKind::OldOrSource, - &id.repo.objects, - )?; - resource_cache.set_resource( - id.inner, - entry_mode.kind(), - tree_change.location, - ResourceKind::NewOrDestination, - &id.repo.objects, - )?; - } - Event::Deletion { entry_mode, id } => { - resource_cache.set_resource( - id.inner, - entry_mode.kind(), - tree_change.location, - ResourceKind::OldOrSource, - &id.repo.objects, - )?; - resource_cache.set_resource( - id.repo.object_hash().null(), - entry_mode.kind(), - tree_change.location, - ResourceKind::NewOrDestination, - &id.repo.objects, - )?; - } - Event::Modification { - previous_entry_mode, - previous_id, - entry_mode, - id, - } => { - resource_cache.set_resource( - previous_id.inner, - previous_entry_mode.kind(), - tree_change.location, - ResourceKind::OldOrSource, - &previous_id.repo.objects, - )?; - resource_cache.set_resource( - id.inner, - entry_mode.kind(), - tree_change.location, - ResourceKind::NewOrDestination, - &id.repo.objects, - )?; - } - Event::Rewrite { - source_location, - source_entry_mode, - source_id, - entry_mode, - id, - diff: _, - copy: _, - } => { - resource_cache.set_resource( - source_id.inner, - source_entry_mode.kind(), - source_location, - ResourceKind::OldOrSource, - &source_id.repo.objects, - )?; - resource_cache.set_resource( - id.inner, - entry_mode.kind(), - tree_change.location, - ResourceKind::NewOrDestination, - &id.repo.objects, - )?; - } - } - Ok(Self { resource_cache }) - } - } - /// pub mod lines { use crate::bstr::BStr; @@ -195,11 +103,11 @@ pub mod diff { let hunk_before = &lines[..end_of_before]; let hunk_after = &lines[end_of_before..]; if hunk_after.is_empty() { - err = process_hunk(Change::Deletion { lines: hunk_before }).err(); + err = process_hunk(lines::Change::Deletion { lines: hunk_before }).err(); } else if hunk_before.is_empty() { - err = process_hunk(Change::Addition { lines: hunk_after }).err(); + err = process_hunk(lines::Change::Addition { lines: hunk_after }).err(); } else { - err = process_hunk(Change::Modification { + err = process_hunk(lines::Change::Modification { lines_before: hunk_before, lines_after: hunk_after, }) diff --git a/gix/src/object/tree/diff/change.rs b/gix/src/object/tree/diff/change.rs index 9d8265ea7a4..a40c7100a91 100644 --- a/gix/src/object/tree/diff/change.rs +++ b/gix/src/object/tree/diff/change.rs @@ -1,173 +1,14 @@ -use crate::bstr::BString; +use crate::bstr::{BStr, ByteSlice}; use crate::ext::ObjectIdExt; -use crate::object::tree::diff::ChangeDetached; -use crate::{bstr::BStr, diff::blob::DiffLineStats, Id, Repository}; +use crate::object::tree::diff::Change; +use crate::Repository; +use gix_diff::tree_with_rewrites::Change as ChangeDetached; -/// An event emitted when finding differences between two trees. -#[derive(Debug, Clone, Copy)] -pub enum Event<'a, 'old, 'new> { - /// An entry was added, like the addition of a file or directory. - Addition { - /// The mode of the added entry. - entry_mode: gix_object::tree::EntryMode, - /// The object id of the added entry. - id: Id<'new>, - }, - /// An entry was deleted, like the deletion of a file or directory. - Deletion { - /// The mode of the deleted entry. - entry_mode: gix_object::tree::EntryMode, - /// The object id of the deleted entry. - id: Id<'old>, - }, - /// An entry was modified, e.g. changing the contents of a file adjusts its object id and turning - /// a file into a symbolic link adjusts its mode. - Modification { - /// The mode of the entry before the modification. - previous_entry_mode: gix_object::tree::EntryMode, - /// The object id of the entry before the modification. - previous_id: Id<'old>, - - /// The mode of the entry after the modification. - entry_mode: gix_object::tree::EntryMode, - /// The object id after the modification. - id: Id<'new>, - }, - /// Entries are considered rewritten if they are not trees and they, according to some understanding of identity, were renamed - /// or copied. - /// In case of renames, this means they originally appeared as [`Deletion`][Event::Deletion] signalling their source as well as an - /// [`Addition`][Event::Addition] acting as destination. - /// - /// In case of copies, the `copy` flag is true and typically represents a perfect copy of a source was made. - /// - /// This variant can only be encountered if [rewrite tracking][super::Platform::track_rewrites()] is enabled. - /// - /// Note that mode changes may have occurred as well, i.e. changes from executable to non-executable or vice-versa. - Rewrite { - /// The location of the source of the rename operation. - /// - /// It may be empty if neither [file names][super::Platform::track_filename()] nor [file paths][super::Platform::track_path()] - /// are tracked. - source_location: &'a BStr, - /// The mode of the entry before the rename. - source_entry_mode: gix_object::tree::EntryMode, - /// The object id of the entry before the rename. - /// - /// Note that this is the same as `id` if we require the [similarity to be 100%][super::Rewrites::percentage], but may - /// be different otherwise. - source_id: Id<'old>, - /// Information about the diff we performed to detect similarity and match the `source_id` with the current state at `id`. - /// It's `None` if `source_id` is equal to `id`, as identity made an actual diff computation unnecessary. - diff: Option, - /// The mode of the entry after the rename. - /// It could differ but still be considered a rename as we are concerned only about content. - entry_mode: gix_object::tree::EntryMode, - /// The object id after the rename. - id: Id<'new>, - /// If true, this rewrite is created by copy, and `source_id` is pointing to its source. Otherwise, it's a rename, and `source_id` - /// points to a deleted object, as renames are tracked as deletions and additions of the same or similar content. - copy: bool, - }, -} - -/// An event emitted when finding differences between two trees. -#[derive(Debug, Clone)] -pub enum EventDetached { - /// An entry was added, like the addition of a file or directory. - Addition { - /// The mode of the added entry. - entry_mode: gix_object::tree::EntryMode, - /// The object id of the added entry. - id: gix_hash::ObjectId, - }, - /// An entry was deleted, like the deletion of a file or directory. - Deletion { - /// The mode of the deleted entry. - entry_mode: gix_object::tree::EntryMode, - /// The object id of the deleted entry. - id: gix_hash::ObjectId, - }, - /// An entry was modified, e.g. changing the contents of a file adjusts its object id and turning - /// a file into a symbolic link adjusts its mode. - Modification { - /// The mode of the entry before the modification. - previous_entry_mode: gix_object::tree::EntryMode, - /// The object id of the entry before the modification. - previous_id: gix_hash::ObjectId, - - /// The mode of the entry after the modification. - entry_mode: gix_object::tree::EntryMode, - /// The object id after the modification. - id: gix_hash::ObjectId, - }, - /// Entries are considered rewritten if they are not trees and they, according to some understanding of identity, were renamed - /// or copied. - /// In case of renames, this means they originally appeared as [`Deletion`][Event::Deletion] signalling their source as well as an - /// [`Addition`][Event::Addition] acting as destination. - /// - /// In case of copies, the `copy` flag is true and typically represents a perfect copy of a source was made. - /// - /// This variant can only be encountered if [rewrite tracking][super::Platform::track_rewrites()] is enabled. - /// - /// Note that mode changes may have occurred as well, i.e. changes from executable to non-executable or vice-versa. - Rewrite { - /// The location of the source of the rename operation. - /// - /// It may be empty if neither [file names][super::Platform::track_filename()] nor [file paths][super::Platform::track_path()] - /// are tracked. - source_location: BString, - /// The mode of the entry before the rename. - source_entry_mode: gix_object::tree::EntryMode, - /// The object id of the entry before the rename. - /// - /// Note that this is the same as `id` if we require the [similarity to be 100%][super::Rewrites::percentage], but may - /// be different otherwise. - source_id: gix_hash::ObjectId, - /// Information about the diff we performed to detect similarity and match the `source_id` with the current state at `id`. - /// It's `None` if `source_id` is equal to `id`, as identity made an actual diff computation unnecessary. - diff: Option, - /// The mode of the entry after the rename. - /// It could differ but still be considered a rename as we are concerned only about content. - entry_mode: gix_object::tree::EntryMode, - /// The object id after the rename. - id: gix_hash::ObjectId, - /// If true, this rewrite is created by copy, and `source_id` is pointing to its source. Otherwise, it's a rename, and `source_id` - /// points to a deleted object, as renames are tracked as deletions and additions of the same or similar content. - copy: bool, - }, -} - -/// Lifecycle -impl super::Change<'_, '_, '_> { - /// Detach the repository instance to obtain a fully-owned version - pub fn detach(self) -> ChangeDetached { - ChangeDetached { - location: self.location.to_owned(), - event: self.event.detach(), - } - } -} - -/// Lifecycle -impl ChangeDetached { - /// Return an attached version of this instance that uses `old_repo` for previous values and `new_repo` for current values. - pub fn attach<'old, 'new>( - &self, - old_repo: &'old Repository, - new_repo: &'new Repository, - ) -> super::Change<'_, 'old, 'new> { - super::Change { - location: self.location.as_ref(), - event: self.event.attach(old_repo, new_repo), - } - } -} - -impl super::Change<'_, '_, '_> { - /// Produce a platform for performing a line-diff no matter whether the underlying [Event] is an addition, modification, +impl Change<'_, '_, '_> { + /// Produce a platform for performing a line-diff no matter whether the underlying [Change] is an addition, modification, /// deletion or rewrite. /// Use `resource_cache` to store the diffable data and possibly reuse previously stored data, usually obtained with - /// [crate::Repository::diff_resource_cache()]. + /// [Repository::diff_resource_cache()]. /// Afterward the platform, which holds on to `resource_cache`, can be used to perform ready-made operations on the /// pre-set resources. /// @@ -178,107 +19,309 @@ impl super::Change<'_, '_, '_> { &self, resource_cache: &'b mut gix_diff::blob::Platform, ) -> Result, crate::object::blob::diff::init::Error> { - crate::object::blob::diff::Platform::from_tree_change(self, resource_cache) + resource_cache.set_resource_by_change((*self).into(), &self.id().repo.objects)?; + Ok(crate::object::blob::diff::Platform { resource_cache }) + } +} + +impl<'a> From> for gix_diff::tree_with_rewrites::ChangeRef<'a> { + fn from(value: Change<'a, '_, '_>) -> Self { + use gix_diff::tree_with_rewrites::ChangeRef; + match value { + Change::Addition { + location, + entry_mode, + relation, + id, + } => ChangeRef::Addition { + location, + entry_mode, + relation, + id: id.detach(), + }, + Change::Deletion { + location, + entry_mode, + relation, + id, + } => ChangeRef::Deletion { + location, + entry_mode, + relation, + id: id.detach(), + }, + Change::Modification { + location, + previous_entry_mode, + previous_id, + entry_mode, + id, + } => ChangeRef::Modification { + location, + previous_entry_mode, + previous_id: previous_id.detach(), + entry_mode, + id: id.detach(), + }, + Change::Rewrite { + source_location, + source_relation, + source_entry_mode, + source_id, + diff, + entry_mode, + location, + id, + relation, + copy, + } => ChangeRef::Rewrite { + source_location, + source_entry_mode, + source_relation, + source_id: source_id.detach(), + diff, + entry_mode, + id: id.detach(), + location, + relation, + copy, + }, + } + } +} + +impl<'a, 'old, 'new> Change<'a, 'old, 'new> { + /// Convert `change` into this instance type, attaching the `old_repo` and `new_repo` to each side respectively. + /// Note that both repos are typically the same. + pub fn from_change_ref( + change: gix_diff::tree_with_rewrites::ChangeRef<'a>, + old_repo: &'old Repository, + new_repo: &'new Repository, + ) -> Self { + use gix_diff::tree_with_rewrites::ChangeRef; + match change { + ChangeRef::Addition { + location, + entry_mode, + relation, + id, + } => Change::Addition { + location, + entry_mode, + relation, + id: id.attach(new_repo), + }, + ChangeRef::Deletion { + location, + entry_mode, + relation, + id, + } => Change::Deletion { + location, + entry_mode, + relation, + id: id.attach(old_repo), + }, + ChangeRef::Modification { + location, + previous_entry_mode, + previous_id, + entry_mode, + id, + } => Change::Modification { + location, + previous_entry_mode, + entry_mode, + previous_id: previous_id.attach(old_repo), + id: id.attach(new_repo), + }, + ChangeRef::Rewrite { + source_location, + source_entry_mode, + source_relation, + source_id, + diff, + entry_mode, + id, + location, + relation, + copy, + } => Change::Rewrite { + source_location, + source_relation, + source_entry_mode, + source_id: source_id.attach(old_repo), + diff, + entry_mode, + location, + id: id.attach(new_repo), + relation, + copy, + }, + } } } /// Lifecycle -impl Event<'_, '_, '_> { +impl Change<'_, '_, '_> { /// Detach the repository instance to obtain a fully-owned version - pub fn detach(self) -> EventDetached { + pub fn detach(self) -> ChangeDetached { match self { - Event::Addition { entry_mode, id } => EventDetached::Addition { + Change::Addition { + entry_mode, + id, + location, + relation, + } => ChangeDetached::Addition { entry_mode, id: id.detach(), + location: location.to_owned(), + relation, }, - Event::Deletion { entry_mode, id } => EventDetached::Deletion { + Change::Deletion { + entry_mode, + id, + location, + relation, + } => ChangeDetached::Deletion { entry_mode, id: id.detach(), + location: location.to_owned(), + relation, }, - Event::Modification { + Change::Modification { previous_entry_mode, previous_id, entry_mode, id, - } => EventDetached::Modification { + location, + } => ChangeDetached::Modification { previous_entry_mode, previous_id: previous_id.detach(), entry_mode, id: id.detach(), + location: location.to_owned(), }, - Event::Rewrite { + Change::Rewrite { source_location, + source_relation, source_entry_mode, source_id, diff, entry_mode, id, + relation, copy, - } => EventDetached::Rewrite { + location, + } => ChangeDetached::Rewrite { source_location: source_location.to_owned(), source_entry_mode, + source_relation, source_id: source_id.detach(), diff, entry_mode, id: id.detach(), copy, + location: location.to_owned(), + relation, }, } } } -impl EventDetached { - /// Return an attached version of this instance that uses `old_repo` for previous values and `new_repo` for current values. - pub fn attach<'old, 'new>(&self, old_repo: &'old Repository, new_repo: &'new Repository) -> Event<'_, 'old, 'new> { +impl crate::ext::TreeDiffChangeExt for gix_diff::tree_with_rewrites::Change { + fn attach<'old, 'new>(&self, old_repo: &'old Repository, new_repo: &'new Repository) -> Change<'_, 'old, 'new> { match self { - EventDetached::Addition { entry_mode, id } => Event::Addition { + ChangeDetached::Addition { + entry_mode, + id, + location, + relation, + } => Change::Addition { entry_mode: *entry_mode, id: id.attach(new_repo), + location: location.as_bstr(), + relation: *relation, }, - EventDetached::Deletion { entry_mode, id } => Event::Deletion { + ChangeDetached::Deletion { + entry_mode, + id, + location, + relation, + } => Change::Deletion { entry_mode: *entry_mode, id: id.attach(old_repo), + location: location.as_bstr(), + relation: *relation, }, - EventDetached::Modification { + ChangeDetached::Modification { previous_entry_mode, previous_id, entry_mode, id, - } => Event::Modification { + location, + } => Change::Modification { previous_entry_mode: *previous_entry_mode, previous_id: previous_id.attach(old_repo), entry_mode: *entry_mode, id: id.attach(new_repo), + location: location.as_bstr(), }, - EventDetached::Rewrite { + ChangeDetached::Rewrite { source_location, + source_relation, source_entry_mode, source_id, diff, entry_mode, id, copy, - } => Event::Rewrite { + location, + relation, + } => Change::Rewrite { source_location: source_location.as_ref(), + source_relation: *source_relation, source_entry_mode: *source_entry_mode, source_id: source_id.attach(old_repo), diff: *diff, entry_mode: *entry_mode, id: id.attach(new_repo), copy: *copy, + relation: *relation, + location: location.as_bstr(), }, } } } -impl Event<'_, '_, '_> { +impl Change<'_, '_, '_> { + /// Return the current ID of the change. + pub fn id(&self) -> crate::Id<'_> { + match self { + Change::Addition { id, .. } + | Change::Deletion { id, .. } + | Change::Modification { id, .. } + | Change::Rewrite { id, .. } => *id, + } + } + + /// Return the location of this instance. + pub fn location(&self) -> &BStr { + match self { + Change::Addition { location, .. } + | Change::Deletion { location, .. } + | Change::Modification { location, .. } + | Change::Rewrite { location, .. } => location.as_bstr(), + } + } + /// Return the current mode of this instance. pub fn entry_mode(&self) -> gix_object::tree::EntryMode { match self { - Event::Addition { entry_mode, .. } - | Event::Deletion { entry_mode, .. } - | Event::Modification { entry_mode, .. } - | Event::Rewrite { entry_mode, .. } => *entry_mode, + Change::Addition { entry_mode, .. } + | Change::Deletion { entry_mode, .. } + | Change::Modification { entry_mode, .. } + | Change::Rewrite { entry_mode, .. } => *entry_mode, } } } diff --git a/gix/src/object/tree/diff/for_each.rs b/gix/src/object/tree/diff/for_each.rs index 2582f765768..1fce28ea2ec 100644 --- a/gix/src/object/tree/diff/for_each.rs +++ b/gix/src/object/tree/diff/for_each.rs @@ -1,20 +1,14 @@ use gix_object::TreeRefIter; -use super::{change, Action, Change, Platform}; -use crate::{ - bstr::BStr, - diff::{rewrites, rewrites::tracker}, - ext::ObjectIdExt, - object::tree::diff, - Repository, Tree, -}; +use super::{Action, Change, Platform}; +use crate::{diff::rewrites::tracker, Tree}; /// The error return by methods on the [diff platform][Platform]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error(transparent)] - Diff(#[from] gix_diff::tree::Error), + Diff(#[from] gix_diff::tree_with_rewrites::Error), #[error("The user-provided callback failed")] ForEach(#[source] Box), #[error(transparent)] @@ -23,13 +17,6 @@ pub enum Error { RenameTracking(#[from] tracker::emit::Error), } -/// -#[derive(Clone, Debug, Copy, PartialEq)] -pub struct Outcome { - /// Available only if [rewrite-tracking was enabled][Platform::track_rewrites()]. - pub rewrites: Option, -} - /// Add the item to compare to. impl<'old> Platform<'_, 'old> { /// Call `for_each` repeatedly with all changes that are needed to convert the source of the diff to the tree to `other`. @@ -40,7 +27,7 @@ impl<'old> Platform<'_, 'old> { &mut self, other: &Tree<'new>, for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, - ) -> Result + ) -> Result, Error> where E: Into>, { @@ -62,7 +49,7 @@ impl<'old> Platform<'_, 'old> { other: &Tree<'new>, resource_cache: &mut gix_diff::blob::Platform, for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, - ) -> Result + ) -> Result, Error> where E: Into>, { @@ -72,289 +59,37 @@ impl<'old> Platform<'_, 'old> { fn for_each_to_obtain_tree_inner<'new, E>( &mut self, other: &Tree<'new>, - for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, + mut for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, resource_cache: Option<&mut gix_diff::blob::Platform>, - ) -> Result + ) -> Result, Error> where E: Into>, { let repo = self.lhs.repo; - let mut delegate = Delegate { - src_tree: self.lhs, - other_repo: other.repo, - recorder: gix_diff::tree::Recorder::default().track_location(self.tracking), - visit: for_each, - location: self.tracking, - tracked: self.rewrites.map(rewrites::Tracker::new), - err: None, - }; - match gix_diff::tree( - TreeRefIter::from_bytes(&self.lhs.data), - TreeRefIter::from_bytes(&other.data), - &mut self.state, - &repo.objects, - &mut delegate, - ) { - Ok(()) => { - let outcome = Outcome { - rewrites: delegate.process_tracked_changes(resource_cache)?, - }; - match delegate.err { - Some(err) => Err(Error::ForEach(err.into())), - None => Ok(outcome), - } - } - Err(gix_diff::tree::Error::Cancelled) => delegate - .err - .map_or(Err(Error::Diff(gix_diff::tree::Error::Cancelled)), |err| { - Err(Error::ForEach(err.into())) - }), - Err(err) => Err(err.into()), - } - } -} - -struct Delegate<'a, 'old, 'new, VisitFn, E> { - src_tree: &'a Tree<'old>, - other_repo: &'new Repository, - recorder: gix_diff::tree::Recorder, - visit: VisitFn, - tracked: Option>, - location: Option, - err: Option, -} - -impl<'old, 'new, VisitFn, E> Delegate<'_, 'old, 'new, VisitFn, E> -where - VisitFn: for<'delegate> FnMut(Change<'delegate, 'old, 'new>) -> Result, - E: Into>, -{ - /// Call `visit` on an attached version of `change`. - fn emit_change( - change: gix_diff::tree::visit::Change, - location: &BStr, - visit: &mut VisitFn, - repo: &'old Repository, - other_repo: &'new Repository, - stored_err: &mut Option, - ) -> gix_diff::tree::visit::Action { - use gix_diff::tree::visit::Change::*; - let event = match change { - Addition { - entry_mode, - oid, - relation: _, - } => change::Event::Addition { - entry_mode, - id: oid.attach(other_repo), - }, - Deletion { - entry_mode, - oid, - relation: _, - } => change::Event::Deletion { - entry_mode, - id: oid.attach(repo), - }, - Modification { - previous_entry_mode, - previous_oid, - entry_mode, - oid, - } => change::Event::Modification { - previous_entry_mode, - entry_mode, - previous_id: previous_oid.attach(repo), - id: oid.attach(other_repo), - }, - }; - match visit(Change { event, location }) { - Ok(Action::Cancel) => gix_diff::tree::visit::Action::Cancel, - Ok(Action::Continue) => gix_diff::tree::visit::Action::Continue, - Err(err) => { - *stored_err = Some(err); - gix_diff::tree::visit::Action::Cancel - } - } - } - - fn process_tracked_changes( - &mut self, - diff_cache: Option<&mut gix_diff::blob::Platform>, - ) -> Result, Error> { - let tracked = match self.tracked.as_mut() { - Some(t) => t, - None => return Ok(None), - }; - - let repo = self.src_tree.repo; let mut storage; - let diff_cache = match diff_cache { - Some(cache) => cache, + let cache = match resource_cache { None => { storage = repo.diff_resource_cache(gix_diff::blob::pipeline::Mode::ToGit, Default::default())?; &mut storage } + Some(cache) => cache, }; - - let outcome = tracked.emit( - |dest, source| match source { - Some(source) => { - let (oid, mode) = dest.change.oid_and_entry_mode(); - let change = diff::Change { - location: dest.location, - event: diff::change::Event::Rewrite { - source_location: source.location, - source_entry_mode: source.entry_mode, - source_id: source.id.attach(self.src_tree.repo), - entry_mode: mode, - id: oid.to_owned().attach(self.other_repo), - diff: source.diff, - copy: match source.kind { - tracker::visit::SourceKind::Rename => false, - tracker::visit::SourceKind::Copy => true, - }, - }, - }; - match (self.visit)(change) { - Ok(Action::Cancel) => gix_diff::tree::visit::Action::Cancel, - Ok(Action::Continue) => gix_diff::tree::visit::Action::Continue, - Err(err) => { - self.err = Some(err); - gix_diff::tree::visit::Action::Cancel - } - } - } - None => Self::emit_change( - dest.change, - dest.location, - &mut self.visit, - self.src_tree.repo, - self.other_repo, - &mut self.err, - ), + Ok(gix_diff::tree_with_rewrites( + TreeRefIter::from_bytes(&self.lhs.data), + TreeRefIter::from_bytes(&other.data), + cache, + &mut self.state, + &repo.objects, + |change| { + for_each(Change::from_change_ref(change, repo, other.repo)).map(|action| match action { + Action::Continue => gix_diff::tree_with_rewrites::Action::Continue, + Action::Cancel => gix_diff::tree_with_rewrites::Action::Cancel, + }) }, - diff_cache, - &self.src_tree.repo.objects, - |push| { - self.src_tree - .traverse() - .breadthfirst(&mut tree_to_changes::Delegate::new(push, self.location)) + gix_diff::tree_with_rewrites::Options { + location: self.location, + rewrites: self.rewrites, }, - )?; - Ok(Some(outcome)) - } -} - -impl<'old, 'new, VisitFn, E> gix_diff::tree::Visit for Delegate<'_, 'old, 'new, VisitFn, E> -where - VisitFn: for<'delegate> FnMut(Change<'delegate, 'old, 'new>) -> Result, - E: Into>, -{ - fn pop_front_tracked_path_and_set_current(&mut self) { - self.recorder.pop_front_tracked_path_and_set_current(); - } - - fn push_back_tracked_path_component(&mut self, component: &BStr) { - self.recorder.push_back_tracked_path_component(component); - } - - fn push_path_component(&mut self, component: &BStr) { - self.recorder.push_path_component(component); - } - - fn pop_path_component(&mut self) { - self.recorder.pop_path_component(); - } - - fn visit(&mut self, change: gix_diff::tree::visit::Change) -> gix_diff::tree::visit::Action { - match self.tracked.as_mut() { - Some(tracked) => tracked.try_push_change(change, self.recorder.path()).map_or( - gix_diff::tree::visit::Action::Continue, - |change| { - Self::emit_change( - change, - self.recorder.path(), - &mut self.visit, - self.src_tree.repo, - self.other_repo, - &mut self.err, - ) - }, - ), - None => Self::emit_change( - change, - self.recorder.path(), - &mut self.visit, - self.src_tree.repo, - self.other_repo, - &mut self.err, - ), - } - } -} - -mod tree_to_changes { - use gix_diff::tree::visit::Change; - use gix_object::tree::EntryRef; - - use crate::bstr::BStr; - - pub struct Delegate<'a> { - push: &'a mut dyn FnMut(Change, &BStr), - recorder: gix_traverse::tree::Recorder, - } - - impl<'a> Delegate<'a> { - pub fn new( - push: &'a mut dyn FnMut(Change, &BStr), - location: Option, - ) -> Self { - let location = location.map(|t| match t { - gix_diff::tree::recorder::Location::FileName => gix_traverse::tree::recorder::Location::FileName, - gix_diff::tree::recorder::Location::Path => gix_traverse::tree::recorder::Location::Path, - }); - Self { - push, - recorder: gix_traverse::tree::Recorder::default().track_location(location), - } - } - } - - impl gix_traverse::tree::Visit for Delegate<'_> { - fn pop_front_tracked_path_and_set_current(&mut self) { - self.recorder.pop_front_tracked_path_and_set_current(); - } - - fn push_back_tracked_path_component(&mut self, component: &BStr) { - self.recorder.push_back_tracked_path_component(component); - } - - fn push_path_component(&mut self, component: &BStr) { - self.recorder.push_path_component(component); - } - - fn pop_path_component(&mut self) { - self.recorder.pop_path_component(); - } - - fn visit_tree(&mut self, _entry: &EntryRef<'_>) -> gix_traverse::tree::visit::Action { - gix_traverse::tree::visit::Action::Continue - } - - fn visit_nontree(&mut self, entry: &EntryRef<'_>) -> gix_traverse::tree::visit::Action { - if entry.mode.is_blob() { - (self.push)( - Change::Modification { - previous_entry_mode: entry.mode, - previous_oid: gix_hash::ObjectId::null(entry.oid.kind()), - entry_mode: entry.mode, - oid: entry.oid.to_owned(), - }, - self.recorder.path(), - ); - } - gix_traverse::tree::visit::Action::Continue - } + )?) } } diff --git a/gix/src/object/tree/diff/mod.rs b/gix/src/object/tree/diff/mod.rs index 354315cbbca..72dd48f4044 100644 --- a/gix/src/object/tree/diff/mod.rs +++ b/gix/src/object/tree/diff/mod.rs @@ -1,7 +1,7 @@ +use gix_diff::tree; use gix_diff::tree::recorder::Location; -use crate::bstr::BString; -use crate::{bstr::BStr, diff::Rewrites, Tree}; +use crate::{bstr::BStr, diff::Rewrites, Id, Tree}; /// Returned by the `for_each` function to control flow. #[derive(Default, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)] @@ -15,25 +15,100 @@ pub enum Action { /// Represents any possible change in order to turn one tree into another. #[derive(Debug, Clone, Copy)] -pub struct Change<'a, 'old, 'new> { - /// The location of the file or directory described by `event`, if tracking was enabled. +pub enum Change<'a, 'old, 'new> { + /// An entry was added, like the addition of a file or directory. + Addition { + /// The location of the file or directory, if [tracking](Platform::track_path) was enabled. + /// + /// It may be empty if neither [file names](Platform::track_filename()) nor [file paths](Platform::track_path()) + /// are tracked. + location: &'a BStr, + /// The mode of the added entry. + entry_mode: gix_object::tree::EntryMode, + /// Identifies a relationship between this instance and another one, + /// making it easy to reconstruct the top-level of directory changes. + relation: Option, + /// The object id of the added entry. + id: Id<'new>, + }, + /// An entry was deleted, like the deletion of a file or directory. + Deletion { + /// The location of the file or directory, if [tracking](Platform::track_path) was enabled. + /// + /// Otherwise, this value is always an empty path. + location: &'a BStr, + /// The mode of the deleted entry. + entry_mode: gix_object::tree::EntryMode, + /// Identifies a relationship between this instance and another one, + /// making it easy to reconstruct the top-level of directory changes. + relation: Option, + /// The object id of the deleted entry. + id: Id<'old>, + }, + /// An entry was modified, e.g. changing the contents of a file adjusts its object id and turning + /// a file into a symbolic link adjusts its mode. + Modification { + /// The location of the file or directory, if [tracking](Platform::track_path) was enabled. + /// + /// It may be empty if neither [file names](Platform::track_filename()) nor [file paths](Platform::track_path()) + /// are tracked. + location: &'a BStr, + /// The mode of the entry before the modification. + previous_entry_mode: gix_object::tree::EntryMode, + /// The object id of the entry before the modification. + previous_id: Id<'old>, + + /// The mode of the entry after the modification. + entry_mode: gix_object::tree::EntryMode, + /// The object id after the modification. + id: Id<'new>, + }, + /// Entries are considered rewritten if they are not trees and they, according to some understanding of identity, were renamed + /// or copied. + /// In case of renames, this means they originally appeared as [`Deletion`](Change::Deletion) signalling their source as well as an + /// [`Addition`](Change::Addition) acting as destination. /// - /// Otherwise, this value is always an empty path. - pub location: &'a BStr, - /// The diff event itself to provide information about what would need to change. - pub event: change::Event<'a, 'old, 'new>, -} - -/// Represents any possible change in order to turn one tree into another, the fully owned version -/// of [`Change`]. -#[derive(Debug, Clone)] -pub struct ChangeDetached { - /// The location of the file or directory described by `event`, if tracking was enabled. + /// In case of copies, the `copy` flag is true and typically represents a perfect copy of a source was made. + /// + /// This variant can only be encountered if [rewrite tracking](Platform::track_rewrites()) is enabled. /// - /// Otherwise, this value is always an empty path. - pub location: BString, - /// The diff event itself to provide information about what would need to change. - pub event: change::EventDetached, + /// Note that mode changes may have occurred as well, i.e. changes from executable to non-executable or vice-versa. + Rewrite { + /// The location of the source of the rename operation. + /// + /// It may be empty if neither [file names](Platform::track_filename()) nor [file paths](Platform::track_path()) + /// are tracked. + source_location: &'a BStr, + /// Identifies a relationship between the source and another source, + /// making it easy to reconstruct the top-level of directory changes. + source_relation: Option, + /// The mode of the entry before the rename. + source_entry_mode: gix_object::tree::EntryMode, + /// The object id of the entry before the rename. + /// + /// Note that this is the same as `id` if we require the [similarity to be 100%](Rewrites::percentage), but may + /// be different otherwise. + source_id: Id<'old>, + /// Information about the diff we performed to detect similarity and match the `source_id` with the current state at `id`. + /// It's `None` if `source_id` is equal to `id`, as identity made an actual diff computation unnecessary. + diff: Option, + /// The mode of the entry after the rename. + /// It could differ but still be considered a rename as we are concerned only about content. + entry_mode: gix_object::tree::EntryMode, + /// The location of the destination file or directory, if [tracking](Platform::track_path) was enabled. + /// + /// It may be empty if neither [file names](Platform::track_filename()) nor [file paths](Platform::track_path()) + /// are tracked. + location: &'a BStr, + /// The object id after the rename. + id: Id<'new>, + /// Identifies a relationship between this destination and another destination, + /// making it easy to reconstruct the top-level of directory changes. + relation: Option, + /// If true, this rewrite is created by copy, and `source_id` is pointing to its source. Otherwise, it's a rename, and `source_id` + /// points to a deleted object, as renames are tracked as deletions and additions of the same or similar content. + copy: bool, + }, } /// @@ -58,7 +133,7 @@ impl<'repo> Tree<'repo> { Ok(Platform { state: Default::default(), lhs: self, - tracking: None, + location: None, rewrites: self.repo.config.diff_renames()?.unwrap_or_default().into(), }) } @@ -69,7 +144,7 @@ impl<'repo> Tree<'repo> { pub struct Platform<'a, 'repo> { state: gix_diff::tree::State, lhs: &'a Tree<'repo>, - tracking: Option, + location: Option, rewrites: Option, } @@ -77,7 +152,7 @@ pub struct Platform<'a, 'repo> { impl Platform<'_, '_> { /// Keep track of file-names, which makes the [`location`][Change::location] field usable with the filename of the changed item. pub fn track_filename(&mut self) -> &mut Self { - self.tracking = Some(Location::FileName); + self.location = Some(Location::FileName); self } @@ -85,7 +160,7 @@ impl Platform<'_, '_> { /// /// This makes the [`location`][Change::location] field usable. pub fn track_path(&mut self) -> &mut Self { - self.tracking = Some(Location::Path); + self.location = Some(Location::Path); self } diff --git a/gix/tests/object/tree/diff.rs b/gix/tests/object/tree/diff.rs index 0fc92644d99..477356b3bdf 100644 --- a/gix/tests/object/tree/diff.rs +++ b/gix/tests/object/tree/diff.rs @@ -1,9 +1,6 @@ use std::convert::Infallible; -use gix::{ - bstr::BString, - object::{blob::diff::lines::Change, tree::diff::change::Event}, -}; +use gix::object::{blob::diff::lines, tree::diff::Change}; use gix_object::{bstr::ByteSlice, tree::EntryKind}; use crate::named_repo; @@ -27,13 +24,18 @@ fn changes_against_tree_modified() -> crate::Result { let (expected_previous_entry_mode, expected_previous_data, expected_entry_mode, expected_data) = expected_modifications[i]; - assert_eq!(change.location, "", "without configuration the location field is empty"); - match change.event { - Event::Modification { + assert_eq!( + change.location(), + "", + "without configuration the location field is empty" + ); + match change { + Change::Modification { previous_entry_mode, previous_id, entry_mode, id, + .. } => { assert_eq!(previous_entry_mode.kind(), expected_previous_entry_mode); assert_eq!(entry_mode.kind(), expected_entry_mode); @@ -46,7 +48,7 @@ fn changes_against_tree_modified() -> crate::Result { assert_eq!(previous_id.object().unwrap().data.as_bstr(), expected_previous_data); assert_eq!(id.object().unwrap().data.as_bstr(), expected_data); } - Event::Rewrite { .. } | Event::Deletion { .. } | Event::Addition { .. } => { + Change::Rewrite { .. } | Change::Deletion { .. } | Change::Addition { .. } => { unreachable!("only modification is expected") } }; @@ -57,12 +59,12 @@ fn changes_against_tree_modified() -> crate::Result { assert_eq!(count.removals, 0); diff.lines(|hunk| { match hunk { - Change::Deletion { .. } => unreachable!("there was no deletion"), - Change::Addition { lines } => assert_eq!( + lines::Change::Deletion { .. } => unreachable!("there was no deletion"), + lines::Change::Addition { lines } => assert_eq!( lines, vec![expected_data[expected_previous_data.len()..].as_bytes().as_bstr()] ), - Change::Modification { .. } => unreachable!("there was no modification"), + lines::Change::Modification { .. } => unreachable!("there was no modification"), }; Ok::<_, Infallible>(()) }) @@ -86,310 +88,21 @@ fn changes_against_tree_modified() -> crate::Result { Ok(()) } -#[test] -fn changes_against_tree_with_filename_tracking() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = repo.empty_tree(); - let to = tree_named(&repo, ":/c1 - initial"); - - let mut expected = vec!["a", "b", "c", "d"]; - from.changes()? - .track_filename() - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - expected.retain(|name| name != change.location); - Ok(Default::default()) - })?; - assert_eq!(expected, Vec::<&str>::new(), "all paths should have been seen"); - - let mut expected = vec!["a", "b", "dir/c", "d"]; - from.changes()? - .track_path() - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - expected.retain(|name| name != change.location); - Ok(Default::default()) - })?; - assert_eq!(expected, Vec::<&str>::new(), "all paths should have been seen"); - - let err = from - .changes()? - .track_path() - .for_each_to_obtain_tree(&to, |_change| { - Err(std::io::Error::new(std::io::ErrorKind::Other, "custom error")) - }) - .unwrap_err(); - assert_eq!( - err.to_string(), - "The user-provided callback failed", - "custom errors made visible and not squelched" - ); - Ok(()) -} - -#[test] -fn changes_against_modified_tree_with_filename_tracking() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/c3-modification}~1"); - let to = tree_named(&repo, ":/c3-modification"); - - let mut expected = vec!["a", "dir", "c"]; - from.changes()? - .track_filename() - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - expected.retain(|name| name != change.location); - Ok(Default::default()) - })?; - assert_eq!(expected, Vec::<&str>::new(), "all paths should have been seen"); - - let mut expected = vec!["a", "dir", "dir/c"]; - from.changes()? - .track_path() - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - expected.retain(|name| name != change.location); - Ok(Default::default()) - })?; - assert_eq!(expected, Vec::<&str>::new(), "all paths should have been seen"); - - let err = from - .changes()? - .track_path() - .for_each_to_obtain_tree(&to, |_change| { - Err(std::io::Error::new(std::io::ErrorKind::Other, "custom error")) - }) - .unwrap_err(); - assert_eq!( - err.to_string(), - "The user-provided callback failed", - "custom errors made visible and not squelched" - ); - Ok(()) -} - -fn tree_named(repo: &gix::Repository, rev_spec: impl AsRef) -> gix::Tree { - repo.rev_parse_single(rev_spec.as_ref()) - .unwrap() - .object() - .unwrap() - .peel_to_kind(gix::object::Kind::Tree) - .unwrap() - .into_tree() -} - mod track_rewrites { use std::collections::HashMap; use std::convert::Infallible; use gix::{ diff::{ - blob::DiffLineStats, rewrites::{Copies, CopySource}, Rewrites, }, - object::tree::diff::change::Event, + object::tree::diff::Change, }; use gix_ref::bstr::BStr; + use crate::object::tree::diff::tree_named; use crate::util::named_subrepo_opts; - use crate::{ - object::tree::diff::{added, deleted, modified, store, tree_named}, - util::named_repo, - }; - - #[test] - fn renames_by_identity() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - for (commit_msg, expected, assert_msg) in [ - ( - "r1-identity", - vec![BStr::new("a"), "dir/a-moved".into()], - "one rename and nothing else", - ), - ( - "r2-ambiguous", - vec![ - "s1".into(), - "b1".into(), - "s2".into(), - "b2".into(), - "s3".into(), - "z".into(), - ], - "multiple possible sources decide by ordering everything lexicographically", - ), - ( - "r4-symlinks", - vec!["link-1".into(), "renamed-link-1".into()], - "symlinks are only tracked by identity", - ), - ( - "c4 - add identical files", - vec![], - "not having any renames is OK as well", - ), - ("tc1-identity", vec![], "copy tracking is off by default"), - ] { - let from = tree_named(&repo, format!("@^{{/{commit_msg}}}~1")); - let to = tree_named(&repo, format!(":/{commit_msg}")); - - for percentage in [None, Some(0.5)] { - let mut actual = Vec::new(); - #[cfg_attr(windows, allow(unused_variables))] - let out = from - .changes()? - .track_path() - .track_rewrites( - Rewrites { - percentage, - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { - source_location, copy, .. - } = change.event - { - actual.push(source_location.to_owned()); - actual.push(change.location.to_owned()); - assert!(!copy); - } - } - Ok(Default::default()) - })?; - assert_eq!(actual, expected, "{assert_msg}"); - #[cfg(not(windows))] - assert_eq!( - out.rewrites.expect("present as its configured").num_similarity_checks, - 0, - "even though fuzzy checks are enabled, we don't end up using them" - ); - } - } - Ok(()) - } - - #[test] - fn rename_by_similarity() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/r3-simple}~1"); - let to = tree_named(&repo, ":/r3-simple"); - - for percentage in [ - None, - Some(0.76), /*cutoff point where git stops seeing it as equal */ - ] { - let mut actual = Vec::new(); - let mut rewrite_count = 0; - let out = from - .changes()? - .track_path() - .track_rewrites( - Rewrites { - percentage, - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { .. } = change.event { - rewrite_count += 0; - } else { - actual.push(change.location.to_owned()); - } - } - Ok(Default::default()) - })?; - assert_eq!( - actual, - vec![BStr::new("b"), "dir/c".into(), "dir/c-moved".into()], - "these items include no rewrite as the cut-off is chosen accordingly" - ); - if percentage.is_some() { - assert_eq!( - out.rewrites - .expect("always set as rewrite tracking is configured") - .num_similarity_checks, - 1 - ); - } - } - - let mut actual = Vec::new(); - let out = from - .changes()? - .track_path() - .track_rewrites( - Rewrites { - percentage: Some(0.6), - limit: 1, // has no effect as it's just one item here. - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { - source_location, copy, .. - } = change.event - { - actual.push(source_location.to_owned()); - actual.push(change.location.to_owned()); - assert!(!copy); - } - } - Ok(Default::default()) - })?; - assert_eq!( - actual, - vec![BStr::new("dir/c"), "dir/c-moved".into()], - "it found all items at the cut-off point, similar to git" - ); - let out = out.rewrites.expect("tracking enabled"); - assert_eq!(out.num_similarity_checks, 1); - assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); - assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); - - Ok(()) - } - - #[test] - fn renames_by_similarity_with_limit() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/r5}~1"); - let to = tree_named(&repo, ":/r5"); - - let mut actual = Vec::new(); - let out = from - .changes()? - .track_path() - .track_rewrites( - Rewrites { - limit: 1, // prevent fuzzy tracking from happening - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { .. } = change.event { - unreachable!("fuzzy tracking is effecitively disabled due to limit"); - } - actual.push(change.location.to_owned()); - } - Ok(Default::default()) - })?; - assert_eq!( - actual, - vec![BStr::new("f1"), "f1-renamed".into(), "f2".into(), "f2-renamed".into()], - ); - let out = out.rewrites.expect("tracking enabled"); - assert_eq!(out.num_similarity_checks, 0); - assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 4); - assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); - - Ok(()) - } #[test] #[cfg_attr( @@ -434,22 +147,23 @@ mod track_rewrites { .into(), ) .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if let Event::Rewrite { + if let Change::Rewrite { source_location, diff: Some(diff), + location, .. - } = change.event + } = change { // Round to percentage points to avoid floating point error. let similarity = (diff.similarity * 100.0) as u32; - let v = expected.remove(change.location); + let v = expected.remove(location); assert_eq!(v, Some((source_location, similarity))); } Ok(Default::default()) })?; assert_eq!(expected, HashMap::new()); - let out = out.rewrites.expect("tracking enabled"); + let out = out.expect("tracking enabled"); assert_eq!( out.num_similarity_checks, 21, "this probably increases once the algorithm improves" @@ -465,842 +179,14 @@ mod track_rewrites { Ok(()) } - - #[test] - fn copies_by_identity() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/tc1-identity}~1"); - let to = tree_named(&repo, ":/tc1-identity"); - - let mut actual = Vec::new(); - let out = from - .changes()? - .track_path() - .track_rewrites( - Rewrites { - copies: Some(Copies { - source: CopySource::FromSetOfModifiedFiles, - percentage: None, - }), - limit: 1, // the limit isn't actually used for identity based checks - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { - source_location, copy, .. - } = change.event - { - actual.push(source_location.to_owned()); - actual.push(change.location.to_owned()); - assert!(copy); - } - } - Ok(Default::default()) - })?; - assert_eq!( - actual, - vec![ - BStr::new("base"), - "c1".into(), - "base".into(), - "c2".into(), - "base".into(), - "dir/c3".into() - ], - ); - let out = out.rewrites.expect("tracking enabled"); - assert_eq!(out.num_similarity_checks, 0); - assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); - assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); - - Ok(()) - } - - #[test] - fn copies_by_similarity() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/tc2-similarity}~1"); - let to = tree_named(&repo, ":/tc2-similarity"); - - let mut actual = Vec::new(); - let mut stat = None; - let out = from - .changes()? - .track_path() - .track_rewrites( - Rewrites { - copies: Some(Copies::default()), - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { - source_location, - copy, - diff, - .. - } = change.event - { - actual.push(source_location.to_owned()); - actual.push(change.location.to_owned()); - stat = diff; - assert!(copy); - } - } - Ok(Default::default()) - })?; - assert_eq!( - actual, - vec![ - BStr::new("base"), - "c4".into(), - "base".into(), - "c5".into(), - "base".into(), - "dir/c6".into() - ], - ); - assert_eq!( - stat, - Some(DiffLineStats { - removals: 0, - insertions: 1, - before: 11, - after: 12, - similarity: 0.8888889 - }), - "by similarity there is a diff" - ); - - let out = out.rewrites.expect("tracking enabled"); - assert_eq!( - out.num_similarity_checks, 2, - "two are similar, the other one is identical" - ); - assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); - assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); - - Ok(()) - } - - #[test] - fn copies_in_entire_tree_by_similarity() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/tc3-find-harder}~1"); - let to = tree_named(&repo, ":/tc3-find-harder"); - - let mut actual = Vec::new(); - let out = from - .changes()? - .track_path() - .track_rewrites( - Rewrites { - copies: Some(Copies::default()), - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { .. } = change.event { - unreachable!("needs --find-copies-harder to detect them here") - } - actual.push(change.location.to_owned()); - } - Ok(Default::default()) - })?; - assert_eq!( - actual, - vec![BStr::new("b"), "c6".into(), "c7".into(), "newly-added".into(),], - ); - - let out = out.rewrites.expect("tracking enabled"); - assert_eq!( - out.num_similarity_checks, 3, - "it does have some candidates, probably for rename tracking" - ); - assert_eq!( - out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, - "no limit configured" - ); - assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); - - let mut actual = Vec::new(); - let mut stat = None; - let out = from - .changes()? - .track_path() - .track_rewrites( - Rewrites { - copies: Some(Copies { - source: CopySource::FromSetOfModifiedFilesAndAllSources, - ..Default::default() - }), - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { - copy, - diff, - source_location, - .. - } = change.event - { - actual.push(source_location.to_owned()); - actual.push(change.location.to_owned()); - stat = diff; - assert!(copy); - } - } - Ok(Default::default()) - })?; - assert_eq!( - actual, - vec![ - BStr::new("base"), - "c6".into(), - "dir/c6".into(), - "c7".into(), - "c5".into(), - "newly-added".into() - ] - ); - - let out = out.rewrites.expect("tracking enabled"); - assert_eq!( - stat, - Some(DiffLineStats { - removals: 0, - insertions: 3, - before: 12, - after: 15, - similarity: 0.75 - }), - "by similarity there is a diff" - ); - assert_eq!(out.num_similarity_checks, 4); - assert_eq!( - out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, - "no limit configured" - ); - assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); - - Ok(()) - } - - #[test] - fn copies_in_entire_tree_by_similarity_with_limit() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/tc3-find-harder}~1"); - let to = tree_named(&repo, ":/tc3-find-harder"); - - let mut actual = Vec::new(); - let mut stat = None; - let out = from - .changes()? - .track_path() - .track_rewrites( - Rewrites { - copies: Some(Copies { - source: CopySource::FromSetOfModifiedFilesAndAllSources, - ..Default::default() - }), - limit: 2, // similarity checks can't be made that way - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { - copy, - diff, - source_location, - .. - } = change.event - { - actual.push(source_location.to_owned()); - actual.push(change.location.to_owned()); - stat = diff; - assert!(copy); - } - } - Ok(Default::default()) - })?; - assert_eq!( - actual, - vec![BStr::new("base"), "c6".into(), "dir/c6".into(), "c7".into(),], - "identification by identity, which is fast due to binary search" - ); - - let out = out.rewrites.expect("tracking enabled"); - assert_eq!(stat, None, "similarity can't run"); - assert_eq!(out.num_similarity_checks, 0); - assert_eq!( - out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, - "no limit configured" - ); - assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 19); - - Ok(()) - } - - #[test] - fn copies_by_similarity_with_limit() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/tc2-similarity}~1"); - let to = tree_named(&repo, ":/tc2-similarity"); - - let mut actual = Vec::new(); - let mut stat = None; - let out = from - .changes()? - .track_path() - .track_rewrites( - Rewrites { - copies: Some(Copies::default()), - limit: 1, - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { - source_location, - copy, - diff, - .. - } = change.event - { - actual.push(source_location.to_owned()); - actual.push(change.location.to_owned()); - stat = diff; - assert!(copy); - } - } - Ok(Default::default()) - })?; - assert_eq!( - actual, - vec![BStr::new("base"), "c4".into()], - "the limit prevents any similarity check from being performed, and identity fails everywhere" - ); - assert_eq!(stat, None, "by identity there is no diff"); - - let out = out.rewrites.expect("tracking enabled"); - assert_eq!(out.num_similarity_checks, 0); - assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); - assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 2); - - Ok(()) - } - - #[test] - fn realistic_renames() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/r1-change}~1"); - let to = tree_named(&repo, ":/r1-change"); - - let mut actual = Vec::new(); - let mut other = Vec::new(); - from.changes()? - .track_path() - .track_rewrites( - Rewrites { - copies: Some(Copies::default()), - limit: 1, - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { - source_location, copy, .. - } = change.event - { - actual.push(source_location.to_owned()); - actual.push(change.location.to_owned()); - assert!(!copy); - } else { - other.push(store(&change)); - } - } - Ok(Default::default()) - })?; - - assert_eq!(actual, vec!["git-index/src/file.rs", "git-index/src/file/mod.rs"]); - assert_eq!( - other, - vec![ - added("git-index/tests/index/file/access.rs"), - modified("git-index/tests/index/file/mod.rs") - ] - ); - - #[cfg(not(windows))] - { - let actual = std::fs::read_to_string(repo.work_dir().expect("non-bare").join("baseline.with-renames"))?; - let expected = r#"commit 0231f5093bd3d760e7ee82984e0453da80e05c87 -Author: author -Date: Sat Jan 1 00:00:00 2000 +0000 - - r1-change - -diff --git a/git-index/src/file.rs b/git-index/src/file/mod.rs -similarity index 100% -rename from git-index/src/file.rs -rename to git-index/src/file/mod.rs -diff --git a/git-index/tests/index/file/access.rs b/git-index/tests/index/file/access.rs -new file mode 100644 -index 0000000..e69de29 -diff --git a/git-index/tests/index/file/mod.rs b/git-index/tests/index/file/mod.rs -index e69de29..8ba3a16 100644 ---- a/git-index/tests/index/file/mod.rs -+++ b/git-index/tests/index/file/mod.rs -@@ -0,0 +1 @@ -+n -"#; - assert_eq!(actual, expected); - } - - Ok(()) - } - - #[test] - fn realistic_renames_disabled() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/r1-change}~1"); - let to = tree_named(&repo, ":/r1-change"); - - let mut actual = Vec::new(); - from.changes()? - .track_path() - .track_rewrites(None) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - actual.push(store(&change)); - if let Event::Rewrite { .. } = change.event { - unreachable!("it's disabled, so cannot happen") - } - } - Ok(Default::default()) - })?; - - assert_eq!( - actual, - vec![ - deleted("git-index/src/file.rs"), - added("git-index/src/file/mod.rs"), - added("git-index/tests/index/file/access.rs"), - modified("git-index/tests/index/file/mod.rs") - ] - ); - - #[cfg(not(windows))] - { - let actual = std::fs::read_to_string(repo.work_dir().expect("non-bare").join("baseline.no-renames"))?; - let expected = r#"commit 0231f5093bd3d760e7ee82984e0453da80e05c87 -Author: author -Date: Sat Jan 1 00:00:00 2000 +0000 - - r1-change - -diff --git a/git-index/src/file.rs b/git-index/src/file.rs -deleted file mode 100644 -index e69de29..0000000 -diff --git a/git-index/src/file/mod.rs b/git-index/src/file/mod.rs -new file mode 100644 -index 0000000..e69de29 -diff --git a/git-index/tests/index/file/access.rs b/git-index/tests/index/file/access.rs -new file mode 100644 -index 0000000..e69de29 -diff --git a/git-index/tests/index/file/mod.rs b/git-index/tests/index/file/mod.rs -index e69de29..8ba3a16 100644 ---- a/git-index/tests/index/file/mod.rs -+++ b/git-index/tests/index/file/mod.rs -@@ -0,0 +1 @@ -+n -"#; - assert_eq!(actual, expected); - } - - Ok(()) - } - - #[test] - fn realistic_renames_disabled_2() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/r2-change}~1"); - let to = tree_named(&repo, ":/r2-change"); - - let mut actual = Vec::new(); - from.changes()? - .track_path() - .track_rewrites(None) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - actual.push(store(&change)); - if let Event::Rewrite { .. } = change.event { - unreachable!("it's disabled, so cannot happen") - } - } - Ok(Default::default()) - })?; - - #[cfg(not(windows))] - { - let expected = r#"commit d78c63c5ea3149040767e4387e7fc743cda118fd -Author: author -Date: Sat Jan 1 00:00:00 2000 +0000 - - r2-change - -diff --git a/git-sec/CHANGELOG.md b/git-sec/CHANGELOG.md -deleted file mode 100644 -index e69de29..0000000 -diff --git a/git-sec/Cargo.toml b/git-sec/Cargo.toml -deleted file mode 100644 -index e69de29..0000000 -diff --git a/git-sec/src/identity.rs b/git-sec/src/identity.rs -deleted file mode 100644 -index e69de29..0000000 -diff --git a/git-sec/src/lib.rs b/git-sec/src/lib.rs -deleted file mode 100644 -index e69de29..0000000 -diff --git a/git-sec/src/permission.rs b/git-sec/src/permission.rs -deleted file mode 100644 -index e69de29..0000000 -diff --git a/git-sec/src/trust.rs b/git-sec/src/trust.rs -deleted file mode 100644 -index e69de29..0000000 -diff --git a/git-sec/tests/identity/mod.rs b/git-sec/tests/identity/mod.rs -deleted file mode 100644 -index e69de29..0000000 -diff --git a/git-sec/tests/sec.rs b/git-sec/tests/sec.rs -deleted file mode 100644 -index e69de29..0000000 -diff --git a/gix-sec/CHANGELOG.md b/gix-sec/CHANGELOG.md -new file mode 100644 -index 0000000..e69de29 -diff --git a/gix-sec/Cargo.toml b/gix-sec/Cargo.toml -new file mode 100644 -index 0000000..e69de29 -diff --git a/gix-sec/src/identity.rs b/gix-sec/src/identity.rs -new file mode 100644 -index 0000000..e69de29 -diff --git a/gix-sec/src/lib.rs b/gix-sec/src/lib.rs -new file mode 100644 -index 0000000..e69de29 -diff --git a/gix-sec/src/permission.rs b/gix-sec/src/permission.rs -new file mode 100644 -index 0000000..e69de29 -diff --git a/gix-sec/src/trust.rs b/gix-sec/src/trust.rs -new file mode 100644 -index 0000000..e69de29 -diff --git a/gix-sec/tests/identity/mod.rs b/gix-sec/tests/identity/mod.rs -new file mode 100644 -index 0000000..e69de29 -diff --git a/gix-sec/tests/sec.rs b/gix-sec/tests/sec.rs -new file mode 100644 -index 0000000..e69de29 -"#; - assert_eq!( - std::fs::read_to_string(repo.work_dir().expect("non-bare").join("baseline-2.no-renames"))?, - expected - ); - } - - assert_eq!( - actual, - vec![ - deleted("git-sec/CHANGELOG.md"), - deleted("git-sec/Cargo.toml"), - added("gix-sec/CHANGELOG.md"), - added("gix-sec/Cargo.toml"), - deleted("git-sec/src/identity.rs"), - deleted("git-sec/src/lib.rs"), - deleted("git-sec/src/permission.rs"), - deleted("git-sec/src/trust.rs"), - deleted("git-sec/tests/sec.rs"), - added("gix-sec/src/identity.rs"), - added("gix-sec/src/lib.rs"), - added("gix-sec/src/permission.rs"), - added("gix-sec/src/trust.rs"), - added("gix-sec/tests/sec.rs"), - deleted("git-sec/tests/identity/mod.rs"), - added("gix-sec/tests/identity/mod.rs"), - ] - ); - - Ok(()) - } - - #[test] - fn realistic_renames_disabled_3() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/r3-change}~1"); - let to = tree_named(&repo, ":/r3-change"); - - let mut actual = Vec::new(); - from.changes()? - .track_path() - .track_rewrites(None) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - actual.push(store(&change)); - if let Event::Rewrite { .. } = change.event { - unreachable!("it's disabled, so cannot happen") - } - } - Ok(Default::default()) - })?; - - #[cfg(not(windows))] - { - let expected = r#"commit 0cf7a4fe3ad6c49ae7beb394a1c1df7cc5173ce4 -Author: author -Date: Sat Jan 1 00:00:00 2000 +0000 - - r3-change - -diff --git a/src/ein.rs b/src/ein.rs -new file mode 100644 -index 0000000..e69de29 -diff --git a/src/gix.rs b/src/gix.rs -new file mode 100644 -index 0000000..e69de29 -diff --git a/src/plumbing-cli.rs b/src/plumbing-cli.rs -deleted file mode 100644 -index e69de29..0000000 -diff --git a/src/porcelain-cli.rs b/src/porcelain-cli.rs -deleted file mode 100644 -index e69de29..0000000 -"#; - - assert_eq!( - std::fs::read_to_string(repo.work_dir().expect("non-bare").join("baseline-3.no-renames"))?, - expected - ); - } - assert_eq!( - actual, - vec![ - added("src/ein.rs"), - added("src/gix.rs"), - deleted("src/plumbing-cli.rs"), - deleted("src/porcelain-cli.rs"), - ] - ); - - Ok(()) - } - - #[test] - fn realistic_renames_3() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/r3-change}~1"); - let to = tree_named(&repo, ":/r3-change"); - - let mut actual = Vec::new(); - let mut other = Vec::new(); - from.changes()? - .track_path() - .track_rewrites( - Rewrites { - copies: Some(Copies::default()), - limit: 1, - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { - source_location, copy, .. - } = change.event - { - actual.push(source_location.to_owned()); - actual.push(change.location.to_owned()); - assert!(!copy); - } else { - other.push(store(&change)); - } - } - Ok(Default::default()) - })?; - - #[cfg(not(windows))] - { - let expected = r#"commit 0cf7a4fe3ad6c49ae7beb394a1c1df7cc5173ce4 -Author: author -Date: Sat Jan 1 00:00:00 2000 +0000 - - r3-change - -diff --git a/src/plumbing-cli.rs b/src/ein.rs -similarity index 100% -rename from src/plumbing-cli.rs -rename to src/ein.rs -diff --git a/src/porcelain-cli.rs b/src/gix.rs -similarity index 100% -rename from src/porcelain-cli.rs -rename to src/gix.rs -"#; - assert_eq!( - std::fs::read_to_string(repo.work_dir().expect("non-bare").join("baseline-3.with-renames"))?, - expected - ); - } - assert_eq!( - actual, - vec![ - "src/plumbing-cli.rs", - "src/ein.rs", - "src/porcelain-cli.rs", - "src/gix.rs" - ] - ); - assert!(other.is_empty()); - - Ok(()) - } - - #[test] - fn realistic_renames_2() -> crate::Result { - let repo = named_repo("make_diff_repo.sh")?; - let from = tree_named(&repo, "@^{/r2-change}~1"); - let to = tree_named(&repo, ":/r2-change"); - - let mut actual = Vec::new(); - from.changes()? - .track_path() - .track_rewrites( - Rewrites { - copies: Some(Copies::default()), - limit: 1, - ..Default::default() - } - .into(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if !change.event.entry_mode().is_tree() { - if let Event::Rewrite { - source_location, copy, .. - } = change.event - { - actual.push(source_location.to_owned()); - actual.push(change.location.to_owned()); - assert!(!copy); - } else { - unreachable!("everything is a rewrite"); - } - } - Ok(Default::default()) - })?; - - #[cfg(not(windows))] - { - let expected = r#"commit d78c63c5ea3149040767e4387e7fc743cda118fd -Author: author -Date: Sat Jan 1 00:00:00 2000 +0000 - - r2-change - -diff --git a/git-sec/CHANGELOG.md b/gix-sec/CHANGELOG.md -similarity index 100% -rename from git-sec/CHANGELOG.md -rename to gix-sec/CHANGELOG.md -diff --git a/git-sec/Cargo.toml b/gix-sec/Cargo.toml -similarity index 100% -rename from git-sec/Cargo.toml -rename to gix-sec/Cargo.toml -diff --git a/git-sec/src/identity.rs b/gix-sec/src/identity.rs -similarity index 100% -rename from git-sec/src/identity.rs -rename to gix-sec/src/identity.rs -diff --git a/git-sec/src/lib.rs b/gix-sec/src/lib.rs -similarity index 100% -rename from git-sec/src/lib.rs -rename to gix-sec/src/lib.rs -diff --git a/git-sec/src/permission.rs b/gix-sec/src/permission.rs -similarity index 100% -rename from git-sec/src/permission.rs -rename to gix-sec/src/permission.rs -diff --git a/git-sec/src/trust.rs b/gix-sec/src/trust.rs -similarity index 100% -rename from git-sec/src/trust.rs -rename to gix-sec/src/trust.rs -diff --git a/git-sec/tests/identity/mod.rs b/gix-sec/tests/identity/mod.rs -similarity index 100% -rename from git-sec/tests/identity/mod.rs -rename to gix-sec/tests/identity/mod.rs -diff --git a/git-sec/tests/sec.rs b/gix-sec/tests/sec.rs -similarity index 100% -rename from git-sec/tests/sec.rs -rename to gix-sec/tests/sec.rs -"#; - assert_eq!( - std::fs::read_to_string(repo.work_dir().expect("non-bare").join("baseline-2.with-renames"))?, - expected - ); - } - - assert_eq!( - actual, - vec![ - "git-sec/CHANGELOG.md", - "gix-sec/CHANGELOG.md", - "git-sec/Cargo.toml", - "gix-sec/Cargo.toml", - "git-sec/src/identity.rs", - "gix-sec/src/identity.rs", - "git-sec/src/lib.rs", - "gix-sec/src/lib.rs", - "git-sec/src/permission.rs", - "gix-sec/src/permission.rs", - "git-sec/src/trust.rs", - "gix-sec/src/trust.rs", - "git-sec/tests/sec.rs", - "gix-sec/tests/sec.rs", - "git-sec/tests/identity/mod.rs", - "gix-sec/tests/identity/mod.rs", - ] - ); - - Ok(()) - } -} -fn store(change: &gix::object::tree::diff::Change<'_, '_, '_>) -> (char, BString) { - (shorthand(&change.event), change.location.to_owned()) } -fn added(path: &str) -> (char, BString) { - ('A', path.into()) -} - -fn deleted(path: &str) -> (char, BString) { - ('D', path.into()) -} - -fn modified(path: &str) -> (char, BString) { - ('M', path.into()) -} - -fn shorthand(change: &Event) -> char { - match change { - Event::Addition { .. } => 'A', - Event::Deletion { .. } => 'D', - Event::Modification { .. } => 'M', - Event::Rewrite { .. } => 'R', - } +fn tree_named(repo: &gix::Repository, rev_spec: impl AsRef) -> gix::Tree { + repo.rev_parse_single(rev_spec.as_ref()) + .unwrap() + .object() + .unwrap() + .peel_to_kind(gix::object::Kind::Tree) + .unwrap() + .into_tree() } From 743695fc345b59e30e75fb6b91357ab7e994bda2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 8 Oct 2024 20:48:34 +0200 Subject: [PATCH 11/13] refactor!: always trackt he full path when producing diffs, but allow to disable it. --- gix/src/object/tree/diff/mod.rs | 10 ++++++++-- gix/tests/object/tree/diff.rs | 7 +++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/gix/src/object/tree/diff/mod.rs b/gix/src/object/tree/diff/mod.rs index 72dd48f4044..1070effd3ab 100644 --- a/gix/src/object/tree/diff/mod.rs +++ b/gix/src/object/tree/diff/mod.rs @@ -133,7 +133,7 @@ impl<'repo> Tree<'repo> { Ok(Platform { state: Default::default(), lhs: self, - location: None, + location: Some(Location::Path), rewrites: self.repo.config.diff_renames()?.unwrap_or_default().into(), }) } @@ -150,13 +150,19 @@ pub struct Platform<'a, 'repo> { /// Configuration impl Platform<'_, '_> { + /// Do not keep track of filepaths at all, which will leave all [`location`][Change::location] fields empty. + pub fn no_locations(&mut self) -> &mut Self { + self.location = Some(Location::FileName); + self + } + /// Keep track of file-names, which makes the [`location`][Change::location] field usable with the filename of the changed item. pub fn track_filename(&mut self) -> &mut Self { self.location = Some(Location::FileName); self } - /// Keep track of the entire path of a change, relative to the repository. + /// Keep track of the entire path of a change, relative to the repository. (default). /// /// This makes the [`location`][Change::location] field usable. pub fn track_path(&mut self) -> &mut Self { diff --git a/gix/tests/object/tree/diff.rs b/gix/tests/object/tree/diff.rs index 477356b3bdf..8c39004c14b 100644 --- a/gix/tests/object/tree/diff.rs +++ b/gix/tests/object/tree/diff.rs @@ -24,10 +24,9 @@ fn changes_against_tree_modified() -> crate::Result { let (expected_previous_entry_mode, expected_previous_data, expected_entry_mode, expected_data) = expected_modifications[i]; - assert_eq!( - change.location(), - "", - "without configuration the location field is empty" + assert!( + !change.location().is_empty(), + "without configuration the location field is set" ); match change { Change::Modification { From 2b81e6c8bd30cc95e91cc92a89f0a0e6047eec6b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 8 Oct 2024 20:15:07 +0200 Subject: [PATCH 12/13] feat: add `Repository::diff_tree_to_tree()` for greater similarity to `git2` --- Cargo.lock | 1 + gitoxide-core/src/hours/core.rs | 5 +- gitoxide-core/src/query/engine/update.rs | 5 +- gix/Cargo.toml | 1 + gix/src/diff.rs | 104 +++++++ gix/src/object/tree/diff/for_each.rs | 6 +- gix/src/object/tree/diff/mod.rs | 66 ++--- gix/src/repository/diff.rs | 44 ++- gix/src/repository/mod.rs | 16 ++ gix/tests/object/tree/diff.rs | 335 ++++++++++++++++++++++- 10 files changed, 514 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f6df147b05..82ee1a6e7bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1370,6 +1370,7 @@ dependencies = [ "gix-worktree 0.36.0", "gix-worktree-state", "gix-worktree-stream", + "insta", "is_ci", "once_cell", "parking_lot", diff --git a/gitoxide-core/src/hours/core.rs b/gitoxide-core/src/hours/core.rs index 7563e64f62d..58221ca638c 100644 --- a/gitoxide-core/src/hours/core.rs +++ b/gitoxide-core/src/hours/core.rs @@ -125,8 +125,9 @@ pub fn spawn_tree_delta_threads<'scope>( None => continue, }; from.changes()? - .track_filename() - .track_rewrites(None) + .options(|opts| { + opts.track_filename().track_rewrites(None); + }) .for_each_to_obtain_tree(&to, |change| { use gix::object::tree::diff::Change::*; changes.fetch_add(1, Ordering::Relaxed); diff --git a/gitoxide-core/src/query/engine/update.rs b/gitoxide-core/src/query/engine/update.rs index 060a87f6e19..5bd8c25e9b4 100644 --- a/gitoxide-core/src/query/engine/update.rs +++ b/gitoxide-core/src/query/engine/update.rs @@ -207,8 +207,9 @@ pub fn update( rewrite_cache.clear_resource_cache_keep_allocation(); diff_cache.clear_resource_cache_keep_allocation(); from.changes()? - .track_path() - .track_rewrites(Some(rewrites)) + .options(|opts| { + opts.track_path().track_rewrites(Some(rewrites)); + }) .for_each_to_obtain_tree_with_cache(&to, &mut rewrite_cache, |change| { use gix::object::tree::diff::Change::*; change_counter.fetch_add(1, Ordering::SeqCst); diff --git a/gix/Cargo.toml b/gix/Cargo.toml index fef8f3a48ba..cd1a6142f08 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -408,6 +408,7 @@ walkdir = "2.3.2" serial_test = { version = "3.1.0", default-features = false } async-std = { version = "1.12.0", features = ["attributes"] } termtree = "0.5.1" +insta = "1.40.0" [package.metadata.docs.rs] features = [ diff --git a/gix/src/diff.rs b/gix/src/diff.rs index 51d11063bff..5308c5c3fbd 100644 --- a/gix/src/diff.rs +++ b/gix/src/diff.rs @@ -1,5 +1,109 @@ +use gix_diff::tree::recorder::Location; pub use gix_diff::*; +/// +pub mod options { + /// + pub mod init { + /// The error returned when instantiating [diff options](crate::diff::Options). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[cfg(feature = "blob-diff")] + #[error(transparent)] + RewritesConfiguration(#[from] crate::diff::new_rewrites::Error), + } + } +} + +/// General diff-related options for configuring rename-tracking and blob diffs. +#[derive(Debug, Copy, Clone)] +pub struct Options { + location: Option, + #[cfg(feature = "blob-diff")] + rewrites: Option, +} + +impl Default for Options { + fn default() -> Self { + Options { + location: Some(Location::Path), + #[cfg(feature = "blob-diff")] + rewrites: None, + } + } +} + +#[cfg(feature = "blob-diff")] +impl From for gix_diff::tree_with_rewrites::Options { + fn from(opts: Options) -> Self { + gix_diff::tree_with_rewrites::Options { + location: opts.location, + #[cfg(feature = "blob-diff")] + rewrites: opts.rewrites, + } + } +} + +/// Lifecycle +impl Options { + #[cfg(feature = "blob-diff")] + pub(crate) fn from_configuration(config: &crate::config::Cache) -> Result { + Ok(Options { + location: Some(Location::Path), + rewrites: config.diff_renames()?.unwrap_or_default().into(), + }) + } +} + +/// Setters +impl Options { + /// Do not keep track of filepaths at all, which will leave all `location` fields empty. + pub fn no_locations(&mut self) -> &mut Self { + self.location = Some(Location::FileName); + self + } + + /// Keep track of file-names, which makes `location` fields usable with the filename of the changed item. + pub fn track_filename(&mut self) -> &mut Self { + self.location = Some(Location::FileName); + self + } + + /// Keep track of the entire path of a change, relative to the repository. (default). + /// + /// This makes the `location` field fully usable. + pub fn track_path(&mut self) -> &mut Self { + self.location = Some(Location::Path); + self + } + + /// Provide `None` to disable rewrite tracking entirely, or pass `Some()` to control to + /// what extent rename and copy tracking is performed. + /// + /// Note that by default, the git configuration determines rewrite tracking and git defaults are used + /// if nothing is configured, which turns rename tracking with 50% similarity on, while not tracking copies at all. + #[cfg(feature = "blob-diff")] + pub fn track_rewrites(&mut self, renames: Option) -> &mut Self { + self.rewrites = renames; + self + } +} + +/// Builder +impl Options { + /// Provide `None` to disable rewrite tracking entirely, or pass `Some()` to control to + /// what extent rename and copy tracking is performed. + /// + /// Note that by default, the git configuration determines rewrite tracking and git defaults are used + /// if nothing is configured, which turns rename tracking with 50% similarity on, while not tracking copies at all. + #[cfg(feature = "blob-diff")] + pub fn with_rewrites(mut self, renames: Option) -> Self { + self.rewrites = renames; + self + } +} + /// pub mod rename { /// Determine how to do rename tracking. diff --git a/gix/src/object/tree/diff/for_each.rs b/gix/src/object/tree/diff/for_each.rs index 1fce28ea2ec..0cb04e74039 100644 --- a/gix/src/object/tree/diff/for_each.rs +++ b/gix/src/object/tree/diff/for_each.rs @@ -74,6 +74,7 @@ impl<'old> Platform<'_, 'old> { } Some(cache) => cache, }; + let opts = self.options.into(); Ok(gix_diff::tree_with_rewrites( TreeRefIter::from_bytes(&self.lhs.data), TreeRefIter::from_bytes(&other.data), @@ -86,10 +87,7 @@ impl<'old> Platform<'_, 'old> { Action::Cancel => gix_diff::tree_with_rewrites::Action::Cancel, }) }, - gix_diff::tree_with_rewrites::Options { - location: self.location, - rewrites: self.rewrites, - }, + opts, )?) } } diff --git a/gix/src/object/tree/diff/mod.rs b/gix/src/object/tree/diff/mod.rs index 1070effd3ab..592600055f6 100644 --- a/gix/src/object/tree/diff/mod.rs +++ b/gix/src/object/tree/diff/mod.rs @@ -1,7 +1,6 @@ use gix_diff::tree; -use gix_diff::tree::recorder::Location; -use crate::{bstr::BStr, diff::Rewrites, Id, Tree}; +use crate::{bstr::BStr, Id, Tree}; /// Returned by the `for_each` function to control flow. #[derive(Default, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)] @@ -18,9 +17,9 @@ pub enum Action { pub enum Change<'a, 'old, 'new> { /// An entry was added, like the addition of a file or directory. Addition { - /// The location of the file or directory, if [tracking](Platform::track_path) was enabled. + /// The location of the file or directory, if [tracking](crate::diff::Options::track_path) was enabled. /// - /// It may be empty if neither [file names](Platform::track_filename()) nor [file paths](Platform::track_path()) + /// It may be empty if neither [file names](crate::diff::Options::track_filename()) nor [file paths](crate::diff::Options::track_path()) /// are tracked. location: &'a BStr, /// The mode of the added entry. @@ -33,7 +32,7 @@ pub enum Change<'a, 'old, 'new> { }, /// An entry was deleted, like the deletion of a file or directory. Deletion { - /// The location of the file or directory, if [tracking](Platform::track_path) was enabled. + /// The location of the file or directory, if [tracking](crate::diff::Options::track_path) was enabled. /// /// Otherwise, this value is always an empty path. location: &'a BStr, @@ -48,9 +47,9 @@ pub enum Change<'a, 'old, 'new> { /// An entry was modified, e.g. changing the contents of a file adjusts its object id and turning /// a file into a symbolic link adjusts its mode. Modification { - /// The location of the file or directory, if [tracking](Platform::track_path) was enabled. + /// The location of the file or directory, if [tracking](crate::diff::Options::track_path) was enabled. /// - /// It may be empty if neither [file names](Platform::track_filename()) nor [file paths](Platform::track_path()) + /// It may be empty if neither [file names](crate::diff::Options::track_filename()) nor [file paths](crate::diff::Options::track_path()) /// are tracked. location: &'a BStr, /// The mode of the entry before the modification. @@ -70,13 +69,13 @@ pub enum Change<'a, 'old, 'new> { /// /// In case of copies, the `copy` flag is true and typically represents a perfect copy of a source was made. /// - /// This variant can only be encountered if [rewrite tracking](Platform::track_rewrites()) is enabled. + /// This variant can only be encountered if [rewrite tracking](crate::diff::Options::track_rewrites()) is enabled. /// /// Note that mode changes may have occurred as well, i.e. changes from executable to non-executable or vice-versa. Rewrite { /// The location of the source of the rename operation. /// - /// It may be empty if neither [file names](Platform::track_filename()) nor [file paths](Platform::track_path()) + /// It may be empty if neither [file names](crate::diff::Options::track_filename()) nor [file paths](crate::diff::Options::track_path()) /// are tracked. source_location: &'a BStr, /// Identifies a relationship between the source and another source, @@ -86,7 +85,7 @@ pub enum Change<'a, 'old, 'new> { source_entry_mode: gix_object::tree::EntryMode, /// The object id of the entry before the rename. /// - /// Note that this is the same as `id` if we require the [similarity to be 100%](Rewrites::percentage), but may + /// Note that this is the same as `id` if we require the [similarity to be 100%](gix_diff::Rewrites::percentage), but may /// be different otherwise. source_id: Id<'old>, /// Information about the diff we performed to detect similarity and match the `source_id` with the current state at `id`. @@ -95,9 +94,9 @@ pub enum Change<'a, 'old, 'new> { /// The mode of the entry after the rename. /// It could differ but still be considered a rename as we are concerned only about content. entry_mode: gix_object::tree::EntryMode, - /// The location of the destination file or directory, if [tracking](Platform::track_path) was enabled. + /// The location of the destination file or directory, if [tracking](crate::diff::Options::track_path) was enabled. /// - /// It may be empty if neither [file names](Platform::track_filename()) nor [file paths](Platform::track_path()) + /// It may be empty if neither [file names](crate::diff::Options::track_filename()) nor [file paths](crate::diff::Options::track_path()) /// are tracked. location: &'a BStr, /// The object id after the rename. @@ -126,15 +125,14 @@ impl<'repo> Tree<'repo> { /// /// Note that if a clone with `--filter=blob=none` was created, rename tracking may fail as it might /// try to access blobs to compute a similarity metric. Thus, it's more compatible to turn rewrite tracking off - /// using [`Platform::track_rewrites()`]. + /// using [`Options::track_rewrites()`](crate::diff::Options::track_rewrites()). #[allow(clippy::result_large_err)] #[doc(alias = "diff_tree_to_tree", alias = "git2")] - pub fn changes<'a>(&'a self) -> Result, crate::diff::new_rewrites::Error> { + pub fn changes<'a>(&'a self) -> Result, crate::diff::options::init::Error> { Ok(Platform { state: Default::default(), lhs: self, - location: Some(Location::Path), - rewrites: self.repo.config.diff_renames()?.unwrap_or_default().into(), + options: crate::diff::Options::from_configuration(&self.repo.config)?, }) } } @@ -144,39 +142,13 @@ impl<'repo> Tree<'repo> { pub struct Platform<'a, 'repo> { state: gix_diff::tree::State, lhs: &'a Tree<'repo>, - location: Option, - rewrites: Option, + options: crate::diff::Options, } -/// Configuration impl Platform<'_, '_> { - /// Do not keep track of filepaths at all, which will leave all [`location`][Change::location] fields empty. - pub fn no_locations(&mut self) -> &mut Self { - self.location = Some(Location::FileName); - self - } - - /// Keep track of file-names, which makes the [`location`][Change::location] field usable with the filename of the changed item. - pub fn track_filename(&mut self) -> &mut Self { - self.location = Some(Location::FileName); - self - } - - /// Keep track of the entire path of a change, relative to the repository. (default). - /// - /// This makes the [`location`][Change::location] field usable. - pub fn track_path(&mut self) -> &mut Self { - self.location = Some(Location::Path); - self - } - - /// Provide `None` to disable rewrite tracking entirely, or pass `Some()` to control to - /// what extent rename and copy tracking is performed. - /// - /// Note that by default, the git configuration determines rewrite tracking and git defaults are used - /// if nothing is configured, which turns rename tracking with 50% similarity on, while not tracking copies at all. - pub fn track_rewrites(&mut self, renames: Option) -> &mut Self { - self.rewrites = renames; + /// Adjust diff options with `change_opts`. + pub fn options(&mut self, change_opts: impl FnOnce(&mut crate::diff::Options)) -> &mut Self { + change_opts(&mut self.options); self } } @@ -214,7 +186,7 @@ impl Platform<'_, '_> { /// /// ### Performance Notes /// - /// Be sure to forcefully disable [`track_rewrites(None)`](Self::track_rewrites) to avoid + /// Be sure to forcefully disable [`track_rewrites(None)`](crate::diff::Options::track_rewrites) to avoid /// rename tracking, an operation that doesn't affect the statistics currently. /// As diffed resources aren't cached, if highly repetitive blobs are expected, performance /// may be diminished. In real-world scenarios where blobs are mostly unique, that's not an issue though. diff --git a/gix/src/repository/diff.rs b/gix/src/repository/diff.rs index 5644c1ceec0..8f7390b6f0d 100644 --- a/gix/src/repository/diff.rs +++ b/gix/src/repository/diff.rs @@ -1,5 +1,6 @@ -use crate::repository::diff_resource_cache; -use crate::Repository; +use crate::repository::{diff_resource_cache, diff_tree_to_tree}; +use crate::{Repository, Tree}; +use gix_object::TreeRefIter; /// Diff-utilities impl Repository { @@ -35,6 +36,45 @@ impl Repository { )?) } + /// Produce the changes that would need to be applied to `old_tree` to create `new_tree`. + /// If `options` are unset, they will be filled in according to the git configuration of this repository, and with + /// [full paths being tracked](crate::diff::Options::track_path()) as well, which typically means that + /// rewrite tracking might be disabled if done so explicitly by the user. + /// If `options` are set, the user can take full control over the settings. + /// + /// Note that this method exists to evoke similarity to `git2`, and makes it easier to fully control diff settings. + /// A more fluent version [may be used as well](Tree::changes()). + pub fn diff_tree_to_tree<'a, 'old_repo: 'a, 'new_repo: 'a>( + &self, + old_tree: impl Into>>, + new_tree: impl Into>>, + options: impl Into>, + ) -> Result, diff_tree_to_tree::Error> { + let mut cache = self.diff_resource_cache(gix_diff::blob::pipeline::Mode::ToGit, Default::default())?; + let opts = options + .into() + .map_or_else(|| crate::diff::Options::from_configuration(&self.config), Ok)? + .into(); + + let empty_tree = self.empty_tree(); + let old_tree = old_tree.into().unwrap_or(&empty_tree); + let new_tree = new_tree.into().unwrap_or(&empty_tree); + let mut out = Vec::new(); + gix_diff::tree_with_rewrites( + TreeRefIter::from_bytes(&old_tree.data), + TreeRefIter::from_bytes(&new_tree.data), + &mut cache, + &mut Default::default(), + &self.objects, + |change| -> Result<_, std::convert::Infallible> { + out.push(change.into_owned()); + Ok(gix_diff::tree_with_rewrites::Action::Continue) + }, + opts, + )?; + Ok(out) + } + /// Return a resource cache suitable for diffing blobs from trees directly, where no worktree checkout exists. /// /// For more control, see [`diff_resource_cache()`](Self::diff_resource_cache). diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 598720fc8eb..a7659a858de 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -58,6 +58,22 @@ mod submodule; mod thread_safe; mod worktree; +/// +#[cfg(feature = "blob-diff")] +pub mod diff_tree_to_tree { + /// The error returned by [Repository::diff_tree_to_tree()](crate::Repository::diff_tree_to_tree()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + DiffOptions(#[from] crate::diff::options::init::Error), + #[error(transparent)] + CreateResourceCache(#[from] super::diff_resource_cache::Error), + #[error(transparent)] + TreeDiff(#[from] gix_diff::tree_with_rewrites::Error), + } +} + /// #[cfg(feature = "blob-merge")] pub mod blob_merge_options { diff --git a/gix/tests/object/tree/diff.rs b/gix/tests/object/tree/diff.rs index 8c39004c14b..1a2f41249e7 100644 --- a/gix/tests/object/tree/diff.rs +++ b/gix/tests/object/tree/diff.rs @@ -74,6 +74,45 @@ fn changes_against_tree_modified() -> crate::Result { })?; assert_eq!(i, 3); + let actual = repo.diff_tree_to_tree(&from, &to, None)?; + insta::assert_debug_snapshot!(actual, @r#" + [ + Modification { + location: "a", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(78981922613b2afb6025042ff6bd878ac1994e85), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(b4f17b61de71d9b2e54ac9e62b1629ae2d97a6a7), + }, + Modification { + location: "dir", + previous_entry_mode: EntryMode( + 16384, + ), + previous_id: Sha1(e5c63aefe4327cb1c780c71966b678ce8e4225da), + entry_mode: EntryMode( + 16384, + ), + id: Sha1(c7ac5f82f536976f3561c9999b5f11e5893358be), + }, + Modification { + location: "dir/c", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(6695780ceb14b05e076a99bbd2babf34723b3464), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), + }, + ] + "#); + assert_eq!( from.changes()?.stats(&to)?, gix::object::tree::diff::Stats { @@ -131,20 +170,19 @@ mod track_rewrites { let from = tree_named(&repo, "@~1"); let to = tree_named(&repo, "@"); + let rewrites = Rewrites { + copies: Some(Copies { + source: CopySource::FromSetOfModifiedFiles, + percentage: Some(0.5), + }), + limit: 1000, + percentage: Some(0.5), + }; let out = from .changes()? - .track_path() - .track_rewrites( - Rewrites { - copies: Some(Copies { - source: CopySource::FromSetOfModifiedFiles, - percentage: Some(0.5), - }), - limit: 1000, - percentage: Some(0.5), - } - .into(), - ) + .options(|opts| { + opts.track_rewrites(rewrites.into()); + }) .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { if let Change::Rewrite { source_location, @@ -176,6 +214,279 @@ mod track_rewrites { "limit disabled" ); + let actual: Vec<_> = repo + .diff_tree_to_tree( + &from, + &to, + Some(gix::diff::Options::default().with_rewrites(Some(rewrites))), + )? + .into_iter() + .filter(|c| !c.entry_mode().is_tree()) + .collect(); + insta::assert_debug_snapshot!(actual, @r#" + [ + Rewrite { + source_location: "cli/src/commands/cat.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(f09e8b0e6bf963d8d6d5b578fea48ff4c9b723fb), + diff: Some( + DiffLineStats { + removals: 3, + insertions: 20, + before: 131, + after: 148, + similarity: 0.900421, + }, + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(081093be2ba0d2be62d14363f43859355bee2aa2), + location: "cli/src/commands/file/print.rs", + relation: Some( + ChildOfParent( + 1, + ), + ), + copy: false, + }, + Rewrite { + source_location: "cli/tests/test_cat_command.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(8c80364c37b7fc364778efb4214575536e6a1df4), + diff: Some( + DiffLineStats { + removals: 17, + insertions: 12, + before: 123, + after: 118, + similarity: 0.77923656, + }, + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(45bb2cf6b7fa96a39c95301f619ca3e4cc3eb0f3), + location: "cli/tests/test_file_print_command.rs", + relation: None, + copy: false, + }, + Rewrite { + source_location: "cli/tests/test_chmod_command.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(c24ae8e04f53b84e09838d232b3e8c0167ccc010), + diff: Some( + DiffLineStats { + removals: 13, + insertions: 19, + before: 244, + after: 250, + similarity: 0.88720536, + }, + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(8defe631bc82bf35a53cd25083f85664516f412f), + location: "cli/tests/test_file_chmod_command.rs", + relation: None, + copy: false, + }, + Rewrite { + source_location: "cli/src/commands/chmod.rs", + source_entry_mode: EntryMode( + 33188, + ), + source_relation: None, + source_id: Sha1(8f55dec5b81779d23539fa7146d713cc42df70f4), + diff: Some( + DiffLineStats { + removals: 0, + insertions: 17, + before: 124, + after: 141, + similarity: 0.9060576, + }, + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(94f78deb408d181ccea9da574d0e45ac32a98092), + location: "cli/src/commands/file/chmod.rs", + relation: Some( + ChildOfParent( + 1, + ), + ), + copy: false, + }, + Modification { + location: "CHANGELOG.md", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(f4cb24f79ec2549a3a8a5028d4c43d953f74137d), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(5a052b7fb0919218b2ecddffbb341277bd443a5c), + }, + Addition { + location: "cli/src/commands/file/mod.rs", + relation: Some( + ChildOfParent( + 1, + ), + ), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(d67f782327ea286136b8532eaf9a509806a87e83), + }, + Modification { + location: "cli/src/commands/mod.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(e7e8c4f00412aa9bc9898f396ef9a7597aa64756), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e3a9ec4524d27aa7035a38fd7c5db414809623c4), + }, + Modification { + location: "cli/tests/cli-reference@.md.snap", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(5c1985fc3c89a8d0edaedc23f76feb7f5c4cc962), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(92853cde19b20cadd74113ea3566c87d4def591b), + }, + Modification { + location: "cli/tests/runner.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(a008cb19a57bd44a5a054fced38384b09c9243fc), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(5253f0ff160e8b7001a7bd271ca4a07968ff81a3), + }, + Modification { + location: "cli/tests/test_acls.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(e7e8f15d7f4c0c50aad13b0f82a632e3d55c33c6), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(f644e4c8dd0be6fbe5493b172ce10839bcd9e25c), + }, + Modification { + location: "cli/tests/test_diffedit_command.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(85e7db4f01d8be8faa7a020647273399f815f597), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(fd57f61e92d4d49b4920c08c3522c066cb03ecd2), + }, + Modification { + location: "cli/tests/test_fix_command.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(16ab056981c9ca40cdd4d298feb70510cc3ced37), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(e0baefc79038fed0bcf56f2d8c3588a26d5bf985), + }, + Modification { + location: "cli/tests/test_global_opts.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(44f49aec05b7dc920cf1f1a554016e74b06ee1c8), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(a0c0340e495fa759c0b705dd46cee322aa0d80c8), + }, + Modification { + location: "cli/tests/test_immutable_commits.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(ba61cefef4328f126283f25935aab2d04ae2016e), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(3d7598b4e4c570eef701f40853ef3e3b0fb224f7), + }, + Modification { + location: "cli/tests/test_move_command.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(cbd36dbc76760ed41c968f369b470b45c176dabe), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(ac9ad5761637cd731abe1bf4a075fedda7bfc61f), + }, + Modification { + location: "cli/tests/test_new_command.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(3e03295d9b4654adccb6cd625376c36d4d38fb3d), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(a03b50a8a9c23c68d641b51b7c887ea088cd0d2b), + }, + Modification { + location: "cli/tests/test_squash_command.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(f921d5bc423586194bd73419f9814ff072212faa), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(ff1c247d4312adb5b372c6d9ff93fa71846ca527), + }, + Modification { + location: "cli/tests/test_unsquash_command.rs", + previous_entry_mode: EntryMode( + 33188, + ), + previous_id: Sha1(0dcc138981223171df13d35444c7aaee4b502c6f), + entry_mode: EntryMode( + 33188, + ), + id: Sha1(b8b29cc0ca0176fafaa97c7421a10ed116bcba8a), + }, + ] + "#); + Ok(()) } } From 3745212abf0353f15fec41556c55ee1d30d69f0a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 8 Oct 2024 22:20:28 +0200 Subject: [PATCH 13/13] refactor integration tests for a more modern look --- gix/tests/gix-init.rs | 13 ++++++++++--- gix/tests/{ => gix}/clone/mod.rs | 0 gix/tests/{ => gix}/commit/mod.rs | 0 gix/tests/{ => gix}/config/mod.rs | 0 gix/tests/{ => gix}/config/tree.rs | 0 gix/tests/{ => gix}/diff/mod.rs | 0 gix/tests/{ => gix}/head/mod.rs | 0 gix/tests/{ => gix}/id/mod.rs | 0 gix/tests/{ => gix}/init/mod.rs | 0 gix/tests/{gix.rs => gix/main.rs} | 0 gix/tests/{ => gix}/object/blob.rs | 0 gix/tests/{ => gix}/object/commit.rs | 0 gix/tests/{ => gix}/object/mod.rs | 0 gix/tests/{ => gix}/object/tree/diff.rs | 0 gix/tests/{ => gix}/object/tree/mod.rs | 0 gix/tests/{ => gix}/reference/mod.rs | 0 gix/tests/{ => gix}/reference/remote.rs | 0 gix/tests/{ => gix}/remote/connect.rs | 0 gix/tests/{ => gix}/remote/fetch.rs | 0 gix/tests/{ => gix}/remote/mod.rs | 0 gix/tests/{ => gix}/remote/ref_map.rs | 0 gix/tests/{ => gix}/remote/save.rs | 0 .../config/config_snapshot/credential_helpers.rs | 0 .../repository/config/config_snapshot/mod.rs | 0 gix/tests/{ => gix}/repository/config/identity.rs | 0 gix/tests/{ => gix}/repository/config/mod.rs | 0 gix/tests/{ => gix}/repository/config/remote.rs | 0 .../repository/config/transport_options.rs | 0 gix/tests/{ => gix}/repository/excludes.rs | 0 gix/tests/{ => gix}/repository/filter.rs | 0 gix/tests/{ => gix}/repository/mod.rs | 0 gix/tests/{ => gix}/repository/object.rs | 0 gix/tests/{ => gix}/repository/open.rs | 0 gix/tests/{ => gix}/repository/pathspec.rs | 0 gix/tests/{ => gix}/repository/reference.rs | 0 gix/tests/{ => gix}/repository/remote.rs | 0 gix/tests/{ => gix}/repository/shallow.rs | 0 gix/tests/{ => gix}/repository/state.rs | 0 gix/tests/{ => gix}/repository/submodule.rs | 0 gix/tests/{ => gix}/repository/worktree.rs | 0 gix/tests/{ => gix}/revision/mod.rs | 0 .../{ => gix}/revision/spec/from_bytes/ambiguous.rs | 0 gix/tests/{ => gix}/revision/spec/from_bytes/mod.rs | 0 .../{ => gix}/revision/spec/from_bytes/peel.rs | 0 .../{ => gix}/revision/spec/from_bytes/reflog.rs | 0 .../{ => gix}/revision/spec/from_bytes/regex.rs | 0 .../{ => gix}/revision/spec/from_bytes/traverse.rs | 0 .../{ => gix}/revision/spec/from_bytes/util.rs | 0 gix/tests/{ => gix}/revision/spec/mod.rs | 0 gix/tests/{ => gix}/status/mod.rs | 0 gix/tests/{ => gix}/submodule/mod.rs | 0 gix/tests/{ => gix}/util/mod.rs | 0 52 files changed, 10 insertions(+), 3 deletions(-) rename gix/tests/{ => gix}/clone/mod.rs (100%) rename gix/tests/{ => gix}/commit/mod.rs (100%) rename gix/tests/{ => gix}/config/mod.rs (100%) rename gix/tests/{ => gix}/config/tree.rs (100%) rename gix/tests/{ => gix}/diff/mod.rs (100%) rename gix/tests/{ => gix}/head/mod.rs (100%) rename gix/tests/{ => gix}/id/mod.rs (100%) rename gix/tests/{ => gix}/init/mod.rs (100%) rename gix/tests/{gix.rs => gix/main.rs} (100%) rename gix/tests/{ => gix}/object/blob.rs (100%) rename gix/tests/{ => gix}/object/commit.rs (100%) rename gix/tests/{ => gix}/object/mod.rs (100%) rename gix/tests/{ => gix}/object/tree/diff.rs (100%) rename gix/tests/{ => gix}/object/tree/mod.rs (100%) rename gix/tests/{ => gix}/reference/mod.rs (100%) rename gix/tests/{ => gix}/reference/remote.rs (100%) rename gix/tests/{ => gix}/remote/connect.rs (100%) rename gix/tests/{ => gix}/remote/fetch.rs (100%) rename gix/tests/{ => gix}/remote/mod.rs (100%) rename gix/tests/{ => gix}/remote/ref_map.rs (100%) rename gix/tests/{ => gix}/remote/save.rs (100%) rename gix/tests/{ => gix}/repository/config/config_snapshot/credential_helpers.rs (100%) rename gix/tests/{ => gix}/repository/config/config_snapshot/mod.rs (100%) rename gix/tests/{ => gix}/repository/config/identity.rs (100%) rename gix/tests/{ => gix}/repository/config/mod.rs (100%) rename gix/tests/{ => gix}/repository/config/remote.rs (100%) rename gix/tests/{ => gix}/repository/config/transport_options.rs (100%) rename gix/tests/{ => gix}/repository/excludes.rs (100%) rename gix/tests/{ => gix}/repository/filter.rs (100%) rename gix/tests/{ => gix}/repository/mod.rs (100%) rename gix/tests/{ => gix}/repository/object.rs (100%) rename gix/tests/{ => gix}/repository/open.rs (100%) rename gix/tests/{ => gix}/repository/pathspec.rs (100%) rename gix/tests/{ => gix}/repository/reference.rs (100%) rename gix/tests/{ => gix}/repository/remote.rs (100%) rename gix/tests/{ => gix}/repository/shallow.rs (100%) rename gix/tests/{ => gix}/repository/state.rs (100%) rename gix/tests/{ => gix}/repository/submodule.rs (100%) rename gix/tests/{ => gix}/repository/worktree.rs (100%) rename gix/tests/{ => gix}/revision/mod.rs (100%) rename gix/tests/{ => gix}/revision/spec/from_bytes/ambiguous.rs (100%) rename gix/tests/{ => gix}/revision/spec/from_bytes/mod.rs (100%) rename gix/tests/{ => gix}/revision/spec/from_bytes/peel.rs (100%) rename gix/tests/{ => gix}/revision/spec/from_bytes/reflog.rs (100%) rename gix/tests/{ => gix}/revision/spec/from_bytes/regex.rs (100%) rename gix/tests/{ => gix}/revision/spec/from_bytes/traverse.rs (100%) rename gix/tests/{ => gix}/revision/spec/from_bytes/util.rs (100%) rename gix/tests/{ => gix}/revision/spec/mod.rs (100%) rename gix/tests/{ => gix}/status/mod.rs (100%) rename gix/tests/{ => gix}/submodule/mod.rs (100%) rename gix/tests/{ => gix}/util/mod.rs (100%) diff --git a/gix/tests/gix-init.rs b/gix/tests/gix-init.rs index 350fe2a276f..bfe92e7f3ce 100644 --- a/gix/tests/gix-init.rs +++ b/gix/tests/gix-init.rs @@ -1,14 +1,21 @@ -pub mod util; - +#![allow(clippy::result_large_err)] mod with_overrides { use std::borrow::Cow; + use gix::{Repository, ThreadSafeRepository}; use gix_object::bstr::BStr; use gix_sec::Permission; use gix_testtools::Env; use serial_test::serial; - use crate::util::named_subrepo_opts; + pub fn named_subrepo_opts( + fixture: &str, + name: &str, + opts: gix::open::Options, + ) -> std::result::Result { + let repo_path = gix_testtools::scripted_fixture_read_only(fixture).unwrap().join(name); + Ok(ThreadSafeRepository::open_opts(repo_path, opts)?.to_thread_local()) + } #[test] #[serial] diff --git a/gix/tests/clone/mod.rs b/gix/tests/gix/clone/mod.rs similarity index 100% rename from gix/tests/clone/mod.rs rename to gix/tests/gix/clone/mod.rs diff --git a/gix/tests/commit/mod.rs b/gix/tests/gix/commit/mod.rs similarity index 100% rename from gix/tests/commit/mod.rs rename to gix/tests/gix/commit/mod.rs diff --git a/gix/tests/config/mod.rs b/gix/tests/gix/config/mod.rs similarity index 100% rename from gix/tests/config/mod.rs rename to gix/tests/gix/config/mod.rs diff --git a/gix/tests/config/tree.rs b/gix/tests/gix/config/tree.rs similarity index 100% rename from gix/tests/config/tree.rs rename to gix/tests/gix/config/tree.rs diff --git a/gix/tests/diff/mod.rs b/gix/tests/gix/diff/mod.rs similarity index 100% rename from gix/tests/diff/mod.rs rename to gix/tests/gix/diff/mod.rs diff --git a/gix/tests/head/mod.rs b/gix/tests/gix/head/mod.rs similarity index 100% rename from gix/tests/head/mod.rs rename to gix/tests/gix/head/mod.rs diff --git a/gix/tests/id/mod.rs b/gix/tests/gix/id/mod.rs similarity index 100% rename from gix/tests/id/mod.rs rename to gix/tests/gix/id/mod.rs diff --git a/gix/tests/init/mod.rs b/gix/tests/gix/init/mod.rs similarity index 100% rename from gix/tests/init/mod.rs rename to gix/tests/gix/init/mod.rs diff --git a/gix/tests/gix.rs b/gix/tests/gix/main.rs similarity index 100% rename from gix/tests/gix.rs rename to gix/tests/gix/main.rs diff --git a/gix/tests/object/blob.rs b/gix/tests/gix/object/blob.rs similarity index 100% rename from gix/tests/object/blob.rs rename to gix/tests/gix/object/blob.rs diff --git a/gix/tests/object/commit.rs b/gix/tests/gix/object/commit.rs similarity index 100% rename from gix/tests/object/commit.rs rename to gix/tests/gix/object/commit.rs diff --git a/gix/tests/object/mod.rs b/gix/tests/gix/object/mod.rs similarity index 100% rename from gix/tests/object/mod.rs rename to gix/tests/gix/object/mod.rs diff --git a/gix/tests/object/tree/diff.rs b/gix/tests/gix/object/tree/diff.rs similarity index 100% rename from gix/tests/object/tree/diff.rs rename to gix/tests/gix/object/tree/diff.rs diff --git a/gix/tests/object/tree/mod.rs b/gix/tests/gix/object/tree/mod.rs similarity index 100% rename from gix/tests/object/tree/mod.rs rename to gix/tests/gix/object/tree/mod.rs diff --git a/gix/tests/reference/mod.rs b/gix/tests/gix/reference/mod.rs similarity index 100% rename from gix/tests/reference/mod.rs rename to gix/tests/gix/reference/mod.rs diff --git a/gix/tests/reference/remote.rs b/gix/tests/gix/reference/remote.rs similarity index 100% rename from gix/tests/reference/remote.rs rename to gix/tests/gix/reference/remote.rs diff --git a/gix/tests/remote/connect.rs b/gix/tests/gix/remote/connect.rs similarity index 100% rename from gix/tests/remote/connect.rs rename to gix/tests/gix/remote/connect.rs diff --git a/gix/tests/remote/fetch.rs b/gix/tests/gix/remote/fetch.rs similarity index 100% rename from gix/tests/remote/fetch.rs rename to gix/tests/gix/remote/fetch.rs diff --git a/gix/tests/remote/mod.rs b/gix/tests/gix/remote/mod.rs similarity index 100% rename from gix/tests/remote/mod.rs rename to gix/tests/gix/remote/mod.rs diff --git a/gix/tests/remote/ref_map.rs b/gix/tests/gix/remote/ref_map.rs similarity index 100% rename from gix/tests/remote/ref_map.rs rename to gix/tests/gix/remote/ref_map.rs diff --git a/gix/tests/remote/save.rs b/gix/tests/gix/remote/save.rs similarity index 100% rename from gix/tests/remote/save.rs rename to gix/tests/gix/remote/save.rs diff --git a/gix/tests/repository/config/config_snapshot/credential_helpers.rs b/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs similarity index 100% rename from gix/tests/repository/config/config_snapshot/credential_helpers.rs rename to gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs diff --git a/gix/tests/repository/config/config_snapshot/mod.rs b/gix/tests/gix/repository/config/config_snapshot/mod.rs similarity index 100% rename from gix/tests/repository/config/config_snapshot/mod.rs rename to gix/tests/gix/repository/config/config_snapshot/mod.rs diff --git a/gix/tests/repository/config/identity.rs b/gix/tests/gix/repository/config/identity.rs similarity index 100% rename from gix/tests/repository/config/identity.rs rename to gix/tests/gix/repository/config/identity.rs diff --git a/gix/tests/repository/config/mod.rs b/gix/tests/gix/repository/config/mod.rs similarity index 100% rename from gix/tests/repository/config/mod.rs rename to gix/tests/gix/repository/config/mod.rs diff --git a/gix/tests/repository/config/remote.rs b/gix/tests/gix/repository/config/remote.rs similarity index 100% rename from gix/tests/repository/config/remote.rs rename to gix/tests/gix/repository/config/remote.rs diff --git a/gix/tests/repository/config/transport_options.rs b/gix/tests/gix/repository/config/transport_options.rs similarity index 100% rename from gix/tests/repository/config/transport_options.rs rename to gix/tests/gix/repository/config/transport_options.rs diff --git a/gix/tests/repository/excludes.rs b/gix/tests/gix/repository/excludes.rs similarity index 100% rename from gix/tests/repository/excludes.rs rename to gix/tests/gix/repository/excludes.rs diff --git a/gix/tests/repository/filter.rs b/gix/tests/gix/repository/filter.rs similarity index 100% rename from gix/tests/repository/filter.rs rename to gix/tests/gix/repository/filter.rs diff --git a/gix/tests/repository/mod.rs b/gix/tests/gix/repository/mod.rs similarity index 100% rename from gix/tests/repository/mod.rs rename to gix/tests/gix/repository/mod.rs diff --git a/gix/tests/repository/object.rs b/gix/tests/gix/repository/object.rs similarity index 100% rename from gix/tests/repository/object.rs rename to gix/tests/gix/repository/object.rs diff --git a/gix/tests/repository/open.rs b/gix/tests/gix/repository/open.rs similarity index 100% rename from gix/tests/repository/open.rs rename to gix/tests/gix/repository/open.rs diff --git a/gix/tests/repository/pathspec.rs b/gix/tests/gix/repository/pathspec.rs similarity index 100% rename from gix/tests/repository/pathspec.rs rename to gix/tests/gix/repository/pathspec.rs diff --git a/gix/tests/repository/reference.rs b/gix/tests/gix/repository/reference.rs similarity index 100% rename from gix/tests/repository/reference.rs rename to gix/tests/gix/repository/reference.rs diff --git a/gix/tests/repository/remote.rs b/gix/tests/gix/repository/remote.rs similarity index 100% rename from gix/tests/repository/remote.rs rename to gix/tests/gix/repository/remote.rs diff --git a/gix/tests/repository/shallow.rs b/gix/tests/gix/repository/shallow.rs similarity index 100% rename from gix/tests/repository/shallow.rs rename to gix/tests/gix/repository/shallow.rs diff --git a/gix/tests/repository/state.rs b/gix/tests/gix/repository/state.rs similarity index 100% rename from gix/tests/repository/state.rs rename to gix/tests/gix/repository/state.rs diff --git a/gix/tests/repository/submodule.rs b/gix/tests/gix/repository/submodule.rs similarity index 100% rename from gix/tests/repository/submodule.rs rename to gix/tests/gix/repository/submodule.rs diff --git a/gix/tests/repository/worktree.rs b/gix/tests/gix/repository/worktree.rs similarity index 100% rename from gix/tests/repository/worktree.rs rename to gix/tests/gix/repository/worktree.rs diff --git a/gix/tests/revision/mod.rs b/gix/tests/gix/revision/mod.rs similarity index 100% rename from gix/tests/revision/mod.rs rename to gix/tests/gix/revision/mod.rs diff --git a/gix/tests/revision/spec/from_bytes/ambiguous.rs b/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs similarity index 100% rename from gix/tests/revision/spec/from_bytes/ambiguous.rs rename to gix/tests/gix/revision/spec/from_bytes/ambiguous.rs diff --git a/gix/tests/revision/spec/from_bytes/mod.rs b/gix/tests/gix/revision/spec/from_bytes/mod.rs similarity index 100% rename from gix/tests/revision/spec/from_bytes/mod.rs rename to gix/tests/gix/revision/spec/from_bytes/mod.rs diff --git a/gix/tests/revision/spec/from_bytes/peel.rs b/gix/tests/gix/revision/spec/from_bytes/peel.rs similarity index 100% rename from gix/tests/revision/spec/from_bytes/peel.rs rename to gix/tests/gix/revision/spec/from_bytes/peel.rs diff --git a/gix/tests/revision/spec/from_bytes/reflog.rs b/gix/tests/gix/revision/spec/from_bytes/reflog.rs similarity index 100% rename from gix/tests/revision/spec/from_bytes/reflog.rs rename to gix/tests/gix/revision/spec/from_bytes/reflog.rs diff --git a/gix/tests/revision/spec/from_bytes/regex.rs b/gix/tests/gix/revision/spec/from_bytes/regex.rs similarity index 100% rename from gix/tests/revision/spec/from_bytes/regex.rs rename to gix/tests/gix/revision/spec/from_bytes/regex.rs diff --git a/gix/tests/revision/spec/from_bytes/traverse.rs b/gix/tests/gix/revision/spec/from_bytes/traverse.rs similarity index 100% rename from gix/tests/revision/spec/from_bytes/traverse.rs rename to gix/tests/gix/revision/spec/from_bytes/traverse.rs diff --git a/gix/tests/revision/spec/from_bytes/util.rs b/gix/tests/gix/revision/spec/from_bytes/util.rs similarity index 100% rename from gix/tests/revision/spec/from_bytes/util.rs rename to gix/tests/gix/revision/spec/from_bytes/util.rs diff --git a/gix/tests/revision/spec/mod.rs b/gix/tests/gix/revision/spec/mod.rs similarity index 100% rename from gix/tests/revision/spec/mod.rs rename to gix/tests/gix/revision/spec/mod.rs diff --git a/gix/tests/status/mod.rs b/gix/tests/gix/status/mod.rs similarity index 100% rename from gix/tests/status/mod.rs rename to gix/tests/gix/status/mod.rs diff --git a/gix/tests/submodule/mod.rs b/gix/tests/gix/submodule/mod.rs similarity index 100% rename from gix/tests/submodule/mod.rs rename to gix/tests/gix/submodule/mod.rs diff --git a/gix/tests/util/mod.rs b/gix/tests/gix/util/mod.rs similarity index 100% rename from gix/tests/util/mod.rs rename to gix/tests/gix/util/mod.rs