Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion gix-negotiate/tests/baseline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ fn run() -> crate::Result {
// }
for tip in lookup_names(&["HEAD"]).into_iter().chain(
refs.iter()?
.prefixed("refs/heads".as_ref())?
.prefixed(b"refs/heads".as_bstr().into())?
.filter_map(Result::ok)
.map(|r| r.target.into_id()),
) {
Expand Down
9 changes: 4 additions & 5 deletions gix-ref/src/namespace.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::path::{Path, PathBuf};
use std::{borrow::Cow, path::Path};

use gix_object::bstr::{BStr, BString, ByteSlice, ByteVec};

Expand All @@ -18,10 +18,9 @@ impl Namespace {
gix_path::from_byte_slice(&self.0)
}
/// Append the given `prefix` to this namespace so it becomes usable for prefixed iteration.
pub fn into_namespaced_prefix(mut self, prefix: &Path) -> PathBuf {
let prefix = gix_path::into_bstr(prefix);
self.0.push_str(prefix.as_ref());
gix_path::to_native_path_on_windows(self.0).into_owned()
pub fn into_namespaced_prefix(mut self, prefix: &BStr) -> Cow<'_, BStr> {
self.0.push_str(prefix);
gix_path::to_unix_separators_on_windows(self.0)
}
pub(crate) fn into_namespaced_name(mut self, name: &FullNameRef) -> FullName {
self.0.push_str(name.as_bstr());
Expand Down
38 changes: 16 additions & 22 deletions gix-ref/src/store/file/loose/iter.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
use std::path::{Path, PathBuf};
use std::{
borrow::Cow,
path::{Path, PathBuf},
};

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

use crate::{file::iter::LooseThenPacked, store_impl::file, BString, FullName};
use crate::{file::iter::LooseThenPacked, store_impl::file, BStr, BString, FullName};

/// An iterator over all valid loose reference paths as seen from a particular base directory.
pub(in crate::store_impl::file) struct SortedLoosePaths {
pub(crate) base: PathBuf,
filename_prefix: Option<BString>,
/// A optional prefix to match against if the prefix is not the same as the `file_walk` path.
prefix: Option<BString>,
file_walk: Option<DirEntryIter>,
}

impl SortedLoosePaths {
pub fn at(path: &Path, base: PathBuf, filename_prefix: Option<BString>, precompose_unicode: bool) -> Self {
pub fn at(path: &Path, base: PathBuf, prefix: Option<BString>, precompose_unicode: bool) -> Self {
SortedLoosePaths {
base,
filename_prefix,
prefix,
file_walk: path.is_dir().then(|| {
// serial iteration as we expect most refs in packed-refs anyway.
gix_features::fs::walkdir_sorted_new(
Expand All @@ -41,31 +45,21 @@ impl Iterator for SortedLoosePaths {
continue;
}
let full_path = entry.path().into_owned();
if let Some((prefix, name)) = self
.filename_prefix
.as_deref()
.and_then(|prefix| full_path.file_name().map(|name| (prefix, name)))
{
match gix_path::os_str_into_bstr(name) {
Ok(name) => {
if !name.starts_with(prefix) {
continue;
}
}
Err(_) => continue, // TODO: silently skipping ill-formed UTF-8 on windows - maybe this can be better?
}
}
let full_name = full_path
.strip_prefix(&self.base)
.expect("prefix-stripping cannot fail as prefix is our root");
.expect("prefix-stripping cannot fail as base is within our root");
let full_name = match gix_path::try_into_bstr(full_name) {
Ok(name) => {
let name = gix_path::to_unix_separators_on_windows(name);
name.into_owned()
}
Err(_) => continue, // TODO: silently skipping ill-formed UTF-8 on windows here, maybe there are better ways?
};

if let Some(prefix) = &self.prefix {
if !full_name.starts_with(prefix) {
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 Expand Up @@ -93,7 +87,7 @@ impl file::Store {
/// Return an iterator over all loose references that start with the given `prefix`.
///
/// Otherwise it's similar to [`loose_iter()`][file::Store::loose_iter()].
pub fn loose_iter_prefixed(&self, prefix: &Path) -> std::io::Result<LooseThenPacked<'_, '_>> {
pub fn loose_iter_prefixed(&self, prefix: Cow<'_, BStr>) -> std::io::Result<LooseThenPacked<'_, '_>> {
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 be &BStr if it was &Path before.
It could also be impl Into<…> if that makes it more convenient to call, even for the test-suite.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I put impl Into<Cow<...>> in most places. Not sure if some should just be &BStr or not.

self.iter_prefixed_packed(prefix, None)
}
}
12 changes: 2 additions & 10 deletions gix-ref/src/store/file/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
use std::path::PathBuf;

use crate::{bstr::BStr, store::WriteReflog, Namespace};
use crate::{store::WriteReflog, Namespace};

/// A store for reference which uses plain files.
///
Expand Down Expand Up @@ -92,11 +89,6 @@ pub struct Transaction<'s, 'p> {
packed_refs: transaction::PackedRefs<'p>,
}

pub(in crate::store_impl::file) fn path_to_name<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, BStr> {
let path = gix_path::into_bstr(path.into());
gix_path::to_unix_separators_on_windows(path)
}

///
pub mod loose;
mod overlay_iter;
Expand Down
76 changes: 29 additions & 47 deletions gix-ref/src/store/file/overlay_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ use std::{
};

use crate::{
file::{loose, loose::iter::SortedLoosePaths, path_to_name},
file::{loose, loose::iter::SortedLoosePaths},
store_impl::{file, packed},
BString, FullName, Namespace, Reference,
BStr, FullName, Namespace, Reference,
};

/// An iterator stepping through sorted input of loose references and packed references, preferring loose refs over otherwise
Expand Down Expand Up @@ -195,10 +195,9 @@ impl Platform<'_> {
self.store.iter_packed(self.packed.as_ref().map(|b| &***b))
}

/// As [`iter(…)`][file::Store::iter()], but filters by `prefix`, i.e. "refs/heads".
///
/// Please note that "refs/heads" or "refs\\heads" is equivalent to "refs/heads/"
pub fn prefixed(&self, prefix: &Path) -> std::io::Result<LooseThenPacked<'_, '_>> {
/// As [`iter(…)`][file::Store::iter()], but filters by `prefix`, i.e. "refs/heads/" or
/// "refs/heads/feature-".
pub fn prefixed(&self, prefix: Cow<'_, BStr>) -> std::io::Result<LooseThenPacked<'_, '_>> {
Copy link
Member

Choose a reason for hiding this comment

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

I think it's fair to have this breaking change, as this prefix is essentially a RepositoryPath, i.e. a slash-separated relative path.
Something I think that should come with it is tests that show that behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The tests are now more explicitly named to test that.

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 be &BStr if i twas &Path before.

self.store
.iter_prefixed_packed(prefix, self.packed.as_ref().map(|b| &***b))
}
Expand Down Expand Up @@ -242,22 +241,19 @@ pub(crate) enum IterInfo<'a> {
/// The top-level directory as boundary of all references, used to create their short-names after iteration
base: &'a Path,
/// The original prefix
prefix: Cow<'a, Path>,
/// The remainder of the prefix that wasn't a valid path
remainder: Option<BString>,
prefix: Cow<'a, BStr>,
/// If `true`, we will convert decomposed into precomposed unicode.
precompose_unicode: bool,
},
}

impl<'a> IterInfo<'a> {
fn prefix(&self) -> Option<&Path> {
fn prefix(&self) -> Option<Cow<'_, BStr>> {
match self {
IterInfo::Base { .. } => None,
IterInfo::PrefixAndBase { prefix, .. } => Some(*prefix),
IterInfo::ComputedIterationRoot { prefix, .. } | IterInfo::BaseAndIterRoot { prefix, .. } => {
prefix.as_ref().into()
}
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()),
}
}

Expand All @@ -281,48 +277,37 @@ impl<'a> IterInfo<'a> {
IterInfo::ComputedIterationRoot {
iter_root,
base,
prefix: _,
remainder,
prefix,
precompose_unicode,
} => SortedLoosePaths::at(&iter_root, base.into(), remainder, precompose_unicode),
} => SortedLoosePaths::at(&iter_root, base.into(), Some(prefix.into_owned()), precompose_unicode),
}
.peekable()
}

fn from_prefix(base: &'a Path, prefix: Cow<'a, Path>, precompose_unicode: bool) -> std::io::Result<Self> {
if prefix.is_absolute() {
fn from_prefix(base: &'a Path, prefix: Cow<'a, BStr>, precompose_unicode: bool) -> std::io::Result<Self> {
let prefix_path = gix_path::from_bstring(prefix.as_ref());
if prefix_path.is_absolute() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"prefix must be a relative path, like 'refs/heads'",
"prefix must be a relative path, like 'refs/heads/'",
));
}
use std::path::Component::*;
if prefix.components().any(|c| matches!(c, CurDir | ParentDir)) {
if prefix_path.components().any(|c| matches!(c, CurDir | ParentDir)) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Refusing to handle prefixes with relative path components",
));
}
let iter_root = base.join(prefix.as_ref());
if iter_root.is_dir() {
let iter_root = base.join(&prefix_path);
if prefix.ends_with(b"/") {
Ok(IterInfo::BaseAndIterRoot {
base,
iter_root,
prefix,
prefix: prefix_path.into(),
precompose_unicode,
})
} else {
let filename_prefix = iter_root
.file_name()
.map(ToOwned::to_owned)
.map(|p| {
gix_path::try_into_bstr(PathBuf::from(p))
.map(std::borrow::Cow::into_owned)
.map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "prefix contains ill-formed UTF-8")
})
})
.transpose()?;
let iter_root = iter_root
.parent()
.expect("a parent is always there unless empty")
Expand All @@ -331,7 +316,6 @@ impl<'a> IterInfo<'a> {
base,
prefix,
iter_root,
remainder: filename_prefix,
precompose_unicode,
})
}
Expand Down Expand Up @@ -374,30 +358,28 @@ impl file::Store {
}
}

/// As [`iter(…)`][file::Store::iter()], but filters by `prefix`, i.e. "refs/heads".
///
/// Please note that "refs/heads" or "refs\\heads" is equivalent to "refs/heads/"
/// As [`iter(…)`][file::Store::iter()], but filters by `prefix`, i.e. "refs/heads/" or
/// "refs/heads/feature-".
pub fn iter_prefixed_packed<'s, 'p>(
&'s self,
prefix: &Path,
prefix: Cow<'_, BStr>,
packed: Option<&'p packed::Buffer>,
) -> std::io::Result<LooseThenPacked<'p, 's>> {
match self.namespace.as_ref() {
None => {
let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix.into(), self.precompose_unicode)?;
let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix.clone(), self.precompose_unicode)?;
let common_dir_info = self
.common_dir()
.map(|base| IterInfo::from_prefix(base, prefix.into(), self.precompose_unicode))
.map(|base| IterInfo::from_prefix(base, prefix, self.precompose_unicode))
.transpose()?;
self.iter_from_info(git_dir_info, common_dir_info, packed)
}
Some(namespace) => {
let prefix = namespace.to_owned().into_namespaced_prefix(prefix);
let git_dir_info =
IterInfo::from_prefix(self.git_dir(), prefix.clone().into(), self.precompose_unicode)?;
let prefix = namespace.to_owned().into_namespaced_prefix(prefix.as_ref());
let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix.clone(), self.precompose_unicode)?;
let common_dir_info = self
.common_dir()
.map(|base| IterInfo::from_prefix(base, prefix.into(), self.precompose_unicode))
.map(|base| IterInfo::from_prefix(base, prefix, self.precompose_unicode))
.transpose()?;
self.iter_from_info(git_dir_info, common_dir_info, packed)
}
Expand All @@ -416,7 +398,7 @@ impl file::Store {
iter_packed: match packed {
Some(packed) => Some(
match git_dir_info.prefix() {
Some(prefix) => packed.iter_prefixed(path_to_name(prefix).into_owned()),
Some(prefix) => packed.iter_prefixed(prefix.into_owned()),
None => packed.iter(),
}
.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?
Expand Down
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ git branch A
git tag -m "tag object" tag-object

mkdir -p .git/refs/remotes/origin
mkdir -p .git/refs/heads/sub/dir

cp .git/refs/heads/main .git/refs/remotes/origin/

echo "ref: refs/remotes/origin/main" > .git/refs/remotes/origin/HEAD
echo "ref: refs/heads/main" > .git/refs/heads-packed
echo "ref: refs/heads/main" > .git/refs/heads/sub/dir/packed
Copy link
Member

Choose a reason for hiding this comment

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

I think these names should be changed so packed isn't included. This is because symbolic refs are never packed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just wanted to make sure that both the loose and packed refs are handled correctly and not accidentally testing the wrong thing. Changed the names to refs/feature-suffix and refs/feature/sub/dir/algo to make the test more explicit.


git pack-refs --all --prune

Expand Down
5 changes: 5 additions & 0 deletions gix-ref/tests/fixtures/make_ref_repository.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ echo "ref: refs/tags/multi-link-target2" > .git/refs/heads/multi-link-target1
echo "ref: refs/remotes/origin/multi-link-target3" > .git/refs/tags/multi-link-target2
git rev-parse HEAD > .git/refs/remotes/origin/multi-link-target3

# Regression test for issue #1934 where prefix refs/m matched refs/heads/main.
mkdir -p .git/refs/heads/sub/dir
echo "ref: refs/remotes/origin/main" > .git/refs/heads-loose
echo "ref: refs/remotes/origin/main" > .git/refs/heads/sub/dir/loose
echo "ref: refs/remotes/origin/main" > .git/refs/remotes/origin/heads

echo "ref: refs/loop-b" > .git/refs/loop-a
echo "ref: refs/loop-a" > .git/refs/loop-b
Expand Down
Loading
Loading