Skip to content

feat: refs support pseudo refs #2061

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion gix-features/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,17 @@ pub mod walkdir {
/// Instantiate a new directory iterator which will not skip hidden files and is sorted, with the given level of `parallelism`.
///
/// Use `precompose_unicode` to represent the `core.precomposeUnicode` configuration option.
pub fn walkdir_sorted_new(root: &Path, _: Parallelism, precompose_unicode: bool) -> WalkDir {
/// Use `max_depth` to limit the depth of the recursive walk.
/// * `0`
/// - Returns only the root path with no children
/// * `1`
/// - Root directory and children.
/// * `1..n`
/// - Root directory, children and {n}-grandchildren
pub fn walkdir_sorted_new(root: &Path, _: Parallelism, max_depth: usize, precompose_unicode: bool) -> WalkDir {
WalkDir {
inner: WalkDirImpl::new(root)
.max_depth(max_depth)
.sort_by(|a, b| {
let storage_a;
let storage_b;
Expand Down
2 changes: 1 addition & 1 deletion gix-ref/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ pub enum Category<'a> {
RemoteBranch,
/// A tag in `refs/notes`
Note,
/// Something outside of `ref/` in the current worktree, typically `HEAD`.
/// Something outside `ref/` in the current worktree, typically `HEAD`.
PseudoRef,
/// A `PseudoRef`, but referenced so that it will always refer to the main worktree by
/// prefixing it with `main-worktree/`.
Expand Down
18 changes: 17 additions & 1 deletion gix-ref/src/store/file/loose/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,30 @@ pub(in crate::store_impl::file) struct SortedLoosePaths {
pub(crate) base: PathBuf,
/// An prefix like `refs/heads/foo/` or `refs/heads/prefix` that a returned reference must match against..
prefix: Option<BString>,
/// A suffix like `HEAD` that a returned reference must match against..
suffix: Option<BString>,
file_walk: Option<DirEntryIter>,
}

impl SortedLoosePaths {
pub fn at(path: &Path, base: PathBuf, prefix: Option<BString>, precompose_unicode: bool) -> Self {
pub fn at(
path: &Path,
base: PathBuf,
prefix: Option<BString>,
suffix: Option<BString>,
precompose_unicode: bool,
) -> Self {
let depth = if suffix.is_some() { 1 } else { usize::MAX };
SortedLoosePaths {
base,
prefix,
suffix,
file_walk: path.is_dir().then(|| {
// serial iteration as we expect most refs in packed-refs anyway.
gix_features::fs::walkdir_sorted_new(
path,
gix_features::fs::walkdir::Parallelism::Serial,
depth,
precompose_unicode,
)
.into_iter()
Expand Down Expand Up @@ -56,6 +67,11 @@ impl Iterator for SortedLoosePaths {
continue;
}
}
if let Some(suffix) = &self.suffix {
if !full_name.ends_with(suffix) {
continue;
}
}
if gix_validate::reference::name_partial(full_name.as_bstr()).is_ok() {
let name = FullName(full_name);
return Some(Ok((full_path, name)));
Expand Down
75 changes: 55 additions & 20 deletions gix-ref/src/store/file/overlay_iter.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use gix_object::bstr::ByteSlice;
use gix_path::RelativePath;
use std::{
borrow::Cow,
cmp::Ordering,
Expand All @@ -6,11 +8,8 @@ use std::{
path::{Path, PathBuf},
};

use gix_object::bstr::ByteSlice;
use gix_path::RelativePath;

use crate::{
file::{loose, loose::iter::SortedLoosePaths},
file::loose::{self, iter::SortedLoosePaths},
store_impl::{file, packed},
BStr, FullName, Namespace, Reference,
};
Expand Down Expand Up @@ -85,25 +84,25 @@ impl<'p> LooseThenPacked<'p, '_> {
}

fn convert_loose(&mut self, res: std::io::Result<(PathBuf, FullName)>) -> Result<Reference, Error> {
let buf = &mut self.buf;
let git_dir = self.git_dir;
let common_dir = self.common_dir;
let (refpath, name) = res.map_err(Error::Traversal)?;
std::fs::File::open(&refpath)
.and_then(|mut f| {
self.buf.clear();
f.read_to_end(&mut self.buf)
buf.clear();
f.read_to_end(buf)
})
.map_err(|err| Error::ReadFileContents {
source: err,
path: refpath.to_owned(),
})?;
loose::Reference::try_from_path(name, &self.buf)
loose::Reference::try_from_path(name, buf)
.map_err(|err| {
let relative_path = refpath
.strip_prefix(self.git_dir)
.strip_prefix(git_dir)
.ok()
.or_else(|| {
self.common_dir
.and_then(|common_dir| refpath.strip_prefix(common_dir).ok())
})
.or_else(|| common_dir.and_then(|common_dir| refpath.strip_prefix(common_dir).ok()))
.expect("one of our bases contains the path");
Error::ReferenceCreation {
source: err,
Expand Down Expand Up @@ -191,9 +190,9 @@ impl Iterator for LooseThenPacked<'_, '_> {
}

impl Platform<'_> {
/// Return an iterator over all references, loose or `packed`, sorted by their name.
/// Return an iterator over all references, loose or packed, sorted by their name.
///
/// Errors are returned similarly to what would happen when loose and packed refs where iterated by themselves.
/// Errors are returned similarly to what would happen when loose and packed refs were iterated by themselves.
pub fn all(&self) -> std::io::Result<LooseThenPacked<'_, '_>> {
self.store.iter_packed(self.packed.as_ref().map(|b| &***b))
}
Expand All @@ -210,12 +209,18 @@ impl Platform<'_> {
self.store
.iter_prefixed_packed(prefix, self.packed.as_ref().map(|b| &***b))
}

/// Return an iterator over the pseudo references, like `HEAD` or `FETCH_HEAD`, or anything else suffixed with `HEAD`
/// in the root of the `.git` directory, sorted by name.
pub fn pseudo(&self) -> std::io::Result<LooseThenPacked<'_, '_>> {
self.store.iter_pseudo()
}
}

impl file::Store {
/// Return a platform to obtain iterator over all references, or prefixed ones, loose or packed, sorted by their name.
///
/// Errors are returned similarly to what would happen when loose and packed refs where iterated by themselves.
/// Errors are returned similarly to what would happen when loose and packed refs were iterated by themselves.
///
/// Note that since packed-refs are storing refs as precomposed unicode if [`Self::precompose_unicode`] is true, for consistency
/// we also return loose references as precomposed unicode.
Expand Down Expand Up @@ -254,6 +259,10 @@ pub(crate) enum IterInfo<'a> {
/// If `true`, we will convert decomposed into precomposed unicode.
precompose_unicode: bool,
},
Pseudo {
base: &'a Path,
precompose_unicode: bool,
},
}

impl<'a> IterInfo<'a> {
Expand All @@ -263,6 +272,7 @@ impl<'a> IterInfo<'a> {
IterInfo::PrefixAndBase { prefix, .. } => Some(gix_path::into_bstr(*prefix)),
IterInfo::BaseAndIterRoot { prefix, .. } => Some(gix_path::into_bstr(prefix.clone())),
IterInfo::ComputedIterationRoot { prefix, .. } => Some(prefix.clone()),
IterInfo::Pseudo { .. } => None,
}
}

Expand All @@ -271,24 +281,34 @@ impl<'a> IterInfo<'a> {
IterInfo::Base {
base,
precompose_unicode,
} => SortedLoosePaths::at(&base.join("refs"), base.into(), None, precompose_unicode),
} => SortedLoosePaths::at(&base.join("refs"), base.into(), None, None, precompose_unicode),
IterInfo::BaseAndIterRoot {
base,
iter_root,
prefix: _,
precompose_unicode,
} => SortedLoosePaths::at(&iter_root, base.into(), None, precompose_unicode),
} => SortedLoosePaths::at(&iter_root, base.into(), None, None, precompose_unicode),
IterInfo::PrefixAndBase {
base,
prefix,
precompose_unicode,
} => SortedLoosePaths::at(&base.join(prefix), base.into(), None, precompose_unicode),
} => SortedLoosePaths::at(&base.join(prefix), base.into(), None, None, precompose_unicode),
IterInfo::ComputedIterationRoot {
iter_root,
base,
prefix,
precompose_unicode,
} => SortedLoosePaths::at(&iter_root, base.into(), Some(prefix.into_owned()), precompose_unicode),
} => SortedLoosePaths::at(
&iter_root,
base.into(),
Some(prefix.into_owned()),
None,
precompose_unicode,
),
IterInfo::Pseudo {
base,
precompose_unicode,
} => SortedLoosePaths::at(base, base.into(), None, Some("HEAD".into()), precompose_unicode),
}
.peekable()
}
Expand Down Expand Up @@ -321,7 +341,7 @@ impl<'a> IterInfo<'a> {
impl file::Store {
/// Return an iterator over all references, loose or `packed`, sorted by their name.
///
/// Errors are returned similarly to what would happen when loose and packed refs where iterated by themselves.
/// Errors are returned similarly to what would happen when loose and packed refs were iterated by themselves.
pub fn iter_packed<'s, 'p>(
&'s self,
packed: Option<&'p packed::Buffer>,
Expand Down Expand Up @@ -354,6 +374,21 @@ impl file::Store {
}
}

/// Return an iterator over the pseudo references, like `HEAD` or `FETCH_HEAD`, or anything else suffixed with `HEAD`
/// in the root of the `.git` directory, sorted by name.
///
/// Errors are returned similarly to what would happen when loose refs were iterated by themselves.
pub fn iter_pseudo<'p>(&'_ self) -> std::io::Result<LooseThenPacked<'p, '_>> {
self.iter_from_info(
IterInfo::Pseudo {
base: self.git_dir(),
precompose_unicode: self.precompose_unicode,
},
None,
None,
)
}

/// As [`iter(…)`](file::Store::iter()), but filters by `prefix`, i.e. `refs/heads/` or
/// `refs/heads/feature-`.
/// Note that if a prefix isn't using a trailing `/`, like in `refs/heads/foo`, it will effectively
Expand Down
Binary file not shown.
14 changes: 14 additions & 0 deletions gix-ref/tests/fixtures/make_pseudo_ref_repository.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -eu -o pipefail

git init -q
git commit -m "init" --allow-empty

git rev-parse HEAD > .git/JIRI_HEAD
touch .git/SOME_ALL_CAPS_FILE
touch .git/refs/SHOULD_BE_EXCLUDED_HEAD

cat <<EOF >> .git/FETCH_HEAD
9064ea31fae4dc59a56bdd3a06c0ddc990ee689e branch 'main' of https://github.com/Byron/gitoxide
1b8d9e6a408e480ae1912e919c37a26e5c46639d not-for-merge branch 'faster-discovery' of https://github.com/Byron/gitoxide
EOF
17 changes: 15 additions & 2 deletions gix-ref/tests/refs/file/store/iter.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use gix_object::bstr::ByteSlice;

use crate::{
file::{store, store_at, store_with_packed_refs},
hex_to_id,
};
use gix_object::bstr::ByteSlice;

mod with_namespace {
use gix_object::bstr::{BString, ByteSlice};
Expand Down Expand Up @@ -257,6 +256,20 @@ fn packed_file_iter() -> crate::Result {
Ok(())
}

#[test]
fn pseudo_refs_iter() -> crate::Result {
let store = store_at("make_pseudo_ref_repository.sh")?;

let actual = store
.iter_pseudo()?
.map(Result::unwrap)
.map(|r: gix_ref::Reference| r.name.as_bstr().to_string())
.collect::<Vec<_>>();

assert_eq!(actual, ["FETCH_HEAD", "HEAD", "JIRI_HEAD"]);
Ok(())
}

#[test]
fn loose_iter_with_broken_refs() -> crate::Result {
let store = store()?;
Expand Down
2 changes: 1 addition & 1 deletion gix-submodule/tests/file/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ fn common_values_and_names_by_path() -> crate::Result {

fn module_files() -> impl Iterator<Item = (PathBuf, PathBuf)> {
let dir = gix_testtools::scripted_fixture_read_only("basic.sh").expect("valid fixture");
gix_features::fs::walkdir_sorted_new(&dir, Parallelism::Serial, false)
gix_features::fs::walkdir_sorted_new(&dir, Parallelism::Serial, usize::MAX, false)
.follow_links(false)
.into_iter()
.filter_map(move |entry| {
Expand Down
9 changes: 8 additions & 1 deletion gix/src/reference/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ impl<'r> Iter<'r> {
}

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

// TODO: tests
/// Return an iterator over all local pseudo references.
pub fn pseudo(&self) -> Result<Iter<'_>, init::Error> {
Ok(Iter::new(self.repo, self.platform.pseudo()?))
}

// TODO: tests
/// Return an iterator over all remote branches.
///
Expand Down
Loading