ignore/
overrides.rs

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