diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-30 18:31:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-30 18:31:44 +0000 |
commit | c23a457e72abe608715ac76f076f47dc42af07a5 (patch) | |
tree | 2772049aaf84b5c9d0ed12ec8d86812f7a7904b6 /vendor/gix-pathspec/src/search | |
parent | Releasing progress-linux version 1.73.0+dfsg1-1~progress7.99u1. (diff) | |
download | rustc-c23a457e72abe608715ac76f076f47dc42af07a5.tar.xz rustc-c23a457e72abe608715ac76f076f47dc42af07a5.zip |
Merging upstream version 1.74.1+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/gix-pathspec/src/search')
-rw-r--r-- | vendor/gix-pathspec/src/search/init.rs | 146 | ||||
-rw-r--r-- | vendor/gix-pathspec/src/search/matching.rs | 149 | ||||
-rw-r--r-- | vendor/gix-pathspec/src/search/mod.rs | 51 |
3 files changed, 346 insertions, 0 deletions
diff --git a/vendor/gix-pathspec/src/search/init.rs b/vendor/gix-pathspec/src/search/init.rs new file mode 100644 index 000000000..bc252f230 --- /dev/null +++ b/vendor/gix-pathspec/src/search/init.rs @@ -0,0 +1,146 @@ +use std::path::Path; + +use crate::{search::Spec, MagicSignature, Pattern, Search}; + +/// Create a new specification to support matches from `pathspec`, [normalizing](Pattern::normalize()) it with `prefix` and `root`. +fn mapping_from_pattern( + mut pathspec: Pattern, + prefix: &Path, + root: &Path, + sequence_number: usize, +) -> Result<gix_glob::search::pattern::Mapping<Spec>, crate::normalize::Error> { + pathspec.normalize(prefix, root)?; + let mut match_all = pathspec.is_nil(); + let glob = { + let mut g = gix_glob::Pattern::from_bytes_without_negation(&pathspec.path).unwrap_or_else(|| { + match_all = true; + // This pattern is setup to match literally as all-whitespace. + gix_glob::Pattern { + text: pathspec.path.clone(), + mode: gix_glob::pattern::Mode::empty(), + first_wildcard_pos: None, + } + }); + g.mode |= gix_glob::pattern::Mode::ABSOLUTE; + if pathspec.signature.contains(MagicSignature::MUST_BE_DIR) { + g.mode |= gix_glob::pattern::Mode::MUST_BE_DIR; + } + g + }; + + Ok(gix_glob::search::pattern::Mapping { + pattern: glob, + value: Spec { + attrs_match: { + (!pathspec.attributes.is_empty()).then(|| { + let mut out = gix_attributes::search::Outcome::default(); + out.initialize_with_selection( + &Default::default(), + pathspec.attributes.iter().map(|a| a.name.as_str()), + ); + out + }) + }, + pattern: pathspec, + }, + sequence_number, + }) +} + +fn common_prefix_len(patterns: &[gix_glob::search::pattern::Mapping<Spec>]) -> usize { + let mut count = 0; + let len = patterns + .iter() + .filter(|p| !p.value.pattern.is_excluded()) + .map(|p| { + count += 1; + if p.value.pattern.signature.contains(MagicSignature::ICASE) { + p.value.pattern.prefix_len + } else { + p.pattern.first_wildcard_pos.unwrap_or(p.pattern.text.len()) + } + }) + .min() + .unwrap_or_default(); + + if len == 0 { + return 0; + } + + let mut max_len = len; + if count < 2 { + return max_len; + } + + let mut patterns = patterns + .iter() + .filter(|p| !p.value.pattern.is_excluded()) + .map(|p| &p.value.pattern.path); + let base = &patterns.next().expect("at least two patterns"); + for path in patterns { + for (idx, (a, b)) in base[..max_len].iter().zip(path[..max_len].iter()).enumerate() { + if *a != *b { + max_len = idx; + break; + } + } + } + max_len +} + +/// Lifecycle +impl Search { + /// Create a search from ready-made `pathspecs`, and [normalize](Pattern::normalize()) them with `prefix` and `root`. + /// `root` is the absolute path to the worktree root, if available, or the `git_dir` in case of bare repositories. + /// If `pathspecs` doesn't yield any pattern, we will match everything automatically. If `prefix` is also provided and not empty, + /// an artificial pattern will be added to yield all. + pub fn from_specs( + pathspecs: impl IntoIterator<Item = Pattern>, + prefix: Option<&std::path::Path>, + root: &std::path::Path, + ) -> Result<Self, crate::normalize::Error> { + fn inner( + pathspecs: &mut dyn Iterator<Item = Pattern>, + prefix: Option<&std::path::Path>, + root: &std::path::Path, + ) -> Result<Search, crate::normalize::Error> { + let prefix = prefix.unwrap_or(std::path::Path::new("")); + let mut patterns = pathspecs + .enumerate() + .map(|(idx, pattern)| mapping_from_pattern(pattern, prefix, root, idx)) + .collect::<Result<Vec<_>, _>>()?; + + if patterns.is_empty() && !prefix.as_os_str().is_empty() { + patterns.push(mapping_from_pattern( + Pattern::from_literal(&[], MagicSignature::MUST_BE_DIR), + prefix, + root, + 0, + )?); + } + + // Excludes should always happen first so we know a match is authoritative (otherwise we could find a non-excluding match first). + patterns.sort_by(|a, b| { + a.value + .pattern + .is_excluded() + .cmp(&b.value.pattern.is_excluded()) + .reverse() + }); + + let common_prefix_len = common_prefix_len(&patterns); + Ok(Search { + all_patterns_are_excluded: patterns.iter().all(|s| s.value.pattern.is_excluded()), + patterns, + source: None, + common_prefix_len, + }) + } + inner(&mut pathspecs.into_iter(), prefix, root) + } + + /// Obtain ownership of the normalized pathspec patterns that were used for the search. + pub fn into_patterns(self) -> impl Iterator<Item = Pattern> { + self.patterns.into_iter().map(|p| p.value.pattern) + } +} diff --git a/vendor/gix-pathspec/src/search/matching.rs b/vendor/gix-pathspec/src/search/matching.rs new file mode 100644 index 000000000..c7c8f2cbb --- /dev/null +++ b/vendor/gix-pathspec/src/search/matching.rs @@ -0,0 +1,149 @@ +use bstr::{BStr, BString, ByteSlice}; +use gix_glob::pattern::Case; + +use crate::{ + search::{Match, Spec}, + MagicSignature, Pattern, Search, SearchMode, +}; + +impl Search { + /// Return the first [`Match`] of `relative_path`, or `None`. + /// `is_dir` is `true` if `relative_path` is a directory. + /// `attributes` is called as `attributes(relative_path, case, is_dir, outcome) -> has_match` to obtain for attributes for `relative_path`, if + /// the underlying pathspec defined an attribute filter, to be stored in `outcome`, returning true if there was a match. + /// All attributes of the pathspec have to be present in the defined value for the pathspec to match. + /// + /// Note that `relative_path` is expected to be starting at the same root as is assumed for this pattern, see [`Pattern::normalize()`]. + /// Further, empty searches match everything, as if `:` was provided. + /// + /// ### Deviation + /// + /// The case-sensivity of the attribute match is controlled by the sensitivity of the pathspec, instead of being based on the + /// case folding settings of the repository. That way we assure that the matching is consistent. + /// Higher-level crates should control this default case folding of pathspecs when instantiating them, which is when they can + /// set it to match the repository setting for more natural behaviour when, for instance, adding files to a repository: + /// as it stands, on a case-insensitive file system, `touch File && git add file` will not add the file, but also not error. + pub fn pattern_matching_relative_path( + &mut self, + relative_path: &BStr, + is_dir: Option<bool>, + attributes: &mut dyn FnMut(&BStr, Case, bool, &mut gix_attributes::search::Outcome) -> bool, + ) -> Option<Match<'_>> { + let basename_not_important = None; + if relative_path + .get(..self.common_prefix_len) + .map_or(true, |rela_path_prefix| rela_path_prefix != self.common_prefix()) + { + return None; + } + + let is_dir = is_dir.unwrap_or(false); + let patterns_len = self.patterns.len(); + let res = self.patterns.iter_mut().find_map(|mapping| { + let ignore_case = mapping.value.pattern.signature.contains(MagicSignature::ICASE); + let prefix = mapping.value.pattern.prefix_directory(); + if ignore_case && !prefix.is_empty() { + let pattern_requirement_is_met = relative_path.get(prefix.len()).map_or_else(|| is_dir, |b| *b == b'/'); + if !pattern_requirement_is_met + || relative_path.get(..prefix.len()).map(ByteSlice::as_bstr) != Some(prefix) + { + return None; + } + } + + let case = if ignore_case { Case::Fold } else { Case::Sensitive }; + let mut is_match = mapping.value.pattern.is_nil() || mapping.value.pattern.path.is_empty(); + if !is_match { + is_match = if mapping.pattern.first_wildcard_pos.is_none() { + match_verbatim(mapping, relative_path, is_dir, case) + } else { + let wildmatch_mode = match mapping.value.pattern.search_mode { + SearchMode::ShellGlob => Some(gix_glob::wildmatch::Mode::empty()), + SearchMode::Literal => None, + SearchMode::PathAwareGlob => Some(gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL), + }; + match wildmatch_mode { + Some(wildmatch_mode) => { + let is_match = mapping.pattern.matches_repo_relative_path( + relative_path, + basename_not_important, + Some(is_dir), + case, + wildmatch_mode, + ); + if !is_match { + match_verbatim(mapping, relative_path, is_dir, case) + } else { + true + } + } + None => match_verbatim(mapping, relative_path, is_dir, case), + } + } + } + + if let Some(attrs) = mapping.value.attrs_match.as_mut() { + if !attributes(relative_path, Case::Sensitive, is_dir, attrs) { + // we have attrs, but it didn't match any + return None; + } + for (actual, expected) in attrs.iter_selected().zip(mapping.value.pattern.attributes.iter()) { + if actual.assignment != expected.as_ref() { + return None; + } + } + } + + is_match.then_some(Match { + pattern: &mapping.value.pattern, + sequence_number: mapping.sequence_number, + }) + }); + + if res.is_none() && self.all_patterns_are_excluded { + static MATCH_ALL_STAND_IN: Pattern = Pattern { + path: BString::new(Vec::new()), + signature: MagicSignature::empty(), + search_mode: SearchMode::ShellGlob, + attributes: Vec::new(), + prefix_len: 0, + nil: true, + }; + Some(Match { + pattern: &MATCH_ALL_STAND_IN, + sequence_number: patterns_len, + }) + } else { + res + } + } +} + +fn match_verbatim( + mapping: &gix_glob::search::pattern::Mapping<Spec>, + relative_path: &BStr, + is_dir: bool, + case: Case, +) -> bool { + let pattern_len = mapping.value.pattern.path.len(); + let mut relative_path_ends_with_slash_at_pattern_len = false; + let match_is_allowed = relative_path.get(pattern_len).map_or_else( + || relative_path.len() == pattern_len, + |b| { + relative_path_ends_with_slash_at_pattern_len = *b == b'/'; + relative_path_ends_with_slash_at_pattern_len + }, + ); + let pattern_requirement_is_met = !mapping.pattern.mode.contains(gix_glob::pattern::Mode::MUST_BE_DIR) + || (relative_path_ends_with_slash_at_pattern_len || is_dir); + + if match_is_allowed && pattern_requirement_is_met { + let dir_or_file = &relative_path[..mapping.value.pattern.path.len()]; + match case { + Case::Sensitive => mapping.value.pattern.path == dir_or_file, + Case::Fold => mapping.value.pattern.path.eq_ignore_ascii_case(dir_or_file), + } + } else { + false + } +} diff --git a/vendor/gix-pathspec/src/search/mod.rs b/vendor/gix-pathspec/src/search/mod.rs new file mode 100644 index 000000000..a9c87377b --- /dev/null +++ b/vendor/gix-pathspec/src/search/mod.rs @@ -0,0 +1,51 @@ +use bstr::{BStr, ByteSlice}; + +use crate::{Pattern, Search}; + +/// Describes a matching pattern within a search for ignored paths. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +pub struct Match<'a> { + /// The matching search specification, which contains the pathspec as well. + pub pattern: &'a Pattern, + /// 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`. + pub sequence_number: usize, +} + +mod init; + +impl Match<'_> { + /// Return `true` if the pathspec that matched was negative, which excludes this item from the set. + pub fn is_excluded(&self) -> bool { + self.pattern.is_excluded() + } +} + +/// Access +impl Search { + /// Return an iterator over the patterns that participate in the search. + pub fn patterns(&self) -> impl Iterator<Item = &Pattern> + '_ { + self.patterns.iter().map(|m| &m.value.pattern) + } + + /// Return the portion of the prefix among all of the pathspecs involved in this search, or an empty string if + /// there is none. It doesn't have to end at a directory boundary though, nor does it denote a directory. + /// + /// Note that the common_prefix is always matched case-sensitively, and it is useful to skip large portions of input. + /// Further, excluded pathspecs don't participate which makes this common prefix inclusive. To work correclty though, + /// one will have to additionally match paths that have the common prefix with that pathspec itself to assure it is + /// not excluded. + pub fn common_prefix(&self) -> &BStr { + self.patterns + .iter() + .find(|p| !p.value.pattern.is_excluded()) + .map_or("".into(), |m| m.value.pattern.path[..self.common_prefix_len].as_bstr()) + } +} + +#[derive(Default, Clone, Debug)] +pub(crate) struct Spec { + pub pattern: Pattern, + pub attrs_match: Option<gix_attributes::search::Outcome>, +} + +mod matching; |