Skip to content

Commit 021d4c1

Browse files
committed
feat!: add search::MatchKind, which is available for any search::Match.
With it the caller can learn how or why the pathspec matched, which allows to make decisions based on it that are relevant to the user interface.
1 parent e409e8d commit 021d4c1

File tree

3 files changed

+88
-21
lines changed

3 files changed

+88
-21
lines changed

gix-pathspec/src/search/matching.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use bstr::{BStr, BString, ByteSlice};
22
use gix_glob::pattern::Case;
33

4+
use crate::search::MatchKind;
5+
use crate::search::MatchKind::*;
46
use crate::{
57
search::{Match, Spec},
68
MagicSignature, Pattern, Search, SearchMode,
@@ -53,9 +55,10 @@ impl Search {
5355

5456
let case = if ignore_case { Case::Fold } else { Case::Sensitive };
5557
let mut is_match = mapping.value.pattern.always_matches();
58+
let mut how = Always;
5659
if !is_match {
5760
is_match = if mapping.pattern.first_wildcard_pos.is_none() {
58-
match_verbatim(mapping, relative_path, is_dir, case)
61+
match_verbatim(mapping, relative_path, is_dir, case, &mut how)
5962
} else {
6063
let wildmatch_mode = match mapping.value.pattern.search_mode {
6164
SearchMode::ShellGlob => Some(gix_glob::wildmatch::Mode::empty()),
@@ -72,12 +75,13 @@ impl Search {
7275
wildmatch_mode,
7376
);
7477
if !is_match {
75-
match_verbatim(mapping, relative_path, is_dir, case)
78+
match_verbatim(mapping, relative_path, is_dir, case, &mut how)
7679
} else {
80+
how = mapping.pattern.first_wildcard_pos.map_or(Verbatim, |_| WildcardMatch);
7781
true
7882
}
7983
}
80-
None => match_verbatim(mapping, relative_path, is_dir, case),
84+
None => match_verbatim(mapping, relative_path, is_dir, case, &mut how),
8185
}
8286
}
8387
}
@@ -97,6 +101,7 @@ impl Search {
97101
is_match.then_some(Match {
98102
pattern: &mapping.value.pattern,
99103
sequence_number: mapping.sequence_number,
104+
kind: how,
100105
})
101106
});
102107

