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 4 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
19 changes: 18 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,31 @@ 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>,
root_only: bool,
precompose_unicode: bool,
) -> Self {
let depth = if root_only { 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 +68,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
111 changes: 79 additions & 32 deletions gix-ref/src/store/file/overlay_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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,36 +85,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 +222,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<LooseThenPacked<'_, '_>> {
self.store.iter_pseudo_refs()
}
}

impl file::Store {
Expand Down Expand Up @@ -254,6 +271,10 @@ pub(crate) enum IterInfo<'a> {
/// If `true`, we will convert decomposed into precomposed unicode.
precompose_unicode: bool,
},
PseudoRefs {
base: &'a Path,
precompose_unicode: bool,
},
}

impl<'a> IterInfo<'a> {
Expand All @@ -263,6 +284,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::PseudoRefs { .. } => None,
}
}

Expand All @@ -271,24 +293,35 @@ 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, false, 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, false, 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, false, 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,
false,
precompose_unicode,
),
IterInfo::PseudoRefs {
base,
precompose_unicode,
} => SortedLoosePaths::at(base, base.into(), None, Some("HEAD".into()), true, precompose_unicode),
}
.peekable()
}
Expand Down Expand Up @@ -354,6 +387,20 @@ impl file::Store {
}
}

/// Return an iterator over all pseudo 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.
pub fn iter_pseudo_refs<'p>(&'_ self) -> std::io::Result<LooseThenPacked<'p, '_>> {
self.iter_from_info(
IterInfo::PseudoRefs {
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.
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)
.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_refs(&self) -> Result<Iter<'_>, init::Error> {
Ok(Iter::new(self.repo, self.platform.psuedo_refs()?))
}

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