Skip to content

Commit cdaba84

Browse files
committed
feat: add Reference::peel_to_kind()
Make it easy to follow a ref and peel it to a given object type. Additional `peel_to_<kind>()` shortcuts are also provided, with the same name as in `git2`.
1 parent d296ee8 commit cdaba84

File tree

6 files changed

+164
-13
lines changed

6 files changed

+164
-13
lines changed

gix/src/reference/errors.rs

+18
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ pub mod peel {
3434
#[error(transparent)]
3535
PackedRefsOpen(#[from] gix_ref::packed::buffer::open::Error),
3636
}
37+
38+
///
39+
#[allow(clippy::empty_docs)]
40+
pub mod to_kind {
41+
/// The error returned by [`Reference::peel_to_kind(…)`](crate::Reference::peel_to_kind()).
42+
#[derive(Debug, thiserror::Error)]
43+
#[allow(missing_docs)]
44+
pub enum Error {
45+
#[error(transparent)]
46+
FollowToObject(#[from] gix_ref::peel::to_object::Error),
47+
#[error(transparent)]
48+
PackedRefsOpen(#[from] gix_ref::packed::buffer::open::Error),
49+
#[error(transparent)]
50+
FindObject(#[from] crate::object::find::existing::Error),
51+
#[error(transparent)]
52+
PeelObject(#[from] crate::object::peel::to_kind::Error),
53+
}
54+
}
3755
}
3856

3957
///

gix/src/reference/mod.rs

+69-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use gix_ref::file::ReferenceExt;
55

6-
use crate::{Id, Reference};
6+
use crate::{Blob, Commit, Id, Object, Reference, Tag, Tree};
77

88
pub mod iter;
99
///
@@ -65,19 +65,21 @@ impl<'repo> Reference<'repo> {
6565

6666
/// Peeling
6767
impl<'repo> Reference<'repo> {
68-
/// Follow all symbolic targets this reference might point to and peel the underlying object
69-
/// to the end of the chain, and return it.
68+
/// Follow all symbolic targets this reference might point to and peel all annotated tags
69+
/// to their first non-tag target, and return it,
7070
///
71-
/// This is useful to learn where this reference is ultimately pointing to.
71+
/// This is useful to learn where this reference is ultimately pointing to after following
72+
/// the chain of symbolic refs and annotated tags.
7273
pub fn peel_to_id_in_place(&mut self) -> Result<Id<'repo>, peel::Error> {
7374
let oid = self.inner.peel_to_id_in_place(&self.repo.refs, &self.repo.objects)?;
7475
Ok(Id::from_id(oid, self.repo))
7576
}
7677

77-
/// Follow all symbolic targets this reference might point to and peel the underlying object
78-
/// to the end of the chain, and return it, reusing the `packed` buffer if available.
78+
/// Follow all symbolic targets this reference might point to and peel all annotated tags
79+
/// to their first non-tag target, and return it, reusing the `packed` buffer if available.
7980
///
80-
/// This is useful to learn where this reference is ultimately pointing to.
81+
/// This is useful to learn where this reference is ultimately pointing to after following
82+
/// the chain of symbolic refs and annotated tags.
8183
pub fn peel_to_id_in_place_packed(
8284
&mut self,
8385
packed: Option<&gix_ref::packed::Buffer>,
@@ -88,11 +90,69 @@ impl<'repo> Reference<'repo> {
8890
Ok(Id::from_id(oid, self.repo))
8991
}
9092

91-
/// Similar to [`peel_to_id_in_place()`][Reference::peel_to_id_in_place()], but consumes this instance.
93+
/// Similar to [`peel_to_id_in_place()`](Reference::peel_to_id_in_place()), but consumes this instance.
9294
pub fn into_fully_peeled_id(mut self) -> Result<Id<'repo>, peel::Error> {
9395
self.peel_to_id_in_place()
9496
}
9597

98+
/// Follow this reference's target until it points at an object directly, and peel that object until
99+
/// its type matches the given `kind`. It's an error to try to peel to a kind that this ref doesn't point to.
100+
///
101+
/// Note that this ref will point to the first target object afterward, which may be a tag. This is different
102+
/// from [`peel_to_id_in_place()`](Self::peel_to_id_in_place()) where it will point to the first non-tag object.
103+
#[doc(alias = "peel", alias = "git2")]
104+
pub fn peel_to_kind(&mut self, kind: gix_object::Kind) -> Result<Object<'repo>, peel::to_kind::Error> {
105+
let packed = self.repo.refs.cached_packed_buffer().map_err(|err| {
106+
peel::to_kind::Error::FollowToObject(gix_ref::peel::to_object::Error::Follow(
107+
file::find::existing::Error::Find(file::find::Error::PackedOpen(err)),
108+
))
109+
})?;
110+
self.peel_to_kind_packed(kind, packed.as_ref().map(|p| &***p))
111+
}
112+
113+
/// Peel this ref until the first commit.
114+
///
115+
/// For details, see [`peel_to_kind`()](Self::peel_to_kind()).
116+
pub fn peel_to_commit(&mut self) -> Result<Commit<'repo>, peel::to_kind::Error> {
117+
Ok(self.peel_to_kind(gix_object::Kind::Commit)?.into_commit())
118+
}
119+
120+
/// Peel this ref until the first annotated tag.
121+
///
122+
/// For details, see [`peel_to_kind`()](Self::peel_to_kind()).
123+
pub fn peel_to_tag(&mut self) -> Result<Tag<'repo>, peel::to_kind::Error> {
124+
Ok(self.peel_to_kind(gix_object::Kind::Tag)?.into_tag())
125+
}
126+
127+
/// Peel this ref until the first tree.
128+
///
129+
/// For details, see [`peel_to_kind`()](Self::peel_to_kind()).
130+
pub fn peel_to_tree(&mut self) -> Result<Tree<'repo>, peel::to_kind::Error> {
131+
Ok(self.peel_to_kind(gix_object::Kind::Tree)?.into_tree())
132+
}
133+
134+
/// Peel this ref until it points to a blob. Note that this is highly uncommon to happen
135+
/// as it would require an annotated tag to point to a blob, instead of a commit.
136+
///
137+
/// For details, see [`peel_to_kind`()](Self::peel_to_kind()).
138+
pub fn peel_to_blob(&mut self) -> Result<Blob<'repo>, peel::to_kind::Error> {
139+
Ok(self.peel_to_kind(gix_object::Kind::Blob)?.into_blob())
140+
}
141+
142+
/// Like [`peel_to_kind()`](Self::peel_to_kind), but allows to provide `packed` for best possible performance
143+
/// when peeling many refs.
144+
pub fn peel_to_kind_packed(
145+
&mut self,
146+
kind: gix_object::Kind,
147+
packed: Option<&gix_ref::packed::Buffer>,
148+
) -> Result<Object<'repo>, peel::to_kind::Error> {
149+
let target = self
150+
.inner
151+
.follow_to_object_in_place_packed(&self.repo.refs, packed)?
152+
.attach(self.repo);
153+
Ok(target.object()?.peel_to_kind(kind)?)
154+
}
155+
96156
/// Follow this symbolic reference one level and return the ref it refers to.
97157
///
98158
/// Returns `None` if this is not a symbolic reference, hence the leaf of the chain.
@@ -108,3 +168,4 @@ impl<'repo> Reference<'repo> {
108168

109169
mod edits;
110170
pub use edits::{delete, set_target_id};
171+
use gix_ref::file;
Binary file not shown.

gix/tests/fixtures/make_references_repo.sh

+2
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,7 @@ echo "ref: refs/loop-a" > .git/refs/loop-b
2727

2828
git tag t1
2929
git tag -m "tag object" dt1
30+
git tag -m "tag object indirect" dt2 dt1
31+
echo "ref: refs/tags/dt2" > .git/refs/tags/dt3
3032

3133
git pack-refs --all --prune

gix/tests/reference/mod.rs

+71-3
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ fn remote_name() -> crate::Result {
4545
}
4646

4747
mod find {
48-
use gix_ref as refs;
49-
use gix_ref::{FullName, FullNameRef, Target};
48+
use gix_ref::{FullName, FullNameRef, Target, TargetRef};
5049

5150
use crate::util::hex_to_id;
5251

@@ -63,7 +62,7 @@ mod find {
6362

6463
assert_eq!(
6564
packed_tag_ref.inner.target,
66-
refs::Target::Object(hex_to_id("4c3f4cce493d7beb45012e478021b5f65295e5a3")),
65+
Target::Object(hex_to_id("4c3f4cce493d7beb45012e478021b5f65295e5a3")),
6766
"it points to a tag object"
6867
);
6968

@@ -85,6 +84,75 @@ mod find {
8584
let expected: &FullNameRef = "refs/remotes/origin/multi-link-target3".try_into()?;
8685
assert_eq!(symbolic_ref.name(), expected, "it follows symbolic refs, too");
8786
assert_eq!(symbolic_ref.into_fully_peeled_id()?, the_commit, "idempotency");
87+
88+
let mut tag_ref = repo.find_reference("dt3")?;
89+
assert_eq!(
90+
tag_ref.target(),
91+
TargetRef::Symbolic("refs/tags/dt2".try_into()?),
92+
"the ref points at another tag"
93+
);
94+
assert_eq!(tag_ref.inner.peeled, None, "it wasn't peeled yet, nothing is stored");
95+
let obj = tag_ref.peel_to_kind(gix::object::Kind::Tag)?;
96+
assert_eq!(tag_ref.peel_to_tag()?.id, obj.id);
97+
assert_eq!(obj.kind, gix::object::Kind::Tag);
98+
assert_eq!(
99+
obj.into_tag().decode()?.name,
100+
"dt2",
101+
"it stop at the first direct target"
102+
);
103+
104+
let first_tag_id = hex_to_id("0f35190769db39bc70f60b6fbec9156370ce2f83");
105+
assert_eq!(
106+
tag_ref.target().id(),
107+
first_tag_id,
108+
"it's now followed to the first target"
109+
);
110+
let target_commit_id = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03");
111+
assert_eq!(
112+
tag_ref.inner.peeled, Some(target_commit_id),
113+
"It only counts as peeled as this ref is packed, and peeling in place is a way to 'make it the target' officially."
114+
);
115+
116+
let err = tag_ref.peel_to_kind(gix::object::Kind::Blob).unwrap_err();
117+
let expectd_err = "Last encountered object 4b825dc was tree while trying to peel to blob";
118+
assert_eq!(
119+
err.to_string(),
120+
expectd_err,
121+
"it's an error if the desired type isn't actually present"
122+
);
123+
match tag_ref.peel_to_blob() {
124+
Ok(_) => {
125+
unreachable!("target is a commit")
126+
}
127+
Err(err) => {
128+
assert_eq!(err.to_string(), expectd_err);
129+
}
130+
}
131+
132+
let obj = tag_ref.peel_to_kind(gix::object::Kind::Tree)?;
133+
assert!(obj.kind.is_tree());
134+
assert_eq!(obj.id, hex_to_id("4b825dc642cb6eb9a060e54bf8d69288fbee4904"),);
135+
assert_eq!(tag_ref.peel_to_tree()?.id, obj.id);
136+
137+
assert_eq!(
138+
tag_ref.target().id(),
139+
first_tag_id,
140+
"nothing changed - it still points to the target"
141+
);
142+
assert_eq!(
143+
tag_ref.inner.peeled,
144+
Some(target_commit_id),
145+
"the peeling cache wasn't changed"
146+
);
147+
148+
let obj = tag_ref.peel_to_kind(gix::object::Kind::Commit)?;
149+
assert!(obj.kind.is_commit());
150+
assert_eq!(
151+
obj.id, target_commit_id,
152+
"the standard-peel peels to right after all tags"
153+
);
154+
assert_eq!(tag_ref.peel_to_commit()?.id, obj.id);
155+
88156
Ok(())
89157
}
90158

gix/tests/repository/reference.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ mod set_namespace {
1111
let (mut repo, _keep) = easy_repo_rw()?;
1212
assert_eq!(
1313
repo.references()?.all()?.count(),
14-
15,
14+
17,
1515
"there are plenty of references in the default namespace"
1616
);
1717
assert!(repo.namespace().is_none(), "no namespace is set initially");
@@ -73,7 +73,7 @@ mod set_namespace {
7373

7474
assert_eq!(
7575
repo.references()?.all()?.count(),
76-
17,
76+
19,
7777
"it lists all references, also the ones in namespaces"
7878
);
7979
Ok(())
@@ -110,6 +110,8 @@ mod iter_references {
110110
"refs/remotes/origin/main",
111111
"refs/remotes/origin/multi-link-target3",
112112
"refs/tags/dt1",
113+
"refs/tags/dt2",
114+
"refs/tags/dt3",
113115
"refs/tags/multi-link-target2",
114116
"refs/tags/t1"
115117
]

0 commit comments

Comments
 (0)