Skip to content

Commit a2ec4e7

Browse files
committed
feat: refs support pseudo refs
1 parent 40295b2 commit a2ec4e7

File tree

8 files changed

+245
-29
lines changed

8 files changed

+245
-29
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,12 @@ pub struct Transaction<'s, 'p> {
121121
///
122122
pub mod loose;
123123
mod overlay_iter;
124+
mod pseudo_ref_iter;
124125

125126
///
126127
pub mod iter {
127128
pub use super::overlay_iter::{LooseThenPacked, Platform};
129+
pub use super::pseudo_ref_iter::SortedPseudoRefIterator;
128130

129131
///
130132
pub mod loose_then_packed {

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

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use gix_object::bstr::ByteSlice;
1010
use gix_path::RelativePath;
1111

1212
use crate::{
13-
file::{loose, loose::iter::SortedLoosePaths},
13+
file::{
14+
iter::SortedPseudoRefIterator,
15+
loose::{self, iter::SortedLoosePaths},
16+
},
1417
store_impl::{file, packed},
1518
BStr, FullName, Namespace, Reference,
1619
};
@@ -85,36 +88,48 @@ impl<'p> LooseThenPacked<'p, '_> {
8588
}
8689

8790
fn convert_loose(&mut self, res: std::io::Result<(PathBuf, FullName)>) -> Result<Reference, Error> {
88-
let (refpath, name) = res.map_err(Error::Traversal)?;
89-
std::fs::File::open(&refpath)
90-
.and_then(|mut f| {
91-
self.buf.clear();
92-
f.read_to_end(&mut self.buf)
93-
})
94-
.map_err(|err| Error::ReadFileContents {
95-
source: err,
96-
path: refpath.to_owned(),
97-
})?;
98-
loose::Reference::try_from_path(name, &self.buf)
99-
.map_err(|err| {
100-
let relative_path = refpath
101-
.strip_prefix(self.git_dir)
102-
.ok()
103-
.or_else(|| {
104-
self.common_dir
105-
.and_then(|common_dir| refpath.strip_prefix(common_dir).ok())
106-
})
107-
.expect("one of our bases contains the path");
108-
Error::ReferenceCreation {
109-
source: err,
110-
relative_path: relative_path.into(),
111-
}
112-
})
113-
.map(Into::into)
114-
.map(|r| self.strip_namespace(r))
91+
convert_loose(&mut self.buf, self.git_dir, self.common_dir, self.namespace, res)
11592
}
11693
}
11794

95+
pub(crate) fn convert_loose(
96+
buf: &mut Vec<u8>,
97+
git_dir: &Path,
98+
common_dir: Option<&Path>,
99+
namespace: Option<&Namespace>,
100+
res: std::io::Result<(PathBuf, FullName)>,
101+
) -> Result<Reference, Error> {
102+
let (refpath, name) = res.map_err(Error::Traversal)?;
103+
std::fs::File::open(&refpath)
104+
.and_then(|mut f| {
105+
buf.clear();
106+
f.read_to_end(buf)
107+
})
108+
.map_err(|err| Error::ReadFileContents {
109+
source: err,
110+
path: refpath.to_owned(),
111+
})?;
112+
loose::Reference::try_from_path(name, buf)
113+
.map_err(|err| {
114+
let relative_path = refpath
115+
.strip_prefix(git_dir)
116+
.ok()
117+
.or_else(|| common_dir.and_then(|common_dir| refpath.strip_prefix(common_dir).ok()))
118+
.expect("one of our bases contains the path");
119+
Error::ReferenceCreation {
120+
source: err,
121+
relative_path: relative_path.into(),
122+
}
123+
})
124+
.map(Into::into)
125+
.map(|mut r: Reference| {
126+
if let Some(namespace) = namespace {
127+
r.strip_namespace(namespace);
128+
}
129+
r
130+
})
131+
}
132+
118133
impl Iterator for LooseThenPacked<'_, '_> {
119134
type Item = Result<Reference, Error>;
120135

@@ -210,6 +225,11 @@ impl Platform<'_> {
210225
self.store
211226
.iter_prefixed_packed(prefix, self.packed.as_ref().map(|b| &***b))
212227
}
228+
229+
/// Return an iterator over the pseudo references
230+
pub fn psuedo_refs(&self) -> std::io::Result<SortedPseudoRefIterator> {
231+
self.store.iter_pseudo_refs()
232+
}
213233
}
214234

215235
impl file::Store {
@@ -354,6 +374,11 @@ impl file::Store {
354374
}
355375
}
356376

