Skip to main content

gix_pathspec/
pattern.rs

1use gix_error::{Result, bail, validation};
2use std::path::{Component, Path, PathBuf};
3
4use bstr::{BStr, BString, ByteSlice, ByteVec};
5
6use crate::{MagicSignature, Pattern, SearchMode};
7
8/// Access
9impl Pattern {
10    /// Returns `true` if this seems to be a pathspec that indicates that 'there is no pathspec'.
11    ///
12    /// Note that such a spec is `:`.
13    pub fn is_nil(&self) -> bool {
14        self.nil
15    }
16
17    /// Return the prefix-portion of the `path` of this spec, which is a *directory*.
18    /// It can be empty if there is no prefix.
19    ///
20    /// A prefix is effectively the CWD seen as relative to the working tree, and it's assumed to
21    /// match case-sensitively. This makes it useful for skipping over large portions of input by
22    /// directly comparing them.
23    pub fn prefix_directory(&self) -> &BStr {
24        self.path[..self.prefix_len].as_bstr()
25    }
26
27    /// Return the path of this spec, typically used for matching.
28    pub fn path(&self) -> &BStr {
29        self.path.as_ref()
30    }
31}
32
33/// Mutation
34impl Pattern {
35    /// Normalize the pattern's path by assuring it's relative to the root of the working tree, and contains
36    /// no relative path components. Further, it assures that `/` are used as path separator.
37    ///
38    /// If `self.path` is a relative path, it will be put in front of the pattern path if `self.signature` isn't indicating `TOP` already.
39    /// If `self.path` is an absolute path, we will use `root` to make it worktree relative if possible.
40    ///
41    /// `prefix` can be empty, we will still normalize this pathspec to resolve relative path components, and
42    /// it is assumed not to contain any relative path components, e.g. '', 'a', 'a/b' are valid.
43    /// `root` is the absolute path to the root of either the worktree or the repository's `git_dir`.
44    /// Errors store the path bytes as `input` in [`gix_error::Message::values`].
45    /// After [wrapping](gix_error::Error::from_error()), inspect them with [metadata](gix_error::Error::metadata()).
46    pub fn normalize(&mut self, prefix: &Path, root: &Path) -> Result<&mut Self> {
47        fn prefix_components_to_subtract(path: &Path) -> usize {
48            let parent_component_end_bound = path.components().enumerate().fold(None::<usize>, |acc, (idx, c)| {
49                matches!(c, Component::ParentDir).then_some(idx + 1).or(acc)
50            });
51            let count = path
52                .components()
53                .take(parent_component_end_bound.unwrap_or(0))
54                .map(|c| match c {
55                    Component::ParentDir => 1_isize,
56                    Component::Normal(_) => -1,
57                    _ => 0,
58                })
59                .sum::<isize>();
60            if count > 0 { count as usize } else { Default::default() }
61        }
62
63        let mut path = gix_path::from_bstr(self.path.as_bstr())?;
64        let mut num_prefix_components = 0;
65        let mut was_absolute = false;
66        if gix_path::is_absolute(path.as_ref()) {
67            was_absolute = true;
68            let rela_path = match path.strip_prefix(root) {
69                Ok(path) => path,
70                Err(_) => {
71                    bail!(
72                        validation(format!("The path is not inside of the worktree \"{}\"", root.display()))
73                            .with_input(gix_path::into_bstr(path.into_owned())?.into_owned())
74                    );
75                }
76            };
77            path = rela_path.to_owned().into();
78        } else if !prefix.as_os_str().is_empty() && !self.signature.contains(MagicSignature::TOP) {
79            debug_assert_eq!(
80                prefix
81                    .components()
82                    .filter(|c| matches!(c, Component::Normal(_)))
83                    .count(),
84                prefix.components().count(),
85                "BUG: prefixes must not have relative path components, or calculations here will be wrong so pattern won't match"
86            );
87            num_prefix_components = prefix
88                .components()
89                .count()
90                .saturating_sub(prefix_components_to_subtract(path.as_ref()));
91            path = prefix.join(path).into();
92        }
93
94        let assure_path_cannot_break_out_upwards = Path::new("");
95        let path = match gix_path::normalize(path.as_ref().into(), assure_path_cannot_break_out_upwards) {
96            Some(path) => {
97                if was_absolute {
98                    num_prefix_components = path.components().count().saturating_sub(
99                        if self.signature.contains(MagicSignature::MUST_BE_DIR) {
100                            0
101                        } else {
102                            1
103                        },
104                    );
105                }
106                path
107            }
108            None => {
109                bail!(
110                    validation("The path leaves the repository")
111                        .with_input(gix_path::into_bstr(path.into_owned())?.into_owned())
112                );
113            }
114        };
115
116        self.path = if path == Path::new(".") {
117            self.nil = true;
118            BString::from(".")
119        } else {
120            let cleaned = PathBuf::from_iter(path.components().filter(|c| !matches!(c, Component::CurDir)));
121            let mut out = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(cleaned)?).into_owned();
122            self.prefix_len = {
123                if self.signature.contains(MagicSignature::MUST_BE_DIR) {
124                    out.push(b'/');
125                }
126                let len = out
127                    .find_iter(b"/")
128                    .take(num_prefix_components)
129                    .last()
130                    .unwrap_or_default();
131                if self.signature.contains(MagicSignature::MUST_BE_DIR) {
132                    out.pop();
133                }
134                len
135            };
136            out
137        };
138
139        Ok(self)
140    }
141}
142
143/// Access
144impl Pattern {
145    /// Return `true` if this pathspec is negated, which means it will exclude an item from the result set instead of including it.
146    pub fn is_excluded(&self) -> bool {
147        self.signature.contains(MagicSignature::EXCLUDE)
148    }
149
150    /// Returns `true` is this pattern is supposed to always match, as it's either empty or designated `nil`.
151    /// Note that technically the pattern might still be excluded.
152    pub fn always_matches(&self) -> bool {
153        self.is_nil() || self.path.is_empty()
154    }
155
156    /// Translate ourselves to a long display format, that when parsed back will yield the same pattern.
157    ///
158    /// Note that the
159    pub fn to_bstring(&self) -> BString {
160        if self.is_nil() {
161            ":".into()
162        } else {
163            let mut buf: BString = ":(".into();
164            if self.signature.contains(MagicSignature::TOP) {
165                buf.push_str("top,");
166            }
167            if self.signature.contains(MagicSignature::EXCLUDE) {
168                buf.push_str("exclude,");
169            }
170            if self.signature.contains(MagicSignature::ICASE) {
171                buf.push_str("icase,");
172            }
173            match self.search_mode {
174                SearchMode::ShellGlob => {}
175                SearchMode::Literal => buf.push_str("literal,"),
176                SearchMode::PathAwareGlob => buf.push_str("glob,"),
177            }
178            if self.attributes.is_empty() {
179                if buf.last() == Some(&b',') {
180                    buf.pop();
181                }
182            } else {
183                buf.push_str("attr:");
184                for attr in &self.attributes {
185                    let attr = attr.as_ref().to_string().replace(',', r"\,");
186                    buf.push_str(&attr);
187                    buf.push(b' ');
188                }
189                buf.pop(); // trailing ' '
190            }
191            buf.push(b')');
192            buf.extend_from_slice(&self.path);
193            if self.signature.contains(MagicSignature::MUST_BE_DIR) {
194                buf.push(b'/');
195            }
196            buf
197        }
198    }
199}
200
201impl std::fmt::Display for Pattern {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        self.to_bstring().fmt(f)
204    }
205}