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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
7 changes: 6 additions & 1 deletion gix-features/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,14 @@ 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.
/// * 2..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
1 change: 1 addition & 0 deletions gix-ref/src/store/file/loose/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ impl SortedLoosePaths {
gix_features::fs::walkdir_sorted_new(
path,
gix_features::fs::walkdir::Parallelism::Serial,
usize::MAX,
precompose_unicode,
)
.into_iter()
Expand Down
2 changes: 2 additions & 0 deletions gix-ref/src/store/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,12 @@ pub struct Transaction<'s, 'p> {
///
pub mod loose;
mod overlay_iter;
mod pseudo_ref_iter;

///
pub mod iter {
pub use super::overlay_iter::{LooseThenPacked, Platform};
pub use super::pseudo_ref_iter::SortedPseudoRefIterator;

///
pub mod loose_then_packed {
Expand Down
81 changes: 53 additions & 28 deletions gix-ref/src/store/file/overlay_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ use gix_object::bstr::ByteSlice;
use gix_path::RelativePath;

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

fn convert_loose(&mut self, res: std::io::Result<(PathBuf, FullName)>) -> Result<Reference, Error> {
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)
})
.map_err(|err| Error::ReadFileContents {
source: err,
path: refpath.to_owned(),
})?;
loose::Reference::try_from_path(name, &self.buf)
.map_err(|err| {
let relative_path = refpath
.strip_prefix(self.git_dir)
.ok()
.or_else(|| {
self.common_dir
.and_then(|common_dir| refpath.strip_prefix(common_dir).ok())
})
.expect("one of our bases contains the path");
Error::ReferenceCreation {
source: err,
relative_path: relative_path.into(),
}
})
.map(Into::into)
.map(|r| self.strip_namespace(r))
convert_loose(&mut self.buf, self.git_dir, self.common_dir, self.namespace, res)
}
}

pub(crate) fn convert_loose(
buf: &mut Vec<u8>,
git_dir: &Path,
common_dir: Option<&Path>,
namespace: Option<&Namespace>,
res: std::io::Result<(PathBuf, FullName)>,
) -> Result<Reference, Error> {
let (refpath, name) = res.map_err(Error::Traversal)?;
std::fs::File::open(&refpath)
.and_then(|mut f| {
buf.clear();
f.read_to_end(buf)
})
.map_err(|err| Error::ReadFileContents {
source: err,
path: refpath.to_owned(),
})?;
loose::Reference::try_from_path(name, buf)
.map_err(|err| {
let relative_path = refpath
.strip_prefix(git_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,
relative_path: relative_path.into(),
}
})
.map(Into::into)
.map(|mut r: Reference| {
if let Some(namespace) = namespace {
r.strip_namespace(namespace);
}
r
})
}

impl Iterator for LooseThenPacked<'_, '_> {
type Item = Result<Reference, Error>;

Expand Down Expand Up @@ -210,6 +225,11 @@ impl Platform<'_> {
self.store
.iter_prefixed_packed(prefix, self.packed.as_ref().map(|b| &***b))
}

/// Return an iterator over the pseudo references
pub fn psuedo_refs(&self) -> std::io::Result<SortedPseudoRefIterator> {
self.store.iter_pseudo_refs()
}
}

impl file::Store {
Expand Down Expand Up @@ -354,6 +374,11 @@ impl file::Store {
}
}

/// Return an iterator over all pseudo references.
pub fn iter_pseudo_refs(&self) -> std::io::Result<SortedPseudoRefIterator> {
Ok(SortedPseudoRefIterator::at(&self.git_dir, self.precompose_unicode))
}

/// 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
97 changes: 97 additions & 0 deletions gix-ref/src/store/file/pseudo_ref_iter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use std::path::PathBuf;

use gix_features::fs::walkdir::DirEntryIter;
use gix_object::bstr::ByteSlice;

use crate::{file::overlay_iter, BString, FullName, Reference};

/// An iterator over all pseudo references in a given git directory
pub struct SortedPseudoRefPaths {
pub(crate) git_dir: PathBuf,
file_walk: Option<DirEntryIter>,
}

impl SortedPseudoRefPaths {
/// Returns an iterator over the pseudo ref paths found at the given git_dir
pub fn at(git_dir: PathBuf, precompose_unicode: bool) -> Self {
SortedPseudoRefPaths {
git_dir: git_dir.to_owned(),
file_walk: git_dir.is_dir().then(|| {
// serial iteration as we expect most refs in packed-refs anyway.
gix_features::fs::walkdir_sorted_new(
&git_dir,
gix_features::fs::walkdir::Parallelism::Serial,
// In a given git directory pseudo refs are only at the root
1,
precompose_unicode,
)
.into_iter()
}),
}
}
}

impl Iterator for SortedPseudoRefPaths {
type Item = std::io::Result<(PathBuf, FullName)>;

fn next(&mut self) -> Option<Self::Item> {
for entry in self.file_walk.as_mut()?.by_ref() {
match entry {
Ok(entry) => {
if !entry.file_type().is_ok_and(|ft| ft.is_file()) {
continue;
}
let full_path = entry.path().into_owned();
let full_name = full_path
.strip_prefix(&self.git_dir)
.expect("prefix-stripping cannot fail as base is within our root");
let Ok(full_name) = gix_path::try_into_bstr(full_name)
.map(|name| gix_path::to_unix_separators_on_windows(name).into_owned())
else {
continue;
};
// Pseudo refs must end with "HEAD"
if !full_name.ends_with(&BString::from("HEAD")) {
continue;
}
if gix_validate::reference::name_partial(full_name.as_bstr()).is_ok() {
let name = FullName(full_name);
return Some(Ok((full_path, name)));
} else {
continue;
}
}
Err(err) => return Some(Err(err.into_io_error().expect("no symlink related errors"))),
}
}
None
}
}

