diff --git a/Cargo.lock b/Cargo.lock index ea9ef059fba..82ee1a6e7bb 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", @@ -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" @@ -1352,6 +1370,7 @@ dependencies = [ "gix-worktree 0.36.0", "gix-worktree-state", "gix-worktree-stream", + "insta", "is_ci", "once_cell", "parking_lot", @@ -1650,6 +1669,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 +1689,7 @@ dependencies = [ "gix-testtools", "gix-traverse 0.41.0", "gix-worktree 0.36.0", + "insta", "pretty_assertions", "shell-words", ] @@ -3127,6 +3148,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 +3415,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 +4535,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/crate-status.md b/crate-status.md index c0e24adabf2..5be7acd9e66 100644 --- a/crate-status.md +++ b/crate-status.md @@ -304,17 +304,24 @@ 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 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 @@ -332,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/gitoxide-core/src/hours/core.rs b/gitoxide-core/src/hours/core.rs index 1fa073b3253..58221ca638c 100644 --- a/gitoxide-core/src/hours/core.rs +++ b/gitoxide-core/src/hours/core.rs @@ -125,22 +125,23 @@ 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::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 +152,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/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..5bd8c25e9b4 100644 --- a/gitoxide-core/src/query/engine/update.rs +++ b/gitoxide-core/src/query/engine/update.rs @@ -207,15 +207,21 @@ 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::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 +229,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 +268,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 +283,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 { @@ -369,7 +377,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/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 1fe8d2e6bb4..1aa2895a84c 100644 --- a/gix-diff/src/lib.rs +++ b/gix-diff/src/lib.rs @@ -45,6 +45,13 @@ 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")] diff --git a/gix-diff/src/rewrites/tracker.rs b/gix-diff/src/rewrites/tracker.rs index 5720ec793d2..bd10edf4ffd 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() @@ -556,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 deleted file mode 100644 index 4391f4fe0cd..00000000000 --- a/gix-diff/src/tree/changes.rs +++ /dev/null @@ -1,377 +0,0 @@ -use std::{borrow::BorrowMut, collections::VecDeque}; - -use gix_object::{tree::EntryRef, FindExt}; - -use crate::{ - tree, - tree::{visit::Change, TreeInfoPair}, -}; - -/// 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<'a> tree::Changes<'a> { - /// 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 R, - ) -> 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 pop_path = false; - - 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))) => { - delegate.pop_front_tracked_path_and_set_current(); - rhs_entries = peekable(objects.find_tree_iter(&rhs, &mut state.buf2)?); - } - Some((Some(lhs), Some(rhs))) => { - 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)?); - } - Some((Some(lhs), None)) => { - delegate.pop_front_tracked_path_and_set_current(); - lhs_entries = peekable(objects.find_tree_iter(&lhs, &mut state.buf1)?); - } - 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, 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)?, - } - } - (Some(lhs), None) => { - let lhs = lhs?; - delete_entry_schedule_recursion(lhs, &mut state.trees, delegate)?; - } - (None, Some(rhs)) => { - let rhs = rhs?; - add_entry_schedule_recursion(rhs, &mut state.trees, delegate)?; - } - } - } - } -} - -fn compare(a: &EntryRef<'_>, b: &EntryRef<'_>) -> std::cmp::Ordering { - let common = a.filename.len().min(b.filename.len()); - a.filename[..common].cmp(&b.filename[..common]).then_with(|| { - let a = a.filename.get(common).or_else(|| a.mode.is_tree().then_some(&b'/')); - let b = b.filename.get(common).or_else(|| b.mode.is_tree().then_some(&b'/')); - a.cmp(&b) - }) -} - -fn delete_entry_schedule_recursion( - entry: EntryRef<'_>, - queue: &mut VecDeque, - delegate: &mut R, -) -> Result<(), Error> { - delegate.push_path_component(entry.filename); - if delegate - .visit(Change::Deletion { - entry_mode: entry.mode, - oid: entry.oid.to_owned(), - }) - .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)); - } - Ok(()) -} - -fn add_entry_schedule_recursion( - entry: EntryRef<'_>, - queue: &mut VecDeque, - delegate: &mut R, -) -> Result<(), Error> { - delegate.push_path_component(entry.filename); - if delegate - .visit(Change::Addition { - entry_mode: entry.mode, - oid: entry.oid.to_owned(), - }) - .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((None, Some(entry.oid.to_owned()))); - } - Ok(()) -} -fn catchup_rhs_with_lhs( - rhs_entries: &mut IteratorType>, - lhs: EntryRef<'_>, - rhs: EntryRef<'_>, - queue: &mut VecDeque, - delegate: &mut R, -) -> Result<(), Error> { - use std::cmp::Ordering::*; - add_entry_schedule_recursion(rhs, queue, 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)?; - 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)?; - } - Less => { - delegate.pop_path_component(); - delete_entry_schedule_recursion(lhs, queue, delegate)?; - break; - } - }, - Some(Err(err)) => return Err(Error::EntriesDecode(err.to_owned())), - None => { - delegate.pop_path_component(); - delete_entry_schedule_recursion(lhs, queue, delegate)?; - break; - } - } - } - Ok(()) -} - -fn catchup_lhs_with_rhs( - lhs_entries: &mut IteratorType>, - lhs: EntryRef<'_>, - rhs: EntryRef<'_>, - queue: &mut VecDeque, - delegate: &mut R, -) -> Result<(), Error> { - use std::cmp::Ordering::*; - delete_entry_schedule_recursion(lhs, queue, 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)?; - 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)?; - } - Greater => { - delegate.pop_path_component(); - add_entry_schedule_recursion(rhs, queue, delegate)?; - break; - } - }, - Some(Err(err)) => return Err(Error::EntriesDecode(err.to_owned())), - None => { - delegate.pop_path_component(); - add_entry_schedule_recursion(rhs, queue, delegate)?; - break; - } - } - } - Ok(()) -} - -fn handle_lhs_and_rhs_with_equal_filenames( - lhs: EntryRef<'_>, - rhs: EntryRef<'_>, - queue: &mut VecDeque, - delegate: &mut R, -) -> Result<(), Error> { - match (lhs.mode.is_tree(), rhs.mode.is_tree()) { - (true, true) => { - delegate.push_back_tracked_path_component(lhs.filename); - if lhs.oid != rhs.oid - && delegate - .visit(Change::Modification { - previous_entry_mode: lhs.mode, - previous_oid: lhs.oid.to_owned(), - entry_mode: rhs.mode, - oid: rhs.oid.to_owned(), - }) - .cancelled() - { - return Err(Error::Cancelled); - } - queue.push_back((Some(lhs.oid.to_owned()), Some(rhs.oid.to_owned()))); - } - (_, true) => { - delegate.push_back_tracked_path_component(lhs.filename); - if delegate - .visit(Change::Deletion { - entry_mode: lhs.mode, - oid: lhs.oid.to_owned(), - }) - .cancelled() - { - return Err(Error::Cancelled); - }; - if delegate - .visit(Change::Addition { - entry_mode: rhs.mode, - oid: rhs.oid.to_owned(), - }) - .cancelled() - { - return Err(Error::Cancelled); - }; - queue.push_back((None, Some(rhs.oid.to_owned()))); - } - (true, _) => { - delegate.push_back_tracked_path_component(lhs.filename); - if delegate - .visit(Change::Deletion { - entry_mode: lhs.mode, - oid: lhs.oid.to_owned(), - }) - .cancelled() - { - return Err(Error::Cancelled); - } - if delegate - .visit(Change::Addition { - entry_mode: rhs.mode, - oid: rhs.oid.to_owned(), - }) - .cancelled() - { - return Err(Error::Cancelled); - }; - queue.push_back((Some(lhs.oid.to_owned()), None)); - } - (false, false) => { - delegate.push_path_component(lhs.filename); - debug_assert!(lhs.mode.is_no_tree() && lhs.mode.is_no_tree()); - if lhs.oid != rhs.oid - && delegate - .visit(Change::Modification { - previous_entry_mode: lhs.mode, - previous_oid: lhs.oid.to_owned(), - entry_mode: rhs.mode, - oid: rhs.oid.to_owned(), - }) - .cancelled() - { - return Err(Error::Cancelled); - } - } - }; - Ok(()) -} - -type IteratorType = std::mem::ManuallyDrop>; - -fn peekable(iter: I) -> IteratorType { - std::mem::ManuallyDrop::new(iter.peekable()) -} - -#[cfg(test)] -mod tests { - use std::cmp::Ordering; - - use gix_object::tree::EntryKind; - - use super::*; - - #[test] - fn compare_select_samples() { - let null = gix_hash::ObjectId::null(gix_hash::Kind::Sha1); - let actual = compare( - &EntryRef { - mode: EntryKind::Blob.into(), - filename: "plumbing-cli.rs".into(), - oid: &null, - }, - &EntryRef { - mode: EntryKind::Tree.into(), - filename: "plumbing".into(), - oid: &null, - }, - ); - assert_eq!(actual, Ordering::Less); - let actual = compare( - &EntryRef { - mode: EntryKind::Tree.into(), - filename: "plumbing-cli.rs".into(), - oid: &null, - }, - &EntryRef { - mode: EntryKind::Blob.into(), - filename: "plumbing".into(), - oid: &null, - }, - ); - assert_eq!(actual, Ordering::Greater); - } -} diff --git a/gix-diff/src/tree/function.rs b/gix-diff/src/tree/function.rs new file mode 100644 index 00000000000..ba371e5789e --- /dev/null +++ b/gix-diff/src/tree/function.rs @@ -0,0 +1,448 @@ +use std::{borrow::BorrowMut, collections::VecDeque}; + +use gix_object::{tree::EntryRef, FindExt, TreeRefIter}; + +use crate::tree::visit::{ChangeId, Relation}; +use crate::tree::{visit::Change, Error, State, TreeInfoTuple, Visit}; + +/// 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; + + 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)?; + } + } + } +} + +fn compare(a: &EntryRef<'_>, b: &EntryRef<'_>) -> std::cmp::Ordering { + let common = a.filename.len().min(b.filename.len()); + a.filename[..common].cmp(&b.filename[..common]).then_with(|| { + let a = a.filename.get(common).or_else(|| a.mode.is_tree().then_some(&b'/')); + let b = b.filename.get(common).or_else(|| b.mode.is_tree().then_some(&b'/')); + a.cmp(&b) + }) +} + +fn delete_entry_schedule_recursion( + entry: EntryRef<'_>, + queue: &mut VecDeque, + change_id: &mut ChangeId, + relation_to_propagate: Option, + delegate: &mut impl 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) + }) + }); + let is_cancelled = delegate + .visit(Change::Deletion { + entry_mode: entry.mode, + oid: entry.oid.to_owned(), + relation, + }) + .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, to_child(relation))); + } + Ok(()) +} + +fn add_entry_schedule_recursion( + entry: EntryRef<'_>, + queue: &mut VecDeque, + change_id: &mut ChangeId, + relation_to_propagate: Option, + delegate: &mut impl 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() + { + return Err(Error::Cancelled); + } + 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()), to_child(relation))); + } + Ok(()) +} + +fn catchup_rhs_with_lhs( + rhs_entries: &mut IteratorType>, + lhs: EntryRef<'_>, + rhs: EntryRef<'_>, + queue: &mut VecDeque, + change_id: &mut ChangeId, + relation_to_propagate: Option, + delegate: &mut impl Visit, +) -> Result<(), Error> { + use std::cmp::Ordering::*; + 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, + 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, change_id, relation_to_propagate, delegate)?; + } + Less => { + delegate.pop_path_component(); + 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, change_id, relation_to_propagate, delegate)?; + break; + } + } + } + Ok(()) +} + +fn catchup_lhs_with_rhs( + lhs_entries: &mut IteratorType>, + lhs: EntryRef<'_>, + rhs: EntryRef<'_>, + queue: &mut VecDeque, + change_id: &mut ChangeId, + relation_to_propagate: Option, + delegate: &mut impl Visit, +) -> Result<(), Error> { + use std::cmp::Ordering::*; + 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, + 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, change_id, relation_to_propagate, delegate)?; + } + Greater => { + delegate.pop_path_component(); + 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, change_id, relation_to_propagate, delegate)?; + break; + } + } + } + Ok(()) +} + +fn handle_lhs_and_rhs_with_equal_filenames( + lhs: EntryRef<'_>, + rhs: EntryRef<'_>, + queue: &mut VecDeque, + change_id: &mut ChangeId, + relation_to_propagate: Option, + delegate: &mut impl Visit, +) -> Result<(), Error> { + match (lhs.mode.is_tree(), rhs.mode.is_tree()) { + (true, true) => { + delegate.push_back_tracked_path_component(lhs.filename); + if lhs.oid != rhs.oid + && delegate + .visit(Change::Modification { + previous_entry_mode: lhs.mode, + previous_oid: lhs.oid.to_owned(), + entry_mode: rhs.mode, + oid: rhs.oid.to_owned(), + }) + .cancelled() + { + return Err(Error::Cancelled); + } + 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); + if delegate + .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()), 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() + { + return Err(Error::Cancelled); + } + if delegate + .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, to_child(relation))); + } + (false, false) => { + delegate.push_path_component(lhs.filename); + debug_assert!(lhs.mode.is_no_tree() && lhs.mode.is_no_tree()); + if lhs.oid != rhs.oid + && delegate + .visit(Change::Modification { + previous_entry_mode: lhs.mode, + previous_oid: lhs.oid.to_owned(), + entry_mode: rhs.mode, + oid: rhs.oid.to_owned(), + }) + .cancelled() + { + return Err(Error::Cancelled); + } + } + }; + Ok(()) +} + +type IteratorType = std::iter::Peekable; + +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 { + iter.peekable() +} + +#[cfg(test)] +mod tests { + use std::cmp::Ordering; + + use gix_object::tree::EntryKind; + + use super::*; + + #[test] + fn compare_select_samples() { + let null = gix_hash::ObjectId::null(gix_hash::Kind::Sha1); + let actual = compare( + &EntryRef { + mode: EntryKind::Blob.into(), + filename: "plumbing-cli.rs".into(), + oid: &null, + }, + &EntryRef { + mode: EntryKind::Tree.into(), + filename: "plumbing".into(), + oid: &null, + }, + ); + assert_eq!(actual, Ordering::Less); + let actual = compare( + &EntryRef { + mode: EntryKind::Tree.into(), + filename: "plumbing-cli.rs".into(), + oid: &null, + }, + &EntryRef { + mode: EntryKind::Blob.into(), + filename: "plumbing".into(), + oid: &null, + }, + ); + assert_eq!(actual, Ordering::Greater); + } +} diff --git a/gix-diff/src/tree/mod.rs b/gix-diff/src/tree/mod.rs index 77e125e910e..7ef2b277a4d 100644 --- a/gix-diff/src/tree/mod.rs +++ b/gix-diff/src/tree/mod.rs @@ -1,45 +1,62 @@ +use crate::tree::visit::Relation; +use bstr::BStr; +use gix_hash::ObjectId; +use gix_object::bstr::BString; use std::collections::VecDeque; -use gix_hash::ObjectId; -use gix_object::{bstr::BString, TreeRefIter}; +/// 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 { + /// 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()`. +/// The state required to run [tree-diffs](super::tree()). #[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; } } -/// 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; -#[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..cf8030e748c 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,28 +105,12 @@ 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; 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 { @@ -106,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, @@ -140,8 +161,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/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 86aa0d446e5..eb977e24eee 100644 --- a/gix-diff/tests/Cargo.toml +++ b/gix-diff/tests/Cargo.toml @@ -14,9 +14,10 @@ rust-version = "1.65" [[test]] doctest = false name = "diff" -path = "diff.rs" +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/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 98% rename from gix-diff/tests/diff.rs rename to gix-diff/tests/diff/main.rs index 6a4201e2051..2163b5d3b01 100644 --- a/gix-diff/tests/diff.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/rewrites/mod.rs b/gix-diff/tests/diff/rewrites/mod.rs new file mode 100644 index 00000000000..c5b152c3245 --- /dev/null +++ b/gix-diff/tests/diff/rewrites/mod.rs @@ -0,0 +1,101 @@ +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, Copy, PartialEq, Eq)] +pub struct Change { + id: ObjectId, + kind: ChangeKind, + mode: EntryMode, + relation: Option, +} + +impl gix_diff::rewrites::tracker::Change for Change { + fn id(&self) -> &oid { + &self.id + } + + fn relation(&self) -> Option { + self.relation + } + + fn kind(&self) -> ChangeKind { + self.kind + } + + fn entry_mode(&self) -> EntryMode { + self.mode + } + + fn id_and_entry_mode(&self) -> (&oid, EntryMode) { + (&self.id, self.mode) + } +} + +const NULL_ID: gix_hash::ObjectId = gix_hash::Kind::Sha1.null(); + +impl Change { + fn modification() -> Self { + Change { + id: NULL_ID, + kind: ChangeKind::Modification, + mode: EntryKind::Blob.into(), + relation: None, + } + } + fn deletion() -> Self { + Change { + id: NULL_ID, + kind: ChangeKind::Deletion, + mode: EntryKind::Blob.into(), + relation: None, + } + } + fn addition() -> Self { + 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/diff/rewrites/tracker.rs similarity index 74% rename from gix-diff/tests/rewrites/tracker.rs rename to gix-diff/tests/diff/rewrites/tracker.rs index 77d3a0f6a6f..1aeb0f442c4 100644 --- a/gix-diff/tests/rewrites/tracker.rs +++ b/gix-diff/tests/diff/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-diff/tests/tree/mod.rs b/gix-diff/tests/diff/tree.rs similarity index 74% rename from gix-diff/tests/tree/mod.rs rename to gix-diff/tests/diff/tree.rs index 54e09f33016..19ba1632387 100644 --- a/gix-diff/tests/tree/mod.rs +++ b/gix-diff/tests/diff/tree.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}, @@ -58,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, @@ -101,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, @@ -161,7 +164,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 +187,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 +200,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 +226,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 +293,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 +332,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 +350,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 +376,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 +394,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 +420,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 +446,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 +465,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 +491,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 +518,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 +573,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 +596,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 +627,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 +678,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 +751,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)), } ] ); 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 00000000000..ecc1fadbcc9 Binary files /dev/null and b/gix-diff/tests/fixtures/generated-archives/make_diff_for_rewrites_repo.tar differ 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 9327d317f3a..e661f5f1abd 100644 Binary files a/gix-diff/tests/fixtures/generated-archives/make_diff_repo.tar and b/gix-diff/tests/fixtures/generated-archives/make_diff_repo.tar differ 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 0a6bbf5031b..e0ece399d6b 100644 Binary files a/gix-diff/tests/fixtures/generated-archives/make_diff_repo_a.tar and b/gix-diff/tests/fixtures/generated-archives/make_diff_repo_a.tar differ 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 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/rewrites/mod.rs b/gix-diff/tests/rewrites/mod.rs deleted file mode 100644 index ddcb12dfc86..00000000000 --- a/gix-diff/tests/rewrites/mod.rs +++ /dev/null @@ -1,56 +0,0 @@ -use gix_diff::rewrites::tracker::ChangeKind; -use gix_hash::{oid, ObjectId}; -use gix_object::tree::{EntryKind, EntryMode}; - -mod tracker; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Change { - id: ObjectId, - kind: ChangeKind, - mode: EntryMode, -} - -impl gix_diff::rewrites::tracker::Change for Change { - fn id(&self) -> &oid { - &self.id - } - - fn kind(&self) -> ChangeKind { - self.kind - } - - fn entry_mode(&self) -> EntryMode { - self.mode - } - - fn id_and_entry_mode(&self) -> (&oid, EntryMode) { - (&self.id, self.mode) - } -} - -const NULL_ID: gix_hash::ObjectId = gix_hash::Kind::Sha1.null(); - -impl Change { - fn modification() -> Self { - Change { - id: NULL_ID, - kind: ChangeKind::Modification, - mode: EntryKind::Blob.into(), - } - } - fn deletion() -> Self { - Change { - id: NULL_ID, - kind: ChangeKind::Deletion, - mode: EntryKind::Blob.into(), - } - } - fn addition() -> Self { - Change { - id: NULL_ID, - kind: ChangeKind::Addition, - mode: EntryKind::Blob.into(), - } - } -} 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-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 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/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/tree.rs b/gix-pack/src/data/output/count/objects/tree.rs index 824b48062ce..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, { @@ -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; } @@ -85,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/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-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 6479877ebaf..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(); @@ -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; @@ -425,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, @@ -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-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/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/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/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/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 9aacc9d5808..dc357c2cc5b 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, @@ -42,9 +42,9 @@ impl<'d> TreeIterExt for TreeRefIter<'d> { } } -/// 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/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..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; @@ -153,7 +61,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 @@ -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/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..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<'a, 'old, 'new> super::Change<'a, 'old, 'new> { - /// 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<'a, 'old, 'new> super::Change<'a, 'old, 'new> { &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<'a, 'old, 'new> Event<'a, 'old, 'new> { +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 0f186429116..0cb04e74039 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::changes::Error), + Diff(#[from] gix_diff::tree_with_rewrites::Error), #[error("The user-provided callback failed")] ForEach(#[source] Box), #[error(transparent)] @@ -23,15 +17,8 @@ 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<'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 @@ -40,7 +27,7 @@ impl<'a, 'old> Platform<'a, '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<'a, 'old> Platform<'a, '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,280 +59,35 @@ impl<'a, 'old> Platform<'a, '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::Changes::from(TreeRefIter::from_bytes(&self.lhs.data)).needed_to_obtain( - 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::changes::Error::Cancelled) => delegate - .err - .map_or(Err(Error::Diff(gix_diff::tree::changes::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<'a, 'old, 'new, VisitFn, E> Delegate<'a, '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 } => change::Event::Addition { - entry_mode, - id: oid.attach(other_repo), - }, - Deletion { entry_mode, oid } => 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, - ), - }, - diff_cache, - &self.src_tree.repo.objects, - |push| { - self.src_tree - .traverse() - .breadthfirst(&mut tree_to_changes::Delegate::new(push, self.location)) + let opts = self.options.into(); + 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, + }) }, - )?; - Ok(Some(outcome)) - } -} - -impl<'a, 'old, 'new, VisitFn, E> gix_diff::tree::Visit for Delegate<'a, '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 - } + opts, + )?) } } diff --git a/gix/src/object/tree/diff/mod.rs b/gix/src/object/tree/diff/mod.rs index 54c96392c78..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::recorder::Location; +use gix_diff::tree; -use crate::bstr::BString; -use crate::{bstr::BStr, diff::Rewrites, 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)] @@ -15,25 +14,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](crate::diff::Options::track_path) was enabled. + /// + /// 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. + 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](crate::diff::Options::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](crate::diff::Options::track_path) was enabled. + /// + /// 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. + 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](crate::diff::Options::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](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, + /// 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%](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`. + /// 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](crate::diff::Options::track_path) was enabled. + /// + /// 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. + 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, + }, } /// @@ -51,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, - tracking: None, - rewrites: self.repo.config.diff_renames()?.unwrap_or_default().into(), + options: crate::diff::Options::from_configuration(&self.repo.config)?, }) } } @@ -69,33 +142,13 @@ impl<'repo> Tree<'repo> { pub struct Platform<'a, 'repo> { state: gix_diff::tree::State, lhs: &'a Tree<'repo>, - tracking: Option, - rewrites: Option, + options: crate::diff::Options, } -/// Configuration -impl<'a, 'repo> Platform<'a, 'repo> { - /// 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 - } - - /// Keep track of the entire path of a change, relative to the repository. - /// - /// This makes the [`location`][Change::location] field usable. - pub fn track_path(&mut self) -> &mut Self { - self.tracking = 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; +impl Platform<'_, '_> { + /// 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 } } @@ -128,12 +181,12 @@ 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 /// - /// 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/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/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/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/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/gix/object/tree/diff.rs b/gix/tests/gix/object/tree/diff.rs new file mode 100644 index 00000000000..1a2f41249e7 --- /dev/null +++ b/gix/tests/gix/object/tree/diff.rs @@ -0,0 +1,502 @@ +use std::convert::Infallible; + +use gix::object::{blob::diff::lines, tree::diff::Change}; +use gix_object::{bstr::ByteSlice, tree::EntryKind}; + +use crate::named_repo; + +#[test] +fn changes_against_tree_modified() -> 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 cache = repo.diff_resource_cache(gix_diff::blob::pipeline::Mode::ToGit, Default::default())?; + + let expected_modifications = [ + (EntryKind::Blob, "a\n", EntryKind::Blob, "a\na1\n"), + (EntryKind::Tree, "", EntryKind::Tree, ""), + (EntryKind::Blob, "dir/c\n", EntryKind::Blob, "dir/c\ndir/c1\n"), + ]; + let mut i = 0; + + from.changes()? + .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { + let (expected_previous_entry_mode, expected_previous_data, expected_entry_mode, expected_data) = + expected_modifications[i]; + + assert!( + !change.location().is_empty(), + "without configuration the location field is set" + ); + 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); + + if matches!(entry_mode.kind(), EntryKind::Tree) { + i += 1; + return Ok(Default::default()); + } + + assert_eq!(previous_id.object().unwrap().data.as_bstr(), expected_previous_data); + assert_eq!(id.object().unwrap().data.as_bstr(), expected_data); + } + Change::Rewrite { .. } | Change::Deletion { .. } | Change::Addition { .. } => { + unreachable!("only modification is expected") + } + }; + + let mut diff = change.diff(&mut cache).expect("objects available"); + let count = diff.line_counts().expect("no diff error").expect("no binary blobs"); + assert_eq!(count.insertions, 1); + assert_eq!(count.removals, 0); + diff.lines(|hunk| { + match hunk { + 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()] + ), + lines::Change::Modification { .. } => unreachable!("there was no modification"), + }; + Ok::<_, Infallible>(()) + }) + .expect("infallible"); + + i += 1; + Ok(Default::default()) + })?; + 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 { + lines_added: 2, + lines_removed: 0, + files_changed: 2, + }, + "two files with one added line each" + ); + + Ok(()) +} + +mod track_rewrites { + use std::collections::HashMap; + use std::convert::Infallible; + + use gix::{ + diff::{ + rewrites::{Copies, CopySource}, + Rewrites, + }, + object::tree::diff::Change, + }; + use gix_ref::bstr::BStr; + + use crate::object::tree::diff::tree_named; + use crate::util::named_subrepo_opts; + + #[test] + #[cfg_attr( + windows, + ignore = "Fails on some Window systems, like the fixture doesn't get set up correctly." + )] + fn jj_realistic_needs_to_be_more_clever() -> crate::Result { + let repo = named_subrepo_opts("make_diff_repos.sh", "jj-trackcopy-1", gix::open::Options::isolated())?; + + let mut expected = HashMap::<&BStr, (&BStr, u32)>::new(); + expected.insert( + "cli/src/commands/file/chmod.rs".into(), + ("cli/src/commands/chmod.rs".into(), 90), + ); + expected.insert( + "cli/src/commands/file/print.rs".into(), + ("cli/src/commands/cat.rs".into(), 90), + ); + expected.insert( + "cli/tests/test_file_chmod_command.rs".into(), + ("cli/tests/test_chmod_command.rs".into(), 88), + ); + expected.insert( + "cli/tests/test_file_print_command.rs".into(), + ("cli/tests/test_cat_command.rs".into(), 77), + ); + + 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()? + .options(|opts| { + opts.track_rewrites(rewrites.into()); + }) + .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { + if let Change::Rewrite { + source_location, + diff: Some(diff), + location, + .. + } = change + { + // Round to percentage points to avoid floating point error. + let similarity = (diff.similarity * 100.0) as u32; + let v = expected.remove(location); + assert_eq!(v, Some((source_location, similarity))); + } + Ok(Default::default()) + })?; + + assert_eq!(expected, HashMap::new()); + let out = out.expect("tracking enabled"); + assert_eq!( + out.num_similarity_checks, 21, + "this probably increases once the algorithm improves" + ); + assert_eq!( + out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, + "limit disabled" + ); + assert_eq!( + out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0, + "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(()) + } +} + +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() +} 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 99% rename from gix/tests/repository/worktree.rs rename to gix/tests/gix/repository/worktree.rs index fe55f4c69c3..a77eb357e86 100644 --- a/gix/tests/repository/worktree.rs +++ b/gix/tests/gix/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/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 diff --git a/gix/tests/object/tree/diff.rs b/gix/tests/object/tree/diff.rs deleted file mode 100644 index e39e6cf3a67..00000000000 --- a/gix/tests/object/tree/diff.rs +++ /dev/null @@ -1,1306 +0,0 @@ -use std::convert::Infallible; - -use gix::{ - bstr::BString, - object::{blob::diff::lines::Change, tree::diff::change::Event}, -}; -use gix_object::{bstr::ByteSlice, tree::EntryKind}; - -use crate::named_repo; - -#[test] -fn changes_against_tree_modified() -> 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 cache = repo.diff_resource_cache(gix_diff::blob::pipeline::Mode::ToGit, Default::default())?; - - let expected_modifications = [ - (EntryKind::Blob, "a\n", EntryKind::Blob, "a\na1\n"), - (EntryKind::Tree, "", EntryKind::Tree, ""), - (EntryKind::Blob, "dir/c\n", EntryKind::Blob, "dir/c\ndir/c1\n"), - ]; - let mut i = 0; - - from.changes()? - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - 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 { - 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); - - if matches!(entry_mode.kind(), EntryKind::Tree) { - i += 1; - return Ok(Default::default()); - } - - 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 { .. } => { - unreachable!("only modification is expected") - } - }; - - let mut diff = change.diff(&mut cache).expect("objects available"); - let count = diff.line_counts().expect("no diff error").expect("no binary blobs"); - assert_eq!(count.insertions, 1); - assert_eq!(count.removals, 0); - diff.lines(|hunk| { - match hunk { - Change::Deletion { .. } => unreachable!("there was no deletion"), - Change::Addition { lines } => assert_eq!( - lines, - vec![expected_data[expected_previous_data.len()..].as_bytes().as_bstr()] - ), - Change::Modification { .. } => unreachable!("there was no modification"), - }; - Ok::<_, Infallible>(()) - }) - .expect("infallible"); - - i += 1; - Ok(Default::default()) - })?; - assert_eq!(i, 3); - - assert_eq!( - from.changes()?.stats(&to)?, - gix::object::tree::diff::Stats { - lines_added: 2, - lines_removed: 0, - files_changed: 2, - }, - "two files with one added line each" - ); - - 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, - }; - use gix_ref::bstr::BStr; - - 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( - windows, - ignore = "Fails on some Window systems, like the fixture doesn't get set up correctly." - )] - fn jj_realistic_needs_to_be_more_clever() -> crate::Result { - let repo = named_subrepo_opts("make_diff_repos.sh", "jj-trackcopy-1", gix::open::Options::isolated())?; - - let mut expected = HashMap::<&BStr, (&BStr, u32)>::new(); - expected.insert( - "cli/src/commands/file/chmod.rs".into(), - ("cli/src/commands/chmod.rs".into(), 90), - ); - expected.insert( - "cli/src/commands/file/print.rs".into(), - ("cli/src/commands/cat.rs".into(), 90), - ); - expected.insert( - "cli/tests/test_file_chmod_command.rs".into(), - ("cli/tests/test_chmod_command.rs".into(), 88), - ); - expected.insert( - "cli/tests/test_file_print_command.rs".into(), - ("cli/tests/test_cat_command.rs".into(), 77), - ); - - let from = tree_named(&repo, "@~1"); - let to = tree_named(&repo, "@"); - 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(), - ) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { - if let Event::Rewrite { - source_location, - diff: Some(diff), - .. - } = change.event - { - // Round to percentage points to avoid floating point error. - let similarity = (diff.similarity * 100.0) as u32; - let v = expected.remove(change.location); - assert_eq!(v, Some((source_location, similarity))); - } - Ok(Default::default()) - })?; - - assert_eq!(expected, HashMap::new()); - let out = out.rewrites.expect("tracking enabled"); - assert_eq!( - out.num_similarity_checks, 21, - "this probably increases once the algorithm improves" - ); - assert_eq!( - out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, - "limit disabled" - ); - assert_eq!( - out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0, - "limit disabled" - ); - - 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', - } -} 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 {