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