/// An iterator over all pseudo references in a given git directory
pub struct SortedPseudoRefIterator {
pub(crate) git_dir: PathBuf,
inner: SortedPseudoRefPaths,
buf: Vec<u8>,
}

impl SortedPseudoRefIterator {
/// Returns an iterator over the pseudo ref paths found at the given git_dir
pub fn at(git_dir: &PathBuf, precompose_unicode: bool) -> Self {
SortedPseudoRefIterator {
inner: SortedPseudoRefPaths::at(git_dir.to_owned(), precompose_unicode),
git_dir: git_dir.to_owned(),
buf: vec![],
}
}
}

impl Iterator for SortedPseudoRefIterator {
type Item = Result<Reference, overlay_iter::Error>;

fn next(&mut self) -> Option<Self::Item> {
self.inner
.next()
.map(|r| overlay_iter::convert_loose(&mut self.buf, &self.git_dir, None, None, r))
}
}
Binary file not shown.
41 changes: 41 additions & 0 deletions gix-ref/tests/fixtures/make_pref_repository.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -eu -o pipefail

git init -q

git checkout -q -b main
git commit -q --allow-empty -m c1
git branch dt1
git branch d1
git branch A

mkdir -p .git/refs/remotes/origin
mkdir -p .git/refs/prefix/feature/sub/dir

cp .git/refs/heads/main .git/refs/remotes/origin/
cp .git/refs/heads/main .git/refs/d1
cp .git/refs/heads/main .git/refs/prefix/feature-suffix
cp .git/refs/heads/main .git/refs/prefix/feature/sub/dir/algo

echo "ref: refs/remotes/origin/main" > .git/refs/remotes/origin/HEAD
echo "notahexsha" > .git/refs/broken

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
43f695a9607f1f85f859f2ef944b785b5b6dd238 not-for-merge branch 'fix-823' of https://github.com/Byron/gitoxide
96267708958ead2646aae8766a50fa060739003c not-for-merge branch 'fix-bare-with-index' of https://github.com/Byron/gitoxide
1397e19375bb98522f951b8a452b08c1b35ffbac not-for-merge branch 'gix-archive' of https://github.com/Byron/gitoxide
db71ec8b7c7f2730c47dde3bb662ab56ae89ae7d not-for-merge branch 'index-from-files' of https://github.com/Byron/gitoxide
9f0c71917e57653d2e7121eae65d9385a188a8df not-for-merge branch 'moonwalk' of https://github.com/Byron/gitoxide
44d2b67de5639d4ea3d08ab030ecfe4bdfc8cbfb not-for-merge branch 'release-gix' of https://github.com/Byron/gitoxide
37c3d073b15dafcb52b2040e4b92a413c69a726d not-for-merge branch 'smart-release-without-git2' of https://github.com/Byron/gitoxide
af3608ad397784795c3758a1ac99ec6a367de9be not-for-merge branch 'walk-with-commitgraph' of https://github.com/Byron/gitoxide
EOF

git tag t1
git tag -m "tag object" dt1
1 change: 1 addition & 0 deletions gix-ref/tests/refs/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mod partialname {
}
mod namespace;
mod packed;
mod pseudo_refs;
mod reference;
mod store;
mod transaction;
21 changes: 21 additions & 0 deletions gix-ref/tests/refs/pseudo_refs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use crate::file::store_at;

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

let prefs = store
.iter_pseudo_refs()?
.map(Result::unwrap)
.map(|r: gix_ref::Reference| r.name)
.collect::<Vec<_>>();

let expected_prefs = vec!["FETCH_HEAD", "HEAD", "JIRI_HEAD"];

assert_eq!(
prefs.iter().map(gix_ref::FullName::as_bstr).collect::<Vec<_>>(),
expected_prefs
);

Ok(())
}
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go into a separate commit right after the one changing gix-ref conventionally saying something like adapt to changes in gix-features.

.follow_links(false)
.into_iter()
.filter_map(move |entry| {
Expand Down
32 changes: 31 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_refs(&self) -> Result<PseudoRefIter<'_>, init::Error> {
Ok(PseudoRefIter::new(self.repo, self.platform.psuedo_refs()?))
}

// TODO: tests
/// Return an iterator over all remote branches.
///
Expand Down Expand Up @@ -122,6 +129,29 @@ impl<'r> Iterator for Iter<'r> {
}
}

/// An iterator over pseudo references.
pub struct PseudoRefIter<'r> {
inner: gix_ref::file::iter::SortedPseudoRefIterator,
repo: &'r crate::Repository,
}

impl<'r> PseudoRefIter<'r> {
fn new(repo: &'r crate::Repository, platform: gix_ref::file::iter::SortedPseudoRefIterator) -> Self {
PseudoRefIter { inner: platform, repo }
}
}

impl<'r> Iterator for PseudoRefIter<'r> {
type Item = Result<crate::Reference<'r>, Box<dyn std::error::Error + Send + Sync + 'static>>;

fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|res| {
res.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)
.map(|r| crate::Reference::from_ref(r, self.repo))
})
}
}

///
pub mod init {
/// The error returned by [`Platform::all()`](super::Platform::all()) or [`Platform::prefixed()`](super::Platform::prefixed()).
Expand Down
Loading