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