Skip to content

Commit 16b5e7e

Browse files
committed
feat!: add ReferenceExt::follow_to_object_in_place().
The missing link that makes it possible to follow to the first object and then peel the object according to any preferred algorithm.
1 parent 5464bfb commit 16b5e7e

File tree

6 files changed

+139
-67
lines changed

6 files changed

+139
-67
lines changed

gix-ref/src/peel.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
///
22
#[allow(clippy::empty_docs)]
33
pub mod to_id {
4-
use std::path::PathBuf;
5-
64
use gix_object::bstr::BString;
75

6+
/// The error returned by [`crate::file::ReferenceExt::peel_to_id_in_place()`].
7+
#[derive(Debug, thiserror::Error)]
8+
#[allow(missing_docs)]
9+
pub enum Error {
10+
#[error(transparent)]
11+
FollowToObject(#[from] super::to_object::Error),
12+
#[error("An error occurred when trying to resolve an object a reference points to")]
13+
Find(#[from] gix_object::find::Error),
14+
#[error("Object {oid} as referred to by {name:?} could not be found")]
15+
NotFound { oid: gix_hash::ObjectId, name: BString },
16+
}
17+
}
18+
19+
///
20+
#[allow(clippy::empty_docs)]
21+
pub mod to_object {
22+
use std::path::PathBuf;
23+
824
use crate::file;
925

10-
/// The error returned by [`crate::file::ReferenceExt::peel_to_id_in_place()`].
26+
/// The error returned by [`file::ReferenceExt::follow_to_object_in_place_packed()`].
1127
#[derive(Debug, thiserror::Error)]
1228
#[allow(missing_docs)]
1329
pub enum Error {
@@ -17,9 +33,5 @@ pub mod to_id {
1733
Cycle { start_absolute: PathBuf },
1834
#[error("Refusing to follow more than {max_depth} levels of indirection")]
1935
DepthLimitExceeded { max_depth: usize },
20-
#[error("An error occurred when trying to resolve an object a reference points to")]
21-
Find(#[from] gix_object::find::Error),
22-
#[error("Object {oid} as referred to by {name:?} could not be found")]
23-
NotFound { oid: gix_hash::ObjectId, name: BString },
2436
}
2537
}

gix-ref/src/store/file/raw_ext.rs

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
use std::collections::BTreeSet;
22

3-
use gix_hash::ObjectId;
4-
53
use crate::{
64
packed, peel,
75
raw::Reference,
86
store_impl::{file, file::log},
97
Target,
108
};
9+
use gix_hash::ObjectId;
1110

1211
pub trait Sealed {}
1312
impl Sealed for crate::Reference {}
@@ -22,16 +21,17 @@ pub trait ReferenceExt: Sealed {
2221

2322
/// Follow all symbolic targets this reference might point to and peel the underlying object
2423
/// to the end of the tag-chain, returning the first non-tag object the annotated tag points to,
25-
/// using `objects` to access them.
24+
/// using `objects` to access them and `store` to lookup symbolic references.
2625
///
27-
/// This is useful to learn where this reference is ultimately pointing to.
26+
/// This is useful to learn where this reference is ultimately pointing to after following all symbolic
27+
/// refs and all annotated tags to the first non-tag object.
2828
fn peel_to_id_in_place(
2929
&mut self,
3030
store: &file::Store,
3131
objects: &dyn gix_object::Find,
3232
) -> Result<ObjectId, peel::to_id::Error>;
3333

34-
/// Like [`ReferenceExt::peel_to_id_in_place()`], but with support for a known stable packed buffer
34+
/// Like [`ReferenceExt::peel_to_id_in_place()`], but with support for a known stable `packed` buffer
3535
/// to use for resolving symbolic links.
3636
fn peel_to_id_in_place_packed(
3737
&mut self,
@@ -40,6 +40,14 @@ pub trait ReferenceExt: Sealed {
4040
packed: Option<&packed::Buffer>,
4141
) -> Result<ObjectId, peel::to_id::Error>;
4242

43+
/// Like [`ReferenceExt::follow()`], but follows all symbolic references while gracefully handling loops,
44+
/// altering this instance in place.
45+
fn follow_to_object_in_place_packed(
46+
&mut self,
47+
store: &file::Store,
48+
packed: Option<&packed::Buffer>,
49+
) -> Result<ObjectId, peel::to_object::Error>;
50+
4351
/// Follow this symbolic reference one level and return the ref it refers to.
4452
///
4553
/// Returns `None` if this is not a symbolic reference, hence the leaf of the chain.
@@ -77,7 +85,9 @@ impl ReferenceExt for Reference {
7785
objects: &dyn gix_object::Find,
7886
) -> Result<ObjectId, peel::to_id::Error> {
7987
let packed = store.assure_packed_refs_uptodate().map_err(|err| {
80-
peel::to_id::Error::Follow(file::find::existing::Error::Find(file::find::Error::PackedOpen(err)))
88+
peel::to_id::Error::FollowToObject(peel::to_object::Error::Follow(file::find::existing::Error::Find(
89+
file::find::Error::PackedOpen(err),
90+
)))
8191
})?;
8292
self.peel_to_id_in_place_packed(store, objects, packed.as_ref().map(|b| &***b))
8393
}
@@ -94,28 +104,8 @@ impl ReferenceExt for Reference {
94104
Ok(peeled)
95105
}
96106
None => {
97-
if self.target.kind() == crate::Kind::Symbolic {
98-
let mut seen = BTreeSet::new();
99-
let cursor = &mut *self;
100-
while let Some(next) = cursor.follow_packed(store, packed) {
101-
let next = next?;
102-
if seen.contains(&next.name) {
103-
return Err(peel::to_id::Error::Cycle {
104-
start_absolute: store.reference_path(cursor.name.as_ref()),
105-
});
106-
}
107-
*cursor = next;
108-
seen.insert(cursor.name.clone());
109-
const MAX_REF_DEPTH: usize = 5;
110-
if seen.len() == MAX_REF_DEPTH {
111-
return Err(peel::to_id::Error::DepthLimitExceeded {
112-
max_depth: MAX_REF_DEPTH,
113-
});
114-
}
115-
}
116-
};
107+
let mut oid = self.follow_to_object_in_place_packed(store, packed)?;
117108
let mut buf = Vec::new();
118-
let mut oid = self.target.try_id().expect("peeled ref").to_owned();
119109
let peeled_id = loop {
120110
let gix_object::Data { kind, data } =
121111
objects
@@ -143,6 +133,38 @@ impl ReferenceExt for Reference {
143133
}
144134
}
145135

136+
fn follow_to_object_in_place_packed(
137+
&mut self,
138+
store: &file::Store,
139+
packed: Option<&packed::Buffer>,
140+
) -> Result<ObjectId, peel::to_object::Error> {
141+
match self.target {
142+
Target::Object(id) => Ok(id),
143+
Target::Symbolic(_) => {
144+
let mut seen = BTreeSet::new();
145+
let cursor = &mut *self;
146+
while let Some(next) = cursor.follow_packed(store, packed) {
147+
let next = next?;
148+
if seen.contains(&next.name) {
149+
return Err(peel::to_object::Error::Cycle {
150+
start_absolute: store.reference_path(cursor.name.as_ref()),
151+
});
152+
}
153+
*cursor = next;
154+
seen.insert(cursor.name.clone());
155+
const MAX_REF_DEPTH: usize = 5;
156+
if seen.len() == MAX_REF_DEPTH {
157+
return Err(peel::to_object::Error::DepthLimitExceeded {
158+
max_depth: MAX_REF_DEPTH,
159+
});
160+
}
161+
}
162+
let oid = self.target.try_id().expect("peeled ref").to_owned();
163+
Ok(oid)
164+
}
165+
}
166+
}
167+
146168
fn follow(&self, store: &file::Store) -> Option<Result<Reference, file::find::existing::Error>> {
147169
let packed = match store
148170
.assure_packed_refs_uptodate()
@@ -159,21 +181,14 @@ impl ReferenceExt for Reference {
159181
store: &file::Store,
160182
packed: Option<&packed::Buffer>,
161183
) -> Option<Result<Reference, file::find::existing::Error>> {
162-
match self.peeled {
163-
Some(peeled) => Some(Ok(Reference {
164-
name: self.name.clone(),
165-
target: Target::Object(peeled),
166-
peeled: None,
167-
})),
168-
None => match &self.target {
169-
Target::Object(_) => None,
170-
Target::Symbolic(full_name) => match store.try_find_packed(full_name.as_ref(), packed) {
171-
Ok(Some(next)) => Some(Ok(next)),
172-
Ok(None) => Some(Err(file::find::existing::Error::NotFound {
173-
name: full_name.to_path().to_owned(),
174-
})),
175-
Err(err) => Some(Err(file::find::existing::Error::Find(err))),
176-
},
184+
match &self.target {
185+
Target::Object(_) => None,
186+
Target::Symbolic(full_name) => match store.try_find_packed(full_name.as_ref(), packed) {
187+
Ok(Some(next)) => Some(Ok(next)),
188+
Ok(None) => Some(Err(file::find::existing::Error::NotFound {
189+
name: full_name.to_path().to_owned(),
190+
})),
191+
Err(err) => Some(Err(file::find::existing::Error::Find(err))),
177192
},
178193
}
179194
}

gix-ref/tests/file/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ pub fn store_at(name: &str) -> crate::Result<Store> {
1818
Ok(Store::at(path.join(".git"), Default::default()))
1919
}
2020

21+
pub fn store_at_with_args(name: &str, args: impl IntoIterator<Item = impl Into<String>>) -> crate::Result<Store> {
22+
let path = gix_testtools::scripted_fixture_read_only_with_args_standalone(name, args)?;
23+
Ok(Store::at(path.join(".git"), Default::default()))
24+
}
25+
2126
fn store_writable(name: &str) -> crate::Result<(gix_testtools::tempfile::TempDir, Store)> {
2227
let dir = gix_testtools::scripted_fixture_writable_standalone(name)?;
2328
let git_dir = dir.path().join(".git");

gix-ref/tests/file/reference.rs

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -94,25 +94,37 @@ mod peel {
9494
fn peel_one_level_with_pack() -> crate::Result {
9595
let store = store_with_packed_refs()?;
9696

97-
let head = store.find("dt1")?;
97+
let mut head = store.find("dt1")?;
9898
assert_eq!(
9999
head.target.try_id().map(ToOwned::to_owned),
100100
Some(hex_to_id("4c3f4cce493d7beb45012e478021b5f65295e5a3"))
101101
);
102102
assert_eq!(
103103
head.kind(),
104104
gix_ref::Kind::Object,
105-
"its peeled, but does have another step to peel to"
105+
"its peeled, but does have another step to peel to…"
106+
);
107+
let final_stop = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03");
108+
assert_eq!(head.peeled, Some(final_stop), "…it knows its peeled object");
109+
110+
assert_eq!(
111+
head.follow(&store).transpose()?,
112+
None,
113+
"but following doesn't do that, only real peeling does"
106114
);
107115

108-
let peeled = head.follow(&store).expect("a peeled ref for the object")?;
116+
head.peel_to_id_in_place(&store, &EmptyCommit)?;
109117
assert_eq!(
110-
peeled.target.try_id().map(ToOwned::to_owned),
111-
Some(hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03")),
118+
head.target.try_id().map(ToOwned::to_owned),
119+
Some(final_stop),
112120
"packed refs are always peeled (at least the ones we choose to read)"
113121
);
114-
assert_eq!(peeled.kind(), gix_ref::Kind::Object, "it's terminally peeled now");
115-
assert!(peeled.follow(&store).is_none());
122+
assert_eq!(head.kind(), gix_ref::Kind::Object, "it's terminally peeled now");
123+
assert_eq!(
124+
head.follow(&store).transpose()?,
125+
None,
126+
"following doesn't change anything"
127+
);
116128
Ok(())
117129
}
118130

@@ -146,17 +158,34 @@ mod peel {
146158

147159
#[test]
148160
fn to_id_long_jump() -> crate::Result {
149-
let store = file::store_at("make_multi_hop_ref.sh")?;
150-
let odb = gix_odb::at(store.git_dir().join("objects"))?;
151-
let mut r: Reference = store.find("multi-hop")?;
152-
r.peel_to_id_in_place(&store, &odb)?;
161+
for packed in [None, Some("packed")] {
162+
let store = file::store_at_with_args("make_multi_hop_ref.sh", packed)?;
163+
let odb = gix_odb::at(store.git_dir().join("objects"))?;
164+
let mut r: Reference = store.find("multi-hop")?;
165+
r.peel_to_id_in_place(&store, &odb)?;
153166

154-
let commit = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03");
155-
assert_eq!(r.peeled, Some(commit));
167+
let commit_id = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03");
168+
assert_eq!(r.peeled, Some(commit_id));
156169

157-
let mut buf = Vec::new();
158-
let obj = odb.find(&commit, &mut buf)?;
159-
assert_eq!(obj.kind, gix_object::Kind::Commit, "always peeled to the first non-tag");
170+
let mut buf = Vec::new();
171+
let obj = odb.find(&commit_id, &mut buf)?;
172+
assert_eq!(obj.kind, gix_object::Kind::Commit, "always peeled to the first non-tag");
173+
174+
let mut r: Reference = store.find("multi-hop")?;
175+
let tag_id =
176+
r.follow_to_object_in_place_packed(&store, store.cached_packed_buffer()?.as_ref().map(|p| &***p))?;
177+
let obj = odb.find(&tag_id, &mut buf)?;
178+
assert_eq!(obj.kind, gix_object::Kind::Tag, "the first direct object target");
179+
assert_eq!(
180+
obj.decode()?.into_tag().expect("tag").name,
181+
"dt2",
182+
"this is the first annotated tag, which points at dt1"
183+
);
184+
let mut r: Reference = store.find("multi-hop2")?;
185+
let other_tag_id =
186+
r.follow_to_object_in_place_packed(&store, store.cached_packed_buffer()?.as_ref().map(|p| &***p))?;
187+
assert_eq!(other_tag_id, tag_id, "it can follow with multiple hops as well");
188+
}
160189
Ok(())
161190
}
162191

@@ -169,9 +198,15 @@ mod peel {
169198

170199
assert!(matches!(
171200
r.peel_to_id_in_place(&store, &gix_object::find::Never).unwrap_err(),
172-
gix_ref::peel::to_id::Error::Cycle { .. }
201+
gix_ref::peel::to_id::Error::FollowToObject(gix_ref::peel::to_object::Error::Cycle { .. })
173202
));
174203
assert_eq!(r.name.as_bstr(), "refs/loop-a", "the ref is not changed on error");
204+
205+
let mut r: Reference = store.find_loose("loop-a")?.into();
206+
let err = r
207+
.follow_to_object_in_place_packed(&store, store.cached_packed_buffer()?.as_ref().map(|p| &***p))
208+
.unwrap_err();
209+
assert!(matches!(err, gix_ref::peel::to_object::Error::Cycle { .. }));
175210
Ok(())
176211
}
177212
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
make_worktree_repo.tar
22
make_worktree_repo_packed.tar
3-
make_multi_hop_ref.tar
3+
make_multi_hop_ref*.tar

gix-ref/tests/fixtures/make_multi_hop_ref.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ git tag -m "tag object" dt1
1111
git tag -m "tag object indirect" dt2 dt1
1212

1313
echo "ref: refs/tags/dt2" > .git/refs/multi-hop
14+
echo "ref: refs/multi-hop" > .git/refs/multi-hop2
15+
16+
if [ "${1:-}" = "packed" ]; then
17+
git pack-refs --all --prune
18+
fi

0 commit comments

Comments
 (0)