377+
/// Return an iterator over all pseudo references.
378+
pub fn iter_pseudo_refs(&self) -> std::io::Result<SortedPseudoRefIterator> {
379+
Ok(SortedPseudoRefIterator::at(&self.git_dir, self.precompose_unicode))
380+
}
381+
357382
/// As [`iter(…)`](file::Store::iter()), but filters by `prefix`, i.e. `refs/heads/` or
358383
/// `refs/heads/feature-`.
359384
/// Note that if a prefix isn't using a trailing `/`, like in `refs/heads/foo`, it will effectively
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
use std::path::PathBuf;
2+
3+
use gix_features::fs::walkdir::DirEntryIter;
4+
use gix_object::bstr::ByteSlice;
5+
6+
use crate::{file::overlay_iter, BString, FullName, Reference};
7+
8+
/// An iterator over all pseudo references in a given git directory
9+
pub struct SortedPseudoRefPaths {
10+
pub(crate) git_dir: PathBuf,
11+
file_walk: Option<DirEntryIter>,
12+
}
13+
14+
impl SortedPseudoRefPaths {
15+
/// Returns an iterator over the pseudo ref paths found at the given git_dir
16+
pub fn at(git_dir: PathBuf, precompose_unicode: bool) -> Self {
17+
SortedPseudoRefPaths {
18+
git_dir: git_dir.to_owned(),
19+
file_walk: git_dir.is_dir().then(|| {
20+
// serial iteration as we expect most refs in packed-refs anyway.
21+
gix_features::fs::walkdir_sorted_new(
22+
&git_dir,
23+
gix_features::fs::walkdir::Parallelism::Serial,
24+
// In a given git directory pseudo refs are only at the root
25+
1,
26+
precompose_unicode,
27+
)
28+
.into_iter()
29+
}),
30+
}
31+
}
32+
}
33+
34+
impl Iterator for SortedPseudoRefPaths {
35+
type Item = std::io::Result<(PathBuf, FullName)>;
36+
37+
fn next(&mut self) -> Option<Self::Item> {
38+
for entry in self.file_walk.as_mut()?.by_ref() {
39+
match entry {
40+
Ok(entry) => {
41+
if !entry.file_type().is_ok_and(|ft| ft.is_file()) {
42+
continue;
43+
}
44+
let full_path = entry.path().into_owned();
45+
let full_name = full_path
46+
.strip_prefix(&self.git_dir)
47+
.expect("prefix-stripping cannot fail as base is within our root");
48+
let Ok(full_name) = gix_path::try_into_bstr(full_name)
49+
.map(|name| gix_path::to_unix_separators_on_windows(name).into_owned())
50+
else {
51+
continue;
52+
};
53+
// Pseudo refs must end with "HEAD"
54+
if !full_name.ends_with(&BString::from("HEAD")) {
55+
continue;
56+
}
57+
if gix_validate::reference::name_partial(full_name.as_bstr()).is_ok() {
58+
let name = FullName(full_name);
59+
return Some(Ok((full_path, name)));
60+
} else {
61+
continue;
62+
}
63+
}
64+
Err(err) => return Some(Err(err.into_io_error().expect("no symlink related errors"))),
65+
}
66+
}
67+
None
68+
}
69+
}
70+
71+
/// An iterator over all pseudo references in a given git directory
72+
pub struct SortedPseudoRefIterator {
73+
pub(crate) git_dir: PathBuf,
74+
inner: SortedPseudoRefPaths,
75+
buf: Vec<u8>,
76+
}
77+
78+
impl SortedPseudoRefIterator {
79+
/// Returns an iterator over the pseudo ref paths found at the given git_dir
80+
pub fn at(git_dir: &PathBuf, precompose_unicode: bool) -> Self {
81+
SortedPseudoRefIterator {
82+
inner: SortedPseudoRefPaths::at(git_dir.to_owned(), precompose_unicode),
83+
git_dir: git_dir.to_owned(),
84+
buf: vec![],
85+
}
86+
}
87+
}
88+
89+
impl Iterator for SortedPseudoRefIterator {
90+
type Item = Result<Reference, overlay_iter::Error>;
91+
92+
fn next(&mut self) -> Option<Self::Item> {
93+
self.inner
94+
.next()
95+
.map(|r| overlay_iter::convert_loose(&mut self.buf, &self.git_dir, None, None, r))
96+
}
97+
}
Binary file not shown.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
set -eu -o pipefail
3+
4+
git init -q
5+
6+
git checkout -q -b main
7+
git commit -q --allow-empty -m c1
8+
git branch dt1
9+
git branch d1
10+
git branch A
11+
12+
mkdir -p .git/refs/remotes/origin
13+
mkdir -p .git/refs/prefix/feature/sub/dir
14+
15+
cp .git/refs/heads/main .git/refs/remotes/origin/
16+
cp .git/refs/heads/main .git/refs/d1
17+
cp .git/refs/heads/main .git/refs/prefix/feature-suffix
18+
cp .git/refs/heads/main .git/refs/prefix/feature/sub/dir/algo
19+
20+
echo "ref: refs/remotes/origin/main" > .git/refs/remotes/origin/HEAD
21+
echo "notahexsha" > .git/refs/broken
22+
23+
git rev-parse HEAD > .git/JIRI_HEAD
24+
touch .git/SOME_ALL_CAPS_FILE
25+
26+
cat <<EOF >> .git/FETCH_HEAD
27+
9064ea31fae4dc59a56bdd3a06c0ddc990ee689e branch 'main' of https://github.com/Byron/gitoxide
28+
1b8d9e6a408e480ae1912e919c37a26e5c46639d not-for-merge branch 'faster-discovery' of https://github.com/Byron/gitoxide
29+
43f695a9607f1f85f859f2ef944b785b5b6dd238 not-for-merge branch 'fix-823' of https://github.com/Byron/gitoxide
30+
96267708958ead2646aae8766a50fa060739003c not-for-merge branch 'fix-bare-with-index' of https://github.com/Byron/gitoxide
31+
1397e19375bb98522f951b8a452b08c1b35ffbac not-for-merge branch 'gix-archive' of https://github.com/Byron/gitoxide
32+
db71ec8b7c7f2730c47dde3bb662ab56ae89ae7d not-for-merge branch 'index-from-files' of https://github.com/Byron/gitoxide
33+
9f0c71917e57653d2e7121eae65d9385a188a8df not-for-merge branch 'moonwalk' of https://github.com/Byron/gitoxide
34+
44d2b67de5639d4ea3d08ab030ecfe4bdfc8cbfb not-for-merge branch 'release-gix' of https://github.com/Byron/gitoxide
35+
37c3d073b15dafcb52b2040e4b92a413c69a726d not-for-merge branch 'smart-release-without-git2' of https://github.com/Byron/gitoxide
36+
af3608ad397784795c3758a1ac99ec6a367de9be not-for-merge branch 'walk-with-commitgraph' of https://github.com/Byron/gitoxide
37+
EOF
38+
39+
git tag t1
40+
git tag -m "tag object" dt1

