Skip to main content

gix_pathspec/search/
init.rs

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