Skip to content

Commit 4e6a4e6

Browse files
committed
feat!: improve head() peeling API
Previously it was partially untested and it was hard to obtain an object of choice. Further breaking changes: * rename `Head::peeled()` to `into_peeled_id()` * rename `Head::into_fully_peeled_id()` to `try_peel_into_id()` * rename `Head::peel_to_id_in_place()` to `Head::try_peel_to_id_in_place()`
1 parent 266c9fc commit 4e6a4e6

File tree

10 files changed

+140
-81
lines changed

10 files changed

+140
-81
lines changed

gix/examples/stats.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
1111
let mut most_recent_commit_id = None;
1212
let num_commits = repo
1313
.head()?
14-
.into_fully_peeled_id()
15-
.ok_or("Cannot provide meaningful stats on empty repos")??
14+
.try_into_peeled_id()?
15+
.ok_or("Cannot provide meaningful stats on empty repos")?
1616
.ancestors()
1717
.all()?
1818
.map_while(Result::ok)

gix/src/clone/checkout.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub mod main_worktree {
8787
let workdir = repo.work_dir().ok_or_else(|| Error::BareRepository {
8888
git_dir: repo.git_dir().to_owned(),
8989
})?;
90-
let root_tree = match repo.head()?.peel_to_id_in_place().transpose()? {
90+
let root_tree = match repo.head()?.try_peel_to_id_in_place()? {
9191
Some(id) => id.object().expect("downloaded from remote").peel_to_tree()?.id,
9292
None => {
9393
return Ok((

gix/src/head/peel.rs

Lines changed: 71 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use crate::{
22
ext::{ObjectIdExt, ReferenceExt},
3+
head::Kind,
34
Head,
45
};
56

67
mod error {
78
use crate::{object, reference};
89

9-
/// The error returned by [`Head::peel_to_id_in_place()`][super::Head::peel_to_id_in_place()] and [`Head::into_fully_peeled_id()`][super::Head::into_fully_peeled_id()].
10+
/// The error returned by [`Head::peel_to_id_in_place()`](super::Head::try_peel_to_id_in_place())
11+
/// and [`Head::into_fully_peeled_id()`](super::Head::try_into_peeled_id()).
1012
#[derive(Debug, thiserror::Error)]
1113
#[allow(missing_docs)]
1214
pub enum Error {
@@ -19,7 +21,22 @@ mod error {
1921

2022
pub use error::Error;
2123

22-
use crate::head::Kind;
24+
///
25+
pub mod into_id {
26+
use crate::object;
27+
28+
/// The error returned by [`Head::into_peeled_id()`](super::Head::into_peeled_id()).
29+
#[derive(Debug, thiserror::Error)]
30+
#[allow(missing_docs)]
31+
pub enum Error {
32+
#[error(transparent)]
33+
Peel(#[from] super::Error),
34+
#[error("Branch '{name}' does not have any commits")]
35+
Unborn { name: gix_ref::FullName },
36+
#[error(transparent)]
37+
ObjectKind(#[from] object::try_into::Error),
38+
}
39+
}
2340

2441
///
2542
pub mod to_commit {
@@ -39,85 +56,79 @@ pub mod to_commit {
3956
}
4057

4158
impl<'repo> Head<'repo> {
42-
// TODO: tests
43-
/// Peel this instance to make obtaining its final target id possible, while returning an error on unborn heads.
44-
pub fn peeled(mut self) -> Result<Self, Error> {
45-
self.peel_to_id_in_place().transpose()?;
46-
Ok(self)
59+
/// Peel this instance and consume to make obtaining its final target id possible, while returning an error on unborn heads.
60+
///
61+
/// The final target is obtained by following symbolic references and peeling tags to their final destination, which
62+
/// typically is a commit, but can be any object.
63+
pub fn into_peeled_id(mut self) -> Result<crate::Id<'repo>, into_id::Error> {
64+
self.try_peel_to_id_in_place()?;
65+
self.id().ok_or_else(|| match self.kind {
66+
Kind::Symbolic(gix_ref::Reference { name, .. }) | Kind::Unborn(name) => into_id::Error::Unborn { name },
67+
Kind::Detached { .. } => unreachable!("id can be returned after peeling"),
68+
})
69+
}
70+
71+
/// Consume this instance and transform it into the final object that it points to, or `Ok(None)` if the `HEAD`
72+
/// reference is yet to be born.
73+
///
74+
/// The final target is obtained by following symbolic references and peeling tags to their final destination, which
75+
/// typically is a commit, but can be any object.
76+
pub fn try_into_peeled_id(mut self) -> Result<Option<crate::Id<'repo>>, Error> {
77+
self.try_peel_to_id_in_place()
4778
}
4879

49-
// TODO: tests
50-
// TODO: Fix this! It's not consistently peeling tags. The whole peeling business should be reconsidered to do what people usually
51-
// want which is to peel references, if present, and then peel objects with control over which object type to end at.
52-
// Finding a good interface for that isn't easy as ideally, it's an iterator that shows the intermediate objects so the user
53-
// can select which tag of a chain to choose.
5480
/// Follow the symbolic reference of this head until its target object and peel it by following tag objects until there is no
5581
/// more object to follow, and return that object id.
5682
///
57-
/// Returns `None` if the head is unborn.
58-
pub fn peel_to_id_in_place(&mut self) -> Option<Result<crate::Id<'repo>, Error>> {
59-
Some(match &mut self.kind {
60-
Kind::Unborn(_name) => return None,
83+
/// Returns `Ok(None)` if the head is unborn.
84+
///
85+
/// The final target is obtained by following symbolic references and peeling tags to their final destination, which
86+
/// typically is a commit, but can be any object.
87+
pub fn try_peel_to_id_in_place(&mut self) -> Result<Option<crate::Id<'repo>>, Error> {
88+
Ok(Some(match &mut self.kind {
89+
Kind::Unborn(_name) => return Ok(None),
6190
Kind::Detached {
6291
peeled: Some(peeled), ..
63-
} => Ok((*peeled).attach(self.repo)),
92+
} => (*peeled).attach(self.repo),
6493
Kind::Detached { peeled: None, target } => {
65-
match target
66-
.attach(self.repo)
67-
.object()
68-
.map_err(Into::into)
69-
.and_then(|obj| obj.peel_tags_to_end().map_err(Into::into))
70-
.map(|peeled| peeled.id)
71-
{
72-
Ok(peeled) => {
73-
self.kind = Kind::Detached {
74-
peeled: Some(peeled),
75-
target: *target,
76-
};
77-
Ok(peeled.attach(self.repo))
94+
let id = target.attach(self.repo);
95+
if id.header()?.kind() == gix_object::Kind::Commit {
96+
id
97+
} else {
98+
match id.object()?.peel_tags_to_end() {
99+
Ok(obj) => {
100+
self.kind = Kind::Detached {
101+
peeled: Some(obj.id),
102+
target: *target,
103+
};
104+
obj.id()
105+
}
106+
Err(err) => return Err(err.into()),
78107
}
79-
Err(err) => Err(err),
80108
}
81109
}
82110
Kind::Symbolic(r) => {
83111
let mut nr = r.clone().attach(self.repo);
84-
let peeled = nr.peel_to_id_in_place().map_err(Into::into);
112+
let peeled = nr.peel_to_id_in_place();
85113
*r = nr.detach();
86-
peeled
114+
peeled?
87115
}
88-
})
116+
}))
89117
}
90118

91-
// TODO: tests
92-
// TODO: something similar in `crate::Reference`
93119
/// Follow the symbolic reference of this head until its target object and peel it by following tag objects until there is no
94120
/// more object to follow, transform the id into a commit if possible and return that.
95121
///
96122
/// Returns an error if the head is unborn or if it doesn't point to a commit.
97123
pub fn peel_to_commit_in_place(&mut self) -> Result<crate::Commit<'repo>, to_commit::Error> {
98-
let id = self.peel_to_id_in_place().ok_or_else(|| to_commit::Error::Unborn {
99-
name: self.referent_name().expect("unborn").to_owned(),
100-
})??;
101-
id.object()
102-
.map_err(|err| to_commit::Error::Peel(Error::FindExistingObject(err)))
103-
.and_then(|object| object.try_into_commit().map_err(Into::into))
104-
}
105-
106-
/// Consume this instance and transform it into the final object that it points to, or `None` if the `HEAD`
107-
/// reference is yet to be born.
108-
pub fn into_fully_peeled_id(self) -> Option<Result<crate::Id<'repo>, Error>> {
109-
Some(match self.kind {
110-
Kind::Unborn(_name) => return None,
111-
Kind::Detached {
112-
peeled: Some(peeled), ..
113-
} => Ok(peeled.attach(self.repo)),
114-
Kind::Detached { peeled: None, target } => target
115-
.attach(self.repo)
116-
.object()
117-
.map_err(Into::into)
118-
.and_then(|obj| obj.peel_tags_to_end().map_err(Into::into))
119-
.map(|obj| obj.id.attach(self.repo)),
120-
Kind::Symbolic(r) => r.attach(self.repo).peel_to_id_in_place().map_err(Into::into),
121-
})
124+
let id = self
125+
.try_peel_to_id_in_place()?
126+
.ok_or_else(|| to_commit::Error::Unborn {
127+
name: self.referent_name().expect("unborn").to_owned(),
128+
})?;
129+
Ok(id
130+
.object()
131+
.map_err(|err| to_commit::Error::Peel(Error::FindExistingObject(err)))?
132+
.try_into_commit()?)
122133
}
123134
}

gix/src/reference/errors.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,7 @@ pub mod head_id {
4343
#[error(transparent)]
4444
Head(#[from] crate::reference::find::existing::Error),
4545
#[error(transparent)]
46-
PeelToId(#[from] crate::head::peel::Error),
47-
#[error("Branch '{name}' does not have any commits")]
48-
Unborn { name: gix_ref::FullName },
46+
PeelToId(#[from] crate::head::peel::into_id::Error),
4947
}
5048
}
5149

gix/src/repository/reference.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,20 +174,16 @@ impl crate::Repository {
174174
.attach(self))
175175
}
176176

177-
/// Resolve the `HEAD` reference, follow and peel its target and obtain its object id.
177+
/// Resolve the `HEAD` reference, follow and peel its target and obtain its object id,
178+
/// following symbolic references and tags until a commit is found.
178179
///
179180
/// Note that this may fail for various reasons, most notably because the repository
180181
/// is freshly initialized and doesn't have any commits yet.
181182
///
182183
/// Also note that the returned id is likely to point to a commit, but could also
183184
/// point to a tree or blob. It won't, however, point to a tag as these are always peeled.
184185
pub fn head_id(&self) -> Result<crate::Id<'_>, reference::head_id::Error> {
185-
let mut head = self.head()?;
186-
head.peel_to_id_in_place()
187-
.ok_or_else(|| reference::head_id::Error::Unborn {
188-
name: head.referent_name().expect("unborn").to_owned(),
189-
})?
190-
.map_err(Into::into)
186+
Ok(self.head()?.into_peeled_id()?)
191187
}
192188

193189
/// Return the name to the symbolic reference `HEAD` points to, or `None` if the head is detached.
@@ -203,7 +199,8 @@ impl crate::Repository {
203199
Ok(self.head()?.try_into_referent())
204200
}
205201

206-
/// Return the commit object the `HEAD` reference currently points to after peeling it fully.
202+
/// Return the commit object the `HEAD` reference currently points to after peeling it fully,
203+
/// following symbolic references and tags until a commit is found.
207204
///
208205
/// Note that this may fail for various reasons, most notably because the repository
209206
/// is freshly initialized and doesn't have any commits yet. It could also fail if the
@@ -212,7 +209,8 @@ impl crate::Repository {
212209
Ok(self.head()?.peel_to_commit_in_place()?)
213210
}
214211

215-
/// Return the tree id the `HEAD` reference currently points to after peeling it fully.
212+
/// Return the tree id the `HEAD` reference currently points to after peeling it fully,
213+
/// following symbolic references and tags until a commit is found.
216214
///
217215
/// Note that this may fail for various reasons, most notably because the repository
218216
/// is freshly initialized and doesn't have any commits yet. It could also fail if the
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:1de90b3e0c6547fda6759fb5df5e4bdf410dbe062ef9ef4f972308452c3224d7
3+
size 11508

gix/tests/fixtures/make_head_repos.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
set -eu -o pipefail
3+
4+
(git init symbolic && cd symbolic
5+
git commit -m "init" --allow-empty
6+
)
7+
8+
git clone symbolic detached
9+
(cd detached
10+
git remote rm origin
11+
git checkout @
12+
)
13+
14+
git clone symbolic tag-symbolic
15+
(cd tag-symbolic
16+
git tag -a -m make-tag-object point-at-commit HEAD
17+
git tag point-at-tag point-at-commit
18+
git tag -a -m make-tag-object point-at-tag-start point-at-tag
19+
git remote rm origin
20+
echo "ref: refs/tags/point-at-tag-start" > .git/HEAD
21+
)
22+
23+
git clone tag-symbolic tag-detached
24+
(cd tag-detached
25+
git remote rm origin
26+
git fetch --tags
27+
git rev-parse point-at-tag-start > .git/HEAD.tmp
28+
mv .git/HEAD.tmp .git/HEAD
29+
)
30+

gix/tests/head/mod.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
1-
mod into_remote {
1+
mod peel {
2+
use crate::util::{hex_to_id, named_subrepo_opts};
23

4+
#[test]
5+
fn all_cases() -> crate::Result {
6+
let expected_commit = hex_to_id("fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2");
7+
for name in ["detached", "symbolic", "tag-detached", "tag-symbolic"] {
8+
let repo = named_subrepo_opts("make_head_repos.sh", name, gix::open::Options::isolated())?;
9+
assert_eq!(repo.head()?.into_peeled_id()?, expected_commit);
10+
assert_eq!(repo.head_id()?, expected_commit);
11+
let commit = repo.head_commit()?;
12+
assert_eq!(commit.id, expected_commit);
13+
assert_eq!(repo.head_tree_id()?, commit.tree_id()?);
14+
assert_eq!(repo.head()?.try_into_peeled_id()?.expect("born"), expected_commit);
15+
assert_eq!(repo.head()?.try_peel_to_id_in_place()?.expect("born"), expected_commit);
16+
}
17+
Ok(())
18+
}
19+
}
20+
21+
mod into_remote {
322
use crate::remote;
423

524
#[test]

gix/tests/id/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ mod ancestors {
7575
fn all() -> crate::Result {
7676
let repo = crate::repo("make_repo_with_fork_and_dates.sh")?.to_thread_local();
7777
for use_commit_graph in [false, true] {
78-
let head = repo.head()?.into_fully_peeled_id().expect("born")?;
78+
let head = repo.head()?.into_peeled_id()?;
7979
let commits_graph_order = head
8080
.ancestors()
8181
.use_commit_graph(use_commit_graph)
@@ -118,7 +118,7 @@ mod ancestors {
118118
fn pre_epoch() -> crate::Result {
119119
let repo = crate::repo("make_pre_epoch_repo.sh")?.to_thread_local();
120120
for use_commit_graph in [false, true] {
121-
let head = repo.head()?.into_fully_peeled_id().expect("born")?;
121+
let head = repo.head()?.into_peeled_id()?;
122122
let commits = head
123123
.ancestors()
124124
.sorting(commit::Sorting::ByCommitTimeNewestFirst) // assure we have time set
@@ -137,7 +137,7 @@ mod ancestors {
137137
#[test]
138138
fn filtered() -> crate::Result {
139139
let repo = crate::repo("make_repo_with_fork_and_dates.sh")?.to_thread_local();
140-
let head = repo.head()?.into_fully_peeled_id().expect("born")?;
140+
let head = repo.head()?.into_peeled_id()?;
141141

142142
for use_commit_graph in [false, true] {
143143
for sorting in [

gix/tests/repository/object.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ mod find {
128128
2 => repo.object_cache_size(128 * 1024),
129129
_ => unreachable!("BUG"),
130130
}
131-
for commit_id in repo.head()?.peeled()?.id().expect("born").ancestors().all()? {
131+
for commit_id in repo.head()?.into_peeled_id()?.ancestors().all()? {
132132
let commit = commit_id?;
133133
assert_eq!(commit.id().object()?.kind, gix_object::Kind::Commit);
134134
assert_eq!(commit.id().header()?.kind(), gix_object::Kind::Commit);
@@ -363,7 +363,7 @@ mod commit {
363363
vec!["commit: hello there", "commit: c2", "commit (initial): c1"],
364364
"we get the actual HEAD log, not the log of some reference"
365365
);
366-
let current_commit = repo.head()?.into_fully_peeled_id().expect("born")?;
366+
let current_commit = repo.head()?.into_peeled_id()?;
367367
assert_eq!(current_commit, first_commit_id, "the commit was set");
368368

369369
let second_commit_id = repo.commit(

0 commit comments

Comments
 (0)