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