gix_pathspec/
pattern.rs

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