@@ -112,6 +117,7 @@ impl Search {
112117
Some(Match {
113118
pattern: &MATCH_ALL_STAND_IN,
114119
sequence_number: patterns_len,
120+
kind: Always,
115121
})
116122
} else {
117123
res
@@ -185,16 +191,18 @@ fn match_verbatim(
185191
relative_path: &BStr,
186192
is_dir: bool,
187193
case: Case,
194+
how: &mut MatchKind,
188195
) -> bool {
189196
let pattern_len = mapping.value.pattern.path.len();
190197
let mut relative_path_ends_with_slash_at_pattern_len = false;
191-
let match_is_allowed = relative_path.get(pattern_len).map_or_else(
192-
|| relative_path.len() == pattern_len,
198+
let (match_is_allowed, probably_how) = relative_path.get(pattern_len).map_or_else(
199+
|| (relative_path.len() == pattern_len, Verbatim),
193200
|b| {
194201
relative_path_ends_with_slash_at_pattern_len = *b == b'/';
195-
relative_path_ends_with_slash_at_pattern_len
202+
(relative_path_ends_with_slash_at_pattern_len, Prefix)
196203
},
197204
);
205+
*how = probably_how;
198206
let pattern_requirement_is_met = !mapping.pattern.mode.contains(gix_glob::pattern::Mode::MUST_BE_DIR)
199207
|| (relative_path_ends_with_slash_at_pattern_len || is_dir);
200208

gix-pathspec/src/search/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ pub struct Match<'a> {
99
pub pattern: &'a Pattern,
1010
/// The number of the sequence the matching pathspec was in, or the line of pathspec file it was read from if [Search::source] is not `None`.
1111
pub sequence_number: usize,
12+
/// How the pattern matched.
13+
pub kind: MatchKind,
14+
}
15+
16+
/// Describe how a pathspec pattern matched.
17+
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
18+
pub enum MatchKind {
19+
/// The match happened because there wasn't any pattern, which matches all, or because there was a nil pattern or one with an empty path.
20+
/// Thus this is not a match by merit.
21+
Always,
22+
/// The first part of a pathspec matches, like `dir/` that matches `dir/a`.
23+
Prefix,
24+
/// The whole pathspec matched and used a wildcard match, like `a/*` matching `a/file`.
25+
WildcardMatch,
26+
/// The entire pathspec matched, letter by letter, e.g. `a/file` matching `a/file`.
27+
Verbatim,
1228
}
1329

1430
mod init;

gix-pathspec/tests/search/mod.rs

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use bstr::BStr;
2+
use gix_pathspec::search::MatchKind::*;
13
use std::path::Path;
24

35
#[test]
@@ -10,29 +12,67 @@ fn no_pathspecs_match_everything() -> crate::Result {
1012
let mut search = gix_pathspec::Search::from_specs([], None, Path::new(""))?;
1113
assert_eq!(search.patterns().count(), 0, "nothing artificial is added");
1214
let m = search
13-
.pattern_matching_relative_path("hello".into(), None, &mut |_, _, _, _| {
14-
unreachable!("must not be called")
15-
})
15+
.pattern_matching_relative_path("hello".into(), None, &mut no_attrs)
1616
.expect("matches");
1717
assert_eq!(m.pattern.prefix_directory(), "", "there is no prefix as none was given");
18+
assert_eq!(m.kind, Always, "no pathspec always matches");
1819
assert_eq!(
1920
m.sequence_number, 0,
2021
"this is actually a fake pattern, as we have to match even though there isn't anything"
2122
);
22-
2323
assert!(search.can_match_relative_path("anything".into(), None));
24+
Ok(())
25+
}
2426

27+
#[test]
28+
fn starts_with() -> crate::Result {
29+
let mut search = gix_pathspec::Search::from_specs(pathspecs(&["a/*"]), None, Path::new(""))?;
30+
assert!(
31+
search
32+
.pattern_matching_relative_path("a".into(), Some(false), &mut no_attrs)
33+
.is_none(),
34+
"this can only match if it's a directory"
35+
);
36+
assert!(
37+
search
38+
.pattern_matching_relative_path("a".into(), Some(true), &mut no_attrs)
39+
.is_none(),
40+
"can't match as the '*' part is missing in value"
41+
);
42+
assert!(
43+
search.can_match_relative_path("a".into(), Some(true)),
44+
"prefix-matches work though"
45+
);
46+
assert!(
47+
search.can_match_relative_path("a".into(), Some(false)),
48+
"but not if it's a file"
49+
);
50+
assert!(
51+
search.can_match_relative_path("a".into(), None),
52+
"if unspecified, we match for good measure"
53+
);
54+
assert_eq!(
55+
search
56+
.pattern_matching_relative_path("a/file".into(), None, &mut no_attrs)
57+
.expect("matches")
58+
.kind,
59+
WildcardMatch,
60+
"a wildmatch is always performed here, even though it looks like a prefix"
61+
);
2562
Ok(())
2663
}
2764

2865
#[test]
2966
fn simplified_search_respects_must_be_dir() -> crate::Result {
3067
let mut search = gix_pathspec::Search::from_specs(pathspecs(&["a/be/"]), None, Path::new(""))?;
31-
search
32-
.pattern_matching_relative_path("a/be/file".into(), Some(false), &mut |_, _, _, _| {
33-
unreachable!("must not be called")
34-
})
35-
.expect("matches as this is a prefix match");
68+
assert_eq!(
69+
search
70+
.pattern_matching_relative_path("a/be/file".into(), Some(false), &mut no_attrs)
71+
.expect("matches as this is a prefix match")
72+
.kind,
73+
Prefix,
74+
"a verbatim part of the spec matches"
75+
);
3676
assert!(
3777
!search.can_match_relative_path("any".into(), Some(false)),
3878
"not our directory: a, and must be dir"
@@ -79,7 +119,7 @@ fn simplified_search_respects_must_be_dir() -> crate::Result {
79119
);
80120
assert!(
81121
search
82-
.pattern_matching_relative_path("a/b".into(), None, &mut |_, _, _, _| unreachable!("must not be called"))
122+
.pattern_matching_relative_path("a/b".into(), None, &mut no_attrs)
83123
.is_none(),
84124
"no match if it's not the whole pattern that matches"
85125
);
@@ -183,21 +223,20 @@ fn no_pathspecs_respect_prefix() -> crate::Result {
183223
);
184224
assert!(
185225
search
186-
.pattern_matching_relative_path("hello".into(), None, &mut |_, _, _, _| unreachable!(
187-
"must not be called"
188-
))
226+
.pattern_matching_relative_path("hello".into(), None, &mut no_attrs)
189227
.is_none(),
190228
"not the right prefix"
191229
);
192230
assert!(!search.can_match_relative_path("hello".into(), None));
193231
let m = search
194-
.pattern_matching_relative_path("a/b".into(), None, &mut |_, _, _, _| unreachable!("must not be called"))
232+
.pattern_matching_relative_path("a/b".into(), None, &mut no_attrs)
195233
.expect("match");
196234
assert_eq!(
197235
m.pattern.prefix_directory(),
198236
"a",
199237
"the prefix directory matched verbatim"
200238
);
239+
assert_eq!(m.kind, Prefix, "the common path also works like a prefix");
201240
assert!(search.can_match_relative_path("a/".into(), Some(true)));
202241
assert!(search.can_match_relative_path("a".into(), Some(true)));
203242
assert!(!search.can_match_relative_path("a".into(), Some(false)));
@@ -249,7 +288,7 @@ fn prefixes_are_always_case_sensitive() -> crate::Result {
249288
.iter()
250289
.filter(|relative_path| {
251290
search
252-
.pattern_matching_relative_path(relative_path.as_str().into(), Some(false), &mut |_, _, _, _| false)
291+
.pattern_matching_relative_path(relative_path.as_str().into(), Some(false), &mut no_attrs)
253292
.is_some()
254293
})
255294
.collect();
@@ -442,3 +481,7 @@ mod baseline {
442481
Ok((root, items, expected))
443482
}
444483
}
484+
485+
fn no_attrs(_: &BStr, _: gix_glob::pattern::Case, _: bool, _: &mut gix_attributes::search::Outcome) -> bool {
486+
unreachable!("must not be called")
487+
}

0 commit comments

Comments
 (0)