Skip to content

Commit f520a51

Browse files
committed
feat: Add Search::directory_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 021d4c1 commit f520a51

File tree

2 files changed

+168
-2
lines changed

2 files changed

+168
-2
lines changed

gix-pathspec/src/search/matching.rs

Lines changed: 55 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,59 @@ 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+
/// When `leading` is `true`, then `d` matches `d/d` as well. Thus `relative_path` must may be
192+
/// partially included in `pathspec`, otherwise it has to be fully included.
193+
pub fn directory_matches_prefix(&self, relative_path: &BStr, leading: bool) -> bool {
194+
if self.patterns.is_empty() {
195+
return true;
196+
}
197+
let common_prefix_len = self.common_prefix_len.min(relative_path.len());
198+
if relative_path.get(..common_prefix_len).map_or(true, |rela_path_prefix| {
199+
rela_path_prefix != self.common_prefix()[..common_prefix_len]
200+
}) {
201+
return false;
202+
}
203+
for mapping in &self.patterns {
204+
let pattern = &mapping.value.pattern;
205+
if mapping.pattern.first_wildcard_pos.is_some() && pattern.is_excluded() {
206+
return true;
207+
}
208+
let mut rightmost_idx = mapping.pattern.first_wildcard_pos.map_or_else(
209+
|| pattern.path.len(),
210+
|idx| pattern.path[..idx].rfind_byte(b'/').unwrap_or(idx),
211+
);
212+
let ignore_case = pattern.signature.contains(MagicSignature::ICASE);
213+
let mut is_match = pattern.always_matches();
214+
if !is_match {
215+
let plen = relative_path.len();
216+
if leading && rightmost_idx > plen {
217+
if let Some(idx) = pattern.path[..plen]
218+
.rfind_byte(b'/')
219+
.or_else(|| pattern.path[plen..].find_byte(b'/').map(|idx| idx + plen))
220+
{
221+
rightmost_idx = idx;
222+
}
223+
}
224+
if let Some(relative_path) = relative_path.get(..rightmost_idx) {
225+
let pattern_path = pattern.path[..rightmost_idx].as_bstr();
226+
is_match = if ignore_case {
227+
pattern_path.eq_ignore_ascii_case(relative_path)
228+
} else {
229+
pattern_path == relative_path
230+
};
231+
}
232+
}
233+
if is_match {
234+
return !pattern.is_excluded();
235+
}
236+
}
237+
238+
self.all_patterns_are_excluded
239+
}
187240
}
188241

