diff --git a/gitoxide-core/src/hours/core.rs b/gitoxide-core/src/hours/core.rs index aacd3494adc..b402de1cddd 100644 --- a/gitoxide-core/src/hours/core.rs +++ b/gitoxide-core/src/hours/core.rs @@ -105,7 +105,7 @@ pub fn spawn_tree_delta_threads<'scope>( for chunk in rx { for (commit_idx, parent_commit, commit) in chunk { if let Some(cache) = cache.as_mut() { - cache.clear_resource_cache(); + cache.clear_resource_cache_keep_allocation(); } commits.fetch_add(1, Ordering::Relaxed); if gix::interrupt::is_triggered() { diff --git a/gitoxide-core/src/query/engine/update.rs b/gitoxide-core/src/query/engine/update.rs index 2e809f0e2b3..20c284b9d06 100644 --- a/gitoxide-core/src/query/engine/update.rs +++ b/gitoxide-core/src/query/engine/update.rs @@ -204,8 +204,8 @@ pub fn update( Some(c) => c, None => continue, }; - rewrite_cache.clear_resource_cache(); - diff_cache.clear_resource_cache(); + rewrite_cache.clear_resource_cache_keep_allocation(); + diff_cache.clear_resource_cache_keep_allocation(); from.changes()? .track_path() .track_rewrites(Some(rewrites)) diff --git a/gix-diff/src/blob/mod.rs b/gix-diff/src/blob/mod.rs index 9af819c9877..ec951b9cd6d 100644 --- a/gix-diff/src/blob/mod.rs +++ b/gix-diff/src/blob/mod.rs @@ -119,6 +119,8 @@ pub struct Platform { /// That way, expensive rewrite-checks with NxM matrix checks would be as fast as possible, /// avoiding duplicate work. diff_cache: HashMap, + /// A list of previously used buffers, ready for re-use. + free_list: Vec>, } mod impls { diff --git a/gix-diff/src/blob/platform.rs b/gix-diff/src/blob/platform.rs index 41b4cc928ea..d8264129e49 100644 --- a/gix-diff/src/blob/platform.rs +++ b/gix-diff/src/blob/platform.rs @@ -1,6 +1,6 @@ -use std::{io::Write, process::Stdio}; - use bstr::{BStr, BString, ByteSlice}; +use std::cmp::Ordering; +use std::{io::Write, process::Stdio}; use super::Algorithm; use crate::blob::{pipeline, Pipeline, Platform, ResourceKind}; @@ -325,6 +325,7 @@ impl Platform { old: None, new: None, diff_cache: Default::default(), + free_list: Vec::with_capacity(2), options, filter, filter_mode, @@ -542,7 +543,7 @@ impl Platform { /// Every call to [set_resource()](Self::set_resource()) will keep the diffable data in memory, and that will never be cleared. /// - /// Use this method to clear the cache, releasing memory. Note that this will also loose all information about resources + /// Use this method to clear the cache, releasing memory. Note that this will also lose all information about resources /// which means diffs would fail unless the resources are set again. /// /// Note that this also has to be called if the same resource is going to be diffed in different states, i.e. using different @@ -551,6 +552,37 @@ impl Platform { self.old = None; self.new = None; self.diff_cache.clear(); + self.free_list.clear(); + } + + /// Every call to [set_resource()](Self::set_resource()) will keep the diffable data in memory, and that will never be cleared. + /// + /// Use this method to clear the cache, but keep the previously used buffers around for later re-use. + /// + /// If there are more buffers on the free-list than there are stored sources, we half that amount each time this method is called, + /// or keep as many resources as were previously stored, or 2 buffers, whatever is larger. + /// If there are fewer buffers in the free-list than are in the resource cache, we will keep as many as needed to match the + /// number of previously stored resources. + /// + /// Returns the number of available buffers. + pub fn clear_resource_cache_keep_allocation(&mut self) -> usize { + self.old = None; + self.new = None; + + let diff_cache = std::mem::take(&mut self.diff_cache); + match self.free_list.len().cmp(&diff_cache.len()) { + Ordering::Less => { + let to_take = diff_cache.len() - self.free_list.len(); + self.free_list + .extend(diff_cache.into_values().map(|v| v.buffer).take(to_take)); + } + Ordering::Equal => {} + Ordering::Greater => { + let new_len = (self.free_list.len() / 2).max(diff_cache.len()).max(2); + self.free_list.truncate(new_len); + } + } + self.free_list.len() } } @@ -591,7 +623,7 @@ impl Platform { kind, rela_path: rela_path.to_owned(), })?; - let mut buf = Vec::new(); + let mut buf = self.free_list.pop().unwrap_or_default(); let out = self.filter.convert_to_diffable( &id, mode, diff --git a/gix-diff/tests/blob/platform.rs b/gix-diff/tests/blob/platform.rs index e3c7caaf882..6ab3a3dbc68 100644 --- a/gix-diff/tests/blob/platform.rs +++ b/gix-diff/tests/blob/platform.rs @@ -121,13 +121,35 @@ fn resources_of_worktree_and_odb_and_check_link() -> crate::Result { "Also obvious that symlinks are definitely special, but it's what git does as well" ); - platform.clear_resource_cache(); + assert_eq!( + platform.clear_resource_cache_keep_allocation(), + 3, + "some buffers are retained and reused" + ); assert_eq!( platform.resources(), None, "clearing the cache voids resources and one has to set it up again" ); + assert_eq!( + platform.clear_resource_cache_keep_allocation(), + 2, + "doing this again keeps 2 buffers" + ); + assert_eq!( + platform.clear_resource_cache_keep_allocation(), + 2, + "no matter what - after all we need at least two resources for a diff" + ); + + platform.clear_resource_cache(); + assert_eq!( + platform.clear_resource_cache_keep_allocation(), + 0, + "after a proper clearing, the free-list is also emptied, and it won't be recreated" + ); + Ok(()) } diff --git a/gix-ref/src/lib.rs b/gix-ref/src/lib.rs index 8c35ed35c57..b7c503eb2bc 100644 --- a/gix-ref/src/lib.rs +++ b/gix-ref/src/lib.rs @@ -150,8 +150,8 @@ pub struct Namespace(BString); #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Kind { - /// A ref that points to an object id - Peeled, + /// A ref that points to an object id directly. + Object, /// A ref that points to another reference, adding a level of indirection. /// /// It can be resolved to an id using the [`peel_in_place_to_id()`][`crate::file::ReferenceExt::peel_to_id_in_place()`] method. @@ -203,8 +203,8 @@ pub enum Category<'a> { #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Target { - /// A ref that points to an object id - Peeled(ObjectId), + /// A ref that points directly to an object id. + Object(ObjectId), /// A ref that points to another reference by its validated name, adding a level of indirection. /// /// Note that this is an extension of gitoxide which will be helpful in logging all reference changes. @@ -214,8 +214,8 @@ pub enum Target { /// Denotes a ref target, equivalent to [`Kind`], but with immutable data. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] pub enum TargetRef<'a> { - /// A ref that points to an object id - Peeled(&'a oid), + /// A ref that points directly to an object id. + Object(&'a oid), /// A ref that points to another reference by its validated name, adding a level of indirection. Symbolic(&'a FullNameRef), } diff --git a/gix-ref/src/peel.rs b/gix-ref/src/peel.rs index 02bee162050..bae2d46e151 100644 --- a/gix-ref/src/peel.rs +++ b/gix-ref/src/peel.rs @@ -1,13 +1,29 @@ /// #[allow(clippy::empty_docs)] pub mod to_id { - use std::path::PathBuf; - use gix_object::bstr::BString; + /// The error returned by [`crate::file::ReferenceExt::peel_to_id_in_place()`]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + FollowToObject(#[from] super::to_object::Error), + #[error("An error occurred when trying to resolve an object a reference points to")] + Find(#[from] gix_object::find::Error), + #[error("Object {oid} as referred to by {name:?} could not be found")] + NotFound { oid: gix_hash::ObjectId, name: BString }, + } +} + +/// +#[allow(clippy::empty_docs)] +pub mod to_object { + use std::path::PathBuf; + use crate::file; - /// The error returned by [`crate::file::ReferenceExt::peel_to_id_in_place()`]. + /// The error returned by [`file::ReferenceExt::follow_to_object_in_place_packed()`]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { @@ -17,9 +33,5 @@ pub mod to_id { Cycle { start_absolute: PathBuf }, #[error("Refusing to follow more than {max_depth} levels of indirection")] DepthLimitExceeded { max_depth: usize }, - #[error("An error occurred when trying to resolve an object a reference points to")] - Find(#[from] gix_object::find::Error), - #[error("Object {oid} as referred to by {name:?} could not be found")] - NotFound { oid: gix_hash::ObjectId, name: BString }, } } diff --git a/gix-ref/src/raw.rs b/gix-ref/src/raw.rs index 14eb09eecb2..34eb6e814dc 100644 --- a/gix-ref/src/raw.rs +++ b/gix-ref/src/raw.rs @@ -10,7 +10,8 @@ pub struct Reference { pub name: FullName, /// The target of the reference, either a symbolic reference by full name or a possibly intermediate object by its id. pub target: Target, - /// The fully peeled object to which this reference ultimately points to. Only guaranteed to be set after + /// The fully peeled object to which this reference ultimately points to after following all symbolic refs and all annotated + /// tags. Only guaranteed to be set after /// [`Reference::peel_to_id_in_place()`](crate::file::ReferenceExt) was called or if this reference originated /// from a packed ref. pub peeled: Option, @@ -48,7 +49,7 @@ mod convert { fn from(value: packed::Reference<'p>) -> Self { Reference { name: value.name.into(), - target: Target::Peeled(value.target()), + target: Target::Object(value.target()), peeled: value .object .map(|hex| ObjectId::from_hex(hex).expect("parser validation")), diff --git a/gix-ref/src/store/file/loose/reference/decode.rs b/gix-ref/src/store/file/loose/reference/decode.rs index 68fb4f65d70..03590eaec95 100644 --- a/gix-ref/src/store/file/loose/reference/decode.rs +++ b/gix-ref/src/store/file/loose/reference/decode.rs @@ -35,7 +35,7 @@ impl TryFrom for Target { fn try_from(v: MaybeUnsafeState) -> Result { Ok(match v { - MaybeUnsafeState::Id(id) => Target::Peeled(id), + MaybeUnsafeState::Id(id) => Target::Object(id), MaybeUnsafeState::UnvalidatedPath(name) => { Target::Symbolic(match gix_validate::reference::name(name.as_ref()) { Ok(_) => FullName(name), diff --git a/gix-ref/src/store/file/raw_ext.rs b/gix-ref/src/store/file/raw_ext.rs index ede518827fd..c933df64911 100644 --- a/gix-ref/src/store/file/raw_ext.rs +++ b/gix-ref/src/store/file/raw_ext.rs @@ -1,13 +1,12 @@ use std::collections::BTreeSet; -use gix_hash::ObjectId; - use crate::{ packed, peel, raw::Reference, store_impl::{file, file::log}, Target, }; +use gix_hash::ObjectId; pub trait Sealed {} impl Sealed for crate::Reference {} @@ -21,16 +20,18 @@ pub trait ReferenceExt: Sealed { fn log_exists(&self, store: &file::Store) -> bool; /// Follow all symbolic targets this reference might point to and peel the underlying object - /// to the end of the chain, and return it, using `objects` to access them. + /// to the end of the tag-chain, returning the first non-tag object the annotated tag points to, + /// using `objects` to access them and `store` to lookup symbolic references. /// - /// This is useful to learn where this reference is ultimately pointing to. + /// This is useful to learn where this reference is ultimately pointing to after following all symbolic + /// refs and all annotated tags to the first non-tag object. fn peel_to_id_in_place( &mut self, store: &file::Store, objects: &dyn gix_object::Find, ) -> Result; - /// Like [`ReferenceExt::peel_to_id_in_place()`], but with support for a known stable packed buffer + /// Like [`ReferenceExt::peel_to_id_in_place()`], but with support for a known stable `packed` buffer /// to use for resolving symbolic links. fn peel_to_id_in_place_packed( &mut self, @@ -39,6 +40,14 @@ pub trait ReferenceExt: Sealed { packed: Option<&packed::Buffer>, ) -> Result; + /// Like [`ReferenceExt::follow()`], but follows all symbolic references while gracefully handling loops, + /// altering this instance in place. + fn follow_to_object_in_place_packed( + &mut self, + store: &file::Store, + packed: Option<&packed::Buffer>, + ) -> Result; + /// Follow this symbolic reference one level and return the ref it refers to. /// /// Returns `None` if this is not a symbolic reference, hence the leaf of the chain. @@ -76,7 +85,9 @@ impl ReferenceExt for Reference { objects: &dyn gix_object::Find, ) -> Result { let packed = store.assure_packed_refs_uptodate().map_err(|err| { - peel::to_id::Error::Follow(file::find::existing::Error::Find(file::find::Error::PackedOpen(err))) + peel::to_id::Error::FollowToObject(peel::to_object::Error::Follow(file::find::existing::Error::Find( + file::find::Error::PackedOpen(err), + ))) })?; self.peel_to_id_in_place_packed(store, objects, packed.as_ref().map(|b| &***b)) } @@ -89,32 +100,12 @@ impl ReferenceExt for Reference { ) -> Result { match self.peeled { Some(peeled) => { - self.target = Target::Peeled(peeled.to_owned()); + self.target = Target::Object(peeled.to_owned()); Ok(peeled) } None => { - if self.target.kind() == crate::Kind::Symbolic { - let mut seen = BTreeSet::new(); - let cursor = &mut *self; - while let Some(next) = cursor.follow_packed(store, packed) { - let next = next?; - if seen.contains(&next.name) { - return Err(peel::to_id::Error::Cycle { - start_absolute: store.reference_path(cursor.name.as_ref()), - }); - } - *cursor = next; - seen.insert(cursor.name.clone()); - const MAX_REF_DEPTH: usize = 5; - if seen.len() == MAX_REF_DEPTH { - return Err(peel::to_id::Error::DepthLimitExceeded { - max_depth: MAX_REF_DEPTH, - }); - } - } - }; + let mut oid = self.follow_to_object_in_place_packed(store, packed)?; let mut buf = Vec::new(); - let mut oid = self.target.try_id().expect("peeled ref").to_owned(); let peeled_id = loop { let gix_object::Data { kind, data } = objects @@ -136,12 +127,44 @@ impl ReferenceExt for Reference { }; }; self.peeled = Some(peeled_id); - self.target = Target::Peeled(peeled_id); + self.target = Target::Object(peeled_id); Ok(peeled_id) } } } + fn follow_to_object_in_place_packed( + &mut self, + store: &file::Store, + packed: Option<&packed::Buffer>, + ) -> Result { + match self.target { + Target::Object(id) => Ok(id), + Target::Symbolic(_) => { + let mut seen = BTreeSet::new(); + let cursor = &mut *self; + while let Some(next) = cursor.follow_packed(store, packed) { + let next = next?; + if seen.contains(&next.name) { + return Err(peel::to_object::Error::Cycle { + start_absolute: store.reference_path(cursor.name.as_ref()), + }); + } + *cursor = next; + seen.insert(cursor.name.clone()); + const MAX_REF_DEPTH: usize = 5; + if seen.len() == MAX_REF_DEPTH { + return Err(peel::to_object::Error::DepthLimitExceeded { + max_depth: MAX_REF_DEPTH, + }); + } + } + let oid = self.target.try_id().expect("peeled ref").to_owned(); + Ok(oid) + } + } + } + fn follow(&self, store: &file::Store) -> Option> { let packed = match store .assure_packed_refs_uptodate() @@ -158,21 +181,14 @@ impl ReferenceExt for Reference { store: &file::Store, packed: Option<&packed::Buffer>, ) -> Option> { - match self.peeled { - Some(peeled) => Some(Ok(Reference { - name: self.name.clone(), - target: Target::Peeled(peeled), - peeled: None, - })), - None => match &self.target { - Target::Peeled(_) => None, - Target::Symbolic(full_name) => match store.try_find_packed(full_name.as_ref(), packed) { - Ok(Some(next)) => Some(Ok(next)), - Ok(None) => Some(Err(file::find::existing::Error::NotFound { - name: full_name.to_path().to_owned(), - })), - Err(err) => Some(Err(file::find::existing::Error::Find(err))), - }, + match &self.target { + Target::Object(_) => None, + Target::Symbolic(full_name) => match store.try_find_packed(full_name.as_ref(), packed) { + Ok(Some(next)) => Some(Ok(next)), + Ok(None) => Some(Err(file::find::existing::Error::NotFound { + name: full_name.to_path().to_owned(), + })), + Err(err) => Some(Err(file::find::existing::Error::Find(err))), }, } } diff --git a/gix-ref/src/store/file/transaction/commit.rs b/gix-ref/src/store/file/transaction/commit.rs index 829e45ebb8e..deb34172c15 100644 --- a/gix-ref/src/store/file/transaction/commit.rs +++ b/gix-ref/src/store/file/transaction/commit.rs @@ -54,17 +54,17 @@ impl<'s, 'p> Transaction<'s, 'p> { // Unless, the ref is new and we can obtain a peeled id // identified by the expectation of what could be there, as is the case when cloning. match expected { - PreviousValue::ExistingMustMatch(Target::Peeled(oid)) => { + PreviousValue::ExistingMustMatch(Target::Object(oid)) => { Some((Some(gix_hash::ObjectId::null(oid.kind())), oid)) } _ => None, } } - Target::Peeled(new_oid) => { + Target::Object(new_oid) => { let previous = match expected { // Here, this means that the ref already existed, and that it will receive (even transitively) // the given value - PreviousValue::MustExistAndMatch(Target::Peeled(oid)) => Some(oid.to_owned()), + PreviousValue::MustExistAndMatch(Target::Object(oid)) => Some(oid.to_owned()), _ => None, } .or(change.leaf_referent_previous_oid); @@ -88,7 +88,7 @@ impl<'s, 'p> Transaction<'s, 'p> { // Don't do anything else while keeping the lock after potentially updating the reflog. // We delay deletion of the reference and dropping the lock to after the packed-refs were // safely written. - if delete_loose_refs && matches!(new, Target::Peeled(_)) { + if delete_loose_refs && matches!(new, Target::Object(_)) { change.lock = lock; continue; } @@ -156,7 +156,7 @@ impl<'s, 'p> Transaction<'s, 'p> { log: LogChange { mode, .. }, new, .. - } => delete_loose_refs && *mode == RefLog::AndReference && matches!(new, Target::Peeled(_)), + } => delete_loose_refs && *mode == RefLog::AndReference && matches!(new, Target::Object(_)), Change::Delete { log: mode, .. } => *mode == RefLog::AndReference, }; if take_lock_and_delete { diff --git a/gix-ref/src/store/file/transaction/prepare.rs b/gix-ref/src/store/file/transaction/prepare.rs index 79d86c6cbd5..4b17b71b2b9 100644 --- a/gix-ref/src/store/file/transaction/prepare.rs +++ b/gix-ref/src/store/file/transaction/prepare.rs @@ -124,7 +124,7 @@ impl<'s, 'p> Transaction<'s, 'p> { | (PreviousValue::MustExist, Some(_)) | (PreviousValue::MustNotExist | PreviousValue::ExistingMustMatch(_), None) => {} (PreviousValue::MustExist, None) => { - let expected = Target::Peeled(store.object_hash.null()); + let expected = Target::Object(store.object_hash.null()); let full_name = change.name(); return Err(Error::MustExist { full_name, expected }); } @@ -163,9 +163,9 @@ impl<'s, 'p> Transaction<'s, 'p> { fn new_would_change_existing(new: &Target, existing: &Target) -> (bool, bool) { match (new, existing) { - (Target::Peeled(new), Target::Peeled(old)) => (old != new, false), + (Target::Object(new), Target::Object(old)) => (old != new, false), (Target::Symbolic(new), Target::Symbolic(old)) => (old != new, true), - (Target::Peeled(_), _) => (true, false), + (Target::Object(_), _) => (true, false), (Target::Symbolic(_), _) => (true, true), } } @@ -182,7 +182,7 @@ impl<'s, 'p> Transaction<'s, 'p> { let mut lock = lock.take().map_or_else(obtain_lock, Ok)?; lock.with_mut(|file| match new { - Target::Peeled(oid) => write!(file, "{oid}"), + Target::Object(oid) => write!(file, "{oid}"), Target::Symbolic(name) => writeln!(file, "ref: {}", name.0), })?; Some(lock.close()?) @@ -277,7 +277,7 @@ impl<'s, 'p> Transaction<'s, 'p> { }; if let Some(ref mut num_updates) = maybe_updates_for_packed_refs { if let Change::Update { - new: Target::Peeled(_), .. + new: Target::Object(_), .. } = edit.update.change { edits_for_packed_transaction.push(RefEdit { @@ -390,7 +390,7 @@ impl<'s, 'p> Transaction<'s, 'p> { // traverse parent chain from leaf/peeled ref and set the leaf previous oid accordingly // to help with their reflog entries - if let (Some(crate::TargetRef::Peeled(oid)), Some(parent_idx)) = + if let (Some(crate::TargetRef::Object(oid)), Some(parent_idx)) = (change.update.change.previous_value(), change.parent_index) { let oid = oid.to_owned(); diff --git a/gix-ref/src/store/packed/transaction.rs b/gix-ref/src/store/packed/transaction.rs index 8c615a10d4b..d4e2503d760 100644 --- a/gix-ref/src/store/packed/transaction.rs +++ b/gix-ref/src/store/packed/transaction.rs @@ -102,7 +102,7 @@ impl packed::Transaction { let mut buf = Vec::new(); for edit in &mut edits { if let Change::Update { - new: Target::Peeled(new), + new: Target::Object(new), .. } = edit.inner.change { @@ -235,7 +235,7 @@ fn write_edit(out: &mut dyn std::io::Write, edit: &Edit, lines_written: &mut i32 match edit.inner.change { Change::Delete { .. } => {} Change::Update { - new: Target::Peeled(target_oid), + new: Target::Object(target_oid), .. } => { write!(out, "{target_oid} ")?; diff --git a/gix-ref/src/target.rs b/gix-ref/src/target.rs index 15d1e3fc0e9..46967f969bd 100644 --- a/gix-ref/src/target.rs +++ b/gix-ref/src/target.rs @@ -9,28 +9,28 @@ impl<'a> TargetRef<'a> { pub fn kind(&self) -> Kind { match self { TargetRef::Symbolic(_) => Kind::Symbolic, - TargetRef::Peeled(_) => Kind::Peeled, + TargetRef::Object(_) => Kind::Object, } } /// Interpret this target as object id which maybe `None` if it is symbolic. pub fn try_id(&self) -> Option<&oid> { match self { TargetRef::Symbolic(_) => None, - TargetRef::Peeled(oid) => Some(oid), + TargetRef::Object(oid) => Some(oid), } } /// Interpret this target as object id or **panic** if it is symbolic. pub fn id(&self) -> &oid { match self { TargetRef::Symbolic(_) => panic!("BUG: tries to obtain object id from symbolic target"), - TargetRef::Peeled(oid) => oid, + TargetRef::Object(oid) => oid, } } /// Interpret this target as name of the reference it points to which maybe `None` if it an object id. pub fn try_name(&self) -> Option<&FullNameRef> { match self { TargetRef::Symbolic(name) => Some(name), - TargetRef::Peeled(_) => None, + TargetRef::Object(_) => None, } } /// Convert this instance into an owned version, without consuming it. @@ -44,14 +44,14 @@ impl Target { pub fn kind(&self) -> Kind { match self { Target::Symbolic(_) => Kind::Symbolic, - Target::Peeled(_) => Kind::Peeled, + Target::Object(_) => Kind::Object, } } /// Return true if this is a peeled target with a null hash pub fn is_null(&self) -> bool { match self { - Target::Peeled(oid) => oid.is_null(), + Target::Object(oid) => oid.is_null(), Target::Symbolic(_) => false, } } @@ -59,7 +59,7 @@ impl Target { /// Interpret this owned Target as shared Target pub fn to_ref(&self) -> TargetRef<'_> { match self { - Target::Peeled(oid) => TargetRef::Peeled(oid), + Target::Object(oid) => TargetRef::Object(oid), Target::Symbolic(name) => TargetRef::Symbolic(name.as_ref()), } } @@ -68,21 +68,21 @@ impl Target { pub fn try_id(&self) -> Option<&oid> { match self { Target::Symbolic(_) => None, - Target::Peeled(oid) => Some(oid), + Target::Object(oid) => Some(oid), } } /// Interpret this target as object id or panic if it is symbolic. pub fn id(&self) -> &oid { match self { Target::Symbolic(_) => panic!("BUG: tries to obtain object id from symbolic target"), - Target::Peeled(oid) => oid, + Target::Object(oid) => oid, } } /// Return the contained object id or panic pub fn into_id(self) -> ObjectId { match self { Target::Symbolic(_) => panic!("BUG: expected peeled reference target but found symbolic one"), - Target::Peeled(oid) => oid, + Target::Object(oid) => oid, } } @@ -90,14 +90,14 @@ impl Target { pub fn try_into_id(self) -> Result { match self { Target::Symbolic(_) => Err(self), - Target::Peeled(oid) => Ok(oid), + Target::Object(oid) => Ok(oid), } } /// Interpret this target as name of the reference it points to which maybe `None` if it an object id. pub fn try_name(&self) -> Option<&FullNameRef> { match self { Target::Symbolic(name) => Some(name.as_ref()), - Target::Peeled(_) => None, + Target::Object(_) => None, } } } @@ -105,7 +105,7 @@ impl Target { impl<'a> From> for Target { fn from(src: TargetRef<'a>) -> Self { match src { - TargetRef::Peeled(oid) => Target::Peeled(oid.to_owned()), + TargetRef::Object(oid) => Target::Object(oid.to_owned()), TargetRef::Symbolic(name) => Target::Symbolic(name.to_owned()), } } @@ -114,7 +114,7 @@ impl<'a> From> for Target { impl<'a> PartialEq> for Target { fn eq(&self, other: &TargetRef<'a>) -> bool { match (self, other) { - (Target::Peeled(lhs), TargetRef::Peeled(rhs)) => lhs == rhs, + (Target::Object(lhs), TargetRef::Object(rhs)) => lhs == rhs, (Target::Symbolic(lhs), TargetRef::Symbolic(rhs)) => lhs.as_bstr() == rhs.as_bstr(), _ => false, } @@ -123,7 +123,7 @@ impl<'a> PartialEq> for Target { impl From for Target { fn from(id: ObjectId) -> Self { - Target::Peeled(id) + Target::Object(id) } } @@ -132,7 +132,7 @@ impl TryFrom for ObjectId { fn try_from(value: Target) -> Result { match value { - Target::Peeled(id) => Ok(id), + Target::Object(id) => Ok(id), Target::Symbolic(_) => Err(value), } } @@ -147,7 +147,7 @@ impl From for Target { impl fmt::Display for Target { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Target::Peeled(oid) => oid.fmt(f), + Target::Object(oid) => oid.fmt(f), Target::Symbolic(name) => write!(f, "ref: {}", name.as_bstr()), } } diff --git a/gix-ref/tests/file/mod.rs b/gix-ref/tests/file/mod.rs index 9c2aa843418..0c71dba67b5 100644 --- a/gix-ref/tests/file/mod.rs +++ b/gix-ref/tests/file/mod.rs @@ -18,6 +18,11 @@ pub fn store_at(name: &str) -> crate::Result { Ok(Store::at(path.join(".git"), Default::default())) } +pub fn store_at_with_args(name: &str, args: impl IntoIterator>) -> crate::Result { + let path = gix_testtools::scripted_fixture_read_only_with_args_standalone(name, args)?; + Ok(Store::at(path.join(".git"), Default::default())) +} + fn store_writable(name: &str) -> crate::Result<(gix_testtools::tempfile::TempDir, Store)> { let dir = gix_testtools::scripted_fixture_writable_standalone(name)?; let git_dir = dir.path().join(".git"); diff --git a/gix-ref/tests/file/reference.rs b/gix-ref/tests/file/reference.rs index d4c071ab3af..0539e9ecb7e 100644 --- a/gix-ref/tests/file/reference.rs +++ b/gix-ref/tests/file/reference.rs @@ -47,6 +47,7 @@ mod reflog { } mod peel { + use gix_object::FindExt; use gix_ref::{file::ReferenceExt, Reference}; use crate::{ @@ -63,13 +64,13 @@ mod peel { let nr = Reference::from(r).follow(&store).expect("exists").expect("no failure"); assert!( - matches!(nr.target.to_ref(), gix_ref::TargetRef::Peeled(_)), + matches!(nr.target.to_ref(), gix_ref::TargetRef::Object(_)), "iteration peels a single level" ); assert!(nr.follow(&store).is_none(), "end of iteration"); assert_eq!( nr.target.to_ref(), - gix_ref::TargetRef::Peeled(&hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03")), + gix_ref::TargetRef::Object(&hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03")), "we still have the peeled target" ); Ok(()) @@ -93,25 +94,37 @@ mod peel { fn peel_one_level_with_pack() -> crate::Result { let store = store_with_packed_refs()?; - let head = store.find("dt1")?; + let mut head = store.find("dt1")?; assert_eq!( head.target.try_id().map(ToOwned::to_owned), Some(hex_to_id("4c3f4cce493d7beb45012e478021b5f65295e5a3")) ); assert_eq!( head.kind(), - gix_ref::Kind::Peeled, - "its peeled, but does have another step to peel to" + gix_ref::Kind::Object, + "its peeled, but does have another step to peel to…" + ); + let final_stop = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); + assert_eq!(head.peeled, Some(final_stop), "…it knows its peeled object"); + + assert_eq!( + head.follow(&store).transpose()?, + None, + "but following doesn't do that, only real peeling does" ); - let peeled = head.follow(&store).expect("a peeled ref for the object")?; + head.peel_to_id_in_place(&store, &EmptyCommit)?; assert_eq!( - peeled.target.try_id().map(ToOwned::to_owned), - Some(hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03")), + head.target.try_id().map(ToOwned::to_owned), + Some(final_stop), "packed refs are always peeled (at least the ones we choose to read)" ); - assert_eq!(peeled.kind(), gix_ref::Kind::Peeled, "it's terminally peeled now"); - assert!(peeled.follow(&store).is_none()); + assert_eq!(head.kind(), gix_ref::Kind::Object, "it's terminally peeled now"); + assert_eq!( + head.follow(&store).transpose()?, + None, + "following doesn't change anything" + ); Ok(()) } @@ -143,6 +156,39 @@ mod peel { Ok(()) } + #[test] + fn to_id_long_jump() -> crate::Result { + for packed in [None, Some("packed")] { + let store = file::store_at_with_args("make_multi_hop_ref.sh", packed)?; + let odb = gix_odb::at(store.git_dir().join("objects"))?; + let mut r: Reference = store.find("multi-hop")?; + r.peel_to_id_in_place(&store, &odb)?; + + let commit_id = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); + assert_eq!(r.peeled, Some(commit_id)); + + let mut buf = Vec::new(); + let obj = odb.find(&commit_id, &mut buf)?; + assert_eq!(obj.kind, gix_object::Kind::Commit, "always peeled to the first non-tag"); + + let mut r: Reference = store.find("multi-hop")?; + let tag_id = + r.follow_to_object_in_place_packed(&store, store.cached_packed_buffer()?.as_ref().map(|p| &***p))?; + let obj = odb.find(&tag_id, &mut buf)?; + assert_eq!(obj.kind, gix_object::Kind::Tag, "the first direct object target"); + assert_eq!( + obj.decode()?.into_tag().expect("tag").name, + "dt2", + "this is the first annotated tag, which points at dt1" + ); + let mut r: Reference = store.find("multi-hop2")?; + let other_tag_id = + r.follow_to_object_in_place_packed(&store, store.cached_packed_buffer()?.as_ref().map(|p| &***p))?; + assert_eq!(other_tag_id, tag_id, "it can follow with multiple hops as well"); + } + Ok(()) + } + #[test] fn to_id_cycle() -> crate::Result { let store = file::store()?; @@ -152,9 +198,15 @@ mod peel { assert!(matches!( r.peel_to_id_in_place(&store, &gix_object::find::Never).unwrap_err(), - gix_ref::peel::to_id::Error::Cycle { .. } + gix_ref::peel::to_id::Error::FollowToObject(gix_ref::peel::to_object::Error::Cycle { .. }) )); assert_eq!(r.name.as_bstr(), "refs/loop-a", "the ref is not changed on error"); + + let mut r: Reference = store.find_loose("loop-a")?.into(); + let err = r + .follow_to_object_in_place_packed(&store, store.cached_packed_buffer()?.as_ref().map(|p| &***p)) + .unwrap_err(); + assert!(matches!(err, gix_ref::peel::to_object::Error::Cycle { .. })); Ok(()) } } @@ -204,7 +256,7 @@ mod parse { mktest!( peeled, b"c5241b835b93af497cda80ce0dceb8f49800df1c\n", - gix_ref::Kind::Peeled, + gix_ref::Kind::Object, Some(hex_to_id("c5241b835b93af497cda80ce0dceb8f49800df1c").as_ref()), None ); diff --git a/gix-ref/tests/file/store/find.rs b/gix-ref/tests/file/store/find.rs index 2343b33fb42..d4eb8dd0e88 100644 --- a/gix-ref/tests/file/store/find.rs +++ b/gix-ref/tests/file/store/find.rs @@ -133,19 +133,19 @@ mod loose { fn success() -> crate::Result { let store = store()?; for (partial_name, expected_path, expected_ref_kind) in &[ - ("dt1", "refs/tags/dt1", gix_ref::Kind::Peeled), // tags before heads - ("FETCH_HEAD", "FETCH_HEAD", gix_ref::Kind::Peeled), // special ref - ("heads/dt1", "refs/heads/dt1", gix_ref::Kind::Peeled), - ("d1", "refs/d1", gix_ref::Kind::Peeled), // direct refs before heads - ("heads/d1", "refs/heads/d1", gix_ref::Kind::Peeled), + ("dt1", "refs/tags/dt1", gix_ref::Kind::Object), // tags before heads + ("FETCH_HEAD", "FETCH_HEAD", gix_ref::Kind::Object), // special ref + ("heads/dt1", "refs/heads/dt1", gix_ref::Kind::Object), + ("d1", "refs/d1", gix_ref::Kind::Object), // direct refs before heads + ("heads/d1", "refs/heads/d1", gix_ref::Kind::Object), ("HEAD", "HEAD", gix_ref::Kind::Symbolic), // it finds shortest paths first ("origin", "refs/remotes/origin/HEAD", gix_ref::Kind::Symbolic), ("origin/HEAD", "refs/remotes/origin/HEAD", gix_ref::Kind::Symbolic), - ("origin/main", "refs/remotes/origin/main", gix_ref::Kind::Peeled), - ("t1", "refs/tags/t1", gix_ref::Kind::Peeled), - ("main", "refs/heads/main", gix_ref::Kind::Peeled), - ("heads/main", "refs/heads/main", gix_ref::Kind::Peeled), - ("refs/heads/main", "refs/heads/main", gix_ref::Kind::Peeled), + ("origin/main", "refs/remotes/origin/main", gix_ref::Kind::Object), + ("t1", "refs/tags/t1", gix_ref::Kind::Object), + ("main", "refs/heads/main", gix_ref::Kind::Object), + ("heads/main", "refs/heads/main", gix_ref::Kind::Object), + ("refs/heads/main", "refs/heads/main", gix_ref::Kind::Object), ] { let reference = store.try_find_loose(*partial_name)?.expect("exists"); assert_eq!(reference.name.as_bstr(), expected_path); diff --git a/gix-ref/tests/file/store/iter.rs b/gix-ref/tests/file/store/iter.rs index 0ed5a971843..900a15d56f0 100644 --- a/gix-ref/tests/file/store/iter.rs +++ b/gix-ref/tests/file/store/iter.rs @@ -394,16 +394,16 @@ fn overlay_iter() -> crate::Result { assert_eq!( ref_names, vec![ - (b"refs/heads/main".as_bstr().to_owned(), Peeled(c1)), - ("refs/heads/newer-as-loose".into(), Peeled(c2)), + (b"refs/heads/main".as_bstr().to_owned(), Object(c1)), + ("refs/heads/newer-as-loose".into(), Object(c2)), ( "refs/remotes/origin/HEAD".into(), Symbolic("refs/remotes/origin/main".try_into()?), ), - ("refs/remotes/origin/main".into(), Peeled(c1)), + ("refs/remotes/origin/main".into(), Object(c1)), ( "refs/tags/tag-object".into(), - Peeled(hex_to_id("b3109a7e51fc593f85b145a76c70ddd1d133fafd")), + Object(hex_to_id("b3109a7e51fc593f85b145a76c70ddd1d133fafd")), ) ] ); @@ -440,8 +440,8 @@ fn overlay_prefixed_iter() -> crate::Result { assert_eq!( ref_names, vec![ - (b"refs/heads/main".as_bstr().to_owned(), Peeled(c1)), - ("refs/heads/newer-as-loose".into(), Peeled(c2)), + (b"refs/heads/main".as_bstr().to_owned(), Object(c1)), + ("refs/heads/newer-as-loose".into(), Object(c2)), ] ); Ok(()) @@ -458,6 +458,6 @@ fn overlay_partial_prefix_iter() -> crate::Result { .map(|r| r.map(|r| (r.name.as_bstr().to_owned(), r.target))) .collect::, _>>()?; let c1 = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); - assert_eq!(ref_names, vec![(b"refs/heads/main".as_bstr().to_owned(), Peeled(c1)),]); + assert_eq!(ref_names, vec![(b"refs/heads/main".as_bstr().to_owned(), Object(c1)),]); Ok(()) } diff --git a/gix-ref/tests/file/store/mod.rs b/gix-ref/tests/file/store/mod.rs index 4cfd6120b03..ceb76e52d9f 100644 --- a/gix-ref/tests/file/store/mod.rs +++ b/gix-ref/tests/file/store/mod.rs @@ -105,7 +105,7 @@ fn precompose_unicode_journey() -> crate::Result { // Intentionally use the decomposed versions of their names store_decomposed .loose_iter()? - .filter_map(|r| r.ok().filter(|r| r.kind() == gix_ref::Kind::Peeled)) + .filter_map(|r| r.ok().filter(|r| r.kind() == gix_ref::Kind::Object)) .map(|r| RefEdit { change: Change::Update { log: LogChange::default(), diff --git a/gix-ref/tests/file/transaction/mod.rs b/gix-ref/tests/file/transaction/mod.rs index 348f76df6e7..926b4d597bf 100644 --- a/gix-ref/tests/file/transaction/mod.rs +++ b/gix-ref/tests/file/transaction/mod.rs @@ -56,7 +56,7 @@ pub(crate) mod prepare_and_commit { message: "log peeled".into(), }, expected: PreviousValue::MustNotExist, - new: Target::Peeled(hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")), + new: Target::Object(hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")), }, name: name.try_into().expect("valid"), deref: false, diff --git a/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/collisions.rs b/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/collisions.rs index 5ef7a0ad1eb..622a589bfb4 100644 --- a/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/collisions.rs +++ b/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/collisions.rs @@ -127,7 +127,7 @@ fn conflicting_creation_into_packed_refs() -> crate::Result { change: Change::Update { log: LogChange::default(), expected: PreviousValue::Any, - new: Target::Peeled(gix_hash::Kind::Sha1.null()), + new: Target::Object(gix_hash::Kind::Sha1.null()), }, name: "refs/a".try_into().expect("valid"), deref: false, @@ -135,10 +135,10 @@ fn conflicting_creation_into_packed_refs() -> crate::Result { RefEdit { change: Change::Update { log: LogChange::default(), - expected: PreviousValue::MustExistAndMatch(Target::Peeled(hex_to_id( + expected: PreviousValue::MustExistAndMatch(Target::Object(hex_to_id( "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", ))), - new: Target::Peeled(gix_hash::Kind::Sha1.null()), + new: Target::Object(gix_hash::Kind::Sha1.null()), }, name: "refs/A".try_into().expect("valid"), deref: false, diff --git a/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs b/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs index 6c71b98ca76..2741a5fda2f 100644 --- a/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs +++ b/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs @@ -106,7 +106,7 @@ fn reference_with_equally_named_empty_or_non_empty_directory_already_in_place_ca fn reference_with_old_value_must_exist_when_creating_it() -> crate::Result { let (_keep, store) = empty_store()?; - let new_target = Target::Peeled(gix_hash::Kind::Sha1.null()); + let new_target = Target::Object(gix_hash::Kind::Sha1.null()); let res = store.transaction().prepare( Some(RefEdit { change: Change::Update { @@ -141,8 +141,8 @@ fn reference_with_explicit_value_must_match_the_value_on_update() -> crate::Resu Some(RefEdit { change: Change::Update { log: LogChange::default(), - new: Target::Peeled(gix_hash::Kind::Sha1.null()), - expected: PreviousValue::MustExistAndMatch(Target::Peeled(hex_to_id( + new: Target::Object(gix_hash::Kind::Sha1.null()), + expected: PreviousValue::MustExistAndMatch(Target::Object(hex_to_id( "28ce6a8b26aa170e1de65536fe8abe1832bd3242", ))), }, @@ -165,14 +165,14 @@ fn reference_with_explicit_value_must_match_the_value_on_update() -> crate::Resu #[test] fn the_existing_must_match_constraint_allow_non_existing_references_to_be_created() -> crate::Result { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; - let expected = PreviousValue::ExistingMustMatch(Target::Peeled(ObjectId::empty_tree(gix_hash::Kind::Sha1))); + let expected = PreviousValue::ExistingMustMatch(Target::Object(ObjectId::empty_tree(gix_hash::Kind::Sha1))); let edits = store .transaction() .prepare( Some(RefEdit { change: Change::Update { log: LogChange::default(), - new: Target::Peeled(gix_hash::Kind::Sha1.null()), + new: Target::Object(gix_hash::Kind::Sha1.null()), expected: expected.clone(), }, name: "refs/heads/new".try_into()?, @@ -188,7 +188,7 @@ fn the_existing_must_match_constraint_allow_non_existing_references_to_be_create vec![RefEdit { change: Change::Update { log: LogChange::default(), - new: Target::Peeled(gix_hash::Kind::Sha1.null()), + new: Target::Object(gix_hash::Kind::Sha1.null()), expected, }, name: "refs/heads/new".try_into()?, @@ -209,8 +209,8 @@ fn the_existing_must_match_constraint_requires_existing_references_to_have_the_g Some(RefEdit { change: Change::Update { log: LogChange::default(), - new: Target::Peeled(gix_hash::Kind::Sha1.null()), - expected: PreviousValue::ExistingMustMatch(Target::Peeled(hex_to_id( + new: Target::Object(gix_hash::Kind::Sha1.null()), + expected: PreviousValue::ExistingMustMatch(Target::Object(hex_to_id( "28ce6a8b26aa170e1de65536fe8abe1832bd3242", ))), }, @@ -273,7 +273,7 @@ fn reference_with_must_exist_constraint_must_exist_already_with_any_value() -> c let target = head.target; let previous_reflog_count = reflog_lines(&store, "HEAD")?.len(); - let new_target = Target::Peeled(ObjectId::empty_tree(gix_hash::Kind::Sha1)); + let new_target = Target::Object(ObjectId::empty_tree(gix_hash::Kind::Sha1)); let edits = store .transaction() .prepare( @@ -404,7 +404,7 @@ fn symbolic_reference_writes_reflog_if_previous_value_is_set() -> crate::Result change: Change::Update { log, new: new_head_value, - expected: PreviousValue::ExistingMustMatch(Target::Peeled(new_oid)), + expected: PreviousValue::ExistingMustMatch(Target::Object(new_oid)), }, name: "refs/heads/symbolic".try_into()?, deref: false, @@ -441,7 +441,7 @@ fn windows_device_name_is_illegal_with_enabled_windows_protections() -> crate::R message: "ignored".into(), }; - let new = Target::Peeled(hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242")); + let new = Target::Object(hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242")); for invalid_name in ["refs/heads/CON", "refs/CON/still-invalid"] { let err = store .transaction() @@ -550,7 +550,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { assert!(store.try_find_loose(referent)?.is_none(), "referent wasn't created"); let new_oid = hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242"); - let new = Target::Peeled(new_oid); + let new = Target::Object(new_oid); let log = LogChange { message: "an actual change".into(), mode: RefLog::AndReference, @@ -614,7 +614,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { ); let referent_ref = store.find_loose(referent)?; - assert_eq!(referent_ref.kind(), gix_ref::Kind::Peeled, "referent is a peeled ref"); + assert_eq!(referent_ref.kind(), gix_ref::Kind::Object, "referent is a peeled ref"); assert_eq!( referent_ref.target.to_ref().try_id(), Some(new_oid.as_ref()), @@ -661,7 +661,7 @@ fn write_reference_to_which_head_points_to_does_not_update_heads_reflog_even_tho message: "".into(), }, expected: PreviousValue::MustExist, - new: Target::Peeled(new_id), + new: Target::Object(new_id), }, name: referent.as_bstr().try_into()?, deref: false, @@ -681,10 +681,10 @@ fn write_reference_to_which_head_points_to_does_not_update_heads_reflog_even_tho force_create_reflog: false, message: "".into(), }, - expected: PreviousValue::MustExistAndMatch(Target::Peeled(hex_to_id( + expected: PreviousValue::MustExistAndMatch(Target::Object(hex_to_id( "02a7a22d90d7c02fb494ed25551850b868e634f0" )),), - new: Target::Peeled(new_id), + new: Target::Object(new_id), }, name: referent.as_bstr().try_into()?, deref: false, @@ -726,8 +726,8 @@ fn packed_refs_are_looked_up_when_checking_existing_values() -> crate::Result { force_create_reflog: false, message: "for pack".into(), }, - expected: PreviousValue::MustExistAndMatch(Target::Peeled(old_id)), - new: Target::Peeled(new_id), + expected: PreviousValue::MustExistAndMatch(Target::Object(old_id)), + new: Target::Object(new_id), }, name: "refs/heads/main".try_into()?, deref: false, @@ -774,7 +774,7 @@ fn packed_refs_creation_with_packed_refs_mode_prune_removes_original_loose_refs( .prepare( store .loose_iter()? - .filter_map(|r| r.ok().filter(|r| r.kind() == gix_ref::Kind::Peeled)) + .filter_map(|r| r.ok().filter(|r| r.kind() == gix_ref::Kind::Object)) .map(|r| RefEdit { change: Change::Update { log: LogChange::default(), @@ -887,7 +887,7 @@ fn packed_refs_deletion_in_deletions_and_updates_mode() -> crate::Result { .prepare( Some(RefEdit { change: Change::Delete { - expected: PreviousValue::MustExistAndMatch(Target::Peeled(old_id)), + expected: PreviousValue::MustExistAndMatch(Target::Object(old_id)), log: RefLog::AndReference, }, name: "refs/heads/d1".try_into()?, diff --git a/gix-ref/tests/file/transaction/prepare_and_commit/delete.rs b/gix-ref/tests/file/transaction/prepare_and_commit/delete.rs index c4a8c94c4fd..6b422e1e113 100644 --- a/gix-ref/tests/file/transaction/prepare_and_commit/delete.rs +++ b/gix-ref/tests/file/transaction/prepare_and_commit/delete.rs @@ -242,7 +242,7 @@ fn delete_broken_ref_that_must_exist_fails_as_it_is_no_valid_ref() -> crate::Res fn non_existing_can_be_deleted_with_the_may_exist_match_constraint() -> crate::Result { let (_keep, store) = empty_store()?; let previous_value = - PreviousValue::ExistingMustMatch(Target::Peeled(hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"))); + PreviousValue::ExistingMustMatch(Target::Object(hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"))); let edits = store .transaction() .prepare( @@ -363,7 +363,7 @@ fn packed_refs_are_consulted_when_determining_previous_value_of_ref_to_be_delete .prepare( Some(RefEdit { change: Change::Delete { - expected: PreviousValue::MustExistAndMatch(Target::Peeled(old_id)), + expected: PreviousValue::MustExistAndMatch(Target::Object(old_id)), log: RefLog::AndReference, }, name: "refs/heads/main".try_into()?, @@ -397,7 +397,7 @@ fn a_loose_ref_with_old_value_check_and_outdated_packed_refs_value_deletes_both_ .prepare( Some(RefEdit { change: Change::Delete { - expected: PreviousValue::MustExistAndMatch(Target::Peeled(branch_id)), + expected: PreviousValue::MustExistAndMatch(Target::Object(branch_id)), log: RefLog::AndReference, }, name: branch.name, @@ -432,8 +432,8 @@ fn all_contained_references_deletes_the_packed_ref_file_too() -> crate::Result { RefEdit { change: Change::Delete { expected: match mode { - "must-exist" => PreviousValue::MustExistAndMatch(Target::Peeled(r.target())), - "may-exist" => PreviousValue::ExistingMustMatch(Target::Peeled(r.target())), + "must-exist" => PreviousValue::MustExistAndMatch(Target::Object(r.target())), + "may-exist" => PreviousValue::ExistingMustMatch(Target::Object(r.target())), _ => unimplemented!("unknown mode: {}", mode), }, log: RefLog::AndReference, diff --git a/gix-ref/tests/file/worktree.rs b/gix-ref/tests/file/worktree.rs index 7b723eca107..321b7d17159 100644 --- a/gix-ref/tests/file/worktree.rs +++ b/gix-ref/tests/file/worktree.rs @@ -209,7 +209,7 @@ mod writable { Change::Update { log: LogChange::default(), expected: PreviousValue::MustNotExist, - new: Target::Peeled(id), + new: Target::Object(id), } } diff --git a/gix-ref/tests/fixtures/generated-archives/.gitignore b/gix-ref/tests/fixtures/generated-archives/.gitignore index 59d684d6015..1350be16f54 100644 --- a/gix-ref/tests/fixtures/generated-archives/.gitignore +++ b/gix-ref/tests/fixtures/generated-archives/.gitignore @@ -1,2 +1,3 @@ make_worktree_repo.tar make_worktree_repo_packed.tar +make_multi_hop_ref*.tar diff --git a/gix-ref/tests/fixtures/make_multi_hop_ref.sh b/gix-ref/tests/fixtures/make_multi_hop_ref.sh new file mode 100644 index 00000000000..36484a1a1fd --- /dev/null +++ b/gix-ref/tests/fixtures/make_multi_hop_ref.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q + +git checkout -q -b main +git commit -q --allow-empty -m c1 + +git tag t1 +git tag -m "tag object" dt1 +git tag -m "tag object indirect" dt2 dt1 + +echo "ref: refs/tags/dt2" > .git/refs/multi-hop +echo "ref: refs/multi-hop" > .git/refs/multi-hop2 + +if [ "${1:-}" = "packed" ]; then + git pack-refs --all --prune +fi diff --git a/gix-ref/tests/transaction/mod.rs b/gix-ref/tests/transaction/mod.rs index 64d7627c0dd..bac6a7bd0bb 100644 --- a/gix-ref/tests/transaction/mod.rs +++ b/gix-ref/tests/transaction/mod.rs @@ -117,7 +117,7 @@ mod refedit_ext { fn non_symbolic_refs_are_ignored_or_if_the_deref_flag_is_not_set() -> crate::Result { let store = MockStore::with(Some(( "refs/heads/anything-but-not-symbolic", - Target::Peeled(gix_hash::Kind::Sha1.null()), + Target::Object(gix_hash::Kind::Sha1.null()), ))); let mut edits = vec![ RefEdit { @@ -204,7 +204,7 @@ mod refedit_ext { force_create_reflog: true, message: "the log message".into(), }, - new: Target::Peeled(gix_hash::Kind::Sha1.null()), + new: Target::Object(gix_hash::Kind::Sha1.null()), }, name: "refs/heads/update-symbolic-1".try_into()?, deref: true, @@ -236,7 +236,7 @@ mod refedit_ext { ), ( "refs/heads/delete-symbolic-3", - Target::Peeled(hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")), + Target::Object(hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")), ), ( "refs/heads/update-symbolic-1", @@ -248,7 +248,7 @@ mod refedit_ext { ), ( "refs/heads/update-symbolic-3", - Target::Peeled(hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")), + Target::Object(hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")), ), ]); let log = LogChange { @@ -274,7 +274,7 @@ mod refedit_ext { change: Change::Update { expected: PreviousValue::MustNotExist, log: log.clone(), - new: Target::Peeled(gix_hash::Kind::Sha1.null()), + new: Target::Object(gix_hash::Kind::Sha1.null()), }, name: "refs/heads/update-symbolic-1".try_into()?, deref: true, @@ -307,7 +307,7 @@ mod refedit_ext { change: Change::Update { expected: PreviousValue::Any, log: log_only.clone(), - new: Target::Peeled(gix_hash::Kind::Sha1.null()), + new: Target::Object(gix_hash::Kind::Sha1.null()), }, name: "refs/heads/update-symbolic-1".try_into()?, deref: false, @@ -324,7 +324,7 @@ mod refedit_ext { change: Change::Update { expected: PreviousValue::Any, log: log_only, - new: Target::Peeled(gix_hash::Kind::Sha1.null()), + new: Target::Object(gix_hash::Kind::Sha1.null()), }, name: "refs/heads/update-symbolic-2".try_into()?, deref: false, @@ -341,7 +341,7 @@ mod refedit_ext { change: Change::Update { expected: PreviousValue::MustNotExist, log, - new: Target::Peeled(gix_hash::Kind::Sha1.null()), + new: Target::Object(gix_hash::Kind::Sha1.null()), }, name: "refs/heads/update-symbolic-3".try_into()?, deref: false, diff --git a/gix/src/clone/fetch/util.rs b/gix/src/clone/fetch/util.rs index fde5241edcd..7a4347a5e9b 100644 --- a/gix/src/clone/fetch/util.rs +++ b/gix/src/clone/fetch/util.rs @@ -129,7 +129,7 @@ pub fn update_head( change: gix_ref::transaction::Change::Update { log: reflog_message(), expected: PreviousValue::Any, - new: Target::Peeled(head_peeled_id.to_owned()), + new: Target::Object(head_peeled_id.to_owned()), }, name: referent.clone(), deref: false, @@ -155,7 +155,7 @@ pub fn update_head( change: gix_ref::transaction::Change::Update { log, expected: PreviousValue::Any, - new: Target::Peeled(head_peeled_id.to_owned()), + new: Target::Object(head_peeled_id.to_owned()), }, name: head, deref: false, @@ -169,7 +169,7 @@ pub fn update_head( change: gix_ref::transaction::Change::Update { log: reflog_message(), expected: PreviousValue::Any, - new: Target::Peeled( + new: Target::Object( head_peeled_id .expect("detached heads always point to something") .to_owned(), diff --git a/gix/src/object/errors.rs b/gix/src/object/errors.rs index 4331c795d81..b170674eebf 100644 --- a/gix/src/object/errors.rs +++ b/gix/src/object/errors.rs @@ -29,6 +29,19 @@ pub mod find { pub mod existing { /// An object could not be found in the database, or an error occurred when trying to obtain it. pub type Error = gix_object::find::existing::Error; + /// + #[allow(clippy::empty_docs)] + pub mod with_conversion { + /// The error returned by [Repository::find_commit()](crate::Repository::find_commit). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Find(#[from] crate::object::find::existing::Error), + #[error(transparent)] + Convert(#[from] crate::object::try_into::Error), + } + } } } diff --git a/gix/src/object/tree/diff/change.rs b/gix/src/object/tree/diff/change.rs index e4eb5f3df81..abfeac8b24d 100644 --- a/gix/src/object/tree/diff/change.rs +++ b/gix/src/object/tree/diff/change.rs @@ -1,4 +1,7 @@ -use crate::{bstr::BStr, diff::blob::DiffLineStats, Id}; +use crate::bstr::BString; +use crate::ext::ObjectIdExt; +use crate::object::tree::diff::ChangeDetached; +use crate::{bstr::BStr, diff::blob::DiffLineStats, Id, Repository}; /// An event emitted when finding differences between two trees. #[derive(Debug, Clone, Copy)] @@ -61,17 +64,111 @@ pub enum Event<'a, 'old, 'new> { 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` + /// 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, /// deletion or rewrite. - /// Use `resource_cache` to store the diffable data and possibly reuse previously stored data. - /// Afterwards the platform, which holds on to `resource_cache`, can be used to perform ready-made operations on the + /// Use `resource_cache` to store the diffable data and possibly reuse previously stored data, usually obtained with + /// [crate::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. /// /// ### Warning about Memory Consumption @@ -85,6 +182,95 @@ impl<'a, 'old, 'new> super::Change<'a, 'old, 'new> { } } +/// Lifecycle +impl Event<'_, '_, '_> { + /// Detach the repository instance to obtain a fully-owned version + pub fn detach(self) -> EventDetached { + match self { + Event::Addition { entry_mode, id } => EventDetached::Addition { + entry_mode, + id: id.detach(), + }, + Event::Deletion { entry_mode, id } => EventDetached::Deletion { + entry_mode, + id: id.detach(), + }, + Event::Modification { + previous_entry_mode, + previous_id, + entry_mode, + id, + } => EventDetached::Modification { + previous_entry_mode, + previous_id: previous_id.detach(), + entry_mode, + id: id.detach(), + }, + Event::Rewrite { + source_location, + source_entry_mode, + source_id, + diff, + entry_mode, + id, + copy, + } => EventDetached::Rewrite { + source_location: source_location.to_owned(), + source_entry_mode, + source_id: source_id.detach(), + diff, + entry_mode, + id: id.detach(), + copy, + }, + } + } +} + +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> { + match self { + EventDetached::Addition { entry_mode, id } => Event::Addition { + entry_mode: *entry_mode, + id: id.attach(new_repo), + }, + EventDetached::Deletion { entry_mode, id } => Event::Deletion { + entry_mode: *entry_mode, + id: id.attach(old_repo), + }, + EventDetached::Modification { + previous_entry_mode, + previous_id, + entry_mode, + id, + } => Event::Modification { + previous_entry_mode: *previous_entry_mode, + previous_id: previous_id.attach(old_repo), + entry_mode: *entry_mode, + id: id.attach(new_repo), + }, + EventDetached::Rewrite { + source_location, + source_entry_mode, + source_id, + diff, + entry_mode, + id, + copy, + } => Event::Rewrite { + source_location: source_location.as_ref(), + 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, + }, + } + } +} + impl<'a, 'old, 'new> Event<'a, 'old, 'new> { /// Return the current mode of this instance. pub fn entry_mode(&self) -> gix_object::tree::EntryMode { diff --git a/gix/src/object/tree/diff/mod.rs b/gix/src/object/tree/diff/mod.rs index 299075781fa..751e6bcc5ea 100644 --- a/gix/src/object/tree/diff/mod.rs +++ b/gix/src/object/tree/diff/mod.rs @@ -1,5 +1,6 @@ use gix_diff::tree::recorder::Location; +use crate::bstr::BString; use crate::{bstr::BStr, diff::Rewrites, Tree}; /// Returned by the `for_each` function to control flow. @@ -17,12 +18,24 @@ pub enum Action { pub struct Change<'a, 'old, 'new> { /// The location of the file or directory described by `event`, if tracking was enabled. /// - /// Otherwise this value is always an empty path. + /// 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. + /// + /// 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, +} + /// #[allow(clippy::empty_docs)] pub mod change; @@ -33,13 +46,15 @@ impl<'repo> Tree<'repo> { /// /// # Performance /// - /// It's highly recommended to set an object cache to avoid extracting the same object multiple times. + /// It's highly recommended to [set an object cache](crate::Repository::compute_object_cache_size_for_tree_diffs) + /// to avoid extracting the same object multiple times. /// By default, similar to `git diff`, rename tracking will be enabled if it is not configured. /// /// 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()`]. #[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> { Ok(Platform { state: Default::default(), @@ -76,7 +91,7 @@ impl<'a, 'repo> Platform<'a, 'repo> { } /// Provide `None` to disable rewrite tracking entirely, or pass `Some()` to control to - /// what extend rename and copy tracking is performed. + /// 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. @@ -86,6 +101,73 @@ impl<'a, 'repo> Platform<'a, 'repo> { } } +/// Provide aggregated information of a diff between two trees. +#[derive(Default, Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +#[doc(alias = "DiffStats", alias = "git2")] +pub struct Stats { + /// The total amount of lines added in the between blobs of the two trees. + #[doc(alias = "insertions", alias = "git2")] + pub lines_added: u64, + /// The total amount of lines removed in the between blobs of the two trees. + #[doc(alias = "deletions", alias = "git2")] + pub lines_removed: u64, + /// The number of files that contributed to these statistics as they were added, removed or modified. + pub files_changed: u64, +} + +/// +#[allow(clippy::empty_docs)] +pub mod stats { + /// The error returned by [`stats()`](super::Platform::stats()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + CreateResourceCache(#[from] crate::repository::diff::resource_cache::Error), + #[error(transparent)] + ForEachChange(#[from] crate::object::tree::diff::for_each::Error), + } +} + +/// Convenience +impl<'a, 'repo> Platform<'a, 'repo> { + /// 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 + /// 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. + pub fn stats(&mut self, other: &Tree<'_>) -> Result { + // let (mut number_of_files, mut lines_added, mut lines_removed) = (0, 0, 0); + let mut resource_cache = self.lhs.repo.diff_resource_cache_for_tree_diff()?; + + let (mut files_changed, mut lines_added, mut lines_removed) = (0, 0, 0); + self.for_each_to_obtain_tree(other, |change| { + if let Some(counts) = change + .diff(&mut resource_cache) + .ok() + .and_then(|mut platform| platform.line_counts().ok()) + .flatten() + { + files_changed += 1; + lines_added += counts.insertions as u64; + lines_removed += counts.removals as u64; + } + + resource_cache.clear_resource_cache_keep_allocation(); + Ok::<_, std::convert::Infallible>(Action::Continue) + })?; + + Ok(Stats { + files_changed, + lines_added, + lines_removed, + }) + } +} + /// #[allow(clippy::empty_docs)] pub mod for_each; diff --git a/gix/src/reference/edits.rs b/gix/src/reference/edits.rs index b00252a3c88..3dd987e57fd 100644 --- a/gix/src/reference/edits.rs +++ b/gix/src/reference/edits.rs @@ -36,11 +36,11 @@ pub mod set_target_id { ) -> Result<(), Error> { match &self.inner.target { Target::Symbolic(name) => return Err(Error::SymbolicReference { name: name.clone() }), - Target::Peeled(current_id) => { + Target::Object(current_id) => { let changed = self.repo.reference( self.name(), id, - PreviousValue::MustExistAndMatch(Target::Peeled(current_id.to_owned())), + PreviousValue::MustExistAndMatch(Target::Object(current_id.to_owned())), reflog_message, )?; *self = changed; diff --git a/gix/src/reference/errors.rs b/gix/src/reference/errors.rs index 92714ec790c..296ccec051c 100644 --- a/gix/src/reference/errors.rs +++ b/gix/src/reference/errors.rs @@ -34,6 +34,42 @@ pub mod peel { #[error(transparent)] PackedRefsOpen(#[from] gix_ref::packed::buffer::open::Error), } + + /// + #[allow(clippy::empty_docs)] + pub mod to_kind { + /// The error returned by [`Reference::peel_to_kind(…)`](crate::Reference::peel_to_kind()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + FollowToObject(#[from] gix_ref::peel::to_object::Error), + #[error(transparent)] + PackedRefsOpen(#[from] gix_ref::packed::buffer::open::Error), + #[error(transparent)] + FindObject(#[from] crate::object::find::existing::Error), + #[error(transparent)] + PeelObject(#[from] crate::object::peel::to_kind::Error), + } + } +} + +/// +#[allow(clippy::empty_docs)] +pub mod follow { + /// + #[allow(clippy::empty_docs)] + pub mod to_object { + /// The error returned by [`Reference::follow_to_object(…)`](crate::Reference::follow_to_object()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + FollowToObject(#[from] gix_ref::peel::to_object::Error), + #[error(transparent)] + PackedRefsOpen(#[from] gix_ref::packed::buffer::open::Error), + } + } } /// diff --git a/gix/src/reference/mod.rs b/gix/src/reference/mod.rs index 62790b0d1b2..45e5577cc7f 100644 --- a/gix/src/reference/mod.rs +++ b/gix/src/reference/mod.rs @@ -3,7 +3,7 @@ use gix_ref::file::ReferenceExt; -use crate::{Id, Reference}; +use crate::{Blob, Commit, Id, Object, Reference, Tag, Tree}; pub mod iter; /// @@ -11,7 +11,7 @@ pub mod iter; pub mod remote; mod errors; -pub use errors::{edit, find, head_commit, head_id, head_tree_id, peel}; +pub use errors::{edit, find, follow, head_commit, head_id, head_tree_id, peel}; use crate::ext::ObjectIdExt; @@ -25,7 +25,7 @@ impl<'repo> Reference<'repo> { pub fn try_id(&self) -> Option> { match self.inner.target { gix_ref::Target::Symbolic(_) => None, - gix_ref::Target::Peeled(oid) => oid.to_owned().attach(self.repo).into(), + gix_ref::Target::Object(oid) => oid.to_owned().attach(self.repo).into(), } } @@ -65,19 +65,21 @@ impl<'repo> Reference<'repo> { /// Peeling impl<'repo> Reference<'repo> { - /// Follow all symbolic targets this reference might point to and peel the underlying object - /// to the end of the chain, and return it. + /// Follow all symbolic targets this reference might point to and peel all annotated tags + /// to their first non-tag target, and return it, /// - /// This is useful to learn where this reference is ultimately pointing to. + /// This is useful to learn where this reference is ultimately pointing to after following + /// the chain of symbolic refs and annotated tags. pub fn peel_to_id_in_place(&mut self) -> Result, peel::Error> { let oid = self.inner.peel_to_id_in_place(&self.repo.refs, &self.repo.objects)?; Ok(Id::from_id(oid, self.repo)) } - /// Follow all symbolic targets this reference might point to and peel the underlying object - /// to the end of the chain, and return it, reusing the `packed` buffer if available. + /// Follow all symbolic targets this reference might point to and peel all annotated tags + /// to their first non-tag target, and return it, reusing the `packed` buffer if available. /// - /// This is useful to learn where this reference is ultimately pointing to. + /// This is useful to learn where this reference is ultimately pointing to after following + /// the chain of symbolic refs and annotated tags. pub fn peel_to_id_in_place_packed( &mut self, packed: Option<&gix_ref::packed::Buffer>, @@ -88,11 +90,96 @@ impl<'repo> Reference<'repo> { Ok(Id::from_id(oid, self.repo)) } - /// Similar to [`peel_to_id_in_place()`][Reference::peel_to_id_in_place()], but consumes this instance. + /// Similar to [`peel_to_id_in_place()`](Reference::peel_to_id_in_place()), but consumes this instance. pub fn into_fully_peeled_id(mut self) -> Result, peel::Error> { self.peel_to_id_in_place() } + /// Follow this reference's target until it points at an object directly, and peel that object until + /// its type matches the given `kind`. It's an error to try to peel to a kind that this ref doesn't point to. + /// + /// Note that this ref will point to the first target object afterward, which may be a tag. This is different + /// from [`peel_to_id_in_place()`](Self::peel_to_id_in_place()) where it will point to the first non-tag object. + #[doc(alias = "peel", alias = "git2")] + pub fn peel_to_kind(&mut self, kind: gix_object::Kind) -> Result, peel::to_kind::Error> { + let packed = self.repo.refs.cached_packed_buffer().map_err(|err| { + peel::to_kind::Error::FollowToObject(gix_ref::peel::to_object::Error::Follow( + file::find::existing::Error::Find(file::find::Error::PackedOpen(err)), + )) + })?; + self.peel_to_kind_packed(kind, packed.as_ref().map(|p| &***p)) + } + + /// Peel this ref until the first commit. + /// + /// For details, see [`peel_to_kind`()](Self::peel_to_kind()). + pub fn peel_to_commit(&mut self) -> Result, peel::to_kind::Error> { + Ok(self.peel_to_kind(gix_object::Kind::Commit)?.into_commit()) + } + + /// Peel this ref until the first annotated tag. + /// + /// For details, see [`peel_to_kind`()](Self::peel_to_kind()). + pub fn peel_to_tag(&mut self) -> Result, peel::to_kind::Error> { + Ok(self.peel_to_kind(gix_object::Kind::Tag)?.into_tag()) + } + + /// Peel this ref until the first tree. + /// + /// For details, see [`peel_to_kind`()](Self::peel_to_kind()). + pub fn peel_to_tree(&mut self) -> Result, peel::to_kind::Error> { + Ok(self.peel_to_kind(gix_object::Kind::Tree)?.into_tree()) + } + + /// Peel this ref until it points to a blob. Note that this is highly uncommon to happen + /// as it would require an annotated tag to point to a blob, instead of a commit. + /// + /// For details, see [`peel_to_kind`()](Self::peel_to_kind()). + pub fn peel_to_blob(&mut self) -> Result, peel::to_kind::Error> { + Ok(self.peel_to_kind(gix_object::Kind::Blob)?.into_blob()) + } + + /// Like [`peel_to_kind()`](Self::peel_to_kind), but allows to provide `packed` for best possible performance + /// when peeling many refs. + pub fn peel_to_kind_packed( + &mut self, + kind: gix_object::Kind, + packed: Option<&gix_ref::packed::Buffer>, + ) -> Result, peel::to_kind::Error> { + let target = self + .inner + .follow_to_object_in_place_packed(&self.repo.refs, packed)? + .attach(self.repo); + Ok(target.object()?.peel_to_kind(kind)?) + } + + /// Follow all symbolic references we point to up to the first object, which is typically (but not always) a tag, + /// returning its id. + /// After this call, this ref will be pointing to an object directly, but may still not consider itself 'peeled' unless + /// a symbolic target ref was looked up from packed-refs. + #[doc(alias = "resolve", alias = "git2")] + pub fn follow_to_object(&mut self) -> Result, follow::to_object::Error> { + let packed = self.repo.refs.cached_packed_buffer().map_err(|err| { + follow::to_object::Error::FollowToObject(gix_ref::peel::to_object::Error::Follow( + file::find::existing::Error::Find(file::find::Error::PackedOpen(err)), + )) + })?; + self.follow_to_object_packed(packed.as_ref().map(|p| &***p)) + } + + /// Like [`follow_to_object`](Self::follow_to_object), but can be used for repeated calls as it won't + /// look up `packed` each time, but can reuse it instead. + #[doc(alias = "resolve", alias = "git2")] + pub fn follow_to_object_packed( + &mut self, + packed: Option<&gix_ref::packed::Buffer>, + ) -> Result, follow::to_object::Error> { + Ok(self + .inner + .follow_to_object_in_place_packed(&self.repo.refs, packed)? + .attach(self.repo)) + } + /// Follow this symbolic reference one level and return the ref it refers to. /// /// Returns `None` if this is not a symbolic reference, hence the leaf of the chain. @@ -108,3 +195,4 @@ impl<'repo> Reference<'repo> { mod edits; pub use edits::{delete, set_target_id}; +use gix_ref::file; diff --git a/gix/src/remote/connection/fetch/update_refs/mod.rs b/gix/src/remote/connection/fetch/update_refs/mod.rs index e5d3eac1b2d..a306c853b10 100644 --- a/gix/src/remote/connection/fetch/update_refs/mod.rs +++ b/gix/src/remote/connection/fetch/update_refs/mod.rs @@ -201,7 +201,9 @@ pub(crate) fn update( PreviousValue::MustExistAndMatch(existing.target().into_owned()), ) } - Err(crate::reference::peel::Error::ToId(gix_ref::peel::to_id::Error::Follow(_))) => { + Err(crate::reference::peel::Error::ToId(gix_ref::peel::to_id::Error::FollowToObject( + gix_ref::peel::to_object::Error::Follow(_), + ))) => { // An unborn reference, always allow it to be changed to whatever the remote wants. ( if existing.target().try_name().map(gix_ref::FullNameRef::as_bstr) @@ -238,14 +240,14 @@ pub(crate) fn update( let new = new_value_by_remote(repo, remote, mappings)?; let type_change = match (&previous_value, &new) { ( - PreviousValue::ExistingMustMatch(Target::Peeled(_)) - | PreviousValue::MustExistAndMatch(Target::Peeled(_)), + PreviousValue::ExistingMustMatch(Target::Object(_)) + | PreviousValue::MustExistAndMatch(Target::Object(_)), Target::Symbolic(_), ) => Some(TypeChange::DirectToSymbolic), ( PreviousValue::ExistingMustMatch(Target::Symbolic(_)) | PreviousValue::MustExistAndMatch(Target::Symbolic(_)), - Target::Peeled(_), + Target::Object(_), ) => Some(TypeChange::SymbolicToDirect), _ => None, }; @@ -349,7 +351,7 @@ fn update_needs_adjustment_as_edits_symbolic_target_is_missing( edits: &[RefEdit], ) -> bool { match edit.change.new_value().expect("here we need a symlink") { - TargetRef::Peeled(_) => unreachable!("BUG: we already know it's symbolic"), + TargetRef::Object(_) => unreachable!("BUG: we already know it's symbolic"), TargetRef::Symbolic(new_target_ref) => { match &edit.change { Change::Update { expected, .. } => match expected { @@ -422,7 +424,7 @@ fn new_value_by_remote( Target::Symbolic(target.try_into()?) } else { // born branches that we don't have in our refspecs we create peeled. That way they can be used. - Target::Peeled(desired_id.to_owned()) + Target::Object(desired_id.to_owned()) } } // Unborn branches we create as such, with the location they point to on the remote which helps mirroring. @@ -431,7 +433,7 @@ fn new_value_by_remote( } } } else { - Target::Peeled(remote_id.expect("unborn case handled earlier").to_owned()) + Target::Object(remote_id.expect("unborn case handled earlier").to_owned()) }, ) } diff --git a/gix/src/remote/connection/fetch/update_refs/tests.rs b/gix/src/remote/connection/fetch/update_refs/tests.rs index 4abf82f1aa0..5ed3334f521 100644 --- a/gix/src/remote/connection/fetch/update_refs/tests.rs +++ b/gix/src/remote/connection/fetch/update_refs/tests.rs @@ -377,7 +377,7 @@ mod update { }] ); assert_eq!(out.edits.len(), 1); - let target = Target::Peeled(hex_to_id("66f16e4e8baf5c77bb6d0484495bebea80e916ce")); + let target = Target::Object(hex_to_id("66f16e4e8baf5c77bb6d0484495bebea80e916ce")); assert_eq!( out.edits[0], RefEdit { @@ -521,7 +521,7 @@ mod update { expected: PreviousValue::MustExistAndMatch(Target::Symbolic( "refs/heads/main".try_into().expect("valid"), )), - new: Target::Peeled(hex_to_id("f99771fe6a1b535783af3163eba95a927aae21d5")), + new: Target::Object(hex_to_id("f99771fe6a1b535783af3163eba95a927aae21d5")), }, name: "refs/heads/symbolic".try_into().expect("valid"), deref: false, @@ -543,7 +543,7 @@ mod update { force_create_reflog: false, message: "action: no update will be performed".into(), }, - expected: PreviousValue::MustExistAndMatch(Target::Peeled(hex_to_id( + expected: PreviousValue::MustExistAndMatch(Target::Object(hex_to_id( "f99771fe6a1b535783af3163eba95a927aae21d5", ))), new: Target::Symbolic("refs/heads/main".try_into().expect("valid")), @@ -938,7 +938,7 @@ mod update { fn into_remote_ref(mut r: gix::Reference<'_>) -> gix_protocol::handshake::Ref { let full_ref_name = r.name().as_bstr().into(); match r.target() { - TargetRef::Peeled(id) => gix_protocol::handshake::Ref::Direct { + TargetRef::Object(id) => gix_protocol::handshake::Ref::Direct { full_ref_name, object: id.into(), }, diff --git a/gix/src/repository/cache.rs b/gix/src/repository/cache.rs index 03c2ff0195e..d1d2f529c15 100644 --- a/gix/src/repository/cache.rs +++ b/gix/src/repository/cache.rs @@ -27,4 +27,15 @@ impl crate::Repository { self.object_cache_size(bytes) } } + + /// Return the amount of bytes the object cache [should be set to](Self::object_cache_size_if_unset) to perform + /// diffs between trees who are similar to `index` in a typical source code repository. + /// + /// Currently, this allocates about 10MB for every 10k files in `index`, and a minimum of 4KB. + #[cfg(feature = "index")] + pub fn compute_object_cache_size_for_tree_diffs(&self, index: &gix_index::State) -> usize { + let num_tracked = index.entries().len(); + let ten_mb_for_every_10k_files = (num_tracked as f32 / 10_000.0) * (10 * 1024 * 1024) as f32; + (ten_mb_for_every_10k_files as usize).max(4 * 1024) + } } diff --git a/gix/src/repository/diff.rs b/gix/src/repository/diff.rs index 87e28d0e9c5..c98032724c0 100644 --- a/gix/src/repository/diff.rs +++ b/gix/src/repository/diff.rs @@ -49,4 +49,14 @@ impl Repository { worktree_roots, )?) } + + /// 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). + pub fn diff_resource_cache_for_tree_diff(&self) -> Result { + self.diff_resource_cache( + gix_diff::blob::pipeline::Mode::ToGit, + gix_diff::blob::pipeline::WorktreeRoots::default(), + ) + } } diff --git a/gix/src/repository/object.rs b/gix/src/repository/object.rs index aedffc90c50..db2344a83c3 100644 --- a/gix/src/repository/object.rs +++ b/gix/src/repository/object.rs @@ -10,7 +10,7 @@ use gix_ref::{ }; use smallvec::SmallVec; -use crate::{commit, ext::ObjectIdExt, object, tag, Blob, Id, Object, Reference, Tree}; +use crate::{commit, ext::ObjectIdExt, object, tag, Blob, Commit, Id, Object, Reference, Tag, Tree}; /// Methods related to object creation. impl crate::Repository { @@ -38,6 +38,35 @@ impl crate::Repository { Ok(Object::from_data(id, kind, buf, self)) } + /// Find a commit with `id` or fail if there was no object or the object wasn't a commit. + pub fn find_commit( + &self, + id: impl Into, + ) -> Result, object::find::existing::with_conversion::Error> { + Ok(self.find_object(id)?.try_into_commit()?) + } + + /// Find a tree with `id` or fail if there was no object or the object wasn't a tree. + pub fn find_tree( + &self, + id: impl Into, + ) -> Result, object::find::existing::with_conversion::Error> { + Ok(self.find_object(id)?.try_into_tree()?) + } + + /// Find an annotated tag with `id` or fail if there was no object or the object wasn't a tag. + pub fn find_tag(&self, id: impl Into) -> Result, object::find::existing::with_conversion::Error> { + Ok(self.find_object(id)?.try_into_tag()?) + } + + /// Find a blob with `id` or fail if there was no object or the object wasn't a blob. + pub fn find_blob( + &self, + id: impl Into, + ) -> Result, object::find::existing::with_conversion::Error> { + Ok(self.find_object(id)?.try_into_blob()?) + } + /// Obtain information about an object without fully decoding it, or fail if the object doesn't exist. /// /// Note that despite being cheaper than [`Self::find_object()`], there is still some effort traversing delta-chains. @@ -275,7 +304,7 @@ impl crate::Repository { force_create_reflog: false, message: crate::reference::log::message("commit", commit.message.as_ref(), commit.parents.len()), }, - expected: match commit.parents.first().map(|p| Target::Peeled(*p)) { + expected: match commit.parents.first().map(|p| Target::Object(*p)) { Some(previous) => { if reference.as_bstr() == "HEAD" { PreviousValue::MustExistAndMatch(previous) @@ -285,7 +314,7 @@ impl crate::Repository { } None => PreviousValue::MustNotExist, }, - new: Target::Peeled(commit_id.inner), + new: Target::Object(commit_id.inner), }, name: reference, deref: true, diff --git a/gix/src/repository/reference.rs b/gix/src/repository/reference.rs index da062e80ae7..70411318c4d 100644 --- a/gix/src/repository/reference.rs +++ b/gix/src/repository/reference.rs @@ -23,7 +23,7 @@ impl crate::Repository { change: Change::Update { log: Default::default(), expected: constraint, - new: Target::Peeled(id), + new: Target::Object(id), }, name: format!("refs/tags/{}", name.as_ref()).try_into()?, deref: false, @@ -106,7 +106,7 @@ impl crate::Repository { message: log_message, }, expected: constraint, - new: Target::Peeled(id), + new: Target::Object(id), }, name, deref: false, @@ -119,7 +119,7 @@ impl crate::Repository { Ok(gix_ref::Reference { name: edits.pop().expect("exactly one edit").name, - target: Target::Peeled(id), + target: Target::Object(id), peeled: None, } .attach(self)) @@ -162,7 +162,7 @@ impl crate::Repository { Err(reference::find::existing::Error::NotFound) => crate::head::Kind::Unborn(branch), Err(err) => return Err(err), }, - Target::Peeled(target) => crate::head::Kind::Detached { + Target::Object(target) => crate::head::Kind::Detached { target, peeled: head.inner.peeled, }, diff --git a/gix/tests/clone/mod.rs b/gix/tests/clone/mod.rs index 0d3e0e1ebd4..d71399ab68e 100644 --- a/gix/tests/clone/mod.rs +++ b/gix/tests/clone/mod.rs @@ -347,7 +347,7 @@ mod blocking_io { "if we set up a symref, the target should exist by now" ) } - TargetRef::Peeled(id) => { + TargetRef::Object(id) => { assert!(repo.objects.exists(id), "part of the fetched pack"); } } @@ -362,7 +362,7 @@ mod blocking_io { .1 .starts_with_str(remote_name)); match r.target() { - TargetRef::Peeled(_) => { + TargetRef::Object(_) => { let mut logs = r.log_iter(); assert_reflog(logs.all()); } diff --git a/gix/tests/fixtures/generated-archives/make_references_repo.tar b/gix/tests/fixtures/generated-archives/make_references_repo.tar index 9c2bc2ea2b9..3f725052415 100644 Binary files a/gix/tests/fixtures/generated-archives/make_references_repo.tar and b/gix/tests/fixtures/generated-archives/make_references_repo.tar differ diff --git a/gix/tests/fixtures/make_references_repo.sh b/gix/tests/fixtures/make_references_repo.sh index daea997523b..0fec9765edc 100755 --- a/gix/tests/fixtures/make_references_repo.sh +++ b/gix/tests/fixtures/make_references_repo.sh @@ -27,5 +27,7 @@ echo "ref: refs/loop-a" > .git/refs/loop-b git tag t1 git tag -m "tag object" dt1 +git tag -m "tag object indirect" dt2 dt1 +echo "ref: refs/tags/dt2" > .git/refs/tags/dt3 git pack-refs --all --prune diff --git a/gix/tests/object/tree/diff.rs b/gix/tests/object/tree/diff.rs index e54d5b8b5a9..032680afdfb 100644 --- a/gix/tests/object/tree/diff.rs +++ b/gix/tests/object/tree/diff.rs @@ -72,6 +72,17 @@ fn changes_against_tree_modified() -> crate::Result { 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(()) } diff --git a/gix/tests/reference/mod.rs b/gix/tests/reference/mod.rs index 4ec74ca85e8..edd58b901b7 100644 --- a/gix/tests/reference/mod.rs +++ b/gix/tests/reference/mod.rs @@ -45,8 +45,7 @@ fn remote_name() -> crate::Result { } mod find { - use gix_ref as refs; - use gix_ref::{FullName, FullNameRef, Target}; + use gix_ref::{FullName, FullNameRef, Target, TargetRef}; use crate::util::hex_to_id; @@ -63,7 +62,7 @@ mod find { assert_eq!( packed_tag_ref.inner.target, - refs::Target::Peeled(hex_to_id("4c3f4cce493d7beb45012e478021b5f65295e5a3")), + Target::Object(hex_to_id("4c3f4cce493d7beb45012e478021b5f65295e5a3")), "it points to a tag object" ); @@ -85,6 +84,93 @@ mod find { let expected: &FullNameRef = "refs/remotes/origin/multi-link-target3".try_into()?; assert_eq!(symbolic_ref.name(), expected, "it follows symbolic refs, too"); assert_eq!(symbolic_ref.into_fully_peeled_id()?, the_commit, "idempotency"); + + let mut tag_ref = repo.find_reference("dt3")?; + assert_eq!( + tag_ref.target(), + TargetRef::Symbolic("refs/tags/dt2".try_into()?), + "the ref points at another tag" + ); + assert_eq!(tag_ref.inner.peeled, None, "it wasn't peeled yet, nothing is stored"); + let obj = tag_ref.peel_to_kind(gix::object::Kind::Tag)?; + assert_eq!(tag_ref.peel_to_tag()?.id, obj.id); + assert_eq!(obj.kind, gix::object::Kind::Tag); + assert_eq!( + obj.into_tag().decode()?.name, + "dt2", + "it stop at the first direct target" + ); + + let first_tag_id = hex_to_id("0f35190769db39bc70f60b6fbec9156370ce2f83"); + assert_eq!( + tag_ref.target().id(), + first_tag_id, + "it's now followed to the first target" + ); + let target_commit_id = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); + assert_eq!( + tag_ref.inner.peeled, Some(target_commit_id), + "It only counts as peeled as this ref is packed, and peeling in place is a way to 'make it the target' officially." + ); + + let err = tag_ref.peel_to_kind(gix::object::Kind::Blob).unwrap_err(); + let expectd_err = "Last encountered object 4b825dc was tree while trying to peel to blob"; + assert_eq!( + err.to_string(), + expectd_err, + "it's an error if the desired type isn't actually present" + ); + match tag_ref.peel_to_blob() { + Ok(_) => { + unreachable!("target is a commit") + } + Err(err) => { + assert_eq!(err.to_string(), expectd_err); + } + } + + let obj = tag_ref.peel_to_kind(gix::object::Kind::Tree)?; + assert!(obj.kind.is_tree()); + assert_eq!(obj.id, hex_to_id("4b825dc642cb6eb9a060e54bf8d69288fbee4904"),); + assert_eq!(tag_ref.peel_to_tree()?.id, obj.id); + + assert_eq!( + tag_ref.target().id(), + first_tag_id, + "nothing changed - it still points to the target" + ); + assert_eq!( + tag_ref.inner.peeled, + Some(target_commit_id), + "the peeling cache wasn't changed" + ); + + let obj = tag_ref.peel_to_kind(gix::object::Kind::Commit)?; + assert!(obj.kind.is_commit()); + assert_eq!( + obj.id, target_commit_id, + "the standard-peel peels to right after all tags" + ); + assert_eq!(tag_ref.peel_to_commit()?.id, obj.id); + + let mut tag_ref = repo.find_reference("dt3")?; + assert_eq!( + tag_ref.follow_to_object()?, + first_tag_id, + "it's similar to peel_to_kind(), but provides the id instead" + ); + assert_eq!(tag_ref.follow_to_object()?, first_tag_id, "it's idempotent"); + assert_eq!( + tag_ref.target().id(), + first_tag_id, + "it now points to the first tag as well" + ); + assert_eq!( + tag_ref.inner.peeled, + Some(target_commit_id), + "as it was read from a packed-ref, it contains peeling information nonetheless" + ); + Ok(()) } @@ -99,7 +185,7 @@ mod find { symbolic_ref = symbolic_ref.follow().expect("another hop")?; assert_eq!(symbolic_ref.target(), second_hop.to_ref()); - let last_hop = Target::Peeled(hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03")); + let last_hop = Target::Object(hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03")); symbolic_ref = symbolic_ref.follow().expect("another hop")?; assert_eq!(symbolic_ref.target(), last_hop.to_ref()); diff --git a/gix/tests/remote/fetch.rs b/gix/tests/remote/fetch.rs index 6a5a5eb72ae..bf6408d76bc 100644 --- a/gix/tests/remote/fetch.rs +++ b/gix/tests/remote/fetch.rs @@ -620,7 +620,7 @@ mod blocking_and_async_io { let edit = edit.expect("refedit present even if it's a no-op"); if dry_run { match edit.change.new_value().expect("no deletions") { - gix_ref::TargetRef::Peeled(id) => { + gix_ref::TargetRef::Object(id) => { assert_eq!(id, mapping.remote.as_id().expect("no unborn")) } gix_ref::TargetRef::Symbolic(target) => { @@ -634,7 +634,7 @@ mod blocking_and_async_io { } else { let r = repo.find_reference(edit.name.as_ref()).unwrap(); match r.target() { - gix_ref::TargetRef::Peeled(id) => { + gix_ref::TargetRef::Object(id) => { assert_eq!( id, mapping.remote.as_id().expect("no unborn"), diff --git a/gix/tests/repository/reference.rs b/gix/tests/repository/reference.rs index dad967ec3d0..40668673022 100644 --- a/gix/tests/repository/reference.rs +++ b/gix/tests/repository/reference.rs @@ -11,7 +11,7 @@ mod set_namespace { let (mut repo, _keep) = easy_repo_rw()?; assert_eq!( repo.references()?.all()?.count(), - 15, + 17, "there are plenty of references in the default namespace" ); assert!(repo.namespace().is_none(), "no namespace is set initially"); @@ -73,7 +73,7 @@ mod set_namespace { assert_eq!( repo.references()?.all()?.count(), - 17, + 19, "it lists all references, also the ones in namespaces" ); Ok(()) @@ -110,6 +110,8 @@ mod iter_references { "refs/remotes/origin/main", "refs/remotes/origin/multi-link-target3", "refs/tags/dt1", + "refs/tags/dt2", + "refs/tags/dt3", "refs/tags/multi-link-target2", "refs/tags/t1" ]