Skip to content

Commit 23cdf31

Browse files
committed
Handle trailing slash in ref list prefix filtering
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. Fixes #1934.
1 parent 705b86d commit 23cdf31

File tree

12 files changed

+156
-103
lines changed

12 files changed

+156
-103
lines changed

Diff for: gix-ref/src/namespace.rs

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::path::{Path, PathBuf};
1+
use std::{borrow::Cow, path::Path};
22

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

@@ -18,10 +18,9 @@ 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+
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)
2524
}
2625
pub(crate) fn into_namespaced_name(mut self, name: &FullNameRef) -> FullName {
2726
self.0.push_str(name.as_bstr());

Diff for: gix-ref/src/store/file/loose/iter.rs

+16-22
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
1-
use std::path::{Path, PathBuf};
1+
use std::{
2+
borrow::Cow,
3+
path::{Path, PathBuf},
4+
};
25

36
use gix_features::fs::walkdir::DirEntryIter;
47
use gix_object::bstr::ByteSlice;
58

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

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

1519
impl SortedLoosePaths {
16-
pub fn at(path: &Path, base: PathBuf, filename_prefix: Option<BString>, precompose_unicode: bool) -> Self {
20+
pub fn at(path: &Path, base: PathBuf, prefix: Option<BString>, precompose_unicode: bool) -> Self {
1721
SortedLoosePaths {
1822
base,
19-
filename_prefix,
23+
prefix,
2024
file_walk: path.is_dir().then(|| {
2125
// serial iteration as we expect most refs in packed-refs anyway.
2226
gix_features::fs::walkdir_sorted_new(
@@ -41,31 +45,21 @@ impl Iterator for SortedLoosePaths {
4145
continue;
4246
}
4347
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-
}
5848
let full_name = full_path
5949
.strip_prefix(&self.base)
60-
.expect("prefix-stripping cannot fail as prefix is our root");
50+
.expect("prefix-stripping cannot fail as base is within our root");
6151
let full_name = match gix_path::try_into_bstr(full_name) {
6252
Ok(name) => {
6353
let name = gix_path::to_unix_separators_on_windows(name);
6454
name.into_owned()
6555
}
6656
Err(_) => continue, // TODO: silently skipping ill-formed UTF-8 on windows here, maybe there are better ways?
6757
};
68-
58+
if let Some(prefix) = &self.prefix {
59+
if !full_name.starts_with(prefix) {
60+
continue;
61+
}
62+
}
6963
if gix_validate::reference::name_partial(full_name.as_bstr()).is_ok() {
7064
let name = FullName(full_name);
7165
return Some(Ok((full_path, name)));
@@ -93,7 +87,7 @@ impl file::Store {
9387
/// Return an iterator over all loose references that start with the given `prefix`.
9488
///
9589
/// Otherwise it's similar to [`loose_iter()`][file::Store::loose_iter()].
96-
pub fn loose_iter_prefixed(&self, prefix: &Path) -> std::io::Result<LooseThenPacked<'_, '_>> {
90+
pub fn loose_iter_prefixed(&self, prefix: Cow<'_, BStr>) -> std::io::Result<LooseThenPacked<'_, '_>> {
9791
self.iter_prefixed_packed(prefix, None)
9892
}
9993
}

Diff for: gix-ref/src/store/file/mod.rs

+2-10
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;

Diff for: gix-ref/src/store/file/overlay_iter.rs

+27-44
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,10 +195,9 @@ 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".
199-
///
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<'_, '_>> {
198+
/// As [`iter(…)`][file::Store::iter()], but filters by `prefix`, i.e. "refs/heads/" or
199+
/// "refs/heads/feature-".
200+
pub fn prefixed(&self, prefix: Cow<'_, BStr>) -> std::io::Result<LooseThenPacked<'_, '_>> {
202201
self.store
203202
.iter_prefixed_packed(prefix, self.packed.as_ref().map(|b| &***b))
204203
}
@@ -242,22 +241,19 @@ pub(crate) enum IterInfo<'a> {
242241
/// The top-level directory as boundary of all references, used to create their short-names after iteration
243242
base: &'a Path,
244243
/// The original prefix
245-
prefix: Cow<'a, Path>,
246-
/// The remainder of the prefix that wasn't a valid path
247-
remainder: Option<BString>,
244+
prefix: Cow<'a, BStr>,
248245
/// If `true`, we will convert decomposed into precomposed unicode.
249246
precompose_unicode: bool,
250247
},
251248
}
252249

253250
impl<'a> IterInfo<'a> {
254-
fn prefix(&self) -> Option<&Path> {
251+
fn prefix(&self) -> Option<Cow<'_, BStr>> {
255252
match self {
256253
IterInfo::Base { .. } => None,
257-
IterInfo::PrefixAndBase { prefix, .. } => Some(*prefix),
258-
IterInfo::ComputedIterationRoot { prefix, .. } | IterInfo::BaseAndIterRoot { prefix, .. } => {
259-
prefix.as_ref().into()
260-
}
254+
IterInfo::PrefixAndBase { prefix, .. } => Some(gix_path::into_bstr(*prefix)),
255+
IterInfo::BaseAndIterRoot { prefix, .. } => Some(gix_path::into_bstr(prefix.clone())),
256+
IterInfo::ComputedIterationRoot { prefix, .. } => Some(prefix.clone()),
261257
}
262258
}
263259

@@ -281,48 +277,37 @@ impl<'a> IterInfo<'a> {
281277
IterInfo::ComputedIterationRoot {
282278
iter_root,
283279
base,
284-
prefix: _,
285-
remainder,
280+
prefix,
286281
precompose_unicode,
287-
} => SortedLoosePaths::at(&iter_root, base.into(), remainder, precompose_unicode),
282+
} => SortedLoosePaths::at(&iter_root, base.into(), Some(prefix.into_owned()), precompose_unicode),
288283
}
289284
.peekable()
290285
}
291286

292-
fn from_prefix(base: &'a Path, prefix: Cow<'a, Path>, precompose_unicode: bool) -> std::io::Result<Self> {
293-
if prefix.is_absolute() {
287+
fn from_prefix(base: &'a Path, prefix: Cow<'a, BStr>, precompose_unicode: bool) -> std::io::Result<Self> {
288+
let prefix_path = gix_path::from_bstring(prefix.as_ref());
289+
if prefix_path.is_absolute() {
294290
return Err(std::io::Error::new(
295291
std::io::ErrorKind::InvalidInput,
296-
"prefix must be a relative path, like 'refs/heads'",
292+
"prefix must be a relative path, like 'refs/heads/'",
297293
));
298294
}
299295
use std::path::Component::*;
300-
if prefix.components().any(|c| matches!(c, CurDir | ParentDir)) {
296+
if prefix_path.components().any(|c| matches!(c, CurDir | ParentDir)) {
301297
return Err(std::io::Error::new(
302298
std::io::ErrorKind::InvalidInput,
303299
"Refusing to handle prefixes with relative path components",
304300
));
305301
}
306-
let iter_root = base.join(prefix.as_ref());
307-
if iter_root.is_dir() {
302+
let iter_root = base.join(&prefix_path);
303+
if prefix.ends_with(b"/") {
308304
Ok(IterInfo::BaseAndIterRoot {
309305
base,
310306
iter_root,
311-
prefix,
307+
prefix: prefix_path.into(),
312308
precompose_unicode,
313309
})
314310
} 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()?;
326311
let iter_root = iter_root
327312
.parent()
328313
.expect("a parent is always there unless empty")
@@ -331,7 +316,6 @@ impl<'a> IterInfo<'a> {
331316
base,
332317
prefix,
333318
iter_root,
334-
remainder: filename_prefix,
335319
precompose_unicode,
336320
})
337321
}
@@ -374,25 +358,24 @@ impl file::Store {
374358
}
375359
}
376360

377-
/// As [`iter(…)`][file::Store::iter()], but filters by `prefix`, i.e. "refs/heads".
378-
///
379-
/// Please note that "refs/heads" or "refs\\heads" is equivalent to "refs/heads/"
361+
/// As [`iter(…)`][file::Store::iter()], but filters by `prefix`, i.e. "refs/heads/" or
362+
/// "refs/heads/feature-".
380363
pub fn iter_prefixed_packed<'s, 'p>(
381364
&'s self,
382-
prefix: &Path,
365+
prefix: Cow<'_, BStr>,
383366
packed: Option<&'p packed::Buffer>,
384367
) -> std::io::Result<LooseThenPacked<'p, 's>> {
385368
match self.namespace.as_ref() {
386369
None => {
387-
let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix.into(), self.precompose_unicode)?;
370+
let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix.clone(), self.precompose_unicode)?;
388371
let common_dir_info = self
389372
.common_dir()
390-
.map(|base| IterInfo::from_prefix(base, prefix.into(), self.precompose_unicode))
373+
.map(|base| IterInfo::from_prefix(base, prefix, self.precompose_unicode))
391374
.transpose()?;
392375
self.iter_from_info(git_dir_info, common_dir_info, packed)
393376
}
394377
Some(namespace) => {
395-
let prefix = namespace.to_owned().into_namespaced_prefix(prefix);
378+
let prefix = namespace.to_owned().into_namespaced_prefix(prefix.as_ref());
396379
let git_dir_info =
397380
IterInfo::from_prefix(self.git_dir(), prefix.clone().into(), self.precompose_unicode)?;
398381
let common_dir_info = self
@@ -416,7 +399,7 @@ impl file::Store {
416399
iter_packed: match packed {
417400
Some(packed) => Some(
418401
match git_dir_info.prefix() {
419-
Some(prefix) => packed.iter_prefixed(path_to_name(prefix).into_owned()),
402+
Some(prefix) => packed.iter_prefixed(prefix.into_owned()),
420403
None => packed.iter(),
421404
}
422405
.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?
Binary file not shown.
Binary file not shown.

Diff for: gix-ref/tests/fixtures/make_packed_ref_repository_for_overlay.sh

+3
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ git branch A
1010
git tag -m "tag object" tag-object
1111

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

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

1617
echo "ref: refs/remotes/origin/main" > .git/refs/remotes/origin/HEAD
18+
echo "ref: refs/heads/main" > .git/refs/heads-packed
19+
echo "ref: refs/heads/main" > .git/refs/heads/sub/dir/packed
1720

1821
git pack-refs --all --prune
1922

Diff for: gix-ref/tests/fixtures/make_ref_repository.sh

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ echo "ref: refs/tags/multi-link-target2" > .git/refs/heads/multi-link-target1
2222
echo "ref: refs/remotes/origin/multi-link-target3" > .git/refs/tags/multi-link-target2
2323
git rev-parse HEAD > .git/refs/remotes/origin/multi-link-target3
2424

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

2631
echo "ref: refs/loop-b" > .git/refs/loop-a
2732
echo "ref: refs/loop-a" > .git/refs/loop-b

0 commit comments

Comments
 (0)