ignore/
dir.rs

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) => {
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}