Skip to main content

gix_pathspec/search/
matching.rs

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