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