Skip to content

Commit 71275d1

Browse files
authored
Merge pull request #1954 from GitoxideLabs/fix-recursive-list-refs-prefix
Fix recursive list refs prefix
2 parents 3aec7fb + 52142b4 commit 71275d1

File tree

19 files changed

+236
-147
lines changed

19 files changed

+236
-147
lines changed

gix-negotiate/tests/baseline/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ fn run() -> crate::Result {
7171
// }
7272
for tip in lookup_names(&["HEAD"]).into_iter().chain(
7373
refs.iter()?
74-
.prefixed("refs/heads".as_ref())?
74+
.prefixed(b"refs/heads")?
7575
.filter_map(Result::ok)
7676
.map(|r| r.target.into_id()),
7777
) {

gix-ref/src/namespace.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::path::{Path, PathBuf};
1+
use std::path::Path;
22

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

@@ -18,10 +18,12 @@ impl Namespace {
1818
gix_path::from_byte_slice(&self.0)
1919
}
2020
/// Append the given `prefix` to this namespace so it becomes usable for prefixed iteration.
21-
pub fn into_namespaced_prefix(mut self, prefix: &Path) -> PathBuf {
22-
let prefix = gix_path::into_bstr(prefix);
23-
self.0.push_str(prefix.as_ref());
24-
gix_path::to_native_path_on_windows(self.0).into_owned()
21+
///
22+
/// The prefix is a relative path with slash-separated path components.
23+
// TODO: use `RelativePath` type instead (see #1921), or a trait that helps convert into it.
24+
pub fn into_namespaced_prefix<'a>(mut self, prefix: impl Into<&'a BStr>) -> BString {
25+
self.0.push_str(prefix.into());
26+
gix_path::to_unix_separators_on_windows(self.0).into_owned()
2527
}
2628
pub(crate) fn into_namespaced_name(mut self, name: &FullNameRef) -> FullName {
2729
self.0.push_str(name.as_bstr());

gix-ref/src/store/file/loose/iter.rs

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@ use std::path::{Path, PathBuf};
33
use gix_features::fs::walkdir::DirEntryIter;
44
use gix_object::bstr::ByteSlice;
55

6-
use crate::{file::iter::LooseThenPacked, store_impl::file, BString, FullName};
6+
use crate::{file::iter::LooseThenPacked, store_impl::file, BStr, BString, FullName};
77

88
/// An iterator over all valid loose reference paths as seen from a particular base directory.
99
pub(in crate::store_impl::file) struct SortedLoosePaths {
1010
pub(crate) base: PathBuf,
11-
filename_prefix: Option<BString>,
11+
/// An prefix like `refs/heads/foo/` or `refs/heads/prefix` that a returned reference must match against..
12+
prefix: Option<BString>,
1213
file_walk: Option<DirEntryIter>,
1314
}
1415

1516
impl SortedLoosePaths {
16-
pub fn at(path: &Path, base: PathBuf, filename_prefix: Option<BString>, precompose_unicode: bool) -> Self {
17+
pub fn at(path: &Path, base: PathBuf, prefix: Option<BString>, precompose_unicode: bool) -> Self {
1718
SortedLoosePaths {
1819
base,
19-
filename_prefix,
20+
prefix,
2021
file_walk: path.is_dir().then(|| {
2122
// serial iteration as we expect most refs in packed-refs anyway.
2223
gix_features::fs::walkdir_sorted_new(
@@ -41,31 +42,19 @@ impl Iterator for SortedLoosePaths {
4142
continue;
4243
}
4344
let full_path = entry.path().into_owned();
44-
if let Some((prefix, name)) = self
45-
.filename_prefix
46-
.as_deref()
47-
.and_then(|prefix| full_path.file_name().map(|name| (prefix, name)))
48-
{
49-
match gix_path::os_str_into_bstr(name) {
50-
Ok(name) => {
51-
if !name.starts_with(prefix) {
52-
continue;
53-
}
54-
}
55-
Err(_) => continue, // TODO: silently skipping ill-formed UTF-8 on windows - maybe this can be better?
56-
}
57-
}
5845
let full_name = full_path
5946
.strip_prefix(&self.base)
60-
.expect("prefix-stripping cannot fail as prefix is our root");
61-
let full_name = match gix_path::try_into_bstr(full_name) {
62-
Ok(name) => {
63-
let name = gix_path::to_unix_separators_on_windows(name);
64-
name.into_owned()
65-
}
66-
Err(_) => continue, // TODO: silently skipping ill-formed UTF-8 on windows here, maybe there are better ways?
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;
6752
};
68-
53+
if let Some(prefix) = &self.prefix {
54+
if !full_name.starts_with(prefix) {
55+
continue;
56+
}
57+
}
6958
if gix_validate::reference::name_partial(full_name.as_bstr()).is_ok() {
7059
let name = FullName(full_name);
7160
return Some(Ok((full_path, name)));
@@ -92,8 +81,15 @@ impl file::Store {
9281

9382
/// Return an iterator over all loose references that start with the given `prefix`.
9483
///
95-
/// Otherwise it's similar to [`loose_iter()`][file::Store::loose_iter()].
96-
pub fn loose_iter_prefixed(&self, prefix: &Path) -> std::io::Result<LooseThenPacked<'_, '_>> {
97-
self.iter_prefixed_packed(prefix, None)
84+
/// Otherwise, it's similar to [`loose_iter()`](file::Store::loose_iter()).
85+
///
86+
/// Note that if a prefix isn't using a trailing `/`, like in `refs/heads/foo`, it will effectively
87+
/// start the traversal in the parent directory, e.g. `refs/heads/` and list everything inside that
88+
/// starts with `foo`, like `refs/heads/foo` and `refs/heads/foobar`.
89+
///
90+
/// Prefixes are relative paths with slash-separated components.
91+
// TODO: use `RelativePath` type instead (see #1921), or a trait that helps convert into it.
92+
pub fn loose_iter_prefixed<'a>(&self, prefix: impl Into<&'a BStr>) -> std::io::Result<LooseThenPacked<'_, '_>> {
93+
self.iter_prefixed_packed(prefix.into(), None)
9894
}
9995
}

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
use std::{
2-
borrow::Cow,
3-
path::{Path, PathBuf},
4-
};
1+
use std::path::PathBuf;
52

6-
use crate::{bstr::BStr, store::WriteReflog, Namespace};
3+
use crate::{store::WriteReflog, Namespace};
74

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

95-
pub(in crate::store_impl::file) fn path_to_name<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, BStr> {
96-
let path = gix_path::into_bstr(path.into());
97-
gix_path::to_unix_separators_on_windows(path)
98-
}
99-
10092
///
10193
pub mod loose;
10294
mod overlay_iter;

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

Lines changed: 50 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ use std::{
77
};
88

99
use crate::{
10-
file::{loose, loose::iter::SortedLoosePaths, path_to_name},
10+
file::{loose, loose::iter::SortedLoosePaths},
1111
store_impl::{file, packed},
12-
BString, FullName, Namespace, Reference,
12+
BStr, FullName, Namespace, Reference,
1313
};
1414

1515
/// An iterator stepping through sorted input of loose references and packed references, preferring loose refs over otherwise
@@ -195,12 +195,18 @@ impl Platform<'_> {
195195
self.store.iter_packed(self.packed.as_ref().map(|b| &***b))
196196
}
197197

198-
/// As [`iter(…)`][file::Store::iter()], but filters by `prefix`, i.e. "refs/heads".
198+
/// As [`iter(…)`](file::Store::iter()), but filters by `prefix`, i.e. "refs/heads/" or
199+
/// "refs/heads/feature-".
199200
///
200-
/// Please note that "refs/heads" or "refs\\heads" is equivalent to "refs/heads/"
201-
pub fn prefixed(&self, prefix: &Path) -> std::io::Result<LooseThenPacked<'_, '_>> {
201+
/// Note that if a prefix isn't using a trailing `/`, like in `refs/heads/foo`, it will effectively
202+
/// start the traversal in the parent directory, e.g. `refs/heads/` and list everything inside that
203+
/// starts with `foo`, like `refs/heads/foo` and `refs/heads/foobar`.
204+
///
205+
/// Prefixes are relative paths with slash-separated components.
206+
// TODO: use `RelativePath` type instead (see #1921), or a trait that helps convert into it.
207+
pub fn prefixed<'a>(&self, prefix: impl Into<&'a BStr>) -> std::io::Result<LooseThenPacked<'_, '_>> {
202208
self.store
203-
.iter_prefixed_packed(prefix, self.packed.as_ref().map(|b| &***b))
209+
.iter_prefixed_packed(prefix.into(), self.packed.as_ref().map(|b| &***b))
204210
}
205211
}
206212

@@ -228,7 +234,7 @@ pub(crate) enum IterInfo<'a> {
228234
BaseAndIterRoot {
229235
base: &'a Path,
230236
iter_root: PathBuf,
231-
prefix: Cow<'a, Path>,
237+
prefix: PathBuf,
232238
precompose_unicode: bool,
233239
},
234240
PrefixAndBase {
@@ -239,25 +245,22 @@ pub(crate) enum IterInfo<'a> {
239245
ComputedIterationRoot {
240246
/// The root to iterate over
241247
iter_root: PathBuf,
242-
/// The top-level directory as boundary of all references, used to create their short-names after iteration
248+
/// The top-level directory as boundary of all references, used to create their short-names after iteration.
243249
base: &'a Path,
244-
/// The original prefix
245-
prefix: Cow<'a, Path>,
246-
/// The remainder of the prefix that wasn't a valid path
247-
remainder: Option<BString>,
250+
/// The original prefix.
251+
prefix: Cow<'a, BStr>,
248252
/// If `true`, we will convert decomposed into precomposed unicode.
249253
precompose_unicode: bool,
250254
},
251255
}
252256

253257
impl<'a> IterInfo<'a> {
254-
fn prefix(&self) -> Option<&Path> {
258+
fn prefix(&self) -> Option<Cow<'_, BStr>> {
255259
match self {
256260
IterInfo::Base { .. } => None,
257-
IterInfo::PrefixAndBase { prefix, .. } => Some(*prefix),
258-
IterInfo::ComputedIterationRoot { prefix, .. } | IterInfo::BaseAndIterRoot { prefix, .. } => {
259-
prefix.as_ref().into()
260-
}
261+
IterInfo::PrefixAndBase { prefix, .. } => Some(gix_path::into_bstr(*prefix)),
262+
IterInfo::BaseAndIterRoot { prefix, .. } => Some(gix_path::into_bstr(prefix.clone())),
263+
IterInfo::ComputedIterationRoot { prefix, .. } => Some(prefix.clone()),
261264
}
262265
}
263266

@@ -281,48 +284,42 @@ impl<'a> IterInfo<'a> {
281284
IterInfo::ComputedIterationRoot {
282285
iter_root,
283286
base,
284-
prefix: _,
285-
remainder,
287+
prefix,
286288
precompose_unicode,
287-
} => SortedLoosePaths::at(&iter_root, base.into(), remainder, precompose_unicode),
289+
} => SortedLoosePaths::at(&iter_root, base.into(), Some(prefix.into_owned()), precompose_unicode),
288290
}
289291
.peekable()
290292
}
291293

292-
fn from_prefix(base: &'a Path, prefix: Cow<'a, Path>, precompose_unicode: bool) -> std::io::Result<Self> {
293-
if prefix.is_absolute() {
294+
fn from_prefix(
295+
base: &'a Path,
296+
prefix: impl Into<Cow<'a, BStr>>,
297+
precompose_unicode: bool,
298+
) -> std::io::Result<Self> {
299+
let prefix = prefix.into();
300+
let prefix_path = gix_path::from_bstr(prefix.as_ref());
301+
if prefix_path.is_absolute() {
294302
return Err(std::io::Error::new(
295303
std::io::ErrorKind::InvalidInput,
296-
"prefix must be a relative path, like 'refs/heads'",
304+
"prefix must be a relative path, like 'refs/heads/'",
297305
));
298306
}
299307
use std::path::Component::*;
300-
if prefix.components().any(|c| matches!(c, CurDir | ParentDir)) {
308+
if prefix_path.components().any(|c| matches!(c, CurDir | ParentDir)) {
301309
return Err(std::io::Error::new(
302310
std::io::ErrorKind::InvalidInput,
303311
"Refusing to handle prefixes with relative path components",
304312
));
305313
}
306-
let iter_root = base.join(prefix.as_ref());
307-
if iter_root.is_dir() {
314+
let iter_root = base.join(&prefix_path);
315+
if prefix.ends_with(b"/") {
308316
Ok(IterInfo::BaseAndIterRoot {
309317
base,
310318
iter_root,
311-
prefix,
319+
prefix: prefix_path.into_owned(),
312320
precompose_unicode,
313321
})
314322
} else {
315-
let filename_prefix = iter_root
316-
.file_name()
317-
.map(ToOwned::to_owned)
318-
.map(|p| {
319-
gix_path::try_into_bstr(PathBuf::from(p))
320-
.map(std::borrow::Cow::into_owned)
321-
.map_err(|_| {
322-
std::io::Error::new(std::io::ErrorKind::InvalidInput, "prefix contains ill-formed UTF-8")
323-
})
324-
})
325-
.transpose()?;
326323
let iter_root = iter_root
327324
.parent()
328325
.expect("a parent is always there unless empty")
@@ -331,7 +328,6 @@ impl<'a> IterInfo<'a> {
331328
base,
332329
prefix,
333330
iter_root,
334-
remainder: filename_prefix,
335331
precompose_unicode,
336332
})
337333
}
@@ -374,30 +370,35 @@ impl file::Store {
374370
}
375371
}
376372

377-
/// As [`iter(…)`][file::Store::iter()], but filters by `prefix`, i.e. "refs/heads".
373+
/// As [`iter(…)`](file::Store::iter()), but filters by `prefix`, i.e. `refs/heads/` or
374+
/// `refs/heads/feature-`.
375+
/// Note that if a prefix isn't using a trailing `/`, like in `refs/heads/foo`, it will effectively
376+
/// start the traversal in the parent directory, e.g. `refs/heads/` and list everything inside that
377+
/// starts with `foo`, like `refs/heads/foo` and `refs/heads/foobar`.
378378
///
379-
/// Please note that "refs/heads" or "refs\\heads" is equivalent to "refs/heads/"
380-
pub fn iter_prefixed_packed<'s, 'p>(
379+
/// Prefixes are relative paths with slash-separated components.
380+
// TODO: use `RelativePath` type instead (see #1921), or a trait that helps convert into it.
381+
pub fn iter_prefixed_packed<'a, 's, 'p>(
381382
&'s self,
382-
prefix: &Path,
383+
prefix: impl Into<&'a BStr>,
383384
packed: Option<&'p packed::Buffer>,
384385
) -> std::io::Result<LooseThenPacked<'p, 's>> {
386+
let prefix = prefix.into();
385387
match self.namespace.as_ref() {
386388
None => {
387-
let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix.into(), self.precompose_unicode)?;
389+
let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix, self.precompose_unicode)?;
388390
let common_dir_info = self
389391
.common_dir()
390-
.map(|base| IterInfo::from_prefix(base, prefix.into(), self.precompose_unicode))
392+
.map(|base| IterInfo::from_prefix(base, prefix, self.precompose_unicode))
391393
.transpose()?;
392394
self.iter_from_info(git_dir_info, common_dir_info, packed)
393395
}
394396
Some(namespace) => {
395397
let prefix = namespace.to_owned().into_namespaced_prefix(prefix);
396-
let git_dir_info =
397-
IterInfo::from_prefix(self.git_dir(), prefix.clone().into(), self.precompose_unicode)?;
398+
let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix.clone(), self.precompose_unicode)?;
398399
let common_dir_info = self
399400
.common_dir()
400-
.map(|base| IterInfo::from_prefix(base, prefix.into(), self.precompose_unicode))
401+
.map(|base| IterInfo::from_prefix(base, prefix, self.precompose_unicode))
401402
.transpose()?;
402403
self.iter_from_info(git_dir_info, common_dir_info, packed)
403404
}
@@ -416,7 +417,7 @@ impl file::Store {
416417
iter_packed: match packed {
417418
Some(packed) => Some(
418419
match git_dir_info.prefix() {
419-
Some(prefix) => packed.iter_prefixed(path_to_name(prefix).into_owned()),
420+
Some(prefix) => packed.iter_prefixed(prefix.into_owned()),
420421
None => packed.iter(),
421422
}
422423
.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?
Binary file not shown.
Binary file not shown.

gix-ref/tests/fixtures/make_packed_ref_repository.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ git branch d1
1010
git branch A
1111

1212
mkdir -p .git/refs/remotes/origin
13+
mkdir -p .git/refs/prefix/feature/sub/dir
1314

1415
cp .git/refs/heads/main .git/refs/remotes/origin/
1516
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
1619

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

gix-ref/tests/fixtures/make_packed_ref_repository_for_overlay.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ git branch A
1010
git tag -m "tag object" tag-object
1111

1212
mkdir -p .git/refs/remotes/origin
13+
mkdir -p .git/refs/prefix/feature/sub/dir
1314

1415
cp .git/refs/heads/main .git/refs/remotes/origin/
16+
cp .git/refs/heads/main .git/refs/prefix/feature-suffix
17+
cp .git/refs/heads/main .git/refs/prefix/feature/sub/dir/algo
1518

1619
echo "ref: refs/remotes/origin/main" > .git/refs/remotes/origin/HEAD
1720

gix-ref/tests/fixtures/make_ref_repository.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ git branch d1
1010
git branch A
1111

1212
mkdir -p .git/refs/remotes/origin
13+
mkdir -p .git/refs/prefix/feature/sub/dir
1314

1415
cp .git/refs/heads/main .git/refs/remotes/origin/
1516
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
1619

1720
echo "ref: refs/remotes/origin/main" > .git/refs/remotes/origin/HEAD
1821
echo "notahexsha" > .git/refs/broken
@@ -22,7 +25,6 @@ echo "ref: refs/tags/multi-link-target2" > .git/refs/heads/multi-link-target1
2225
echo "ref: refs/remotes/origin/multi-link-target3" > .git/refs/tags/multi-link-target2
2326
git rev-parse HEAD > .git/refs/remotes/origin/multi-link-target3
2427

25-
2628
echo "ref: refs/loop-b" > .git/refs/loop-a
2729
echo "ref: refs/loop-a" > .git/refs/loop-b
2830

0 commit comments

Comments
 (0)