ignore/
gitignore.rs

1/*!
2The gitignore module provides a way to match globs from a gitignore file
3against file paths.
4
5Note 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
7the `git` command line tool.
8*/
9
10use std::cell::RefCell;
11use std::env;
12use std::fs::File;
13use std::io::{self, BufRead, Read};
14use std::path::{Path, PathBuf};
15use std::str;
16use std::sync::Arc;
17
18use globset::{Candidate, GlobBuilder, GlobSet, GlobSetBuilder};
19use regex::bytes::Regex;
20use thread_local::ThreadLocal;
21
22use crate::pathutil::{is_file_name, strip_prefix};
23use 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)]
30pub 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
43impl Glob {
44    /// Returns the file path that defined this glob.
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.
50    pub fn original(&self) -> &str {
51        &self.original
52    }
53
54    /// The actual glob that was compiled to respect gitignore
55    /// semantics.
56    pub fn actual(&self) -> &str {
57        &self.actual
58    }
59
60    /// Whether this was a whitelisted glob or not.
61    pub fn is_whitelist(&self) -> bool {
62        self.is_whitelist
63    }
64
65    /// Whether this glob must match a directory or not.
66    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.
71    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)]
79pub 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
88impl 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`.
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.
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.
135    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.
149    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.
155    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`.
161    pub fn len(&self) -> usize {
162        self.set.len()
163    }
164
165    /// Returns the total number of ignore globs.
166    pub fn num_ignores(&self) -> u64 {
167        self.num_ignores
168    }
169
170    /// Returns the total number of whitelisted globs.
171    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.
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.
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.
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.
271    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)]
305pub struct GitignoreBuilder {
306    builder: GlobSetBuilder,
307    root: PathBuf,
308    globs: Vec<Glob>,
309    case_insensitive: bool,
310}
311
312impl 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.
319    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.
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.
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.
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)]
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.
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 line.as_bytes().last() == Some(&b'/') {
478            glob.is_only_dir = true;
479            line = &line[..line.len() - 1];
480            // If the slash was escaped, then remove the escape.
481            // See: https://github.com/BurntSushi/ripgrep/issues/2236
482            if line.as_bytes().last() == Some(&b'\\') {
483                line = &line[..line.len() - 1];
484            }
485        }
486        glob.actual = line.to_string();
487        // If there is a literal slash, then this is a glob that must match the
488        // entire path name. Otherwise, we should let it match anywhere, so use
489        // a **/ prefix.
490        if !is_absolute && !line.chars().any(|c| c == '/') {
491            // ... but only if we don't already have a **/ prefix.
492            if !glob.has_doublestar_prefix() {
493                glob.actual = format!("**/{}", glob.actual);
494            }
495        }
496        // If the glob ends with `/**`, then we should only match everything
497        // inside a directory, but not the directory itself. Standard globs
498        // will match the directory. So we add `/*` to force the issue.
499        if glob.actual.ends_with("/**") {
500            glob.actual = format!("{}/*", glob.actual);
501        }
502        let parsed = GlobBuilder::new(&glob.actual)
503            .literal_separator(true)
504            .case_insensitive(self.case_insensitive)
505            .backslash_escape(true)
506            .build()
507            .map_err(|err| Error::Glob {
508                glob: Some(glob.original.clone()),
509                err: err.kind().to_string(),
510            })?;
511        self.builder.add(parsed);
512        self.globs.push(glob);
513        Ok(self)
514    }
515
516    /// Toggle whether the globs should be matched case insensitively or not.
517    ///
518    /// When this option is changed, only globs added after the change will be
519    /// affected.
520    ///
521    /// This is disabled by default.
522    pub fn case_insensitive(
523        &mut self,
524        yes: bool,
525    ) -> Result<&mut GitignoreBuilder, Error> {
526        // TODO: This should not return a `Result`. Fix this in the next semver
527        // release.
528        self.case_insensitive = yes;
529        Ok(self)
530    }
531}
532
533/// Return the file path of the current environment's global gitignore file.
534///
535/// Note that the file path returned may not exist.
536fn gitconfig_excludes_path() -> Option<PathBuf> {
537    // git supports $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config. Notably,
538    // both can be active at the same time, where $HOME/.gitconfig takes
539    // precedent. So if $HOME/.gitconfig defines a `core.excludesFile`, then
540    // we're done.
541    match gitconfig_home_contents().and_then(|x| parse_excludes_file(&x)) {
542        Some(path) => return Some(path),
543        None => {}
544    }
545    match gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x)) {
546        Some(path) => return Some(path),
547        None => {}
548    }
549    excludes_file_default()
550}
551
552/// Returns the file contents of git's global config file, if one exists, in
553/// the user's home directory.
554fn gitconfig_home_contents() -> Option<Vec<u8>> {
555    let home = match home_dir() {
556        None => return None,
557        Some(home) => home,
558    };
559    let mut file = match File::open(home.join(".gitconfig")) {
560        Err(_) => return None,
561        Ok(file) => io::BufReader::new(file),
562    };
563    let mut contents = vec![];
564    file.read_to_end(&mut contents).ok().map(|_| contents)
565}
566
567/// Returns the file contents of git's global config file, if one exists, in
568/// the user's XDG_CONFIG_HOME directory.
569fn gitconfig_xdg_contents() -> Option<Vec<u8>> {
570    let path = env::var_os("XDG_CONFIG_HOME")
571        .and_then(|x| if x.is_empty() { None } else { Some(PathBuf::from(x)) })
572        .or_else(|| home_dir().map(|p| p.join(".config")))
573        .map(|x| x.join("git/config"));
574    let mut file = match path.and_then(|p| File::open(p).ok()) {
575        None => return None,
576        Some(file) => io::BufReader::new(file),
577    };
578    let mut contents = vec![];
579    file.read_to_end(&mut contents).ok().map(|_| contents)
580}
581
582/// Returns the default file path for a global .gitignore file.
583///
584/// Specifically, this respects XDG_CONFIG_HOME.
585fn excludes_file_default() -> Option<PathBuf> {
586    env::var_os("XDG_CONFIG_HOME")
587        .and_then(|x| if x.is_empty() { None } else { Some(PathBuf::from(x)) })
588        .or_else(|| home_dir().map(|p| p.join(".config")))
589        .map(|x| x.join("git/ignore"))
590}
591
592/// Extract git's `core.excludesfile` config setting from the raw file contents
593/// given.
594fn parse_excludes_file(data: &[u8]) -> Option<PathBuf> {
595    // N.B. This is the lazy approach, and isn't technically correct, but
596    // probably works in more circumstances. I guess we would ideally have
597    // a full INI parser. Yuck.
598    lazy_static::lazy_static! {
599        static ref RE: Regex =
600            Regex::new(r"(?im)^\s*excludesfile\s*=\s*(.+)\s*$").unwrap();
601    };
602    let caps = match RE.captures(data) {
603        None => return None,
604        Some(caps) => caps,
605    };
606    str::from_utf8(&caps[1]).ok().map(|s| PathBuf::from(expand_tilde(s)))
607}
608
609/// Expands ~ in file paths to the value of $HOME.
610fn expand_tilde(path: &str) -> String {
611    let home = match home_dir() {
612        None => return path.to_string(),
613        Some(home) => home.to_string_lossy().into_owned(),
614    };
615    path.replace("~", &home)
616}
617
618/// Returns the location of the user's home directory.
619fn home_dir() -> Option<PathBuf> {
620    // We're fine with using env::home_dir for now. Its bugs are, IMO, pretty
621    // minor corner cases. We should still probably eventually migrate to
622    // the `dirs` crate to get a proper implementation.
623    #![allow(deprecated)]
624    env::home_dir()
625}
626
627#[cfg(test)]
628mod tests {
629    use super::{Gitignore, GitignoreBuilder};
630    use std::path::Path;
631
632    fn gi_from_str<P: AsRef<Path>>(root: P, s: &str) -> Gitignore {
633        let mut builder = GitignoreBuilder::new(root);
634        builder.add_str(None, s).unwrap();
635        builder.build().unwrap()
636    }
637
638    macro_rules! ignored {
639        ($name:ident, $root:expr, $gi:expr, $path:expr) => {
640            ignored!($name, $root, $gi, $path, false);
641        };
642        ($name:ident, $root:expr, $gi:expr, $path:expr, $is_dir:expr) => {
643            #[test]
644            fn $name() {
645                let gi = gi_from_str($root, $gi);
646                assert!(gi.matched($path, $is_dir).is_ignore());
647            }
648        };
649    }
650
651    macro_rules! not_ignored {
652        ($name:ident, $root:expr, $gi:expr, $path:expr) => {
653            not_ignored!($name, $root, $gi, $path, false);
654        };
655        ($name:ident, $root:expr, $gi:expr, $path:expr, $is_dir:expr) => {
656            #[test]
657            fn $name() {
658                let gi = gi_from_str($root, $gi);
659                assert!(!gi.matched($path, $is_dir).is_ignore());
660            }
661        };
662    }
663
664    const ROOT: &'static str = "/home/foobar/rust/rg";
665
666    ignored!(ig1, ROOT, "months", "months");
667    ignored!(ig2, ROOT, "*.lock", "Cargo.lock");
668    ignored!(ig3, ROOT, "*.rs", "src/main.rs");
669    ignored!(ig4, ROOT, "src/*.rs", "src/main.rs");
670    ignored!(ig5, ROOT, "/*.c", "cat-file.c");
671    ignored!(ig6, ROOT, "/src/*.rs", "src/main.rs");
672    ignored!(ig7, ROOT, "!src/main.rs\n*.rs", "src/main.rs");
673    ignored!(ig8, ROOT, "foo/", "foo", true);
674    ignored!(ig9, ROOT, "**/foo", "foo");
675    ignored!(ig10, ROOT, "**/foo", "src/foo");
676    ignored!(ig11, ROOT, "**/foo/**", "src/foo/bar");
677    ignored!(ig12, ROOT, "**/foo/**", "wat/src/foo/bar/baz");
678    ignored!(ig13, ROOT, "**/foo/bar", "foo/bar");
679    ignored!(ig14, ROOT, "**/foo/bar", "src/foo/bar");
680    ignored!(ig15, ROOT, "abc/**", "abc/x");
681    ignored!(ig16, ROOT, "abc/**", "abc/x/y");
682    ignored!(ig17, ROOT, "abc/**", "abc/x/y/z");
683    ignored!(ig18, ROOT, "a/**/b", "a/b");
684    ignored!(ig19, ROOT, "a/**/b", "a/x/b");
685    ignored!(ig20, ROOT, "a/**/b", "a/x/y/b");
686    ignored!(ig21, ROOT, r"\!xy", "!xy");
687    ignored!(ig22, ROOT, r"\#foo", "#foo");
688    ignored!(ig23, ROOT, "foo", "./foo");
689    ignored!(ig24, ROOT, "target", "grep/target");
690    ignored!(ig25, ROOT, "Cargo.lock", "./tabwriter-bin/Cargo.lock");
691    ignored!(ig26, ROOT, "/foo/bar/baz", "./foo/bar/baz");
692    ignored!(ig27, ROOT, "foo/", "xyz/foo", true);
693    ignored!(ig28, "./src", "/llvm/", "./src/llvm", true);
694    ignored!(ig29, ROOT, "node_modules/ ", "node_modules", true);
695    ignored!(ig30, ROOT, "**/", "foo/bar", true);
696    ignored!(ig31, ROOT, "path1/*", "path1/foo");
697    ignored!(ig32, ROOT, ".a/b", ".a/b");
698    ignored!(ig33, "./", ".a/b", ".a/b");
699    ignored!(ig34, ".", ".a/b", ".a/b");
700    ignored!(ig35, "./.", ".a/b", ".a/b");
701    ignored!(ig36, "././", ".a/b", ".a/b");
702    ignored!(ig37, "././.", ".a/b", ".a/b");
703    ignored!(ig38, ROOT, "\\[", "[");
704    ignored!(ig39, ROOT, "\\?", "?");
705    ignored!(ig40, ROOT, "\\*", "*");
706    ignored!(ig41, ROOT, "\\a", "a");
707    ignored!(ig42, ROOT, "s*.rs", "sfoo.rs");
708    ignored!(ig43, ROOT, "**", "foo.rs");
709    ignored!(ig44, ROOT, "**/**/*", "a/foo.rs");
710
711    not_ignored!(ignot1, ROOT, "amonths", "months");
712    not_ignored!(ignot2, ROOT, "monthsa", "months");
713    not_ignored!(ignot3, ROOT, "/src/*.rs", "src/grep/src/main.rs");
714    not_ignored!(ignot4, ROOT, "/*.c", "mozilla-sha1/sha1.c");
715    not_ignored!(ignot5, ROOT, "/src/*.rs", "src/grep/src/main.rs");
716    not_ignored!(ignot6, ROOT, "*.rs\n!src/main.rs", "src/main.rs");
717    not_ignored!(ignot7, ROOT, "foo/", "foo", false);
718    not_ignored!(ignot8, ROOT, "**/foo/**", "wat/src/afoo/bar/baz");
719    not_ignored!(ignot9, ROOT, "**/foo/**", "wat/src/fooa/bar/baz");
720    not_ignored!(ignot10, ROOT, "**/foo/bar", "foo/src/bar");
721    not_ignored!(ignot11, ROOT, "#foo", "#foo");
722    not_ignored!(ignot12, ROOT, "\n\n\n", "foo");
723    not_ignored!(ignot13, ROOT, "foo/**", "foo", true);
724    not_ignored!(
725        ignot14,
726        "./third_party/protobuf",
727        "m4/ltoptions.m4",
728        "./third_party/protobuf/csharp/src/packages/repositories.config"
729    );
730    not_ignored!(ignot15, ROOT, "!/bar", "foo/bar");
731    not_ignored!(ignot16, ROOT, "*\n!**/", "foo", true);
732    not_ignored!(ignot17, ROOT, "src/*.rs", "src/grep/src/main.rs");
733    not_ignored!(ignot18, ROOT, "path1/*", "path2/path1/foo");
734    not_ignored!(ignot19, ROOT, "s*.rs", "src/foo.rs");
735
736    fn bytes(s: &str) -> Vec<u8> {
737        s.to_string().into_bytes()
738    }
739
740    fn path_string<P: AsRef<Path>>(path: P) -> String {
741        path.as_ref().to_str().unwrap().to_string()
742    }
743
744    #[test]
745    fn parse_excludes_file1() {
746        let data = bytes("[core]\nexcludesFile = /foo/bar");
747        let got = super::parse_excludes_file(&data).unwrap();
748        assert_eq!(path_string(got), "/foo/bar");
749    }
750
751    #[test]
752    fn parse_excludes_file2() {
753        let data = bytes("[core]\nexcludesFile = ~/foo/bar");
754        let got = super::parse_excludes_file(&data).unwrap();
755        assert_eq!(path_string(got), super::expand_tilde("~/foo/bar"));
756    }
757
758    #[test]
759    fn parse_excludes_file3() {
760        let data = bytes("[core]\nexcludeFile = /foo/bar");
761        assert!(super::parse_excludes_file(&data).is_none());
762    }
763
764    // See: https://github.com/BurntSushi/ripgrep/issues/106
765    #[test]
766    fn regression_106() {
767        gi_from_str("/", " ");
768    }
769
770    #[test]
771    fn case_insensitive() {
772        let gi = GitignoreBuilder::new(ROOT)
773            .case_insensitive(true)
774            .unwrap()
775            .add_str(None, "*.html")
776            .unwrap()
777            .build()
778            .unwrap();
779        assert!(gi.matched("foo.html", false).is_ignore());
780        assert!(gi.matched("foo.HTML", false).is_ignore());
781        assert!(!gi.matched("foo.htm", false).is_ignore());
782        assert!(!gi.matched("foo.HTM", false).is_ignore());
783    }
784
785    ignored!(cs1, ROOT, "*.html", "foo.html");
786    not_ignored!(cs2, ROOT, "*.html", "foo.HTML");
787    not_ignored!(cs3, ROOT, "*.html", "foo.htm");
788    not_ignored!(cs4, ROOT, "*.html", "foo.HTM");
789}