gix-ref/tests/refs/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ mod partialname {
3939
}
4040
mod namespace;
4141
mod packed;
42+
mod pseudo_refs;
4243
mod reference;
4344
mod store;
4445
mod transaction;

gix-ref/tests/refs/pseudo_refs.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use crate::file::store_at;
2+
3+
#[test]
4+
fn pseudo_refs_iterate_valid_pseudorefs() -> crate::Result {
5+
let store = store_at("make_pref_repository.sh")?;
6+
7+
let prefs = store
8+
.iter_pseudo_refs()?
9+
.map(Result::unwrap)
10+
.map(|r: gix_ref::Reference| r.name)
11+
.collect::<Vec<_>>();
12+
13+
let expected_prefs = vec!["FETCH_HEAD", "HEAD", "JIRI_HEAD"];
14+
15+
assert_eq!(
16+
prefs.iter().map(gix_ref::FullName::as_bstr).collect::<Vec<_>>(),
17+
expected_prefs
18+
);
19+
20+
Ok(())
21+
}

gix/src/reference/iter.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ impl<'r> Iter<'r> {
3232
}
3333

3434
impl Platform<'_> {
35-
/// Return an iterator over all references in the repository.
35+
/// Return an iterator over all references in the repository, excluding
36+
/// pseudo references.
3637
///
3738
/// Even broken or otherwise unparsable or inaccessible references are returned and have to be handled by the caller on a
3839
/// case by case basis.
@@ -69,6 +70,12 @@ impl Platform<'_> {
6970
))
7071
}
7172

73+
// TODO: tests
74+
/// Return an iterator over all local pseudo references.
75+
pub fn pseudo_refs(&self) -> Result<PseudoRefIter<'_>, init::Error> {
76+
Ok(PseudoRefIter::new(self.repo, self.platform.psuedo_refs()?))
77+
}
78+
7279
// TODO: tests
7380
/// Return an iterator over all remote branches.
7481
///
@@ -122,6 +129,29 @@ impl<'r> Iterator for Iter<'r> {
122129
}
123130
}
124131

132+
/// An iterator over pseudo references.
133+
pub struct PseudoRefIter<'r> {
134+
inner: gix_ref::file::iter::SortedPseudoRefIterator,
135+
repo: &'r crate::Repository,
136+
}
137+
138+
impl<'r> PseudoRefIter<'r> {
139+
fn new(repo: &'r crate::Repository, platform: gix_ref::file::iter::SortedPseudoRefIterator) -> Self {
140+
PseudoRefIter { inner: platform, repo }
141+
}
142+
}
143+
144+
impl<'r> Iterator for PseudoRefIter<'r> {
145+
type Item = Result<crate::Reference<'r>, Box<dyn std::error::Error + Send + Sync + 'static>>;
146+
147+
fn next(&mut self) -> Option<Self::Item> {
148+
self.inner.next().map(|res| {
149+
res.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)
150+
.map(|r| crate::Reference::from_ref(r, self.repo))
151+
})
152+
}
153+
}
154+
125155
///
126156
pub mod init {
127157
/// The error returned by [`Platform::all()`](super::Platform::all()) or [`Platform::prefixed()`](super::Platform::prefixed()).

0 commit comments

Comments
 (0)