Skip to content

Commit 9cd6d64

Browse files
committed
feat: Add Search::matches_prefix() to see if the prefix of a pathspec matches.
That way it's possible to see if some paths can never match.
1 parent 4094ffb commit 9cd6d64

File tree

2 files changed

+90
-2
lines changed

2 files changed

+90
-2
lines changed

gix-pathspec/src/search/matching.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,11 @@ impl Search {
149149
let max_usable_pattern_len = mapping.pattern.first_wildcard_pos.unwrap_or_else(|| pattern.path.len());
150150
let common_len = max_usable_pattern_len.min(relative_path.len());
151151

152-
let pattern_path = pattern.path[..common_len].as_bstr();
153-
let longest_possible_relative_path = &relative_path[..common_len];
154152
let ignore_case = pattern.signature.contains(MagicSignature::ICASE);
155153
let mut is_match = pattern.always_matches();
156154
if !is_match && common_len != 0 {
155+
let pattern_path = pattern.path[..common_len].as_bstr();
156+
let longest_possible_relative_path = &relative_path[..common_len];
157157
is_match = if ignore_case {
158158
pattern_path.eq_ignore_ascii_case(longest_possible_relative_path)
159159
} else {
@@ -184,6 +184,48 @@ impl Search {
184184

185185
self.all_patterns_are_excluded
186186
}
187+
188+
/// Returns `true` if `relative_path` matches the prefix of this pathspec.
189+
///
190+
/// For example, the relative path `d` matches `d/`, `d*/`, `d/` and `d/*`, but not `d/d/*` or `dir`.
191+
pub fn directory_matches_prefix(&self, relative_path: &BStr) -> bool {
192+
if self.patterns.is_empty() {
193+
return true;
194+
}
195+
let common_prefix_len = self.common_prefix_len.min(relative_path.len());
196+
if relative_path.get(..common_prefix_len).map_or(true, |rela_path_prefix| {
197+
rela_path_prefix != self.common_prefix()[..common_prefix_len]
198+
}) {
199+
return false;
200+
}
201+
for mapping in &self.patterns {
202+
let pattern = &mapping.value.pattern;
203+
if mapping.pattern.first_wildcard_pos == Some(0) && !pattern.is_excluded() {
204+
return true;
205+
}
206+
let rightmost_idx = mapping.pattern.first_wildcard_pos.map_or_else(
207+
|| pattern.path.len(),
208+
|idx| pattern.path[..idx].rfind_byte(b'/').unwrap_or(idx),
209+
);
210+
let ignore_case = pattern.signature.contains(MagicSignature::ICASE);
211+
let mut is_match = pattern.always_matches();
212+
if !is_match {
213+
if let Some(relative_path) = relative_path.get(..rightmost_idx) {
214+
let pattern_path = pattern.path[..rightmost_idx].as_bstr();
215+
is_match = if ignore_case {
216+
pattern_path.eq_ignore_ascii_case(relative_path)
217+
} else {
218+
pattern_path == relative_path
219+
};
220+
}
221+
}
222+
if is_match {
223+
return !pattern.is_excluded();
224+
}
225+
}
226+
227+
self.all_patterns_are_excluded
228+
}
187229
}
188230

189231
fn match_verbatim(

gix-pathspec/tests/search/mod.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,49 @@ fn directories() -> crate::Result {
77
baseline::run("directory", true, baseline::directories)
88
}
99

10+
#[test]
11+
fn directory_matches_prefix() -> crate::Result {
12+
for spec in ["dir", "dir/", "di*", "dir/*", "dir/*.o"] {
13+
for specs in [&[spec] as &[_], &[spec, "other"]] {
14+
let search = gix_pathspec::Search::from_specs(pathspecs(specs), None, Path::new(""))?;
15+
assert!(search.directory_matches_prefix("dir".into()), "{spec}: must match");
16+
assert!(!search.directory_matches_prefix("d".into()), "{spec}: must not match");
17+
}
18+
}
19+
20+
for spec in ["dir/d", "dir/d/", "dir/*/*", "dir/d/*.o"] {
21+
for specs in [&[spec] as &[_], &[spec, "other"]] {
22+
let search = gix_pathspec::Search::from_specs(pathspecs(specs), None, Path::new(""))?;
23+
assert!(search.directory_matches_prefix("dir/d".into()), "{spec}: must match");
24+
assert!(!search.directory_matches_prefix("d".into()), "{spec}: must not match");
25+
assert!(!search.directory_matches_prefix("di".into()), "{spec}: must not match");
26+
}
27+
}
28+
Ok(())
29+
}
30+
31+
#[test]
32+
fn directory_matches_prefix_starting_wildcards_always_match() -> crate::Result {
33+
let search = gix_pathspec::Search::from_specs(pathspecs(&["*ir"]), None, Path::new(""))?;
34+
assert!(search.directory_matches_prefix("dir".into()));
35+
assert!(search.directory_matches_prefix("d".into()));
36+
Ok(())
37+
}
38+
39+
#[test]
40+
fn directory_matches_prefix_all_excluded() -> crate::Result {
41+
for spec in ["!dir", "!dir/", "!d*", "!di*", "!dir/*", "!dir/*.o", "!*ir"] {
42+
for specs in [&[spec] as &[_], &[spec, "other"]] {
43+
let search = gix_pathspec::Search::from_specs(pathspecs(specs), None, Path::new(""))?;
44+
assert!(
45+
!search.directory_matches_prefix("dir".into()),
46+
"{spec}: must not match, it's excluded"
47+
);
48+
}
49+
}
50+
Ok(())
51+
}
52+
1053
#[test]
1154
fn no_pathspecs_match_everything() -> crate::Result {
1255
let mut search = gix_pathspec::Search::from_specs([], None, Path::new(""))?;
@@ -21,6 +64,7 @@ fn no_pathspecs_match_everything() -> crate::Result {
2164
"this is actually a fake pattern, as we have to match even though there isn't anything"
2265
);
2366
assert!(search.can_match_relative_path("anything".into(), None));
67+
assert!(search.directory_matches_prefix("anything".into()));
2468
Ok(())
2569
}
2670

@@ -51,6 +95,8 @@ fn starts_with() -> crate::Result {
5195
search.can_match_relative_path("a".into(), None),
5296
"if unspecified, we match for good measure"
5397
);
98+
assert!(search.directory_matches_prefix("a".into()));
99+
assert!(!search.directory_matches_prefix("ab".into()));
54100
assert_eq!(
55101
search
56102
.pattern_matching_relative_path("a/file".into(), None, &mut no_attrs)

0 commit comments

Comments
 (0)