ignore/
overrides.rs

1/*!
2The overrides module provides a way to specify a set of override globs.
3
4This provides functionality similar to `--include` or `--exclude` in command
5line tools.
6*/
7
8use std::path::Path;
9
10use crate::{
11    Error, Match,
12    gitignore::{self, Gitignore, GitignoreBuilder},
13};
14
15/// Glob represents a single glob in an override matcher.
16///
17/// This is used to report information about the highest precedent glob
18/// that matched.
19///
20/// Note that not all matches necessarily correspond to a specific glob. For
21/// example, if there are one or more whitelist globs and a file path doesn't
22/// match any glob in the set, then the file path is considered to be ignored.
23///
24/// The lifetime `'a` refers to the lifetime of the matcher that produced
25/// this glob.
26#[derive(Clone, Debug)]
27#[allow(dead_code)]
28pub struct Glob<'a>(GlobInner<'a>);
29
30#[derive(Clone, Debug)]
31#[allow(dead_code)]
32enum GlobInner<'a> {
33    /// No glob matched, but the file path should still be ignored.
34    UnmatchedIgnore,
35    /// A glob matched.
36    Matched(&'a gitignore::Glob),
37}
38
39impl<'a> Glob<'a> {
40    fn unmatched() -> Glob<'a> {
41        Glob(GlobInner::UnmatchedIgnore)
42    }
43}
44
45/// Manages a set of overrides provided explicitly by the end user.
46#[derive(Clone, Debug)]
47pub struct Override(Gitignore);
48
49impl Override {
50    /// Returns an empty matcher that never matches any file path.
51    pub fn empty() -> Override {
52        Override(Gitignore::empty())
53    }
54
55    /// Returns the directory of this override set.
56    ///
57    /// All matches are done relative to this path.
58    pub fn path(&self) -> &Path {
59        self.0.path()
60    }
61
62    /// Returns true if and only if this matcher is empty.
63    ///
64    /// When a matcher is empty, it will never match any file path.
65    pub fn is_empty(&self) -> bool {
66        self.0.is_empty()
67    }
68
69    /// Returns the total number of ignore globs.
70    pub fn num_ignores(&self) -> u64 {
71        self.0.num_whitelists()
72    }
73
74    /// Returns the total number of whitelisted globs.
75    pub fn num_whitelists(&self) -> u64 {
76        self.0.num_ignores()
77    }
78
79    /// Returns whether the given file path matched a pattern in this override
80    /// matcher.
81    ///
82    /// `is_dir` should be true if the path refers to a directory and false
83    /// otherwise.
84    ///
85    /// If there are no overrides, then this always returns `Match::None`.
86    ///
87    /// If there is at least one whitelist override and `is_dir` is false, then
88    /// this never returns `Match::None`, since non-matches are interpreted as
89    /// ignored.
90    ///
91    /// The given path is matched to the globs relative to the path given
92    /// when building the override matcher. Specifically, before matching
93    /// `path`, its prefix (as determined by a common suffix of the directory
94    /// given) is stripped. If there is no common suffix/prefix overlap, then
95    /// `path` is assumed to reside in the same directory as the root path for
96    /// this set of overrides.
97    pub fn matched<'a, P: AsRef<Path>>(
98        &'a self,
99        path: P,
100        is_dir: bool,
101    ) -> Match<Glob<'a>> {
102        if self.is_empty() {
103            return Match::None;
104        }
105        let mat = self.0.matched(path, is_dir).invert();
106        if mat.is_none() && self.num_whitelists() > 0 && !is_dir {
107            return Match::Ignore(Glob::unmatched());
108        }
109        mat.map(move |giglob| Glob(GlobInner::Matched(giglob)))
110    }
111}
112
113/// Builds a matcher for a set of glob overrides.
114#[derive(Clone, Debug)]
115pub struct OverrideBuilder {
116    builder: GitignoreBuilder,
117}
118
119impl OverrideBuilder {
120    /// Create a new override builder.
121    ///
122    /// Matching is done relative to the directory path provided.
123    pub fn new<P: AsRef<Path>>(path: P) -> OverrideBuilder {
124        let mut builder = GitignoreBuilder::new(path);
125        builder.allow_unclosed_class(false);
126        OverrideBuilder { builder }
127    }
128
129    /// Builds a new override matcher from the globs added so far.
130    ///
131    /// Once a matcher is built, no new globs can be added to it.
132    pub fn build(&self) -> Result<Override, Error> {
133        Ok(Override(self.builder.build()?))
134    }
135
136    /// Add a glob to the set of overrides.
137    ///
138    /// Globs provided here have precisely the same semantics as a single
139    /// line in a `gitignore` file, where the meaning of `!` is inverted:
140    /// namely, `!` at the beginning of a glob will ignore a file. Without `!`,
141    /// all matches of the glob provided are treated as whitelist matches.
142    pub fn add(&mut self, glob: &str) -> Result<&mut OverrideBuilder, Error> {
143        self.builder.add_line(None, glob)?;
144        Ok(self)
145    }
146
147    /// Toggle whether the globs should be matched case insensitively or not.
148    ///
149    /// When this option is changed, only globs added after the change will be
150    /// affected.
151    ///
152    /// This is disabled by default.
153    pub fn case_insensitive(
154        &mut self,
155        yes: bool,
156    ) -> Result<&mut OverrideBuilder, Error> {
157        // TODO: This should not return a `Result`. Fix this in the next semver
158        // release.
159        self.builder.case_insensitive(yes)?;
160        Ok(self)
161    }
162
163    /// Toggle whether unclosed character classes are allowed. When allowed,
164    /// a `[` without a matching `]` is treated literally instead of resulting
165    /// in a parse error.
166    ///
167    /// For example, if this is set then the glob `[abc` will be treated as the
168    /// literal string `[abc` instead of returning an error.
169    ///
170    /// By default, this is false. Generally speaking, enabling this leads to
171    /// worse failure modes since the glob parser becomes more permissive. You
172    /// might want to enable this when compatibility (e.g., with POSIX glob
173    /// implementations) is more important than good error messages.
174    ///
175    /// This default is different from the default for [`Gitignore`]. Namely,
176    /// [`Gitignore`] is intended to match git's behavior as-is. But this
177    /// abstraction for "override" globs does not necessarily conform to any
178    /// other known specification and instead prioritizes better error
179    /// messages.
180    pub fn allow_unclosed_class(&mut self, yes: bool) -> &mut OverrideBuilder {
181        self.builder.allow_unclosed_class(yes);
182        self
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::{Override, OverrideBuilder};
189
190    const ROOT: &'static str = "/home/andrew/foo";
191
192    fn ov(globs: &[&str]) -> Override {
193        let mut builder = OverrideBuilder::new(ROOT);
194        for glob in globs {
195            builder.add(glob).unwrap();
196        }
197        builder.build().unwrap()
198    }
199
200    #[test]
201    fn empty() {
202        let ov = ov(&[]);
203        assert!(ov.matched("a.foo", false).is_none());
204        assert!(ov.matched("a", false).is_none());
205        assert!(ov.matched("", false).is_none());
206    }
207
208    #[test]
209    fn simple() {
210        let ov = ov(&["*.foo", "!*.bar"]);
211        assert!(ov.matched("a.foo", false).is_whitelist());
212        assert!(ov.matched("a.foo", true).is_whitelist());
213        assert!(ov.matched("a.rs", false).is_ignore());
214        assert!(ov.matched("a.rs", true).is_none());
215        assert!(ov.matched("a.bar", false).is_ignore());
216        assert!(ov.matched("a.bar", true).is_ignore());
217    }
218
219    #[test]
220    fn only_ignores() {
221        let ov = ov(&["!*.bar"]);
222        assert!(ov.matched("a.rs", false).is_none());
223        assert!(ov.matched("a.rs", true).is_none());
224        assert!(ov.matched("a.bar", false).is_ignore());
225        assert!(ov.matched("a.bar", true).is_ignore());
226    }
227
228    #[test]
229    fn precedence() {
230        let ov = ov(&["*.foo", "!*.bar.foo"]);
231        assert!(ov.matched("a.foo", false).is_whitelist());
232        assert!(ov.matched("a.baz", false).is_ignore());
233        assert!(ov.matched("a.bar.foo", false).is_ignore());
234    }
235
236    #[test]
237    fn gitignore() {
238        let ov = ov(&["/foo", "bar/*.rs", "baz/**"]);
239        assert!(ov.matched("bar/lib.rs", false).is_whitelist());
240        assert!(ov.matched("bar/wat/lib.rs", false).is_ignore());
241        assert!(ov.matched("wat/bar/lib.rs", false).is_ignore());
242        assert!(ov.matched("foo", false).is_whitelist());
243        assert!(ov.matched("wat/foo", false).is_ignore());
244        assert!(ov.matched("baz", false).is_ignore());
245        assert!(ov.matched("baz/a", false).is_whitelist());
246        assert!(ov.matched("baz/a/b", false).is_whitelist());
247    }
248
249    #[test]
250    fn allow_directories() {
251        // This tests that directories are NOT ignored when they are unmatched.
252        let ov = ov(&["*.rs"]);
253        assert!(ov.matched("foo.rs", false).is_whitelist());
254        assert!(ov.matched("foo.c", false).is_ignore());
255        assert!(ov.matched("foo", false).is_ignore());
256        assert!(ov.matched("foo", true).is_none());
257        assert!(ov.matched("src/foo.rs", false).is_whitelist());
258        assert!(ov.matched("src/foo.c", false).is_ignore());
259        assert!(ov.matched("src/foo", false).is_ignore());
260        assert!(ov.matched("src/foo", true).is_none());
261    }
262
263    #[test]
264    fn absolute_path() {
265        let ov = ov(&["!/bar"]);
266        assert!(ov.matched("./foo/bar", false).is_none());
267    }
268
269    #[test]
270    fn case_insensitive() {
271        let ov = OverrideBuilder::new(ROOT)
272            .case_insensitive(true)
273            .unwrap()
274            .add("*.html")
275            .unwrap()
276            .build()
277            .unwrap();
278        assert!(ov.matched("foo.html", false).is_whitelist());
279        assert!(ov.matched("foo.HTML", false).is_whitelist());
280        assert!(ov.matched("foo.htm", false).is_ignore());
281        assert!(ov.matched("foo.HTM", false).is_ignore());
282    }
283
284    #[test]
285    fn default_case_sensitive() {
286        let ov =
287            OverrideBuilder::new(ROOT).add("*.html").unwrap().build().unwrap();
288        assert!(ov.matched("foo.html", false).is_whitelist());
289        assert!(ov.matched("foo.HTML", false).is_ignore());
290        assert!(ov.matched("foo.htm", false).is_ignore());
291        assert!(ov.matched("foo.HTM", false).is_ignore());
292    }
293}