summaryrefslogtreecommitdiffstats
path: root/vendor/gix-pathspec/src/search
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-30 18:31:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-30 18:31:44 +0000
commitc23a457e72abe608715ac76f076f47dc42af07a5 (patch)
tree2772049aaf84b5c9d0ed12ec8d86812f7a7904b6 /vendor/gix-pathspec/src/search
parentReleasing progress-linux version 1.73.0+dfsg1-1~progress7.99u1. (diff)
downloadrustc-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.rs146
-rw-r--r--vendor/gix-pathspec/src/search/matching.rs149
-rw-r--r--vendor/gix-pathspec/src/search/mod.rs51
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;