Skip to content

Commit f538559

Browse files
committed
fix!: prefixed ref iteration now properly deals with slashes.
Previously, `refs/heads/foo/bar` would be listed when running `repo.references()?.prefixed("refs/heads/b")`. The code identified that the last component was not a directory and started to match it as a filename prefix for all files in all recursive directories, effectively matching `refs/heads/**/b*`. This commit fixes that bug but also allows to use a trailing `/` in the prefix, allowing to filter for `refs/heads/foo/` and not get `refs/heads/foo-bar` as a result.
1 parent 57c9014 commit f538559

File tree

6 files changed

+74
-64
lines changed

6 files changed

+74
-64
lines changed

gix-ref/src/namespace.rs

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

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

@@ -18,9 +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: &BStr) -> Cow<'_, BStr> {
22-
self.0.push_str(prefix);
23-
gix_path::to_unix_separators_on_windows(self.0)
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()
2427
}
2528
pub(crate) fn into_namespaced_name(mut self, name: &FullNameRef) -> FullName {
2629
self.0.push_str(name.as_bstr());

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

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

63
use gix_features::fs::walkdir::DirEntryIter;
74
use gix_object::bstr::ByteSlice;
@@ -11,7 +8,7 @@ use crate::{file::iter::LooseThenPacked, store_impl::file, BStr, BString, FullNa
118
/// An iterator over all valid loose reference paths as seen from a particular base directory.
129
pub(in crate::store_impl::file) struct SortedLoosePaths {
1310
pub(crate) base: PathBuf,
14-
/// A optional prefix to match against if the prefix is not the same as the `file_walk` path.
11+
/// An prefix like `refs/heads/foo/` or `refs/heads/prefix` that a returned reference must match against..
1512
prefix: Option<BString>,
1613
file_walk: Option<DirEntryIter>,
1714
}
@@ -48,12 +45,10 @@ impl Iterator for SortedLoosePaths {
4845
let full_name = full_path
4946
.strip_prefix(&self.base)
5047
.expect("prefix-stripping cannot fail as base is within our root");
51-
let full_name = match gix_path::try_into_bstr(full_name) {
52-
Ok(name) => {
53-
let name = gix_path::to_unix_separators_on_windows(name);
54-
name.into_owned()
55-
}
56-
Err(_) => continue, // TODO: silently skipping ill-formed UTF-8 on windows here, maybe there are better ways?
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;
5752
};
5853
if let Some(prefix) = &self.prefix {
5954
if !full_name.starts_with(prefix) {
@@ -86,11 +81,15 @@ impl file::Store {
8681

8782
/// Return an iterator over all loose references that start with the given `prefix`.
8883
///
89-
/// Otherwise it's similar to [`loose_iter()`][file::Store::loose_iter()].
90-
pub fn loose_iter_prefixed<'a>(
91-
&self,
92-
prefix: impl Into<Cow<'a, BStr>>,
93-
) -> std::io::Result<LooseThenPacked<'_, '_>> {
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<'_, '_>> {
9493
self.iter_prefixed_packed(prefix.into(), None)
9594
}
9695
}

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

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,16 @@ 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/" or
198+
/// As [`iter(…)`](file::Store::iter()), but filters by `prefix`, i.e. "refs/heads/" or
199199
/// "refs/heads/feature-".
200-
pub fn prefixed<'a>(&self, prefix: impl Into<Cow<'a, BStr>>) -> std::io::Result<LooseThenPacked<'_, '_>> {
200+
///
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<'_, '_>> {
201208
self.store
202209
.iter_prefixed_packed(prefix.into(), self.packed.as_ref().map(|b| &***b))
203210
}
@@ -227,7 +234,7 @@ pub(crate) enum IterInfo<'a> {
227234
BaseAndIterRoot {
228235
base: &'a Path,
229236
iter_root: PathBuf,
230-
prefix: Cow<'a, Path>,
237+
prefix: PathBuf,
231238
precompose_unicode: bool,
232239
},
233240
PrefixAndBase {
@@ -238,9 +245,9 @@ pub(crate) enum IterInfo<'a> {
238245
ComputedIterationRoot {
239246
/// The root to iterate over
240247
iter_root: PathBuf,
241-
/// 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.
242249
base: &'a Path,
243-
/// The original prefix
250+
/// The original prefix.
244251
prefix: Cow<'a, BStr>,
245252
/// If `true`, we will convert decomposed into precomposed unicode.
246253
precompose_unicode: bool,
@@ -290,7 +297,7 @@ impl<'a> IterInfo<'a> {
290297
precompose_unicode: bool,
291298
) -> std::io::Result<Self> {
292299
let prefix = prefix.into();
293-
let prefix_path = gix_path::from_bstring(prefix.clone().into_owned());
300+
let prefix_path = gix_path::from_bstr(prefix.as_ref());
294301
if prefix_path.is_absolute() {
295302
return Err(std::io::Error::new(
296303
std::io::ErrorKind::InvalidInput,
@@ -309,7 +316,7 @@ impl<'a> IterInfo<'a> {
309316
Ok(IterInfo::BaseAndIterRoot {
310317
base,
311318
iter_root,
312-
prefix: prefix_path.into(),
319+
prefix: prefix_path.into_owned(),
313320
precompose_unicode,
314321
})
315322
} else {
@@ -363,24 +370,31 @@ impl file::Store {
363370
}
364371
}
365372

366-
/// As [`iter(…)`][file::Store::iter()], but filters by `prefix`, i.e. "refs/heads/" or
367-
/// "refs/heads/feature-".
368-
pub fn iter_prefixed_packed<'s, 'p>(
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`.
378+
///
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>(
369382
&'s self,
370-
prefix: Cow<'_, BStr>,
383+
prefix: impl Into<&'a BStr>,
371384
packed: Option<&'p packed::Buffer>,
372385
) -> std::io::Result<LooseThenPacked<'p, 's>> {
386+
let prefix = prefix.into();
373387
match self.namespace.as_ref() {
374388
None => {
375-
let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix.clone(), self.precompose_unicode)?;
389+
let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix, self.precompose_unicode)?;
376390
let common_dir_info = self
377391
.common_dir()
378392
.map(|base| IterInfo::from_prefix(base, prefix, self.precompose_unicode))
379393
.transpose()?;
380394
self.iter_from_info(git_dir_info, common_dir_info, packed)
381395
}
382396
Some(namespace) => {
383-
let prefix = namespace.to_owned().into_namespaced_prefix(&prefix);
397+
let prefix = namespace.to_owned().into_namespaced_prefix(prefix);
384398
let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix.clone(), self.precompose_unicode)?;
385399
let common_dir_info = self
386400
.common_dir()

gix-ref/tests/refs/file/store/iter.rs

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::borrow::Cow;
2-
31
use gix_object::bstr::ByteSlice;
42

53
use crate::{
@@ -326,7 +324,7 @@ fn loose_iter_with_prefix_wont_allow_absolute_paths() -> crate::Result {
326324
#[cfg(windows)]
327325
let abs_path = r"c:\hello";
328326

329-
match store.loose_iter_prefixed(Cow::Borrowed(abs_path.as_ref())) {
327+
match store.loose_iter_prefixed(abs_path) {
330328
Ok(_) => unreachable!("absolute paths aren't allowed"),
331329
Err(err) => assert_eq!(err.to_string(), "prefix must be a relative path, like 'refs/heads/'"),
332330
}
@@ -335,11 +333,9 @@ fn loose_iter_with_prefix_wont_allow_absolute_paths() -> crate::Result {
335333

336334
#[test]
337335
fn loose_iter_with_prefix() -> crate::Result {
338-
// Test 'refs/heads/' with slash.
339-
let store = store()?;
340-
341-
let actual = store
342-
.loose_iter_prefixed(b"refs/heads/".as_bstr())?
336+
let prefix_with_slash = b"refs/heads/";
337+
let actual = store()?
338+
.loose_iter_prefixed(prefix_with_slash)?
343339
.collect::<Result<Vec<_>, _>>()
344340
.expect("no broken ref in this subset")
345341
.into_iter()
@@ -365,11 +361,9 @@ fn loose_iter_with_prefix() -> crate::Result {
365361

366362
#[test]
367363
fn loose_iter_with_partial_prefix_dir() -> crate::Result {
368-
// Test 'refs/heads/' without slash.
369-
let store = store()?;
370-
371-
let actual = store
372-
.loose_iter_prefixed(b"refs/heads".as_bstr())?
364+
let prefix_without_slash = b"refs/heads";
365+
let actual = store()?
366+
.loose_iter_prefixed(prefix_without_slash)?
373367
.collect::<Result<Vec<_>, _>>()
374368
.expect("no broken ref in this subset")
375369
.into_iter()
@@ -395,9 +389,7 @@ fn loose_iter_with_partial_prefix_dir() -> crate::Result {
395389

396390
#[test]
397391
fn loose_iter_with_partial_prefix() -> crate::Result {
398-
let store = store()?;
399-
400-
let actual = store
392+
let actual = store()?
401393
.loose_iter_prefixed(b"refs/heads/d".as_bstr())?
402394
.collect::<Result<Vec<_>, _>>()
403395
.expect("no broken ref in this subset")
@@ -542,7 +534,7 @@ fn overlay_iter_with_prefix_wont_allow_absolute_paths() -> crate::Result {
542534
#[cfg(windows)]
543535
let abs_path = r"c:\hello";
544536

545-
match store.iter()?.prefixed(Cow::Borrowed(abs_path.as_ref())) {
537+
match store.iter()?.prefixed(abs_path) {
546538
Ok(_) => unreachable!("absolute paths aren't allowed"),
547539
Err(err) => assert_eq!(err.to_string(), "prefix must be a relative path, like 'refs/heads/'"),
548540
}
@@ -556,7 +548,7 @@ fn overlay_prefixed_iter() -> crate::Result {
556548
let store = store_at("make_packed_ref_repository_for_overlay.sh")?;
557549
let ref_names = store
558550
.iter()?
559-
.prefixed(b"refs/heads/".as_bstr())?
551+
.prefixed(b"refs/heads/")?
560552
.map(|r| r.map(|r| (r.name.as_bstr().to_owned(), r.target)))
561553
.collect::<Result<Vec<_>, _>>()?;
562554
let c1 = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03");
@@ -579,7 +571,7 @@ fn overlay_partial_prefix_iter() -> crate::Result {
579571
let store = store_at("make_packed_ref_repository_for_overlay.sh")?;
580572
let ref_names = store
581573
.iter()?
582-
.prefixed(b"refs/heads/m".as_bstr())? // 'm' is partial
574+
.prefixed(b"refs/heads/m")? // 'm' is partial
583575
.map(|r| r.map(|r| (r.name.as_bstr().to_owned(), r.target)))
584576
.collect::<Result<Vec<_>, _>>()?;
585577
let c1 = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03");
@@ -597,11 +589,14 @@ fn overlay_partial_prefix_iter_reproduce_1934() -> crate::Result {
597589

598590
let ref_names = store
599591
.iter()?
600-
.prefixed(b"refs/d".as_bstr())?
592+
.prefixed(b"refs/d")?
601593
.map(|r| r.map(|r| (r.name.as_bstr().to_owned(), r.target)))
602594
.collect::<Result<Vec<_>, _>>()?;
603-
// Should not match `refs/heads/d1`.
604-
assert_eq!(ref_names, vec![("refs/d1".into(), Object(c1)),]);
595+
assert_eq!(
596+
ref_names,
597+
vec![("refs/d1".into(), Object(c1))],
598+
"Should not match `refs/heads/d1`"
599+
);
605600
Ok(())
606601
}
607602

@@ -615,7 +610,7 @@ fn overlay_partial_prefix_iter_when_prefix_is_dir() -> crate::Result {
615610

616611
let ref_names = store
617612
.iter()?
618-
.prefixed(b"refs/prefix/feature".as_bstr())?
613+
.prefixed(b"refs/prefix/feature")?
619614
.map(|r| r.map(|r| (r.name.as_bstr().to_owned(), r.target)))
620615
.collect::<Result<Vec<_>, _>>()?;
621616
assert_eq!(
@@ -628,7 +623,7 @@ fn overlay_partial_prefix_iter_when_prefix_is_dir() -> crate::Result {
628623

629624
let ref_names = store
630625
.iter()?
631-
.prefixed(b"refs/prefix/feature/".as_bstr())?
626+
.prefixed(b"refs/prefix/feature/")?
632627
.map(|r| r.map(|r| (r.name.as_bstr().to_owned(), r.target)))
633628
.collect::<Result<Vec<_>, _>>()?;
634629
assert_eq!(

gix-ref/tests/refs/file/worktree.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,6 @@ mod read_only {
191191
mod writable {
192192
use gix_lock::acquire::Fail;
193193
use gix_ref::{
194-
bstr::ByteSlice,
195194
file::{transaction::PackedRefs, Store},
196195
transaction::{Change, LogChange, PreviousValue, RefEdit},
197196
FullName, FullNameRef, Target,
@@ -291,7 +290,7 @@ mod writable {
291290
assert_eq!(
292291
store
293292
.iter()?
294-
.prefixed(b"refs/stacks/".as_bstr())?
293+
.prefixed(b"refs/stacks/")?
295294
.map(Result::unwrap)
296295
.map(|r| (r.name.to_string(), r.target.to_string()))
297296
.collect::<Vec<_>>(),
@@ -572,7 +571,7 @@ mod writable {
572571
assert_eq!(
573572
store
574573
.iter()?
575-
.prefixed(b"refs/stacks/".as_bstr())?
574+
.prefixed(b"refs/stacks/")?
576575
.map(Result::unwrap)
577576
.map(|r| (r.name.to_string(), r.target.to_string()))
578577
.collect::<Vec<_>>(),

gix-ref/tests/refs/namespace.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ fn into_namespaced_prefix() {
33
assert_eq!(
44
gix_ref::namespace::expand("foo")
55
.unwrap()
6-
.into_namespaced_prefix("prefix".as_ref()),
7-
"refs/namespaces/foo/prefix".as_ref(),
6+
.into_namespaced_prefix("prefix"),
7+
"refs/namespaces/foo/prefix",
88
);
99
assert_eq!(
1010
gix_ref::namespace::expand("foo")
1111
.unwrap()
12-
.into_namespaced_prefix("prefix/".as_ref()),
13-
"refs/namespaces/foo/prefix/".as_ref(),
12+
.into_namespaced_prefix("prefix/"),
13+
"refs/namespaces/foo/prefix/",
1414
);
1515
}
1616

0 commit comments

Comments
 (0)