gix_pathspec/search/
init.rs

1use std::path::Path;
2
3use crate::{search::Spec, MagicSignature, Pattern, Search};
4
5/// Create a new specification to support matches from `pathspec`, [normalizing](Pattern::normalize()) it with `prefix` and `root`.
6fn mapping_from_pattern(
7    mut pathspec: Pattern,
8    prefix: &Path,
9    root: &Path,
10    sequence_number: usize,
11) -> Result<gix_glob::search::pattern::Mapping<Spec>, crate::normalize::Error> {
12    pathspec.normalize(prefix, root)?;
13    let mut match_all = pathspec.is_nil();
14    let glob = {
15        let mut g = gix_glob::Pattern::from_bytes_without_negation(&pathspec.path).unwrap_or_else(|| {
16            match_all = true;
17            // This pattern is setup to match literally as all-whitespace.
18            gix_glob::Pattern {
19                text: pathspec.path.clone(),
20                mode: gix_glob::pattern::Mode::empty(),
21                first_wildcard_pos: None,
22            }
23        });
24        g.mode |= gix_glob::pattern::Mode::ABSOLUTE;
25        if pathspec.signature.contains(MagicSignature::MUST_BE_DIR) {
26            g.mode |= gix_glob::pattern::Mode::MUST_BE_DIR;
27        }
28        g
29    };
30
31    Ok(gix_glob::search::pattern::Mapping {
32        pattern: glob,
33        value: Spec {
34            attrs_match: {
35                (!pathspec.attributes.is_empty()).then(|| {
36                    let mut out = gix_attributes::search::Outcome::default();
37                    out.initialize_with_selection(
38                        &Default::default(),
39                        pathspec.attributes.iter().map(|a| a.name.as_str()),
40                    );
41                    out
42                })
43            },
44            pattern: pathspec,
45        },
46        sequence_number,
47    })
48}
49
50fn common_prefix_len(patterns: &[gix_glob::search::pattern::Mapping<Spec>]) -> usize {
51    let mut count = 0;
52    let len = patterns
53        .iter()
54        .filter(|p| !p.value.pattern.is_excluded())
55        .map(|p| {
56            count += 1;
57            if p.value.pattern.signature.contains(MagicSignature::ICASE) {
58                p.value.pattern.prefix_len
59            } else {
60                p.pattern.first_wildcard_pos.unwrap_or(p.pattern.text.len())
61            }
62        })
63        .min()
64        .unwrap_or_default();
65
66    if len == 0 {
67        return 0;
68    }
69
70    let mut max_len = len;
71    if count < 2 {
72        return max_len;
73    }
74
75    let mut patterns = patterns
76        .iter()
77        .filter(|p| !p.value.pattern.is_excluded())
78        .map(|p| &p.value.pattern.path);
79    let base = &patterns.next().expect("at least two patterns");
80    for path in patterns {
81        for (idx, (a, b)) in base[..max_len].iter().zip(path[..max_len].iter()).enumerate() {
82            if *a != *b {
83                max_len = idx;
84                break;
85            }
86        }
87    }
88    max_len
89}
90
91/// Lifecycle
92impl Search {
93    /// Create a search from ready-made `pathspecs`, and [normalize](Pattern::normalize()) them with `prefix` and `root`.
94    /// `root` is the absolute path to the worktree root, if available, or the `git_dir` in case of bare repositories.
95    /// If `pathspecs` doesn't yield any pattern, we will match everything automatically. If `prefix` is also provided and not empty,
96    /// an artificial pattern will be added to yield all.
97    pub fn from_specs(
98        pathspecs: impl IntoIterator<Item = Pattern>,
99        prefix: Option<&std::path::Path>,
100        root: &std::path::Path,
101    ) -> Result<Self, crate::normalize::Error> {
102        fn inner(
103            pathspecs: &mut dyn Iterator<Item = Pattern>,
104            prefix: Option<&std::path::Path>,
105            root: &std::path::Path,
106        ) -> Result<Search, crate::normalize::Error> {
107            let prefix = prefix.unwrap_or(std::path::Path::new(""));
108            let mut patterns = pathspecs
109                .enumerate()
110                .map(|(idx, pattern)| mapping_from_pattern(pattern, prefix, root, idx))
111                .collect::<Result<Vec<_>, _>>()?;
112
113            if patterns.is_empty() && !prefix.as_os_str().is_empty() {
114                patterns.push(mapping_from_pattern(
115                    Pattern::from_literal(&[], MagicSignature::MUST_BE_DIR),
116                    prefix,
117                    root,
118                    0,
119                )?);
120            }
121
122            // Excludes should always happen first so we know a match is authoritative (otherwise we could find a non-excluding match first).
123            patterns.sort_by(|a, b| {
124                a.value
125                    .pattern
126                    .is_excluded()
127                    .cmp(&b.value.pattern.is_excluded())
128                    .reverse()
129            });
130
131            let common_prefix_len = common_prefix_len(&patterns);
132            Ok(Search {
133                all_patterns_are_excluded: patterns.iter().all(|s| s.value.pattern.is_excluded()),
134                patterns,
135                source: None,
136                common_prefix_len,
137            })
138        }
139        inner(&mut pathspecs.into_iter(), prefix, root)
140    }
141
142    /// Obtain ownership of the normalized pathspec patterns that were used for the search.
143    pub fn into_patterns(self) -> impl Iterator<Item = Pattern> {
144        self.patterns.into_iter().map(|p| p.value.pattern)
145    }
146}