diff --git a/gix/src/reference/errors.rs b/gix/src/reference/errors.rs index 92714ec790c..02745b54b66 100644 --- a/gix/src/reference/errors.rs +++ b/gix/src/reference/errors.rs @@ -34,6 +34,24 @@ 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), + } + } } /// diff --git a/gix/src/reference/mod.rs b/gix/src/reference/mod.rs index 99ecb9a081c..1faf8006591 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; /// @@ -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,69 @@ 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 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 +168,4 @@ impl<'repo> Reference<'repo> { mod edits; pub use edits::{delete, set_target_id}; +use gix_ref::file; 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/reference/mod.rs b/gix/tests/reference/mod.rs index 497aa3d3885..3e72ad91388 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::Object(hex_to_id("4c3f4cce493d7beb45012e478021b5f65295e5a3")), + Target::Object(hex_to_id("4c3f4cce493d7beb45012e478021b5f65295e5a3")), "it points to a tag object" ); @@ -85,6 +84,75 @@ 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); + Ok(()) } 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" ]