1 // This module provides a data structure, `Ignore`, that connects "directory
2 // traversal" with "ignore matchers." Specifically, it knows about gitignore
3 // semantics and precedence, and is organized based on directory hierarchy.
4 // Namely, every matcher logically corresponds to ignore rules from a single
5 // directory, and points to the matcher for its corresponding parent directory.
6 // In this sense, `Ignore` is a *persistent* data structure.
7 //
8 // This design was specifically chosen to make it possible to use this data
9 // structure in a parallel directory iterator.
10 //
11 // My initial intention was to expose this module as part of this crate's
12 // public API, but I think the data structure's public API is too complicated
13 // with non-obvious failure modes. Alas, such things haven't been documented
14 // well.
15 
16 use std::collections::HashMap;
17 use std::ffi::{OsStr, OsString};
18 use std::fs::{File, FileType};
19 use std::io::{self, BufRead};
20 use std::path::{Path, PathBuf};
21 use std::sync::{Arc, RwLock};
22 
23 use crate::gitignore::{self, Gitignore, GitignoreBuilder};
24 use crate::overrides::{self, Override};
25 use crate::pathutil::{is_hidden, strip_prefix};
26 use crate::types::{self, Types};
27 use crate::walk::DirEntry;
28 use crate::{Error, Match, PartialErrorBuilder};
29 
30 /// IgnoreMatch represents information about where a match came from when using
31 /// the `Ignore` matcher.
32 #[derive(Clone, Debug)]
33 pub struct IgnoreMatch<'a>(IgnoreMatchInner<'a>);
34 
35 /// IgnoreMatchInner describes precisely where the match information came from.
36 /// This is private to allow expansion to more matchers in the future.
37 #[derive(Clone, Debug)]
38 enum IgnoreMatchInner<'a> {
39     Override(overrides::Glob<'a>),
40     Gitignore(&'a gitignore::Glob),
41     Types(types::Glob<'a>),
42     Hidden,
43 }
44 
45 impl<'a> IgnoreMatch<'a> {
overrides(x: overrides::Glob<'a>) -> IgnoreMatch<'a>46     fn overrides(x: overrides::Glob<'a>) -> IgnoreMatch<'a> {
47         IgnoreMatch(IgnoreMatchInner::Override(x))
48     }
49 
gitignore(x: &'a gitignore::Glob) -> IgnoreMatch<'a>50     fn gitignore(x: &'a gitignore::Glob) -> IgnoreMatch<'a> {
51         IgnoreMatch(IgnoreMatchInner::Gitignore(x))
52     }
53 
types(x: types::Glob<'a>) -> IgnoreMatch<'a>54     fn types(x: types::Glob<'a>) -> IgnoreMatch<'a> {
55         IgnoreMatch(IgnoreMatchInner::Types(x))
56     }
57 
hidden() -> IgnoreMatch<'static>58     fn hidden() -> IgnoreMatch<'static> {
59         IgnoreMatch(IgnoreMatchInner::Hidden)
60     }
61 }
62 
63 /// Options for the ignore matcher, shared between the matcher itself and the
64 /// builder.
65 #[derive(Clone, Copy, Debug)]
66 struct IgnoreOptions {
67     /// Whether to ignore hidden file paths or not.
68     hidden: bool,
69     /// Whether to read .ignore files.
70     ignore: bool,
71     /// Whether to respect any ignore files in parent directories.
72     parents: bool,
73     /// Whether to read git's global gitignore file.
74     git_global: bool,
75     /// Whether to read .gitignore files.
76     git_ignore: bool,
77     /// Whether to read .git/info/exclude files.
78     git_exclude: bool,
79     /// Whether to ignore files case insensitively
80     ignore_case_insensitive: bool,
81     /// Whether a git repository must be present in order to apply any
82     /// git-related ignore rules.
83     require_git: bool,
84 }
85 
86 /// Ignore is a matcher useful for recursively walking one or more directories.
87 #[derive(Clone, Debug)]
88 pub struct Ignore(Arc<IgnoreInner>);
89 
90 #[derive(Clone, Debug)]
91 struct IgnoreInner {
92     /// A map of all existing directories that have already been
93     /// compiled into matchers.
94     ///
95     /// Note that this is never used during matching, only when adding new
96     /// parent directory matchers. This avoids needing to rebuild glob sets for
97     /// parent directories if many paths are being searched.
98     compiled: Arc<RwLock<HashMap<OsString, Ignore>>>,
99     /// The path to the directory that this matcher was built from.
100     dir: PathBuf,
101     /// An override matcher (default is empty).
102     overrides: Arc<Override>,
103     /// A file type matcher.
104     types: Arc<Types>,
105     /// The parent directory to match next.
106     ///
107     /// If this is the root directory or there are otherwise no more
108     /// directories to match, then `parent` is `None`.
109     parent: Option<Ignore>,
110     /// Whether this is an absolute parent matcher, as added by add_parent.
111     is_absolute_parent: bool,
112     /// The absolute base path of this matcher. Populated only if parent
113     /// directories are added.
114     absolute_base: Option<Arc<PathBuf>>,
115     /// Explicit global ignore matchers specified by the caller.
116     explicit_ignores: Arc<Vec<Gitignore>>,
117     /// Ignore files used in addition to `.ignore`
118     custom_ignore_filenames: Arc<Vec<OsString>>,
119     /// The matcher for custom ignore files
120     custom_ignore_matcher: Gitignore,
121     /// The matcher for .ignore files.
122     ignore_matcher: Gitignore,
123     /// A global gitignore matcher, usually from $XDG_CONFIG_HOME/git/ignore.
124     git_global_matcher: Arc<Gitignore>,
125     /// The matcher for .gitignore files.
126     git_ignore_matcher: Gitignore,
127     /// Special matcher for `.git/info/exclude` files.
128     git_exclude_matcher: Gitignore,
129     /// Whether this directory contains a .git sub-directory.
130     has_git: bool,
131     /// Ignore config.
132     opts: IgnoreOptions,
133 }
134 
135 impl Ignore {
136     /// Return the directory path of this matcher.
path(&self) -> &Path137     pub fn path(&self) -> &Path {
138         &self.0.dir
139     }
140 
141     /// Return true if this matcher has no parent.
is_root(&self) -> bool142     pub fn is_root(&self) -> bool {
143         self.0.parent.is_none()
144     }
145 
146     /// Returns true if this matcher was added via the `add_parents` method.
is_absolute_parent(&self) -> bool147     pub fn is_absolute_parent(&self) -> bool {
148         self.0.is_absolute_parent
149     }
150 
151     /// Return this matcher's parent, if one exists.
parent(&self) -> Option<Ignore>152     pub fn parent(&self) -> Option<Ignore> {
153         self.0.parent.clone()
154     }
155 
156     /// Create a new `Ignore` matcher with the parent directories of `dir`.
157     ///
158     /// Note that this can only be called on an `Ignore` matcher with no
159     /// parents (i.e., `is_root` returns `true`). This will panic otherwise.
add_parents<P: AsRef<Path>>( &self, path: P, ) -> (Ignore, Option<Error>)160     pub fn add_parents<P: AsRef<Path>>(
161         &self,
162         path: P,
163     ) -> (Ignore, Option<Error>) {
164         if !self.0.opts.parents
165             && !self.0.opts.git_ignore
166             && !self.0.opts.git_exclude
167             && !self.0.opts.git_global
168         {
169             // If we never need info from parent directories, then don't do
170             // anything.
171             return (self.clone(), None);
172         }
173         if !self.is_root() {
174             panic!("Ignore::add_parents called on non-root matcher");
175         }
176         let absolute_base = match path.as_ref().canonicalize() {
177             Ok(path) => Arc::new(path),
178             Err(_) => {
179                 // There's not much we can do here, so just return our
180                 // existing matcher. We drop the error to be consistent
181                 // with our general pattern of ignoring I/O errors when
182                 // processing ignore files.
183                 return (self.clone(), None);
184             }
185         };
186         // List of parents, from child to root.
187         let mut parents = vec![];
188         let mut path = &**absolute_base;
189         while let Some(parent) = path.parent() {
190             parents.push(parent);
191             path = parent;
192         }
193         let mut errs = PartialErrorBuilder::default();
194         let mut ig = self.clone();
195         for parent in parents.into_iter().rev() {
196             let mut compiled = self.0.compiled.write().unwrap();
197             if let Some(prebuilt) = compiled.get(parent.as_os_str()) {
198                 ig = prebuilt.clone();
199                 continue;
200             }
201             let (mut igtmp, err) = ig.add_child_path(parent);
202             errs.maybe_push(err);
203             igtmp.is_absolute_parent = true;
204             igtmp.absolute_base = Some(absolute_base.clone());
205             igtmp.has_git = if self.0.opts.git_ignore {
206                 parent.join(".git").exists()
207             } else {
208                 false
209             };
210             ig = Ignore(Arc::new(igtmp));
211             compiled.insert(parent.as_os_str().to_os_string(), ig.clone());
212         }
213         (ig, errs.into_error_option())
214     }
215 
216     /// Create a new `Ignore` matcher for the given child directory.
217     ///
218     /// Since building the matcher may require reading from multiple
219     /// files, it's possible that this method partially succeeds. Therefore,
220     /// a matcher is always returned (which may match nothing) and an error is
221     /// returned if it exists.
222     ///
223     /// Note that all I/O errors are completely ignored.
add_child<P: AsRef<Path>>( &self, dir: P, ) -> (Ignore, Option<Error>)224     pub fn add_child<P: AsRef<Path>>(
225         &self,
226         dir: P,
227     ) -> (Ignore, Option<Error>) {
228         let (ig, err) = self.add_child_path(dir.as_ref());
229         (Ignore(Arc::new(ig)), err)
230     }
231 
232     /// Like add_child, but takes a full path and returns an IgnoreInner.
add_child_path(&self, dir: &Path) -> (IgnoreInner, Option<Error>)233     fn add_child_path(&self, dir: &Path) -> (IgnoreInner, Option<Error>) {
234         let git_type = if self.0.opts.git_ignore || self.0.opts.git_exclude {
235             dir.join(".git").metadata().ok().map(|md| md.file_type())
236         } else {
237             None
238         };
239         let has_git = git_type.map(|_| true).unwrap_or(false);
240 
241         let mut errs = PartialErrorBuilder::default();
242         let custom_ig_matcher = if self.0.custom_ignore_filenames.is_empty() {
243             Gitignore::empty()
244         } else {
245             let (m, err) = create_gitignore(
246                 &dir,
247                 &dir,
248                 &self.0.custom_ignore_filenames,
249                 self.0.opts.ignore_case_insensitive,
250             );
251             errs.maybe_push(err);
252             m
253         };
254         let ig_matcher = if !self.0.opts.ignore {
255             Gitignore::empty()
256         } else {
257             let (m, err) = create_gitignore(
258                 &dir,
259                 &dir,
260                 &[".ignore"],
261                 self.0.opts.ignore_case_insensitive,
262             );
263             errs.maybe_push(err);
264             m
265         };
266         let gi_matcher = if !self.0.opts.git_ignore {
267             Gitignore::empty()
268         } else {
269             let (m, err) = create_gitignore(
270                 &dir,
271                 &dir,
272                 &[".gitignore"],
273                 self.0.opts.ignore_case_insensitive,
274             );
275             errs.maybe_push(err);
276             m
277         };
278         let gi_exclude_matcher = if !self.0.opts.git_exclude {
279             Gitignore::empty()
280         } else {
281             match resolve_git_commondir(dir, git_type) {
282                 Ok(git_dir) => {
283                     let (m, err) = create_gitignore(
284                         &dir,
285                         &git_dir,
286                         &["info/exclude"],
287                         self.0.opts.ignore_case_insensitive,
288                     );
289                     errs.maybe_push(err);
290                     m
291                 }
292                 Err(err) => {
293                     errs.maybe_push(err);
294                     Gitignore::empty()
295                 }
296             }
297         };
298         let ig = IgnoreInner {
299             compiled: self.0.compiled.clone(),
300             dir: dir.to_path_buf(),
301             overrides: self.0.overrides.clone(),
302             types: self.0.types.clone(),
303             parent: Some(self.clone()),
304             is_absolute_parent: false,
305             absolute_base: self.0.absolute_base.clone(),
306             explicit_ignores: self.0.explicit_ignores.clone(),
307             custom_ignore_filenames: self.0.custom_ignore_filenames.clone(),
308             custom_ignore_matcher: custom_ig_matcher,
309             ignore_matcher: ig_matcher,
310             git_global_matcher: self.0.git_global_matcher.clone(),
311             git_ignore_matcher: gi_matcher,
312             git_exclude_matcher: gi_exclude_matcher,
313             has_git,
314             opts: self.0.opts,
315         };
316         (ig, errs.into_error_option())
317     }
318 
319     /// Returns true if at least one type of ignore rule should be matched.
has_any_ignore_rules(&self) -> bool320     fn has_any_ignore_rules(&self) -> bool {
321         let opts = self.0.opts;
322         let has_custom_ignore_files =
323             !self.0.custom_ignore_filenames.is_empty();
324         let has_explicit_ignores = !self.0.explicit_ignores.is_empty();
325 
326         opts.ignore
327             || opts.git_global
328             || opts.git_ignore
329             || opts.git_exclude
330             || has_custom_ignore_files
331             || has_explicit_ignores
332     }
333 
334     /// Like `matched`, but works with a directory entry instead.
matched_dir_entry<'a>( &'a self, dent: &DirEntry, ) -> Match<IgnoreMatch<'a>>335     pub fn matched_dir_entry<'a>(
336         &'a self,
337         dent: &DirEntry,
338     ) -> Match<IgnoreMatch<'a>> {
339         let m = self.matched(dent.path(), dent.is_dir());
340         if m.is_none() && self.0.opts.hidden && is_hidden(dent) {
341             return Match::Ignore(IgnoreMatch::hidden());
342         }
343         m
344     }
345 
346     /// Returns a match indicating whether the given file path should be
347     /// ignored or not.
348     ///
349     /// The match contains information about its origin.
matched<'a, P: AsRef<Path>>( &'a self, path: P, is_dir: bool, ) -> Match<IgnoreMatch<'a>>350     fn matched<'a, P: AsRef<Path>>(
351         &'a self,
352         path: P,
353         is_dir: bool,
354     ) -> Match<IgnoreMatch<'a>> {
355         // We need to be careful with our path. If it has a leading ./, then
356         // strip it because it causes nothing but trouble.
357         let mut path = path.as_ref();
358         if let Some(p) = strip_prefix("./", path) {
359             path = p;
360         }
361         // Match against the override patterns. If an override matches
362         // regardless of whether it's whitelist/ignore, then we quit and
363         // return that result immediately. Overrides have the highest
364         // precedence.
365         if !self.0.overrides.is_empty() {
366             let mat = self
367                 .0
368                 .overrides
369                 .matched(path, is_dir)
370                 .map(IgnoreMatch::overrides);
371             if !mat.is_none() {
372                 return mat;
373             }
374         }
375         let mut whitelisted = Match::None;
376         if self.has_any_ignore_rules() {
377             let mat = self.matched_ignore(path, is_dir);
378             if mat.is_ignore() {
379                 return mat;
380             } else if mat.is_whitelist() {
381                 whitelisted = mat;
382             }
383         }
384         if !self.0.types.is_empty() {
385             let mat =
386                 self.0.types.matched(path, is_dir).map(IgnoreMatch::types);
387             if mat.is_ignore() {
388                 return mat;
389             } else if mat.is_whitelist() {
390                 whitelisted = mat;
391             }
392         }
393         whitelisted
394     }
395 
396     /// Performs matching only on the ignore files for this directory and
397     /// all parent directories.
matched_ignore<'a>( &'a self, path: &Path, is_dir: bool, ) -> Match<IgnoreMatch<'a>>398     fn matched_ignore<'a>(
399         &'a self,
400         path: &Path,
401         is_dir: bool,
402     ) -> Match<IgnoreMatch<'a>> {
403         let (
404             mut m_custom_ignore,
405             mut m_ignore,
406             mut m_gi,
407             mut m_gi_exclude,
408             mut m_explicit,
409         ) = (Match::None, Match::None, Match::None, Match::None, Match::None);
410         let any_git =
411             !self.0.opts.require_git || self.parents().any(|ig| ig.0.has_git);
412         let mut saw_git = false;
413         for ig in self.parents().take_while(|ig| !ig.0.is_absolute_parent) {
414             if m_custom_ignore.is_none() {
415                 m_custom_ignore =
416                     ig.0.custom_ignore_matcher
417                         .matched(path, is_dir)
418                         .map(IgnoreMatch::gitignore);
419             }
420             if m_ignore.is_none() {
421                 m_ignore =
422                     ig.0.ignore_matcher
423                         .matched(path, is_dir)
424                         .map(IgnoreMatch::gitignore);
425             }
426             if any_git && !saw_git && m_gi.is_none() {
427                 m_gi =
428                     ig.0.git_ignore_matcher
429                         .matched(path, is_dir)
430                         .map(IgnoreMatch::gitignore);
431             }
432             if any_git && !saw_git && m_gi_exclude.is_none() {
433                 m_gi_exclude =
434                     ig.0.git_exclude_matcher
435                         .matched(path, is_dir)
436                         .map(IgnoreMatch::gitignore);
437             }
438             saw_git = saw_git || ig.0.has_git;
439         }
440         if self.0.opts.parents {
441             if let Some(abs_parent_path) = self.absolute_base() {
442                 let path = abs_parent_path.join(path);
443                 for ig in
444                     self.parents().skip_while(|ig| !ig.0.is_absolute_parent)
445                 {
446                     if m_custom_ignore.is_none() {
447                         m_custom_ignore =
448                             ig.0.custom_ignore_matcher
449                                 .matched(&path, is_dir)
450                                 .map(IgnoreMatch::gitignore);
451                     }
452                     if m_ignore.is_none() {
453                         m_ignore =
454                             ig.0.ignore_matcher
455                                 .matched(&path, is_dir)
456                                 .map(IgnoreMatch::gitignore);
457                     }
458                     if any_git && !saw_git && m_gi.is_none() {
459                         m_gi =
460                             ig.0.git_ignore_matcher
461                                 .matched(&path, is_dir)
462                                 .map(IgnoreMatch::gitignore);
463                     }
464                     if any_git && !saw_git && m_gi_exclude.is_none() {
465                         m_gi_exclude =
466                             ig.0.git_exclude_matcher
467                                 .matched(&path, is_dir)
468                                 .map(IgnoreMatch::gitignore);
469                     }
470                     saw_git = saw_git || ig.0.has_git;
471                 }
472             }
473         }
474         for gi in self.0.explicit_ignores.iter().rev() {
475             if !m_explicit.is_none() {
476                 break;
477             }
478             m_explicit = gi.matched(&path, is_dir).map(IgnoreMatch::gitignore);
479         }
480         let m_global = if any_git {
481             self.0
482                 .git_global_matcher
483                 .matched(&path, is_dir)
484                 .map(IgnoreMatch::gitignore)
485         } else {
486             Match::None
487         };
488 
489         m_custom_ignore
490             .or(m_ignore)
491             .or(m_gi)
492             .or(m_gi_exclude)
493             .or(m_global)
494             .or(m_explicit)
495     }
496 
497     /// Returns an iterator over parent ignore matchers, including this one.
parents(&self) -> Parents<'_>498     pub fn parents(&self) -> Parents<'_> {
499         Parents(Some(self))
500     }
501 
502     /// Returns the first absolute path of the first absolute parent, if
503     /// one exists.
absolute_base(&self) -> Option<&Path>504     fn absolute_base(&self) -> Option<&Path> {
505         self.0.absolute_base.as_ref().map(|p| &***p)
506     }
507 }
508 
509 /// An iterator over all parents of an ignore matcher, including itself.
510 ///
511 /// The lifetime `'a` refers to the lifetime of the initial `Ignore` matcher.
512 pub struct Parents<'a>(Option<&'a Ignore>);
513 
514 impl<'a> Iterator for Parents<'a> {
515     type Item = &'a Ignore;
516 
next(&mut self) -> Option<&'a Ignore>517     fn next(&mut self) -> Option<&'a Ignore> {
518         match self.0.take() {
519             None => None,
520             Some(ig) => {
521                 self.0 = ig.0.parent.as_ref();
522                 Some(ig)
523             }
524         }
525     }
526 }
527 
528 /// A builder for creating an Ignore matcher.
529 #[derive(Clone, Debug)]
530 pub struct IgnoreBuilder {
531     /// The root directory path for this ignore matcher.
532     dir: PathBuf,
533     /// An override matcher (default is empty).
534     overrides: Arc<Override>,
535     /// A type matcher (default is empty).
536     types: Arc<Types>,
537     /// Explicit global ignore matchers.
538     explicit_ignores: Vec<Gitignore>,
539     /// Ignore files in addition to .ignore.
540     custom_ignore_filenames: Vec<OsString>,
541     /// Ignore config.
542     opts: IgnoreOptions,
543 }
544 
545 impl IgnoreBuilder {
546     /// Create a new builder for an `Ignore` matcher.
547     ///
548     /// All relative file paths are resolved with respect to the current
549     /// working directory.
new() -> IgnoreBuilder550     pub fn new() -> IgnoreBuilder {
551         IgnoreBuilder {
552             dir: Path::new("").to_path_buf(),
553             overrides: Arc::new(Override::empty()),
554             types: Arc::new(Types::empty()),
555             explicit_ignores: vec![],
556             custom_ignore_filenames: vec![],
557             opts: IgnoreOptions {
558                 hidden: true,
559                 ignore: true,
560                 parents: true,
561                 git_global: true,
562                 git_ignore: true,
563                 git_exclude: true,
564                 ignore_case_insensitive: false,
565                 require_git: true,
566             },
567         }
568     }
569 
570     /// Builds a new `Ignore` matcher.
571     ///
572     /// The matcher returned won't match anything until ignore rules from
573     /// directories are added to it.
build(&self) -> Ignore574     pub fn build(&self) -> Ignore {
575         let git_global_matcher = if !self.opts.git_global {
576             Gitignore::empty()
577         } else {
578             let mut builder = GitignoreBuilder::new("");
579             builder
580                 .case_insensitive(self.opts.ignore_case_insensitive)
581                 .unwrap();
582             let (gi, err) = builder.build_global();
583             if let Some(err) = err {
584                 log::debug!("{}", err);
585             }
586             gi
587         };
588 
589         Ignore(Arc::new(IgnoreInner {
590             compiled: Arc::new(RwLock::new(HashMap::new())),
591             dir: self.dir.clone(),
592             overrides: self.overrides.clone(),
593             types: self.types.clone(),
594             parent: None,
595             is_absolute_parent: true,
596             absolute_base: None,
597             explicit_ignores: Arc::new(self.explicit_ignores.clone()),
598             custom_ignore_filenames: Arc::new(
599                 self.custom_ignore_filenames.clone(),
600             ),
601             custom_ignore_matcher: Gitignore::empty(),
602             ignore_matcher: Gitignore::empty(),
603             git_global_matcher: Arc::new(git_global_matcher),
604             git_ignore_matcher: Gitignore::empty(),
605             git_exclude_matcher: Gitignore::empty(),
606             has_git: false,
607             opts: self.opts,
608         }))
609     }
610 
611     /// Add an override matcher.
612     ///
613     /// By default, no override matcher is used.
614     ///
615     /// This overrides any previous setting.
overrides(&mut self, overrides: Override) -> &mut IgnoreBuilder616     pub fn overrides(&mut self, overrides: Override) -> &mut IgnoreBuilder {
617         self.overrides = Arc::new(overrides);
618         self
619     }
620 
621     /// Add a file type matcher.
622     ///
623     /// By default, no file type matcher is used.
624     ///
625     /// This overrides any previous setting.
types(&mut self, types: Types) -> &mut IgnoreBuilder626     pub fn types(&mut self, types: Types) -> &mut IgnoreBuilder {
627         self.types = Arc::new(types);
628         self
629     }
630 
631     /// Adds a new global ignore matcher from the ignore file path given.
add_ignore(&mut self, ig: Gitignore) -> &mut IgnoreBuilder632     pub fn add_ignore(&mut self, ig: Gitignore) -> &mut IgnoreBuilder {
633         self.explicit_ignores.push(ig);
634         self
635     }
636 
637     /// Add a custom ignore file name
638     ///
639     /// These ignore files have higher precedence than all other ignore files.
640     ///
641     /// When specifying multiple names, earlier names have lower precedence than
642     /// later names.
add_custom_ignore_filename<S: AsRef<OsStr>>( &mut self, file_name: S, ) -> &mut IgnoreBuilder643     pub fn add_custom_ignore_filename<S: AsRef<OsStr>>(
644         &mut self,
645         file_name: S,
646     ) -> &mut IgnoreBuilder {
647         self.custom_ignore_filenames.push(file_name.as_ref().to_os_string());
648         self
649     }
650 
651     /// Enables ignoring hidden files.
652     ///
653     /// This is enabled by default.
hidden(&mut self, yes: bool) -> &mut IgnoreBuilder654     pub fn hidden(&mut self, yes: bool) -> &mut IgnoreBuilder {
655         self.opts.hidden = yes;
656         self
657     }
658 
659     /// Enables reading `.ignore` files.
660     ///
661     /// `.ignore` files have the same semantics as `gitignore` files and are
662     /// supported by search tools such as ripgrep and The Silver Searcher.
663     ///
664     /// This is enabled by default.
ignore(&mut self, yes: bool) -> &mut IgnoreBuilder665     pub fn ignore(&mut self, yes: bool) -> &mut IgnoreBuilder {
666         self.opts.ignore = yes;
667         self
668     }
669 
670     /// Enables reading ignore files from parent directories.
671     ///
672     /// If this is enabled, then .gitignore files in parent directories of each
673     /// file path given are respected. Otherwise, they are ignored.
674     ///
675     /// This is enabled by default.
parents(&mut self, yes: bool) -> &mut IgnoreBuilder676     pub fn parents(&mut self, yes: bool) -> &mut IgnoreBuilder {
677         self.opts.parents = yes;
678         self
679     }
680 
681     /// Add a global gitignore matcher.
682     ///
683     /// Its precedence is lower than both normal `.gitignore` files and
684     /// `.git/info/exclude` files.
685     ///
686     /// This overwrites any previous global gitignore setting.
687     ///
688     /// This is enabled by default.
git_global(&mut self, yes: bool) -> &mut IgnoreBuilder689     pub fn git_global(&mut self, yes: bool) -> &mut IgnoreBuilder {
690         self.opts.git_global = yes;
691         self
692     }
693 
694     /// Enables reading `.gitignore` files.
695     ///
696     /// `.gitignore` files have match semantics as described in the `gitignore`
697     /// man page.
698     ///
699     /// This is enabled by default.
git_ignore(&mut self, yes: bool) -> &mut IgnoreBuilder700     pub fn git_ignore(&mut self, yes: bool) -> &mut IgnoreBuilder {
701         self.opts.git_ignore = yes;
702         self
703     }
704 
705     /// Enables reading `.git/info/exclude` files.
706     ///
707     /// `.git/info/exclude` files have match semantics as described in the
708     /// `gitignore` man page.
709     ///
710     /// This is enabled by default.
git_exclude(&mut self, yes: bool) -> &mut IgnoreBuilder711     pub fn git_exclude(&mut self, yes: bool) -> &mut IgnoreBuilder {
712         self.opts.git_exclude = yes;
713         self
714     }
715 
716     /// Whether a git repository is required to apply git-related ignore
717     /// rules (global rules, .gitignore and local exclude rules).
718     ///
719     /// When disabled, git-related ignore rules are applied even when searching
720     /// outside a git repository.
require_git(&mut self, yes: bool) -> &mut IgnoreBuilder721     pub fn require_git(&mut self, yes: bool) -> &mut IgnoreBuilder {
722         self.opts.require_git = yes;
723         self
724     }
725 
726     /// Process ignore files case insensitively
727     ///
728     /// This is disabled by default.
ignore_case_insensitive( &mut self, yes: bool, ) -> &mut IgnoreBuilder729     pub fn ignore_case_insensitive(
730         &mut self,
731         yes: bool,
732     ) -> &mut IgnoreBuilder {
733         self.opts.ignore_case_insensitive = yes;
734         self
735     }
736 }
737 
738 /// Creates a new gitignore matcher for the directory given.
739 ///
740 /// The matcher is meant to match files below `dir`.
741 /// Ignore globs are extracted from each of the file names relative to
742 /// `dir_for_ignorefile` in the order given (earlier names have lower
743 /// precedence than later names).
744 ///
745 /// I/O errors are ignored.
create_gitignore<T: AsRef<OsStr>>( dir: &Path, dir_for_ignorefile: &Path, names: &[T], case_insensitive: bool, ) -> (Gitignore, Option<Error>)746 pub fn create_gitignore<T: AsRef<OsStr>>(
747     dir: &Path,
748     dir_for_ignorefile: &Path,
749     names: &[T],
750     case_insensitive: bool,
751 ) -> (Gitignore, Option<Error>) {
752     let mut builder = GitignoreBuilder::new(dir);
753     let mut errs = PartialErrorBuilder::default();
754     builder.case_insensitive(case_insensitive).unwrap();
755     for name in names {
756         let gipath = dir_for_ignorefile.join(name.as_ref());
757         // This check is not necessary, but is added for performance. Namely,
758         // a simple stat call checking for existence can often be just a bit
759         // quicker than actually trying to open a file. Since the number of
760         // directories without ignore files likely greatly exceeds the number
761         // with ignore files, this check generally makes sense.
762         //
763         // However, until demonstrated otherwise, we speculatively do not do
764         // this on Windows since Windows is notorious for having slow file
765         // system operations. Namely, it's not clear whether this analysis
766         // makes sense on Windows.
767         //
768         // For more details: https://github.com/BurntSushi/ripgrep/pull/1381
769         if cfg!(windows) || gipath.exists() {
770             errs.maybe_push_ignore_io(builder.add(gipath));
771         }
772     }
773     let gi = match builder.build() {
774         Ok(gi) => gi,
775         Err(err) => {
776             errs.push(err);
777             GitignoreBuilder::new(dir).build().unwrap()
778         }
779     };
780     (gi, errs.into_error_option())
781 }
782 
783 /// Find the GIT_COMMON_DIR for the given git worktree.
784 ///
785 /// This is the directory that may contain a private ignore file
786 /// "info/exclude". Unlike git, this function does *not* read environment
787 /// variables GIT_DIR and GIT_COMMON_DIR, because it is not clear how to use
788 /// them when multiple repositories are searched.
789 ///
790 /// Some I/O errors are ignored.
resolve_git_commondir( dir: &Path, git_type: Option<FileType>, ) -> Result<PathBuf, Option<Error>>791 fn resolve_git_commondir(
792     dir: &Path,
793     git_type: Option<FileType>,
794 ) -> Result<PathBuf, Option<Error>> {
795     let git_dir_path = || dir.join(".git");
796     let git_dir = git_dir_path();
797     if !git_type.map_or(false, |ft| ft.is_file()) {
798         return Ok(git_dir);
799     }
800     let file = match File::open(git_dir) {
801         Ok(file) => io::BufReader::new(file),
802         Err(err) => {
803             return Err(Some(Error::Io(err).with_path(git_dir_path())));
804         }
805     };
806     let dot_git_line = match file.lines().next() {
807         Some(Ok(line)) => line,
808         Some(Err(err)) => {
809             return Err(Some(Error::Io(err).with_path(git_dir_path())));
810         }
811         None => return Err(None),
812     };
813     if !dot_git_line.starts_with("gitdir: ") {
814         return Err(None);
815     }
816     let real_git_dir = PathBuf::from(&dot_git_line["gitdir: ".len()..]);
817     let git_commondir_file = || real_git_dir.join("commondir");
818     let file = match File::open(git_commondir_file()) {
819         Ok(file) => io::BufReader::new(file),
820         Err(_) => return Err(None),
821     };
822     let commondir_line = match file.lines().next() {
823         Some(Ok(line)) => line,
824         Some(Err(err)) => {
825             return Err(Some(Error::Io(err).with_path(git_commondir_file())));
826         }
827         None => return Err(None),
828     };
829     let commondir_abs = if commondir_line.starts_with(".") {
830         real_git_dir.join(commondir_line) // relative commondir
831     } else {
832         PathBuf::from(commondir_line)
833     };
834     Ok(commondir_abs)
835 }
836 
837 #[cfg(test)]
838 mod tests {
839     use std::fs::{self, File};
840     use std::io::Write;
841     use std::path::Path;
842 
843     use crate::dir::IgnoreBuilder;
844     use crate::gitignore::Gitignore;
845     use crate::tests::TempDir;
846     use crate::Error;
847 
wfile<P: AsRef<Path>>(path: P, contents: &str)848     fn wfile<P: AsRef<Path>>(path: P, contents: &str) {
849         let mut file = File::create(path).unwrap();
850         file.write_all(contents.as_bytes()).unwrap();
851     }
852 
mkdirp<P: AsRef<Path>>(path: P)853     fn mkdirp<P: AsRef<Path>>(path: P) {
854         fs::create_dir_all(path).unwrap();
855     }
856 
partial(err: Error) -> Vec<Error>857     fn partial(err: Error) -> Vec<Error> {
858         match err {
859             Error::Partial(errs) => errs,
860             _ => panic!("expected partial error but got {:?}", err),
861         }
862     }
863 
tmpdir() -> TempDir864     fn tmpdir() -> TempDir {
865         TempDir::new().unwrap()
866     }
867 
868     #[test]
explicit_ignore()869     fn explicit_ignore() {
870         let td = tmpdir();
871         wfile(td.path().join("not-an-ignore"), "foo\n!bar");
872 
873         let (gi, err) = Gitignore::new(td.path().join("not-an-ignore"));
874         assert!(err.is_none());
875         let (ig, err) =
876             IgnoreBuilder::new().add_ignore(gi).build().add_child(td.path());
877         assert!(err.is_none());
878         assert!(ig.matched("foo", false).is_ignore());
879         assert!(ig.matched("bar", false).is_whitelist());
880         assert!(ig.matched("baz", false).is_none());
881     }
882 
883     #[test]
git_exclude()884     fn git_exclude() {
885         let td = tmpdir();
886         mkdirp(td.path().join(".git/info"));
887         wfile(td.path().join(".git/info/exclude"), "foo\n!bar");
888 
889         let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
890         assert!(err.is_none());
891         assert!(ig.matched("foo", false).is_ignore());
892         assert!(ig.matched("bar", false).is_whitelist());
893         assert!(ig.matched("baz", false).is_none());
894     }
895 
896     #[test]
gitignore()897     fn gitignore() {
898         let td = tmpdir();
899         mkdirp(td.path().join(".git"));
900         wfile(td.path().join(".gitignore"), "foo\n!bar");
901 
902         let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
903         assert!(err.is_none());
904         assert!(ig.matched("foo", false).is_ignore());
905         assert!(ig.matched("bar", false).is_whitelist());
906         assert!(ig.matched("baz", false).is_none());
907     }
908 
909     #[test]
gitignore_no_git()910     fn gitignore_no_git() {
911         let td = tmpdir();
912         wfile(td.path().join(".gitignore"), "foo\n!bar");
913 
914         let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
915         assert!(err.is_none());
916         assert!(ig.matched("foo", false).is_none());
917         assert!(ig.matched("bar", false).is_none());
918         assert!(ig.matched("baz", false).is_none());
919     }
920 
921     #[test]
gitignore_allowed_no_git()922     fn gitignore_allowed_no_git() {
923         let td = tmpdir();
924         wfile(td.path().join(".gitignore"), "foo\n!bar");
925 
926         let (ig, err) = IgnoreBuilder::new()
927             .require_git(false)
928             .build()
929             .add_child(td.path());
930         assert!(err.is_none());
931         assert!(ig.matched("foo", false).is_ignore());
932         assert!(ig.matched("bar", false).is_whitelist());
933         assert!(ig.matched("baz", false).is_none());
934     }
935 
936     #[test]
ignore()937     fn ignore() {
938         let td = tmpdir();
939         wfile(td.path().join(".ignore"), "foo\n!bar");
940 
941         let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
942         assert!(err.is_none());
943         assert!(ig.matched("foo", false).is_ignore());
944         assert!(ig.matched("bar", false).is_whitelist());
945         assert!(ig.matched("baz", false).is_none());
946     }
947 
948     #[test]
custom_ignore()949     fn custom_ignore() {
950         let td = tmpdir();
951         let custom_ignore = ".customignore";
952         wfile(td.path().join(custom_ignore), "foo\n!bar");
953 
954         let (ig, err) = IgnoreBuilder::new()
955             .add_custom_ignore_filename(custom_ignore)
956             .build()
957             .add_child(td.path());
958         assert!(err.is_none());
959         assert!(ig.matched("foo", false).is_ignore());
960         assert!(ig.matched("bar", false).is_whitelist());
961         assert!(ig.matched("baz", false).is_none());
962     }
963 
964     // Tests that a custom ignore file will override an .ignore.
965     #[test]
custom_ignore_over_ignore()966     fn custom_ignore_over_ignore() {
967         let td = tmpdir();
968         let custom_ignore = ".customignore";
969         wfile(td.path().join(".ignore"), "foo");
970         wfile(td.path().join(custom_ignore), "!foo");
971 
972         let (ig, err) = IgnoreBuilder::new()
973             .add_custom_ignore_filename(custom_ignore)
974             .build()
975             .add_child(td.path());
976         assert!(err.is_none());
977         assert!(ig.matched("foo", false).is_whitelist());
978     }
979 
980     // Tests that earlier custom ignore files have lower precedence than later.
981     #[test]
custom_ignore_precedence()982     fn custom_ignore_precedence() {
983         let td = tmpdir();
984         let custom_ignore1 = ".customignore1";
985         let custom_ignore2 = ".customignore2";
986         wfile(td.path().join(custom_ignore1), "foo");
987         wfile(td.path().join(custom_ignore2), "!foo");
988 
989         let (ig, err) = IgnoreBuilder::new()
990             .add_custom_ignore_filename(custom_ignore1)
991             .add_custom_ignore_filename(custom_ignore2)
992             .build()
993             .add_child(td.path());
994         assert!(err.is_none());
995         assert!(ig.matched("foo", false).is_whitelist());
996     }
997 
998     // Tests that an .ignore will override a .gitignore.
999     #[test]
ignore_over_gitignore()1000     fn ignore_over_gitignore() {
1001         let td = tmpdir();
1002         wfile(td.path().join(".gitignore"), "foo");
1003         wfile(td.path().join(".ignore"), "!foo");
1004 
1005         let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
1006         assert!(err.is_none());
1007         assert!(ig.matched("foo", false).is_whitelist());
1008     }
1009 
1010     // Tests that exclude has lower precedent than both .ignore and .gitignore.
1011     #[test]
exclude_lowest()1012     fn exclude_lowest() {
1013         let td = tmpdir();
1014         wfile(td.path().join(".gitignore"), "!foo");
1015         wfile(td.path().join(".ignore"), "!bar");
1016         mkdirp(td.path().join(".git/info"));
1017         wfile(td.path().join(".git/info/exclude"), "foo\nbar\nbaz");
1018 
1019         let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
1020         assert!(err.is_none());
1021         assert!(ig.matched("baz", false).is_ignore());
1022         assert!(ig.matched("foo", false).is_whitelist());
1023         assert!(ig.matched("bar", false).is_whitelist());
1024     }
1025 
1026     #[test]
errored()1027     fn errored() {
1028         let td = tmpdir();
1029         wfile(td.path().join(".gitignore"), "{foo");
1030 
1031         let (_, err) = IgnoreBuilder::new().build().add_child(td.path());
1032         assert!(err.is_some());
1033     }
1034 
1035     #[test]
errored_both()1036     fn errored_both() {
1037         let td = tmpdir();
1038         wfile(td.path().join(".gitignore"), "{foo");
1039         wfile(td.path().join(".ignore"), "{bar");
1040 
1041         let (_, err) = IgnoreBuilder::new().build().add_child(td.path());
1042         assert_eq!(2, partial(err.expect("an error")).len());
1043     }
1044 
1045     #[test]
errored_partial()1046     fn errored_partial() {
1047         let td = tmpdir();
1048         mkdirp(td.path().join(".git"));
1049         wfile(td.path().join(".gitignore"), "{foo\nbar");
1050 
1051         let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
1052         assert!(err.is_some());
1053         assert!(ig.matched("bar", false).is_ignore());
1054     }
1055 
1056     #[test]
errored_partial_and_ignore()1057     fn errored_partial_and_ignore() {
1058         let td = tmpdir();
1059         wfile(td.path().join(".gitignore"), "{foo\nbar");
1060         wfile(td.path().join(".ignore"), "!bar");
1061 
1062         let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
1063         assert!(err.is_some());
1064         assert!(ig.matched("bar", false).is_whitelist());
1065     }
1066 
1067     #[test]
not_present_empty()1068     fn not_present_empty() {
1069         let td = tmpdir();
1070 
1071         let (_, err) = IgnoreBuilder::new().build().add_child(td.path());
1072         assert!(err.is_none());
1073     }
1074 
1075     #[test]
stops_at_git_dir()1076     fn stops_at_git_dir() {
1077         // This tests that .gitignore files beyond a .git barrier aren't
1078         // matched, but .ignore files are.
1079         let td = tmpdir();
1080         mkdirp(td.path().join(".git"));
1081         mkdirp(td.path().join("foo/.git"));
1082         wfile(td.path().join(".gitignore"), "foo");
1083         wfile(td.path().join(".ignore"), "bar");
1084 
1085         let ig0 = IgnoreBuilder::new().build();
1086         let (ig1, err) = ig0.add_child(td.path());
1087         assert!(err.is_none());
1088         let (ig2, err) = ig1.add_child(ig1.path().join("foo"));
1089         assert!(err.is_none());
1090 
1091         assert!(ig1.matched("foo", false).is_ignore());
1092         assert!(ig2.matched("foo", false).is_none());
1093 
1094         assert!(ig1.matched("bar", false).is_ignore());
1095         assert!(ig2.matched("bar", false).is_ignore());
1096     }
1097 
1098     #[test]
absolute_parent()1099     fn absolute_parent() {
1100         let td = tmpdir();
1101         mkdirp(td.path().join(".git"));
1102         mkdirp(td.path().join("foo"));
1103         wfile(td.path().join(".gitignore"), "bar");
1104 
1105         // First, check that the parent gitignore file isn't detected if the
1106         // parent isn't added. This establishes a baseline.
1107         let ig0 = IgnoreBuilder::new().build();
1108         let (ig1, err) = ig0.add_child(td.path().join("foo"));
1109         assert!(err.is_none());
1110         assert!(ig1.matched("bar", false).is_none());
1111 
1112         // Second, check that adding a parent directory actually works.
1113         let ig0 = IgnoreBuilder::new().build();
1114         let (ig1, err) = ig0.add_parents(td.path().join("foo"));
1115         assert!(err.is_none());
1116         let (ig2, err) = ig1.add_child(td.path().join("foo"));
1117         assert!(err.is_none());
1118         assert!(ig2.matched("bar", false).is_ignore());
1119     }
1120 
1121     #[test]
absolute_parent_anchored()1122     fn absolute_parent_anchored() {
1123         let td = tmpdir();
1124         mkdirp(td.path().join(".git"));
1125         mkdirp(td.path().join("src/llvm"));
1126         wfile(td.path().join(".gitignore"), "/llvm/\nfoo");
1127 
1128         let ig0 = IgnoreBuilder::new().build();
1129         let (ig1, err) = ig0.add_parents(td.path().join("src"));
1130         assert!(err.is_none());
1131         let (ig2, err) = ig1.add_child("src");
1132         assert!(err.is_none());
1133 
1134         assert!(ig1.matched("llvm", true).is_none());
1135         assert!(ig2.matched("llvm", true).is_none());
1136         assert!(ig2.matched("src/llvm", true).is_none());
1137         assert!(ig2.matched("foo", false).is_ignore());
1138         assert!(ig2.matched("src/foo", false).is_ignore());
1139     }
1140 
1141     #[test]
git_info_exclude_in_linked_worktree()1142     fn git_info_exclude_in_linked_worktree() {
1143         let td = tmpdir();
1144         let git_dir = td.path().join(".git");
1145         mkdirp(git_dir.join("info"));
1146         wfile(git_dir.join("info/exclude"), "ignore_me");
1147         mkdirp(git_dir.join("worktrees/linked-worktree"));
1148         let commondir_path =
1149             || git_dir.join("worktrees/linked-worktree/commondir");
1150         mkdirp(td.path().join("linked-worktree"));
1151         let worktree_git_dir_abs = format!(
1152             "gitdir: {}",
1153             git_dir.join("worktrees/linked-worktree").to_str().unwrap(),
1154         );
1155         wfile(td.path().join("linked-worktree/.git"), &worktree_git_dir_abs);
1156 
1157         // relative commondir
1158         wfile(commondir_path(), "../..");
1159         let ib = IgnoreBuilder::new().build();
1160         let (ignore, err) = ib.add_child(td.path().join("linked-worktree"));
1161         assert!(err.is_none());
1162         assert!(ignore.matched("ignore_me", false).is_ignore());
1163 
1164         // absolute commondir
1165         wfile(commondir_path(), git_dir.to_str().unwrap());
1166         let (ignore, err) = ib.add_child(td.path().join("linked-worktree"));
1167         assert!(err.is_none());
1168         assert!(ignore.matched("ignore_me", false).is_ignore());
1169 
1170         // missing commondir file
1171         assert!(fs::remove_file(commondir_path()).is_ok());
1172         let (_, err) = ib.add_child(td.path().join("linked-worktree"));
1173         // We squash the error in this case, because it occurs in repositories
1174         // that are not linked worktrees but have submodules.
1175         assert!(err.is_none());
1176 
1177         wfile(td.path().join("linked-worktree/.git"), "garbage");
1178         let (_, err) = ib.add_child(td.path().join("linked-worktree"));
1179         assert!(err.is_none());
1180 
1181         wfile(td.path().join("linked-worktree/.git"), "gitdir: garbage");
1182         let (_, err) = ib.add_child(td.path().join("linked-worktree"));
1183         assert!(err.is_none());
1184     }
1185 }
1186