189242
fn match_verbatim(

gix-pathspec/tests/search/mod.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,116 @@ 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!(
16+
search.directory_matches_prefix("dir".into(), false),
17+
"{spec}: must match"
18+
);
19+
assert!(
20+
!search.directory_matches_prefix("d".into(), false),
21+
"{spec}: must not match"
22+
);
23+
}
24+
}
25+
26+
for spec in ["dir/d", "dir/d/", "dir/*/*", "dir/d/*.o"] {
27+
for specs in [&[spec] as &[_], &[spec, "other"]] {
28+
let search = gix_pathspec::Search::from_specs(pathspecs(specs), None, Path::new(""))?;
29+
assert!(
30+
search.directory_matches_prefix("dir/d".into(), false),
31+
"{spec}: must match"
32+
);
33+
assert!(
34+
search.directory_matches_prefix("dir/d".into(), true),
35+
"{spec}: must match"
36+
);
37+
for leading in [false, true] {
38+
assert!(
39+
!search.directory_matches_prefix("d".into(), leading),
40+
"{spec}: must not match"
41+
);
42+
assert!(
43+
!search.directory_matches_prefix("di".into(), leading),
44+
"{spec}: must not match"
45+
);
46+
}
47+
}
48+
}
49+
Ok(())
50+
}
51+
52+
#[test]
53+
fn directory_matches_prefix_starting_wildcards_always_match() -> crate::Result {
54+
let search = gix_pathspec::Search::from_specs(pathspecs(&["*ir"]), None, Path::new(""))?;
55+
assert!(search.directory_matches_prefix("dir".into(), false));
56+
assert!(search.directory_matches_prefix("d".into(), false));
57+
Ok(())
58+
}
59+
60+
#[test]
61+
fn directory_matches_prefix_leading() -> crate::Result {
62+
let search = gix_pathspec::Search::from_specs(pathspecs(&["d/d/generated/b"]), None, Path::new(""))?;
63+
assert!(!search.directory_matches_prefix("di".into(), false));
64+
assert!(!search.directory_matches_prefix("di".into(), true));
65+
assert!(search.directory_matches_prefix("d".into(), true));
66+
assert!(!search.directory_matches_prefix("d".into(), false));
67+
assert!(search.directory_matches_prefix("d/d".into(), true));
68+
assert!(!search.directory_matches_prefix("d/d".into(), false));
69+
assert!(search.directory_matches_prefix("d/d/generated".into(), true));
70+
assert!(!search.directory_matches_prefix("d/d/generated".into(), false));
71+
assert!(!search.directory_matches_prefix("d/d/generatedfoo".into(), false));
72+
assert!(!search.directory_matches_prefix("d/d/generatedfoo".into(), true));
73+
74+
let search = gix_pathspec::Search::from_specs(pathspecs(&[":(icase)d/d/GENERATED/b"]), None, Path::new(""))?;
75+
assert!(
76+
search.directory_matches_prefix("d/d/generated".into(), true),
77+
"icase is respected as well"
78+
);
79+
assert!(!search.directory_matches_prefix("d/d/generated".into(), false));
80+
Ok(())
81+
}
82+
83+
#[test]
84+
fn directory_matches_prefix_negative_wildcard() -> crate::Result {
85+
let search = gix_pathspec::Search::from_specs(pathspecs(&[":!*generated*"]), None, Path::new(""))?;
86+
assert!(
87+
search.directory_matches_prefix("di".into(), false),
88+
"it's always considered matching, we can't really tell anyway"
89+
);
90+
assert!(search.directory_matches_prefix("di".into(), true));
91+
assert!(search.directory_matches_prefix("d".into(), true));
92+
assert!(search.directory_matches_prefix("d".into(), false));
93+
assert!(search.directory_matches_prefix("d/d".into(), true));
94+
assert!(search.directory_matches_prefix("d/d".into(), false));
95+
assert!(search.directory_matches_prefix("d/d/generated".into(), true));
96+
assert!(search.directory_matches_prefix("d/d/generated".into(), false));
97+
assert!(search.directory_matches_prefix("d/d/generatedfoo".into(), false));
98+
assert!(search.directory_matches_prefix("d/d/generatedfoo".into(), true));
99+
100+
let search = gix_pathspec::Search::from_specs(pathspecs(&[":(exclude,icase)*GENERATED*"]), None, Path::new(""))?;
101+
assert!(search.directory_matches_prefix("d/d/generated".into(), true));
102+
assert!(search.directory_matches_prefix("d/d/generated".into(), false));
103+
Ok(())
104+
}
105+
106+
#[test]
107+
fn directory_matches_prefix_all_excluded() -> crate::Result {
108+
for spec in ["!dir", "!dir/", "!d*", "!di*", "!dir/*", "!dir/*.o", "!*ir"] {
109+
for specs in [&[spec] as &[_], &[spec, "other"]] {
110+
let search = gix_pathspec::Search::from_specs(pathspecs(specs), None, Path::new(""))?;
111+
assert!(
112+
!search.directory_matches_prefix("dir".into(), false),
113+
"{spec}: must not match, it's excluded"
114+
);
115+
}
116+
}
117+
Ok(())
118+
}
119+
10120
#[test]
11121
fn no_pathspecs_match_everything() -> crate::Result {
12122
let mut search = gix_pathspec::Search::from_specs([], None, Path::new(""))?;
@@ -21,6 +131,7 @@ fn no_pathspecs_match_everything() -> crate::Result {
21131
"this is actually a fake pattern, as we have to match even though there isn't anything"
22132
);
23133
assert!(search.can_match_relative_path("anything".into(), None));
134+
assert!(search.directory_matches_prefix("anything".into(), false));
24135
Ok(())
25136
}
26137

@@ -51,6 +162,8 @@ fn starts_with() -> crate::Result {
51162
search.can_match_relative_path("a".into(), None),
52163
"if unspecified, we match for good measure"
53164
);
165+
assert!(search.directory_matches_prefix("a".into(), false));
166+
assert!(!search.directory_matches_prefix("ab".into(), false));
54167
assert_eq!(
55168
search
56169
.pattern_matching_relative_path("a/file".into(), None, &mut no_attrs)

0 commit comments

Comments
 (0)