1 /*!
2 The gitignore module provides a way to match globs from a gitignore file
3 against file paths.
4 
5 Note that this module implements the specification as described in the
6 `gitignore` man page from scratch. That is, this module does *not* shell out to
7 the `git` command line tool.
8 */
9 
10 use std::cell::RefCell;
11 use std::env;
12 use std::fs::File;
13 use std::io::{self, BufRead, Read};
14 use std::path::{Path, PathBuf};
15 use std::str;
16 use std::sync::Arc;
17 
18 use globset::{Candidate, GlobBuilder, GlobSet, GlobSetBuilder};
19 use regex::bytes::Regex;
20 use thread_local::ThreadLocal;
21 
22 use crate::pathutil::{is_file_name, strip_prefix};
23 use crate::{Error, Match, PartialErrorBuilder};
24 
25 /// Glob represents a single glob in a gitignore file.
26 ///
27 /// This is used to report information about the highest precedent glob that
28 /// matched in one or more gitignore files.
29 #[derive(Clone, Debug)]
30 pub struct Glob {
31     /// The file path that this glob was extracted from.
32     from: Option<PathBuf>,
33     /// The original glob string.
34     original: String,
35     /// The actual glob string used to convert to a regex.
36     actual: String,
37     /// Whether this is a whitelisted glob or not.
38     is_whitelist: bool,
39     /// Whether this glob should only match directories or not.
40     is_only_dir: bool,
41 }
42 
43 impl Glob {
44     /// Returns the file path that defined this glob.
from(&self) -> Option<&Path>45     pub fn from(&self) -> Option<&Path> {
46         self.from.as_ref().map(|p| &**p)
47     }
48 
49     /// The original glob as it was defined in a gitignore file.
original(&self) -> &str50     pub fn original(&self) -> &str {
51         &self.original
52     }
53 
54     /// The actual glob that was compiled to respect gitignore
55     /// semantics.
actual(&self) -> &str56     pub fn actual(&self) -> &str {
57         &self.actual
58     }
59 
60     /// Whether this was a whitelisted glob or not.
is_whitelist(&self) -> bool61     pub fn is_whitelist(&self) -> bool {
62         self.is_whitelist
63     }
64 
65     /// Whether this glob must match a directory or not.
is_only_dir(&self) -> bool66     pub fn is_only_dir(&self) -> bool {
67         self.is_only_dir
68     }
69 
70     /// Returns true if and only if this glob has a `**/` prefix.
has_doublestar_prefix(&self) -> bool71     fn has_doublestar_prefix(&self) -> bool {
72         self.actual.starts_with("**/") || self.actual == "**"
73     }
74 }
75 
76 /// Gitignore is a matcher for the globs in one or more gitignore files
77 /// in the same directory.
78 #[derive(Clone, Debug)]
79 pub struct Gitignore {
80     set: GlobSet,
81     root: PathBuf,
82     globs: Vec<Glob>,
83     num_ignores: u64,
84     num_whitelists: u64,
85     matches: Option<Arc<ThreadLocal<RefCell<Vec<usize>>>>>,
86 }
87 
88 impl Gitignore {
89     /// Creates a new gitignore matcher from the gitignore file path given.
90     ///
91     /// If it's desirable to include multiple gitignore files in a single
92     /// matcher, or read gitignore globs from a different source, then
93     /// use `GitignoreBuilder`.
94     ///
95     /// This always returns a valid matcher, even if it's empty. In particular,
96     /// a Gitignore file can be partially valid, e.g., when one glob is invalid
97     /// but the rest aren't.
98     ///
99     /// Note that I/O errors are ignored. For more granular control over
100     /// errors, use `GitignoreBuilder`.
new<P: AsRef<Path>>( gitignore_path: P, ) -> (Gitignore, Option<Error>)101     pub fn new<P: AsRef<Path>>(
102         gitignore_path: P,
103     ) -> (Gitignore, Option<Error>) {
104         let path = gitignore_path.as_ref();
105         let parent = path.parent().unwrap_or(Path::new("/"));
106         let mut builder = GitignoreBuilder::new(parent);
107         let mut errs = PartialErrorBuilder::default();
108         errs.maybe_push_ignore_io(builder.add(path));
109         match builder.build() {
110             Ok(gi) => (gi, errs.into_error_option()),
111             Err(err) => {
112                 errs.push(err);
113                 (Gitignore::empty(), errs.into_error_option())
114             }
115         }
116     }
117 
118     /// Creates a new gitignore matcher from the global ignore file, if one
119     /// exists.
120     ///
121     /// The global config file path is specified by git's `core.excludesFile`
122     /// config option.
123     ///
124     /// Git's config file location is `$HOME/.gitconfig`. If `$HOME/.gitconfig`
125     /// does not exist or does not specify `core.excludesFile`, then
126     /// `$XDG_CONFIG_HOME/git/ignore` is read. If `$XDG_CONFIG_HOME` is not
127     /// set or is empty, then `$HOME/.config/git/ignore` is used instead.
global() -> (Gitignore, Option<Error>)128     pub fn global() -> (Gitignore, Option<Error>) {
129         GitignoreBuilder::new("").build_global()
130     }
131 
132     /// Creates a new empty gitignore matcher that never matches anything.
133     ///
134     /// Its path is empty.
empty() -> Gitignore135     pub fn empty() -> Gitignore {
136         Gitignore {
137             set: GlobSet::empty(),
138             root: PathBuf::from(""),
139             globs: vec![],
140             num_ignores: 0,
141             num_whitelists: 0,
142             matches: None,
143         }
144     }
145 
146     /// Returns the directory containing this gitignore matcher.
147     ///
148     /// All matches are done relative to this path.
path(&self) -> &Path149     pub fn path(&self) -> &Path {
150         &*self.root
151     }
152 
153     /// Returns true if and only if this gitignore has zero globs, and
154     /// therefore never matches any file path.
is_empty(&self) -> bool155     pub fn is_empty(&self) -> bool {
156         self.set.is_empty()
157     }
158 
159     /// Returns the total number of globs, which should be equivalent to
160     /// `num_ignores + num_whitelists`.
len(&self) -> usize161     pub fn len(&self) -> usize {
162         self.set.len()
163     }
164 
165     /// Returns the total number of ignore globs.
num_ignores(&self) -> u64166     pub fn num_ignores(&self) -> u64 {
167         self.num_ignores
168     }
169 
170     /// Returns the total number of whitelisted globs.
num_whitelists(&self) -> u64171     pub fn num_whitelists(&self) -> u64 {
172         self.num_whitelists
173     }
174 
175     /// Returns whether the given path (file or directory) matched a pattern in
176     /// this gitignore matcher.
177     ///
178     /// `is_dir` should be true if the path refers to a directory and false
179     /// otherwise.
180     ///
181     /// The given path is matched relative to the path given when building
182     /// the matcher. Specifically, before matching `path`, its prefix (as
183     /// determined by a common suffix of the directory containing this
184     /// gitignore) is stripped. If there is no common suffix/prefix overlap,
185     /// then `path` is assumed to be relative to this matcher.
matched<P: AsRef<Path>>( &self, path: P, is_dir: bool, ) -> Match<&Glob>186     pub fn matched<P: AsRef<Path>>(
187         &self,
188         path: P,
189         is_dir: bool,
190     ) -> Match<&Glob> {
191         if self.is_empty() {
192             return Match::None;
193         }
194         self.matched_stripped(self.strip(path.as_ref()), is_dir)
195     }
196 
197     /// Returns whether the given path (file or directory, and expected to be
198     /// under the root) or any of its parent directories (up to the root)
199     /// matched a pattern in this gitignore matcher.
200     ///
201     /// NOTE: This method is more expensive than walking the directory hierarchy
202     /// top-to-bottom and matching the entries. But, is easier to use in cases
203     /// when a list of paths are available without a hierarchy.
204     ///
205     /// `is_dir` should be true if the path refers to a directory and false
206     /// otherwise.
207     ///
208     /// The given path is matched relative to the path given when building
209     /// the matcher. Specifically, before matching `path`, its prefix (as
210     /// determined by a common suffix of the directory containing this
211     /// gitignore) is stripped. If there is no common suffix/prefix overlap,
212     /// then `path` is assumed to be relative to this matcher.
213     ///
214     /// # Panics
215     ///
216     /// This method panics if the given file path is not under the root path
217     /// of this matcher.
matched_path_or_any_parents<P: AsRef<Path>>( &self, path: P, is_dir: bool, ) -> Match<&Glob>218     pub fn matched_path_or_any_parents<P: AsRef<Path>>(
219         &self,
220         path: P,
221         is_dir: bool,
222     ) -> Match<&Glob> {
223         if self.is_empty() {
224             return Match::None;
225         }
226         let mut path = self.strip(path.as_ref());
227         assert!(!path.has_root(), "path is expected to be under the root");
228 
229         match self.matched_stripped(path, is_dir) {
230             Match::None => (), // walk up
231             a_match => return a_match,
232         }
233         while let Some(parent) = path.parent() {
234             match self.matched_stripped(parent, /* is_dir */ true) {
235                 Match::None => path = parent, // walk up
236                 a_match => return a_match,
237             }
238         }
239         Match::None
240     }
241 
242     /// Like matched, but takes a path that has already been stripped.
matched_stripped<P: AsRef<Path>>( &self, path: P, is_dir: bool, ) -> Match<&Glob>243     fn matched_stripped<P: AsRef<Path>>(
244         &self,
245         path: P,
246         is_dir: bool,
247     ) -> Match<&Glob> {
248         if self.is_empty() {
249             return Match::None;
250         }
251         let path = path.as_ref();
252         let _matches = self.matches.as_ref().unwrap().get_or_default();
253         let mut matches = _matches.borrow_mut();
254         let candidate = Candidate::new(path);
255         self.set.matches_candidate_into(&candidate, &mut *matches);
256         for &i in matches.iter().rev() {
257             let glob = &self.globs[i];
258             if !glob.is_only_dir() || is_dir {
259                 return if glob.is_whitelist() {
260                     Match::Whitelist(glob)
261                 } else {
262                     Match::Ignore(glob)
263                 };
264             }
265         }
266         Match::None
267     }
268 
269     /// Strips the given path such that it's suitable for matching with this
270     /// gitignore matcher.
strip<'a, P: 'a + AsRef<Path> + ?Sized>( &'a self, path: &'a P, ) -> &'a Path271     fn strip<'a, P: 'a + AsRef<Path> + ?Sized>(
272         &'a self,
273         path: &'a P,
274     ) -> &'a Path {
275         let mut path = path.as_ref();
276         // A leading ./ is completely superfluous. We also strip it from
277         // our gitignore root path, so we need to strip it from our candidate
278         // path too.
279         if let Some(p) = strip_prefix("./", path) {
280             path = p;
281         }
282         // Strip any common prefix between the candidate path and the root
283         // of the gitignore, to make sure we get relative matching right.
284         // BUT, a file name might not have any directory components to it,
285         // in which case, we don't want to accidentally strip any part of the
286         // file name.
287         //
288         // As an additional special case, if the root is just `.`, then we
289         // shouldn't try to strip anything, e.g., when path begins with a `.`.
290         if self.root != Path::new(".") && !is_file_name(path) {
291             if let Some(p) = strip_prefix(&self.root, path) {
292                 path = p;
293                 // If we're left with a leading slash, get rid of it.
294                 if let Some(p) = strip_prefix("/", path) {
295                     path = p;
296                 }
297             }
298         }
299         path
300     }
301 }
302 
303 /// Builds a matcher for a single set of globs from a .gitignore file.
304 #[derive(Clone, Debug)]
305 pub struct GitignoreBuilder {
306     builder: GlobSetBuilder,
307     root: PathBuf,
308     globs: Vec<Glob>,
309     case_insensitive: bool,
310 }
311 
312 impl GitignoreBuilder {
313     /// Create a new builder for a gitignore file.
314     ///
315     /// The path given should be the path at which the globs for this gitignore
316     /// file should be matched. Note that paths are always matched relative
317     /// to the root path given here. Generally, the root path should correspond
318     /// to the *directory* containing a `.gitignore` file.
new<P: AsRef<Path>>(root: P) -> GitignoreBuilder319     pub fn new<P: AsRef<Path>>(root: P) -> GitignoreBuilder {
320         let root = root.as_ref();
321         GitignoreBuilder {
322             builder: GlobSetBuilder::new(),
323             root: strip_prefix("./", root).unwrap_or(root).to_path_buf(),
324             globs: vec![],
325             case_insensitive: false,
326         }
327     }
328 
329     /// Builds a new matcher from the globs added so far.
330     ///
331     /// Once a matcher is built, no new globs can be added to it.
build(&self) -> Result<Gitignore, Error>332     pub fn build(&self) -> Result<Gitignore, Error> {
333         let nignore = self.globs.iter().filter(|g| !g.is_whitelist()).count();
334         let nwhite = self.globs.iter().filter(|g| g.is_whitelist()).count();
335         let set = self
336             .builder
337             .build()
338             .map_err(|err| Error::Glob { glob: None, err: err.to_string() })?;
339         Ok(Gitignore {
340             set: set,
341             root: self.root.clone(),
342             globs: self.globs.clone(),
343             num_ignores: nignore as u64,
344             num_whitelists: nwhite as u64,
345             matches: Some(Arc::new(ThreadLocal::default())),
346         })
347     }
348 
349     /// Build a global gitignore matcher using the configuration in this
350     /// builder.
351     ///
352     /// This consumes ownership of the builder unlike `build` because it
353     /// must mutate the builder to add the global gitignore globs.
354     ///
355     /// Note that this ignores the path given to this builder's constructor
356     /// and instead derives the path automatically from git's global
357     /// configuration.
build_global(mut self) -> (Gitignore, Option<Error>)358     pub fn build_global(mut self) -> (Gitignore, Option<Error>) {
359         match gitconfig_excludes_path() {
360             None => (Gitignore::empty(), None),
361             Some(path) => {
362                 if !path.is_file() {
363                     (Gitignore::empty(), None)
364                 } else {
365                     let mut errs = PartialErrorBuilder::default();
366                     errs.maybe_push_ignore_io(self.add(path));
367                     match self.build() {
368                         Ok(gi) => (gi, errs.into_error_option()),
369                         Err(err) => {
370                             errs.push(err);
371                             (Gitignore::empty(), errs.into_error_option())
372                         }
373                     }
374                 }
375             }
376         }
377     }
378 
379     /// Add each glob from the file path given.
380     ///
381     /// The file given should be formatted as a `gitignore` file.
382     ///
383     /// Note that partial errors can be returned. For example, if there was
384     /// a problem adding one glob, an error for that will be returned, but
385     /// all other valid globs will still be added.
add<P: AsRef<Path>>(&mut self, path: P) -> Option<Error>386     pub fn add<P: AsRef<Path>>(&mut self, path: P) -> Option<Error> {
387         let path = path.as_ref();
388         let file = match File::open(path) {
389             Err(err) => return Some(Error::Io(err).with_path(path)),
390             Ok(file) => file,
391         };
392         let rdr = io::BufReader::new(file);
393         let mut errs = PartialErrorBuilder::default();
394         for (i, line) in rdr.lines().enumerate() {
395             let lineno = (i + 1) as u64;
396             let line = match line {
397                 Ok(line) => line,
398                 Err(err) => {
399                     errs.push(Error::Io(err).tagged(path, lineno));
400                     break;
401                 }
402             };
403             if let Err(err) = self.add_line(Some(path.to_path_buf()), &line) {
404                 errs.push(err.tagged(path, lineno));
405             }
406         }
407         errs.into_error_option()
408     }
409 
410     /// Add each glob line from the string given.
411     ///
412     /// If this string came from a particular `gitignore` file, then its path
413     /// should be provided here.
414     ///
415     /// The string given should be formatted as a `gitignore` file.
416     #[cfg(test)]
add_str( &mut self, from: Option<PathBuf>, gitignore: &str, ) -> Result<&mut GitignoreBuilder, Error>417     fn add_str(
418         &mut self,
419         from: Option<PathBuf>,
420         gitignore: &str,
421     ) -> Result<&mut GitignoreBuilder, Error> {
422         for line in gitignore.lines() {
423             self.add_line(from.clone(), line)?;
424         }
425         Ok(self)
426     }
427 
428     /// Add a line from a gitignore file to this builder.
429     ///
430     /// If this line came from a particular `gitignore` file, then its path
431     /// should be provided here.
432     ///
433     /// If the line could not be parsed as a glob, then an error is returned.
add_line( &mut self, from: Option<PathBuf>, mut line: &str, ) -> Result<&mut GitignoreBuilder, Error>434     pub fn add_line(
435         &mut self,
436         from: Option<PathBuf>,
437         mut line: &str,
438     ) -> Result<&mut GitignoreBuilder, Error> {
439         #![allow(deprecated)]
440 
441         if line.starts_with("#") {
442             return Ok(self);
443         }
444         if !line.ends_with("\\ ") {
445             line = line.trim_right();
446         }
447         if line.is_empty() {
448             return Ok(self);
449         }
450         let mut glob = Glob {
451             from: from,
452             original: line.to_string(),
453             actual: String::new(),
454             is_whitelist: false,
455             is_only_dir: false,
456         };
457         let mut is_absolute = false;
458         if line.starts_with("\\!") || line.starts_with("\\#") {
459             line = &line[1..];
460             is_absolute = line.chars().nth(0) == Some('/');
461         } else {
462             if line.starts_with("!") {
463                 glob.is_whitelist = true;
464                 line = &line[1..];
465             }
466             if line.starts_with("/") {
467                 // `man gitignore` says that if a glob starts with a slash,
468                 // then the glob can only match the beginning of a path
469                 // (relative to the location of gitignore). We achieve this by
470                 // simply banning wildcards from matching /.
471                 line = &line[1..];
472                 is_absolute = true;
473             }
474         }
475         // If it ends with a slash, then this should only match directories,
476         // but the slash should otherwise not be used while globbing.
477         if let Some((i, c)) = line.char_indices().rev().nth(0) {
478             if c == '/' {
479                 glob.is_only_dir = true;
480                 line = &line[..i];
481             }
482         }
483         glob.actual = line.to_string();
484         // If there is a literal slash, then this is a glob that must match the
485         // entire path name. Otherwise, we should let it match anywhere, so use
486         // a **/ prefix.
487         if !is_absolute && !line.chars().any(|c| c == '/') {
488             // ... but only if we don't already have a **/ prefix.
489             if !glob.has_doublestar_prefix() {
490                 glob.actual = format!("**/{}", glob.actual);
491             }
492         }
493         // If the glob ends with `/**`, then we should only match everything
494         // inside a directory, but not the directory itself. Standard globs
495         // will match the directory. So we add `/*` to force the issue.
496         if glob.actual.ends_with("/**") {
497             glob.actual = format!("{}/*", glob.actual);
498         }
499         let parsed = GlobBuilder::new(&glob.actual)
500             .literal_separator(true)
501             .case_insensitive(self.case_insensitive)
502             .backslash_escape(true)
503             .build()
504             .map_err(|err| Error::Glob {
505                 glob: Some(glob.original.clone()),
506                 err: err.kind().to_string(),
507             })?;
508         self.builder.add(parsed);
509         self.globs.push(glob);
510         Ok(self)
511     }
512 
513     /// Toggle whether the globs should be matched case insensitively or not.
514     ///
515     /// When this option is changed, only globs added after the change will be
516     /// affected.
517     ///
518     /// This is disabled by default.
case_insensitive( &mut self, yes: bool, ) -> Result<&mut GitignoreBuilder, Error>519     pub fn case_insensitive(
520         &mut self,
521         yes: bool,
522     ) -> Result<&mut GitignoreBuilder, Error> {
523         // TODO: This should not return a `Result`. Fix this in the next semver
524         // release.
525         self.case_insensitive = yes;
526         Ok(self)
527     }
528 }
529 
530 /// Return the file path of the current environment's global gitignore file.
531 ///
532 /// Note that the file path returned may not exist.
gitconfig_excludes_path() -> Option<PathBuf>533 fn gitconfig_excludes_path() -> Option<PathBuf> {
534     // git supports $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config. Notably,
535     // both can be active at the same time, where $HOME/.gitconfig takes
536     // precedent. So if $HOME/.gitconfig defines a `core.excludesFile`, then
537     // we're done.
538     match gitconfig_home_contents().and_then(|x| parse_excludes_file(&x)) {
539         Some(path) => return Some(path),
540         None => {}
541     }
542     match gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x)) {
543         Some(path) => return Some(path),
544         None => {}
545     }
546     excludes_file_default()
547 }
548 
549 /// Returns the file contents of git's global config file, if one exists, in
550 /// the user's home directory.
gitconfig_home_contents() -> Option<Vec<u8>>551 fn gitconfig_home_contents() -> Option<Vec<u8>> {
552     let home = match home_dir() {
553         None => return None,
554         Some(home) => home,
555     };
556     let mut file = match File::open(home.join(".gitconfig")) {
557         Err(_) => return None,
558         Ok(file) => io::BufReader::new(file),
559     };
560     let mut contents = vec![];
561     file.read_to_end(&mut contents).ok().map(|_| contents)
562 }
563 
564 /// Returns the file contents of git's global config file, if one exists, in
565 /// the user's XDG_CONFIG_HOME directory.
gitconfig_xdg_contents() -> Option<Vec<u8>>566 fn gitconfig_xdg_contents() -> Option<Vec<u8>> {
567     let path = env::var_os("XDG_CONFIG_HOME")
568         .and_then(|x| if x.is_empty() { None } else { Some(PathBuf::from(x)) })
569         .or_else(|| home_dir().map(|p| p.join(".config")))
570         .map(|x| x.join("git/config"));
571     let mut file = match path.and_then(|p| File::open(p).ok()) {
572         None => return None,
573         Some(file) => io::BufReader::new(file),
574     };
575     let mut contents = vec![];
576     file.read_to_end(&mut contents).ok().map(|_| contents)
577 }
578 
579 /// Returns the default file path for a global .gitignore file.
580 ///
581 /// Specifically, this respects XDG_CONFIG_HOME.
excludes_file_default() -> Option<PathBuf>582 fn excludes_file_default() -> Option<PathBuf> {
583     env::var_os("XDG_CONFIG_HOME")
584         .and_then(|x| if x.is_empty() { None } else { Some(PathBuf::from(x)) })
585         .or_else(|| home_dir().map(|p| p.join(".config")))
586         .map(|x| x.join("git/ignore"))
587 }
588 
589 /// Extract git's `core.excludesfile` config setting from the raw file contents
590 /// given.
parse_excludes_file(data: &[u8]) -> Option<PathBuf>591 fn parse_excludes_file(data: &[u8]) -> Option<PathBuf> {
592     // N.B. This is the lazy approach, and isn't technically correct, but
593     // probably works in more circumstances. I guess we would ideally have
594     // a full INI parser. Yuck.
595     lazy_static::lazy_static! {
596         static ref RE: Regex =
597             Regex::new(r"(?im)^\s*excludesfile\s*=\s*(.+)\s*$").unwrap();
598     };
599     let caps = match RE.captures(data) {
600         None => return None,
601         Some(caps) => caps,
602     };
603     str::from_utf8(&caps[1]).ok().map(|s| PathBuf::from(expand_tilde(s)))
604 }
605 
606 /// Expands ~ in file paths to the value of $HOME.
expand_tilde(path: &str) -> String607 fn expand_tilde(path: &str) -> String {
608     let home = match home_dir() {
609         None => return path.to_string(),
610         Some(home) => home.to_string_lossy().into_owned(),
611     };
612     path.replace("~", &home)
613 }
614 
615 /// Returns the location of the user's home directory.
home_dir() -> Option<PathBuf>616 fn home_dir() -> Option<PathBuf> {
617     // We're fine with using env::home_dir for now. Its bugs are, IMO, pretty
618     // minor corner cases. We should still probably eventually migrate to
619     // the `dirs` crate to get a proper implementation.
620     #![allow(deprecated)]
621     env::home_dir()
622 }
623 
624 #[cfg(test)]
625 mod tests {
626     use super::{Gitignore, GitignoreBuilder};
627     use std::path::Path;
628 
gi_from_str<P: AsRef<Path>>(root: P, s: &str) -> Gitignore629     fn gi_from_str<P: AsRef<Path>>(root: P, s: &str) -> Gitignore {
630         let mut builder = GitignoreBuilder::new(root);
631         builder.add_str(None, s).unwrap();
632         builder.build().unwrap()
633     }
634 
635     macro_rules! ignored {
636         ($name:ident, $root:expr, $gi:expr, $path:expr) => {
637             ignored!($name, $root, $gi, $path, false);
638         };
639         ($name:ident, $root:expr, $gi:expr, $path:expr, $is_dir:expr) => {
640             #[test]
641             fn $name() {
642                 let gi = gi_from_str($root, $gi);
643                 assert!(gi.matched($path, $is_dir).is_ignore());
644             }
645         };
646     }
647 
648     macro_rules! not_ignored {
649         ($name:ident, $root:expr, $gi:expr, $path:expr) => {
650             not_ignored!($name, $root, $gi, $path, false);
651         };
652         ($name:ident, $root:expr, $gi:expr, $path:expr, $is_dir:expr) => {
653             #[test]
654             fn $name() {
655                 let gi = gi_from_str($root, $gi);
656                 assert!(!gi.matched($path, $is_dir).is_ignore());
657             }
658         };
659     }
660 
661     const ROOT: &'static str = "/home/foobar/rust/rg";
662 
663     ignored!(ig1, ROOT, "months", "months");
664     ignored!(ig2, ROOT, "*.lock", "Cargo.lock");
665     ignored!(ig3, ROOT, "*.rs", "src/main.rs");
666     ignored!(ig4, ROOT, "src/*.rs", "src/main.rs");
667     ignored!(ig5, ROOT, "/*.c", "cat-file.c");
668     ignored!(ig6, ROOT, "/src/*.rs", "src/main.rs");
669     ignored!(ig7, ROOT, "!src/main.rs\n*.rs", "src/main.rs");
670     ignored!(ig8, ROOT, "foo/", "foo", true);
671     ignored!(ig9, ROOT, "**/foo", "foo");
672     ignored!(ig10, ROOT, "**/foo", "src/foo");
673     ignored!(ig11, ROOT, "**/foo/**", "src/foo/bar");
674     ignored!(ig12, ROOT, "**/foo/**", "wat/src/foo/bar/baz");
675     ignored!(ig13, ROOT, "**/foo/bar", "foo/bar");
676     ignored!(ig14, ROOT, "**/foo/bar", "src/foo/bar");
677     ignored!(ig15, ROOT, "abc/**", "abc/x");
678     ignored!(ig16, ROOT, "abc/**", "abc/x/y");
679     ignored!(ig17, ROOT, "abc/**", "abc/x/y/z");
680     ignored!(ig18, ROOT, "a/**/b", "a/b");
681     ignored!(ig19, ROOT, "a/**/b", "a/x/b");
682     ignored!(ig20, ROOT, "a/**/b", "a/x/y/b");
683     ignored!(ig21, ROOT, r"\!xy", "!xy");
684     ignored!(ig22, ROOT, r"\#foo", "#foo");
685     ignored!(ig23, ROOT, "foo", "./foo");
686     ignored!(ig24, ROOT, "target", "grep/target");
687     ignored!(ig25, ROOT, "Cargo.lock", "./tabwriter-bin/Cargo.lock");
688     ignored!(ig26, ROOT, "/foo/bar/baz", "./foo/bar/baz");
689     ignored!(ig27, ROOT, "foo/", "xyz/foo", true);
690     ignored!(ig28, "./src", "/llvm/", "./src/llvm", true);
691     ignored!(ig29, ROOT, "node_modules/ ", "node_modules", true);
692     ignored!(ig30, ROOT, "**/", "foo/bar", true);
693     ignored!(ig31, ROOT, "path1/*", "path1/foo");
694     ignored!(ig32, ROOT, ".a/b", ".a/b");
695     ignored!(ig33, "./", ".a/b", ".a/b");
696     ignored!(ig34, ".", ".a/b", ".a/b");
697     ignored!(ig35, "./.", ".a/b", ".a/b");
698     ignored!(ig36, "././", ".a/b", ".a/b");
699     ignored!(ig37, "././.", ".a/b", ".a/b");
700     ignored!(ig38, ROOT, "\\[", "[");
701     ignored!(ig39, ROOT, "\\?", "?");
702     ignored!(ig40, ROOT, "\\*", "*");
703     ignored!(ig41, ROOT, "\\a", "a");
704     ignored!(ig42, ROOT, "s*.rs", "sfoo.rs");
705     ignored!(ig43, ROOT, "**", "foo.rs");
706     ignored!(ig44, ROOT, "**/**/*", "a/foo.rs");
707 
708     not_ignored!(ignot1, ROOT, "amonths", "months");
709     not_ignored!(ignot2, ROOT, "monthsa", "months");
710     not_ignored!(ignot3, ROOT, "/src/*.rs", "src/grep/src/main.rs");
711     not_ignored!(ignot4, ROOT, "/*.c", "mozilla-sha1/sha1.c");
712     not_ignored!(ignot5, ROOT, "/src/*.rs", "src/grep/src/main.rs");
713     not_ignored!(ignot6, ROOT, "*.rs\n!src/main.rs", "src/main.rs");
714     not_ignored!(ignot7, ROOT, "foo/", "foo", false);
715     not_ignored!(ignot8, ROOT, "**/foo/**", "wat/src/afoo/bar/baz");
716     not_ignored!(ignot9, ROOT, "**/foo/**", "wat/src/fooa/bar/baz");
717     not_ignored!(ignot10, ROOT, "**/foo/bar", "foo/src/bar");
718     not_ignored!(ignot11, ROOT, "#foo", "#foo");
719     not_ignored!(ignot12, ROOT, "\n\n\n", "foo");
720     not_ignored!(ignot13, ROOT, "foo/**", "foo", true);
721     not_ignored!(
722         ignot14,
723         "./third_party/protobuf",
724         "m4/ltoptions.m4",
725         "./third_party/protobuf/csharp/src/packages/repositories.config"
726     );
727     not_ignored!(ignot15, ROOT, "!/bar", "foo/bar");
728     not_ignored!(ignot16, ROOT, "*\n!**/", "foo", true);
729     not_ignored!(ignot17, ROOT, "src/*.rs", "src/grep/src/main.rs");
730     not_ignored!(ignot18, ROOT, "path1/*", "path2/path1/foo");
731     not_ignored!(ignot19, ROOT, "s*.rs", "src/foo.rs");
732 
bytes(s: &str) -> Vec<u8>733     fn bytes(s: &str) -> Vec<u8> {
734         s.to_string().into_bytes()
735     }
736 
path_string<P: AsRef<Path>>(path: P) -> String737     fn path_string<P: AsRef<Path>>(path: P) -> String {
738         path.as_ref().to_str().unwrap().to_string()
739     }
740 
741     #[test]
parse_excludes_file1()742     fn parse_excludes_file1() {
743         let data = bytes("[core]\nexcludesFile = /foo/bar");
744         let got = super::parse_excludes_file(&data).unwrap();
745         assert_eq!(path_string(got), "/foo/bar");
746     }
747 
748     #[test]
parse_excludes_file2()749     fn parse_excludes_file2() {
750         let data = bytes("[core]\nexcludesFile = ~/foo/bar");
751         let got = super::parse_excludes_file(&data).unwrap();
752         assert_eq!(path_string(got), super::expand_tilde("~/foo/bar"));
753     }
754 
755     #[test]
parse_excludes_file3()756     fn parse_excludes_file3() {
757         let data = bytes("[core]\nexcludeFile = /foo/bar");
758         assert!(super::parse_excludes_file(&data).is_none());
759     }
760 
761     // See: https://github.com/BurntSushi/ripgrep/issues/106
762     #[test]
regression_106()763     fn regression_106() {
764         gi_from_str("/", " ");
765     }
766 
767     #[test]
case_insensitive()768     fn case_insensitive() {
769         let gi = GitignoreBuilder::new(ROOT)
770             .case_insensitive(true)
771             .unwrap()
772             .add_str(None, "*.html")
773             .unwrap()
774             .build()
775             .unwrap();
776         assert!(gi.matched("foo.html", false).is_ignore());
777         assert!(gi.matched("foo.HTML", false).is_ignore());
778         assert!(!gi.matched("foo.htm", false).is_ignore());
779         assert!(!gi.matched("foo.HTM", false).is_ignore());
780     }
781 
782     ignored!(cs1, ROOT, "*.html", "foo.html");
783     not_ignored!(cs2, ROOT, "*.html", "foo.HTML");
784     not_ignored!(cs3, ROOT, "*.html", "foo.htm");
785     not_ignored!(cs4, ROOT, "*.html", "foo.HTM");
786 }
787