gix_pathspec/search/
matching.rs

1use bstr::{BStr, BString, ByteSlice};
2use gix_glob::pattern::Case;
3
4use crate::search::MatchKind;
5use crate::search::MatchKind::*;
6use crate::{
7    search::{Match, Spec},
8    MagicSignature, Pattern, Search, SearchMode,
9};
10
11impl Search {
12    /// Return the first [`Match`] of `relative_path`, or `None`.
13    /// `is_dir` is `true` if `relative_path` is a directory, or assumed `false` if `None`.
14    /// `attributes` is called as `attributes(relative_path, case, is_dir, outcome) -> has_match` to obtain for attributes for `relative_path`, if
15    /// the underlying pathspec defined an attribute filter, to be stored in `outcome`, returning true if there was a match.
16    /// All attributes of the pathspec have to be present in the defined value for the pathspec to match.
17    ///
18    /// Note that `relative_path` is expected to be starting at the same root as is assumed for this pattern, see [`Pattern::normalize()`].
19    /// Further, empty searches match everything, as if `:` was provided.
20    ///
21    /// ### Deviation
22    ///
23    /// The case-sensitivity of the attribute match is controlled by the sensitivity of the pathspec, instead of being based on the
24    /// case folding settings of the repository. That way we assure that the matching is consistent.
25    /// Higher-level crates should control this default case folding of pathspecs when instantiating them, which is when they can
26    /// set it to match the repository setting for more natural behaviour when, for instance, adding files to a repository:
27    /// as it stands, on a case-insensitive file system, `touch File && git add file` will not add the file, but also not error.
28    pub fn pattern_matching_relative_path(
29        &mut self,
30        relative_path: &BStr,
31        is_dir: Option<bool>,
32        attributes: &mut dyn FnMut(&BStr, Case, bool, &mut gix_attributes::search::Outcome) -> bool,
33    ) -> Option<Match<'_>> {
34        static MATCH_ALL_STAND_IN: Pattern = Pattern {
35            path: BString::new(Vec::new()),
36            signature: MagicSignature::empty(),
37            search_mode: SearchMode::ShellGlob,
38            attributes: Vec::new(),
39            prefix_len: 0,
40            nil: true,
41        };
42        if relative_path.is_empty() {
43            return Some(Match {
44                pattern: &MATCH_ALL_STAND_IN,
45                sequence_number: 0,
46                kind: Always,
47            });
48        }
49        let basename_not_important = None;
50        if relative_path
51            .get(..self.common_prefix_len)
52            .map_or(true, |rela_path_prefix| rela_path_prefix != self.common_prefix())
53        {
54            return None;
55        }
56
57        let is_dir = is_dir.unwrap_or(false);
58        let patterns_len = self.patterns.len();
59        let res = self.patterns.iter_mut().find_map(|mapping| {
60            let ignore_case = mapping.value.pattern.signature.contains(MagicSignature::ICASE);
61            let prefix = mapping.value.pattern.prefix_directory();
62            if ignore_case && !prefix.is_empty() {
63                let pattern_requirement_is_met = relative_path.get(prefix.len()).map_or_else(|| is_dir, |b| *b == b'/');
64                if !pattern_requirement_is_met
65                    || relative_path.get(..prefix.len()).map(ByteSlice::as_bstr) != Some(prefix)
66                {
67                    return None;
68                }
69            }
70
71            let case = if ignore_case { Case::Fold } else { Case::Sensitive };
72            let mut is_match = mapping.value.pattern.always_matches();
73            let mut how = Always;
74            if !is_match {
75                is_match = if mapping.pattern.first_wildcard_pos.is_none() {
76                    match_verbatim(mapping, relative_path, is_dir, case, &mut how)
77                } else {
78                    let wildmatch_mode = match mapping.value.pattern.search_mode {
79                        SearchMode::ShellGlob => Some(gix_glob::wildmatch::Mode::empty()),
80                        SearchMode::Literal => None,
81                        SearchMode::PathAwareGlob => Some(gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL),
82                    };
83                    match wildmatch_mode {
84                        Some(wildmatch_mode) => {
85                            let is_match = mapping.pattern.matches_repo_relative_path(
86                                relative_path,
87                                basename_not_important,
88                                Some(is_dir),
89                                case,
90                                wildmatch_mode,
91                            );
92                            if !is_match {
93                                match_verbatim(mapping, relative_path, is_dir, case, &mut how)
94                            } else {
95                                how = mapping.pattern.first_wildcard_pos.map_or(Verbatim, |_| WildcardMatch);
96                                true
97                            }
98                        }
99                        None => match_verbatim(mapping, relative_path, is_dir, case, &mut how),
100                    }
101                }
102            }
103
104            if let Some(attrs) = mapping.value.attrs_match.as_mut() {
105                if !attributes(relative_path, Case::Sensitive, is_dir, attrs) {
106                    // we have attrs, but it didn't match any
107                    return None;
108                }
109                for (actual, expected) in attrs.iter_selected().zip(mapping.value.pattern.attributes.iter()) {
110                    if actual.assignment != expected.as_ref() {
111                        return None;
112                    }
113                }
114            }
115
116            is_match.then_some(Match {
117                pattern: &mapping.value.pattern,
118                sequence_number: mapping.sequence_number,
119                kind: how,
120            })
121        });
122
123        if res.is_none() && self.all_patterns_are_excluded {
124            Some(Match {
125                pattern: &MATCH_ALL_STAND_IN,
126                sequence_number: patterns_len,
127                kind: Always,
128            })
129        } else {
130            res
131        }
132    }
133
134    /// As opposed to [`Self::pattern_matching_relative_path()`], this method will return `true` for a possibly partial `relative_path`
135    /// if this pathspec *could* match by looking at the shortest shared prefix only.
136    ///
137    /// This is useful if `relative_path` is a directory leading up to the item that is going to be matched in full later.
138    /// Note that it should not end with `/` to indicate it's a directory, rather, use `is_dir` to indicate this.
139    /// `is_dir` is `true` if `relative_path` is a directory. If `None`, the fact that a pathspec might demand a directory match
140    /// is ignored.
141    /// Returns `false` if this pathspec has no chance of ever matching `relative_path`.
142    pub fn can_match_relative_path(&self, relative_path: &BStr, is_dir: Option<bool>) -> bool {
143        if self.patterns.is_empty() || relative_path.is_empty() {
144            return true;
145        }
146        let common_prefix_len = self.common_prefix_len.min(relative_path.len());
147        if relative_path.get(..common_prefix_len).map_or(true, |rela_path_prefix| {
148            rela_path_prefix != self.common_prefix()[..common_prefix_len]
149        }) {
150            return false;
151        }
152        for mapping in &self.patterns {
153            let pattern = &mapping.value.pattern;
154            if mapping.pattern.first_wildcard_pos == Some(0) && !pattern.is_excluded() {
155                return true;
156            }
157            let max_usable_pattern_len = mapping.pattern.first_wildcard_pos.unwrap_or_else(|| pattern.path.len());
158            let common_len = max_usable_pattern_len.min(relative_path.len());
159
160            let ignore_case = pattern.signature.contains(MagicSignature::ICASE);
161            let mut is_match = pattern.always_matches();
162            if !is_match && common_len != 0 {
163                let pattern_path = pattern.path[..common_len].as_bstr();
164                let longest_possible_relative_path = &relative_path[..common_len];
165                is_match = if ignore_case {
166                    pattern_path.eq_ignore_ascii_case(longest_possible_relative_path)
167                } else {
168                    pattern_path == longest_possible_relative_path
169                };
170
171                if is_match {
172                    is_match = if common_len < max_usable_pattern_len {
173                        pattern.path.get(common_len) == Some(&b'/')
174                    } else if relative_path.len() > max_usable_pattern_len
175                        && mapping.pattern.first_wildcard_pos.is_none()
176                    {
177                        relative_path.get(common_len) == Some(&b'/')
178                    } else {
179                        is_match
180                    };
181                    if let Some(is_dir) = is_dir.filter(|_| pattern.signature.contains(MagicSignature::MUST_BE_DIR)) {
182                        is_match = if is_dir {
183                            matches!(pattern.path.get(common_len), None | Some(&b'/'))
184                        } else {
185                            relative_path.get(common_len) == Some(&b'/')
186                        };
187                    }
188                }
189            }
190            if is_match && (!pattern.is_excluded() || pattern.always_matches()) {
191                return !pattern.is_excluded();
192            }
193        }
194
195        self.all_patterns_are_excluded
196    }
197
198    /// Returns `true` if `relative_path` matches the prefix of this pathspec.
199    ///
200    /// For example, the relative path `d` matches `d/`, `d*/`, `d/` and `d/*`, but not `d/d/*` or `dir`.
201    /// When `leading` is `true`, then `d` matches `d/d` as well. Thus, `relative_path` must may be
202    /// partially included in `pathspec`, otherwise it has to be fully included.
203    pub fn directory_matches_prefix(&self, relative_path: &BStr, leading: bool) -> bool {
204        if self.patterns.is_empty() || relative_path.is_empty() {
205            return true;
206        }
207        let common_prefix_len = self.common_prefix_len.min(relative_path.len());
208        if relative_path.get(..common_prefix_len).map_or(true, |rela_path_prefix| {
209            rela_path_prefix != self.common_prefix()[..common_prefix_len]
210        }) {
211            return false;
212        }
213        for mapping in &self.patterns {
214            let pattern = &mapping.value.pattern;
215            if mapping.pattern.first_wildcard_pos.is_some() && pattern.is_excluded() {
216                return true;
217            }
218            let mut rightmost_idx = mapping.pattern.first_wildcard_pos.map_or_else(
219                || pattern.path.len(),
220                |idx| pattern.path[..idx].rfind_byte(b'/').unwrap_or(idx),
221            );
222            let ignore_case = pattern.signature.contains(MagicSignature::ICASE);
223            let mut is_match = pattern.always_matches();
224            if !is_match {
225                let plen = relative_path.len();
226                if leading && rightmost_idx > plen {
227                    if let Some(idx) = pattern.path[..plen]
228                        .rfind_byte(b'/')
229                        .or_else(|| pattern.path[plen..].find_byte(b'/').map(|idx| idx + plen))
230                    {
231                        rightmost_idx = idx;
232                    }
233                }
234                if let Some(relative_path) = relative_path.get(..rightmost_idx) {
235                    let pattern_path = pattern.path[..rightmost_idx].as_bstr();
236                    is_match = if ignore_case {
237                        pattern_path.eq_ignore_ascii_case(relative_path)
238                    } else {
239                        pattern_path == relative_path
240                    };
241                }
242            }
243            if is_match && (!pattern.is_excluded() || pattern.always_matches()) {
244                return !pattern.is_excluded();
245            }
246        }
247
248        self.all_patterns_are_excluded
249    }
250}
251
252fn match_verbatim(
253    mapping: &gix_glob::search::pattern::Mapping<Spec>,
254    relative_path: &BStr,
255    is_dir: bool,
256    case: Case,
257    how: &mut MatchKind,
258) -> bool {
259    let pattern_len = mapping.value.pattern.path.len();
260    let mut relative_path_ends_with_slash_at_pattern_len = false;
261    let (match_is_allowed, probably_how) = relative_path.get(pattern_len).map_or_else(
262        || (relative_path.len() == pattern_len, Verbatim),
263        |b| {
264            relative_path_ends_with_slash_at_pattern_len = *b == b'/';
265            (relative_path_ends_with_slash_at_pattern_len, Prefix)
266        },
267    );
268    *how = probably_how;
269    let pattern_requirement_is_met = !mapping.pattern.mode.contains(gix_glob::pattern::Mode::MUST_BE_DIR)
270        || (relative_path_ends_with_slash_at_pattern_len || is_dir);
271
272    if match_is_allowed && pattern_requirement_is_met {
273        let dir_or_file = &relative_path[..mapping.value.pattern.path.len()];
274        match case {
275            Case::Sensitive => mapping.value.pattern.path == dir_or_file,
276            Case::Fold => mapping.value.pattern.path.eq_ignore_ascii_case(dir_or_file),
277        }
278    } else {
279        false
280    }
281}