// This module provides a data structure, `Ignore`, that connects "directory
// traversal" with "ignore matchers." Specifically, it knows about gitignore
// semantics and precedence, and is organized based on directory hierarchy.
// Namely, every matcher logically corresponds to ignore rules from a single
// directory, and points to the matcher for its corresponding parent directory.
// In this sense, `Ignore` is a *persistent* data structure.
//
// This design was specifically chosen to make it possible to use this data
// structure in a parallel directory iterator.
//
// My initial intention was to expose this module as part of this crate's
// public API, but I think the data structure's public API is too complicated
// with non-obvious failure modes. Alas, such things haven't been documented
// well.

use std::{
    collections::HashMap,
    ffi::{OsStr, OsString},
    fs::{self, File, FileType},
    io::{self, BufRead},
    path::{Path, PathBuf},
    sync::{Arc, RwLock, Weak},
};

use crate::{
    gitignore::{self, Gitignore, GitignoreBuilder},
    overrides::{self, Override},
    pathutil::{is_hidden, strip_prefix},
    types::{self, Types},
    walk::DirEntry,
    {Error, Match, PartialErrorBuilder},
};

/// IgnoreMatch represents information about where a match came from when using
/// the `Ignore` matcher.
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub(crate) struct IgnoreMatch<'a>(IgnoreMatchInner<'a>);

/// IgnoreMatchInner describes precisely where the match information came from.
/// This is private to allow expansion to more matchers in the future.
#[derive(Clone, Debug)]
#[allow(dead_code)]
enum IgnoreMatchInner<'a> {
    Override(overrides::Glob<'a>),
    Gitignore(&'a gitignore::Glob),
    Types(types::Glob<'a>),
    Hidden,
}

impl<'a> IgnoreMatch<'a> {
    fn overrides(x: overrides::Glob<'a>) -> IgnoreMatch<'a> {
        IgnoreMatch(IgnoreMatchInner::Override(x))
    }

    fn gitignore(x: &'a gitignore::Glob) -> IgnoreMatch<'a> {
        IgnoreMatch(IgnoreMatchInner::Gitignore(x))
    }

    fn types(x: types::Glob<'a>) -> IgnoreMatch<'a> {
        IgnoreMatch(IgnoreMatchInner::Types(x))
    }

    fn hidden() -> IgnoreMatch<'static> {
        IgnoreMatch(IgnoreMatchInner::Hidden)
    }
}

/// Options for the ignore matcher, shared between the matcher itself and the
/// builder.
#[derive(Clone, Copy, Debug)]
struct IgnoreOptions {
    /// Whether to ignore hidden file paths or not.
    hidden: bool,
    /// Whether to read .ignore files.
    ignore: bool,
    /// Whether to respect any ignore files in parent directories.
    parents: bool,
    /// Whether to read git's global gitignore file.
    git_global: bool,
    /// Whether to read .gitignore files.
    git_ignore: bool,
    /// Whether to read .git/info/exclude files.
    git_exclude: bool,
    /// Whether to ignore files case insensitively
    ignore_case_insensitive: bool,
    /// Whether a git repository must be present in order to apply any
    /// git-related ignore rules.
    require_git: bool,
}

/// Ignore is a matcher useful for recursively walking one or more directories.
#[derive(Clone, Debug)]
pub(crate) struct Ignore {
    inner: Arc<IgnoreInner>,
    // Parent matchers are cached independently of the path being walked, but
    // matching them still needs the canonicalized path originally passed to
    // `add_parents`. For example, when walking `/tmp/project/src`, parent
    // matchers use `/tmp/project/src` to rewrite `/tmp/project/src/foo.py`
    // before matching it against ignore files from `/tmp/project` and its
    // ancestors.
    //
    // For ripgrep itself, this means that `rg pat src tests` must rewrite
    // `src/foo` relative to `.../src`, and not whatever root was prepared
    // first.
    //
    // See: https://github.com/BurntSushi/ripgrep/pull/3420
    // See: https://github.com/BurntSushi/ripgrep/issues/3376
    // See: https://github.com/BurntSushi/ripgrep/issues/3419
    // See: https://github.com/BurntSushi/ripgrep/issues/3320
    absolute_base: Option<Arc<PathBuf>>,
}

#[derive(Clone, Debug)]
struct IgnoreInner {
    /// A map of all existing directories that have already been
    /// compiled into matchers.
    ///
    /// Note that this is never used during matching, only when adding new
    /// parent directory matchers. This avoids needing to rebuild glob sets for
    /// parent directories if many paths are being searched.
    compiled: Arc<RwLock<HashMap<OsString, Weak<IgnoreInner>>>>,
    /// The path to the directory that this matcher was built from.
    dir: PathBuf,
    /// An override matcher (default is empty).
    overrides: Arc<Override>,
    /// A file type matcher.
    types: Arc<Types>,
    /// The parent directory to match next.
    ///
    /// If this is the root directory or there are otherwise no more
    /// directories to match, then `parent` is `None`.
    parent: Option<Arc<IgnoreInner>>,
    /// Whether this is an absolute parent matcher, as added by add_parent.
    is_absolute_parent: bool,
    /// The directory that gitignores should be interpreted relative to.
    ///
    /// Usually this is the directory containing the gitignore file. But in
    /// some cases, like for global gitignores or for gitignores specified
    /// explicitly, this should generally be set to the current working
    /// directory. This is only used for global gitignores or "explicit"
    /// gitignores.
    ///
    /// When `None`, this means the CWD could not be determined or is unknown.
    /// In this case, global gitignore files are ignored because they otherwise
    /// cannot be matched correctly.
    global_gitignores_relative_to: Option<PathBuf>,
    /// Explicit global ignore matchers specified by the caller.
    explicit_ignores: Arc<Vec<Gitignore>>,
    /// Ignore files used in addition to `.ignore`
    custom_ignore_filenames: Arc<Vec<OsString>>,
    /// The matcher for custom ignore files
    custom_ignore_matcher: Gitignore,
    /// The matcher for .ignore files.
    ignore_matcher: Gitignore,
    /// A global gitignore matcher, usually from $XDG_CONFIG_HOME/git/ignore.
    git_global_matcher: Arc<Gitignore>,
    /// The matcher for .gitignore files.
    git_ignore_matcher: Gitignore,
    /// Special matcher for `.git/info/exclude` files.
    git_exclude_matcher: Gitignore,
    /// Whether this directory contains a .git sub-directory.
    has_git: bool,
    /// Ignore config.
    opts: IgnoreOptions,
}

impl Ignore {
    /// Return the directory path of this matcher.
    #[cfg(test)]
    pub(crate) fn path(&self) -> &Path {
        &self.inner.dir
    }

    /// Return true if this matcher has no parent.
    pub(crate) fn is_root(&self) -> bool {
        self.inner.parent.is_none()
    }

    /// Return this matcher's parent, if one exists.
    pub(crate) fn parent(&self) -> Option<Ignore> {
        self.inner.parent.as_ref().map(|parent| Ignore {
            inner: parent.clone(),
            absolute_base: self.absolute_base.clone(),
        })
    }

    /// Create a new `Ignore` matcher with the parent directories of `dir`.
    ///
    /// Note that this can only be called on an `Ignore` matcher with no
    /// parents (i.e., `is_root` returns `true`). This will panic otherwise.
    pub(crate) fn add_parents<P: AsRef<Path>>(
        &self,
        path: P,
    ) -> (Ignore, Option<Error>) {
        if !self.inner.opts.parents
            && !self.inner.opts.git_ignore
            && !self.inner.opts.git_exclude
            && !self.inner.opts.git_global
        {
            // If we never need info from parent directories, then don't do
            // anything.
            return (self.clone(), None);
        }
        if !self.is_root() {
            panic!("Ignore::add_parents called on non-root matcher");
        }
        let absolute_base = match path.as_ref().canonicalize() {
            Ok(path) => Arc::new(path),
            Err(_) => {
                // There's not much we can do here, so just return our
                // existing matcher. We drop the error to be consistent
                // with our general pattern of ignoring I/O errors when
                // processing ignore files.
                return (self.clone(), None);
            }
        };
        // List of parents, from child to root.
        let mut parents = vec![];
        let mut path = &**absolute_base;
        while let Some(parent) = path.parent() {
            parents.push(parent);
            path = parent;
        }
        let mut errs = PartialErrorBuilder::default();
        let mut ig = self.clone();
        for parent in parents.into_iter().rev() {
            let mut compiled = self.inner.compiled.write().unwrap();
            if let Some(weak) = compiled.get(parent.as_os_str()) {
                if let Some(prebuilt) = weak.upgrade() {
                    ig = Ignore {
                        inner: prebuilt,
                        absolute_base: Some(absolute_base.clone()),
                    };
                    continue;
                }
            }
            let (mut igtmp, err) = ig.add_child_path(parent);
            errs.maybe_push(err);
            igtmp.is_absolute_parent = true;
            igtmp.has_git =
                if self.inner.opts.require_git && self.inner.opts.git_ignore {
                    parent.join(".git").exists() || parent.join(".jj").exists()
                } else {
                    false
                };
            let ig_arc = Arc::new(igtmp);
            ig = Ignore {
                inner: ig_arc.clone(),
                absolute_base: Some(absolute_base.clone()),
            };
            compiled.insert(
                parent.as_os_str().to_os_string(),
                Arc::downgrade(&ig_arc),
            );
        }
        (ig, errs.into_error_option())
    }

    /// Create a new `Ignore` matcher for the given child directory.
    ///
    /// Since building the matcher may require reading from multiple
    /// files, it's possible that this method partially succeeds. Therefore,
    /// a matcher is always returned (which may match nothing) and an error is
    /// returned if it exists.
    ///
    /// Note that all I/O errors are completely ignored.
    pub(crate) fn add_child<P: AsRef<Path>>(
        &self,
        dir: P,
    ) -> (Ignore, Option<Error>) {
        let (ig, err) = self.add_child_path(dir.as_ref());
        (
            Ignore {
                inner: Arc::new(ig),
                absolute_base: self.absolute_base.clone(),
            },
            err,
        )
    }

    /// Like add_child, but uses successful read_dir entries to reduce
    /// probing when discovering ignore files.
    pub(crate) fn add_child_with_entries<P: AsRef<Path>>(
        &self,
        dir: P,
        entries: &[fs::DirEntry],
    ) -> (Ignore, Option<Error>) {
        let files = self.collect_ignore_files(entries);
        let (ig, err) = self.add_child_path_with_found_ignore_files(
            dir.as_ref(),
            Some(&files),
        );
        (
            Ignore {
                inner: Arc::new(ig),
                absolute_base: self.absolute_base.clone(),
            },
            err,
        )
    }

    /// Like add_child, but takes a full path and returns an IgnoreInner.
    fn add_child_path(&self, dir: &Path) -> (IgnoreInner, Option<Error>) {
        self.add_child_path_with_found_ignore_files(dir, None)
    }

    fn collect_ignore_files(
        &self,
        entries: &[fs::DirEntry],
    ) -> IgnoreFilesFound {
        let custom_ignore_filenames = &self.inner.custom_ignore_filenames;
        let mut files = IgnoreFilesFound {
            has_ignore: false,
            has_git_ignore: false,
            has_git_dir: false,
            has_jj_dir: false,
            custom_ignore_files: vec![false; custom_ignore_filenames.len()],
        };
        for entry in entries {
            let file_name = entry.file_name();
            if file_name == OsStr::new(".ignore") {
                files.has_ignore = true;
            } else if file_name == OsStr::new(".gitignore") {
                files.has_git_ignore = true;
            } else if file_name == OsStr::new(".git") {
                files.has_git_dir = true;
            } else if file_name == OsStr::new(".jj") {
                files.has_jj_dir = true;
            }
            for (i, name) in custom_ignore_filenames.iter().enumerate() {
                if file_name == name.as_os_str() {
                    files.custom_ignore_files[i] = true;
                }
            }
        }
        files
    }

    fn add_child_path_with_found_ignore_files(
        &self,
        dir: &Path,
        ignore_files_list: Option<&IgnoreFilesFound>,
    ) -> (IgnoreInner, Option<Error>) {
        let check_vcs_dir = self.inner.opts.require_git
            && (self.inner.opts.git_ignore || self.inner.opts.git_exclude);
        let git_type = if check_vcs_dir
            && ignore_files_list.is_none_or(|i| i.has_git_dir)
        {
            dir.join(".git").metadata().ok().map(|md| md.file_type())
        } else {
            None
        };
        let has_jj = check_vcs_dir
            && ignore_files_list.is_none_or(|i| i.has_jj_dir)
            && dir.join(".jj").exists();
        let has_git = check_vcs_dir && (git_type.is_some() || has_jj);

        let mut errs = PartialErrorBuilder::default();
        let custom_ig_matcher = if self
            .inner
            .custom_ignore_filenames
            .is_empty()
        {
            Gitignore::empty()
        } else {
            let custom_ignore_names: Vec<&OsString> = match ignore_files_list {
                None => self.inner.custom_ignore_filenames.iter().collect(),
                Some(m) => self
                    .inner
                    .custom_ignore_filenames
                    .iter()
                    .zip(m.custom_ignore_files.iter())
                    .filter(|&(_, &matched)| matched)
                    .map(|(name, _)| name)
                    .collect(),
            };
            if custom_ignore_names.is_empty() {
                Gitignore::empty()
            } else {
                let (m, err) = create_gitignore(
                    &dir,
                    &dir,
                    &custom_ignore_names,
                    self.inner.opts.ignore_case_insensitive,
                );
                errs.maybe_push(err);
                m
            }
        };
        let ig_matcher = if !self.inner.opts.ignore
            || !ignore_files_list.is_none_or(|i| i.has_ignore)
        {
            Gitignore::empty()
        } else {
            let (m, err) = create_gitignore(
                &dir,
                &dir,
                &[".ignore"],
                self.inner.opts.ignore_case_insensitive,
            );
            errs.maybe_push(err);
            m
        };
        let gi_matcher = if !self.inner.opts.git_ignore
            || !ignore_files_list.is_none_or(|i| i.has_git_ignore)
        {
            Gitignore::empty()
        } else {
            let (m, err) = create_gitignore(
                &dir,
                &dir,
                &[".gitignore"],
                self.inner.opts.ignore_case_insensitive,
            );
            errs.maybe_push(err);
            m
        };

        let gi_exclude_matcher = if !self.inner.opts.git_exclude
            || !ignore_files_list.is_none_or(|i| i.has_git_dir)
        {
            Gitignore::empty()
        } else {
            match resolve_git_commondir(dir, git_type) {
                Ok(git_dir) => {
                    let (m, err) = create_gitignore(
                        &dir,
                        &git_dir,
                        &["info/exclude"],
                        self.inner.opts.ignore_case_insensitive,
                    );
                    errs.maybe_push(err);
                    m
                }
                Err(err) => {
                    errs.maybe_push(err);
                    Gitignore::empty()
                }
            }
        };
        let ig = IgnoreInner {
            compiled: self.inner.compiled.clone(),
            dir: dir.to_path_buf(),
            overrides: self.inner.overrides.clone(),
            types: self.inner.types.clone(),
            parent: Some(self.inner.clone()),
            is_absolute_parent: false,
            global_gitignores_relative_to: self
                .inner
                .global_gitignores_relative_to
                .clone(),
            explicit_ignores: self.inner.explicit_ignores.clone(),
            custom_ignore_filenames: self
                .inner
                .custom_ignore_filenames
                .clone(),
            custom_ignore_matcher: custom_ig_matcher,
            ignore_matcher: ig_matcher,
            git_global_matcher: self.inner.git_global_matcher.clone(),
            git_ignore_matcher: gi_matcher,
            git_exclude_matcher: gi_exclude_matcher,
            has_git,
            opts: self.inner.opts,
        };
        (ig, errs.into_error_option())
    }

    /// Returns true if at least one type of ignore rule should be matched.
    fn has_any_ignore_rules(&self) -> bool {
        let opts = self.inner.opts;
        let has_custom_ignore_files =
            !self.inner.custom_ignore_filenames.is_empty();
        let has_explicit_ignores = !self.inner.explicit_ignores.is_empty();

        opts.ignore
            || opts.git_global
            || opts.git_ignore
            || opts.git_exclude
            || has_custom_ignore_files
            || has_explicit_ignores
    }

    /// Like `matched`, but works with a directory entry instead.
    pub(crate) fn matched_dir_entry<'a>(
        &'a self,
        dent: &DirEntry,
    ) -> Match<IgnoreMatch<'a>> {
        let m = self.matched(dent.path(), dent.is_dir());
        if m.is_none() && self.inner.opts.hidden && is_hidden(dent) {
            return Match::Ignore(IgnoreMatch::hidden());
        }
        m
    }

    /// Returns a match indicating whether the given file path should be
    /// ignored or not.
    ///
    /// The match contains information about its origin.
    fn matched<'a, P: AsRef<Path>>(
        &'a self,
        path: P,
        is_dir: bool,
    ) -> Match<IgnoreMatch<'a>> {
        // We need to be careful with our path. If it has a leading ./, then
        // strip it because it causes nothing but trouble.
        let mut path = path.as_ref();
        if let Some(p) = strip_prefix("./", path) {
            path = p;
        }
        // Match against the override patterns. If an override matches
        // regardless of whether it's whitelist/ignore, then we quit and
        // return that result immediately. Overrides have the highest
        // precedence.
        if !self.inner.overrides.is_empty() {
            let mat = self
                .inner
                .overrides
                .matched(path, is_dir)
                .map(IgnoreMatch::overrides);
            if !mat.is_none() {
                return mat;
            }
        }
        let mut whitelisted = Match::None;
        if self.has_any_ignore_rules() {
            let mat = self.matched_ignore(path, is_dir);
            if mat.is_ignore() {
                return mat;
            } else if mat.is_whitelist() {
                whitelisted = mat;
            }
        }
        if !self.inner.types.is_empty() {
            let mat =
                self.inner.types.matched(path, is_dir).map(IgnoreMatch::types);
            if mat.is_ignore() {
                return mat;
            } else if mat.is_whitelist() {
                whitelisted = mat;
            }
        }
        whitelisted
    }

    /// Performs matching only on the ignore files for this directory and
    /// all parent directories.
    fn matched_ignore<'a>(
        &'a self,
        path: &Path,
        is_dir: bool,
    ) -> Match<IgnoreMatch<'a>> {
        let (
            mut m_custom_ignore,
            mut m_ignore,
            mut m_gi,
            mut m_gi_exclude,
            mut m_explicit,
        ) = (Match::None, Match::None, Match::None, Match::None, Match::None);
        let any_git = !self.inner.opts.require_git
            || self.parents().any(|ig| ig.inner.has_git);
        let mut saw_git = false;
        for ig in self.parents().take_while(|ig| !ig.inner.is_absolute_parent)
        {
            if m_custom_ignore.is_none() {
                m_custom_ignore = ig
                    .inner
                    .custom_ignore_matcher
                    .matched(path, is_dir)
                    .map(IgnoreMatch::gitignore);
            }
            if m_ignore.is_none() {
                m_ignore = ig
                    .inner
                    .ignore_matcher
                    .matched(path, is_dir)
                    .map(IgnoreMatch::gitignore);
            }
            if any_git && !saw_git && m_gi.is_none() {
                m_gi = ig
                    .inner
                    .git_ignore_matcher
                    .matched(path, is_dir)
                    .map(IgnoreMatch::gitignore);
            }
            if any_git && !saw_git && m_gi_exclude.is_none() {
                m_gi_exclude = ig
                    .inner
                    .git_exclude_matcher
                    .matched(path, is_dir)
                    .map(IgnoreMatch::gitignore);
            }
            saw_git = saw_git || ig.inner.has_git;
        }
        if self.inner.opts.parents {
            if let Some(abs_parent_path) = self.absolute_base() {
                // What we want to do here is take the absolute base path of
                // this directory and join it with the path we're searching.
                // The main issue we want to avoid is accidentally duplicating
                // directory components, so we try to strip any common prefix
                // off of `path`. Overall, this seems a little ham-fisted, but
                // it does fix a nasty bug. It should do fine until we overhaul
                // this crate.
                let path = abs_parent_path.join(
                    self.parents()
                        .take_while(|ig| !ig.inner.is_absolute_parent)
                        .last()
                        .map_or(path, |ig| {
                            // This is a weird special case when ripgrep users
                            // search with just a `.`, as some tools do
                            // automatically (like consult). In this case, if
                            // we don't bail out now, the code below will strip
                            // a leading `.` from `path`, which might mangle
                            // a hidden file name!
                            if ig.inner.dir.as_path() == Path::new(".") {
                                return path;
                            }
                            let without_dot_slash = strip_if_is_prefix(
                                "./",
                                ig.inner.dir.as_path(),
                            );
                            let relative_base =
                                strip_if_is_prefix(without_dot_slash, path);
                            strip_if_is_prefix("/", relative_base)
                        }),
                );

                for ig in self
                    .parents()
                    .skip_while(|ig| !ig.inner.is_absolute_parent)
                {
                    if m_custom_ignore.is_none() {
                        m_custom_ignore = ig
                            .inner
                            .custom_ignore_matcher
                            .matched(&path, is_dir)
                            .map(IgnoreMatch::gitignore);
                    }
                    if m_ignore.is_none() {
                        m_ignore = ig
                            .inner
                            .ignore_matcher
                            .matched(&path, is_dir)
                            .map(IgnoreMatch::gitignore);
                    }
                    if any_git && !saw_git && m_gi.is_none() {
                        m_gi = ig
                            .inner
                            .git_ignore_matcher
                            .matched(&path, is_dir)
                            .map(IgnoreMatch::gitignore);
                    }
                    if any_git && !saw_git && m_gi_exclude.is_none() {
                        m_gi_exclude = ig
                            .inner
                            .git_exclude_matcher
                            .matched(&path, is_dir)
                            .map(IgnoreMatch::gitignore);
                    }
                    saw_git = saw_git || ig.inner.has_git;
                }
            }
        }
        for gi in self.inner.explicit_ignores.iter().rev() {
            if !m_explicit.is_none() {
                break;
            }
            m_explicit = gi.matched(&path, is_dir).map(IgnoreMatch::gitignore);
        }
        let m_global = if any_git {
            self.inner
                .git_global_matcher
                .matched(&path, is_dir)
                .map(IgnoreMatch::gitignore)
        } else {
            Match::None
        };

        m_custom_ignore
            .or(m_ignore)
            .or(m_gi)
            .or(m_gi_exclude)
            .or(m_global)
            .or(m_explicit)
    }

    /// Returns an iterator over parent ignore matchers, including this one.
    pub(crate) fn parents(&self) -> Parents<'_> {
        Parents(Some(IgnoreRef { inner: &self.inner }))
    }

    /// Returns the first absolute path of the first absolute parent, if
    /// one exists.
    fn absolute_base(&self) -> Option<&Path> {
        self.absolute_base.as_ref().map(|p| &***p)
    }
}

/// State for tracking what kinds of files ripgrep is interested in for a
/// given directory.
///
/// This is computed over the entire set of files in a directory instead of
/// trying to stat each file individually. If a file is present, it's only then
/// that we stat it for more information, instead of relying on the stat to
/// determine its existence.
#[derive(Debug)]
struct IgnoreFilesFound {
    has_ignore: bool,
    has_git_ignore: bool,
    has_git_dir: bool,
    has_jj_dir: bool,
    custom_ignore_files: Vec<bool>,
}

#[derive(Clone, Copy)]
pub(crate) struct IgnoreRef<'a> {
    inner: &'a IgnoreInner,
}

impl IgnoreRef<'_> {
    pub(crate) fn path(&self) -> &Path {
        &self.inner.dir
    }

    pub(crate) fn is_absolute_parent(&self) -> bool {
        self.inner.is_absolute_parent
    }
}

/// An iterator over all parents of an ignore matcher, including itself.
pub(crate) struct Parents<'a>(Option<IgnoreRef<'a>>);

impl<'a> Iterator for Parents<'a> {
    type Item = IgnoreRef<'a>;

    fn next(&mut self) -> Option<IgnoreRef<'a>> {
        match self.0.take() {
            None => None,
            Some(ig) => {
                self.0 = ig
                    .inner
                    .parent
                    .as_deref()
                    .map(|inner| IgnoreRef { inner });
                Some(ig)
            }
        }
    }
}

/// A builder for creating an Ignore matcher.
#[derive(Clone, Debug)]
pub(crate) struct IgnoreBuilder {
    /// The root directory path for this ignore matcher.
    dir: PathBuf,
    /// An override matcher (default is empty).
    overrides: Arc<Override>,
    /// A type matcher (default is empty).
    types: Arc<Types>,
    /// Explicit global ignore matchers.
    explicit_ignores: Vec<Gitignore>,
    /// Ignore files in addition to .ignore.
    custom_ignore_filenames: Vec<OsString>,
    /// The directory that gitignores should be interpreted relative to.
    ///
    /// Usually this is the directory containing the gitignore file. But in
    /// some cases, like for global gitignores or for gitignores specified
    /// explicitly, this should generally be set to the current working
    /// directory. This is only used for global gitignores or "explicit"
    /// gitignores.
    ///
    /// When `None`, global gitignores are ignored.
    global_gitignores_relative_to: Option<PathBuf>,
    /// Ignore config.
    opts: IgnoreOptions,
}

impl IgnoreBuilder {
    /// Create a new builder for an `Ignore` matcher.
    ///
    /// It is likely a bug to use this without also calling `current_dir()`
    /// outside of tests. This isn't made mandatory because this is an internal
    /// abstraction and it's annoying to update tests.
    pub(crate) fn new() -> IgnoreBuilder {
        IgnoreBuilder {
            dir: Path::new("").to_path_buf(),
            overrides: Arc::new(Override::empty()),
            types: Arc::new(Types::empty()),
            explicit_ignores: vec![],
            custom_ignore_filenames: vec![],
            global_gitignores_relative_to: None,
            opts: IgnoreOptions {
                hidden: true,
                ignore: true,
                parents: true,
                git_global: true,
                git_ignore: true,
                git_exclude: true,
                ignore_case_insensitive: false,
                require_git: true,
            },
        }
    }

    /// Builds a new `Ignore` matcher.
    ///
    /// The matcher returned won't match anything until ignore rules from
    /// directories are added to it.
    pub(crate) fn build(&self) -> Ignore {
        self.build_with_cwd(None)
    }

    /// Builds a new `Ignore` matcher using the given CWD directory.
    ///
    /// The matcher returned won't match anything until ignore rules from
    /// directories are added to it.
    pub(crate) fn build_with_cwd(&self, cwd: Option<PathBuf>) -> Ignore {
        let global_gitignores_relative_to =
            cwd.or_else(|| self.global_gitignores_relative_to.clone());
        let git_global_matcher = if !self.opts.git_global {
            Gitignore::empty()
        } else if let Some(ref cwd) = global_gitignores_relative_to {
            let mut builder = GitignoreBuilder::new(cwd);
            builder
                .case_insensitive(self.opts.ignore_case_insensitive)
                .unwrap();
            let (gi, err) = builder.build_global();
            if let Some(err) = err {
                log::debug!("{}", err);
            }
            gi
        } else {
            log::debug!(
                "ignoring global gitignore file because CWD is not known"
            );
            Gitignore::empty()
        };

        Ignore {
            inner: Arc::new(IgnoreInner {
                compiled: Arc::new(RwLock::new(HashMap::new())),
                dir: self.dir.clone(),
                overrides: self.overrides.clone(),
                types: self.types.clone(),
                parent: None,
                is_absolute_parent: true,
                global_gitignores_relative_to,
                explicit_ignores: Arc::new(self.explicit_ignores.clone()),
                custom_ignore_filenames: Arc::new(
                    self.custom_ignore_filenames.clone(),
                ),
                custom_ignore_matcher: Gitignore::empty(),
                ignore_matcher: Gitignore::empty(),
                git_global_matcher: Arc::new(git_global_matcher),
                git_ignore_matcher: Gitignore::empty(),
                git_exclude_matcher: Gitignore::empty(),
                has_git: false,
                opts: self.opts,
            }),
            absolute_base: None,
        }
    }

    /// Set the current directory used for matching global gitignores.
    pub(crate) fn current_dir(
        &mut self,
        cwd: impl Into<PathBuf>,
    ) -> &mut IgnoreBuilder {
        self.global_gitignores_relative_to = Some(cwd.into());
        self
    }

    /// Add an override matcher.
    ///
    /// By default, no override matcher is used.
    ///
    /// This overrides any previous setting.
    pub(crate) fn overrides(
        &mut self,
        overrides: Override,
    ) -> &mut IgnoreBuilder {
        self.overrides = Arc::new(overrides);
        self
    }

    /// Add a file type matcher.
    ///
    /// By default, no file type matcher is used.
    ///
    /// This overrides any previous setting.
    pub(crate) fn types(&mut self, types: Types) -> &mut IgnoreBuilder {
        self.types = Arc::new(types);
        self
    }

    /// Adds a new global ignore matcher from the ignore file path given.
    pub(crate) fn add_ignore(&mut self, ig: Gitignore) -> &mut IgnoreBuilder {
        self.explicit_ignores.push(ig);
        self
    }

    /// Add a custom ignore file name
    ///
    /// These ignore files have higher precedence than all other ignore files.
    ///
    /// When specifying multiple names, earlier names have lower precedence than
    /// later names.
    pub(crate) fn add_custom_ignore_filename<S: AsRef<OsStr>>(
        &mut self,
        file_name: S,
    ) -> &mut IgnoreBuilder {
        self.custom_ignore_filenames.push(file_name.as_ref().to_os_string());
        self
    }

    /// Enables ignoring hidden files.
    ///
    /// This is enabled by default.
    pub(crate) fn hidden(&mut self, yes: bool) -> &mut IgnoreBuilder {
        self.opts.hidden = yes;
        self
    }

    /// Enables reading `.ignore` files.
    ///
    /// `.ignore` files have the same semantics as `gitignore` files and are
    /// supported by search tools such as ripgrep and The Silver Searcher.
    ///
    /// This is enabled by default.
    pub(crate) fn ignore(&mut self, yes: bool) -> &mut IgnoreBuilder {
        self.opts.ignore = yes;
        self
    }

    /// Enables reading ignore files from parent directories.
    ///
    /// If this is enabled, then .gitignore files in parent directories of each
    /// file path given are respected. Otherwise, they are ignored.
    ///
    /// This is enabled by default.
    pub(crate) fn parents(&mut self, yes: bool) -> &mut IgnoreBuilder {
        self.opts.parents = yes;
        self
    }

    /// Add a global gitignore matcher.
    ///
    /// Its precedence is lower than both normal `.gitignore` files and
    /// `.git/info/exclude` files.
    ///
    /// This overwrites any previous global gitignore setting.
    ///
    /// This is enabled by default.
    pub(crate) fn git_global(&mut self, yes: bool) -> &mut IgnoreBuilder {
        self.opts.git_global = yes;
        self
    }

    /// Enables reading `.gitignore` files.
    ///
    /// `.gitignore` files have match semantics as described in the `gitignore`
    /// man page.
    ///
    /// This is enabled by default.
    pub(crate) fn git_ignore(&mut self, yes: bool) -> &mut IgnoreBuilder {
        self.opts.git_ignore = yes;
        self
    }

    /// Enables reading `.git/info/exclude` files.
    ///
    /// `.git/info/exclude` files have match semantics as described in the
    /// `gitignore` man page.
    ///
    /// This is enabled by default.
    pub(crate) fn git_exclude(&mut self, yes: bool) -> &mut IgnoreBuilder {
        self.opts.git_exclude = yes;
        self
    }

    /// Whether a git repository is required to apply git-related ignore
    /// rules (global rules, .gitignore and local exclude rules).
    ///
    /// When disabled, git-related ignore rules are applied even when searching
    /// outside a git repository.
    pub(crate) fn require_git(&mut self, yes: bool) -> &mut IgnoreBuilder {
        self.opts.require_git = yes;
        self
    }

    /// Process ignore files case insensitively
    ///
    /// This is disabled by default.
    pub(crate) fn ignore_case_insensitive(
        &mut self,
        yes: bool,
    ) -> &mut IgnoreBuilder {
        self.opts.ignore_case_insensitive = yes;
        self
    }
}

/// Creates a new gitignore matcher for the directory given.
///
/// The matcher is meant to match files below `dir`.
/// Ignore globs are extracted from each of the file names relative to
/// `dir_for_ignorefile` in the order given (earlier names have lower
/// precedence than later names).
///
/// I/O errors are ignored.
pub(crate) fn create_gitignore<T: AsRef<OsStr>>(
    dir: &Path,
    dir_for_ignorefile: &Path,
    names: &[T],
    case_insensitive: bool,
) -> (Gitignore, Option<Error>) {
    let mut builder = GitignoreBuilder::new(dir);
    let mut errs = PartialErrorBuilder::default();
    builder.case_insensitive(case_insensitive).unwrap();
    for name in names {
        let gipath = dir_for_ignorefile.join(name.as_ref());
        // This check is not necessary, but is added for performance. Namely,
        // a simple stat call checking for existence can often be just a bit
        // quicker than actually trying to open a file. Since the number of
        // directories without ignore files likely greatly exceeds the number
        // with ignore files, this check generally makes sense.
        //
        // However, until demonstrated otherwise, we speculatively do not do
        // this on Windows since Windows is notorious for having slow file
        // system operations. Namely, it's not clear whether this analysis
        // makes sense on Windows.
        //
        // For more details: https://github.com/BurntSushi/ripgrep/pull/1381
        if cfg!(windows) || gipath.exists() {
            errs.maybe_push_ignore_io(builder.add(gipath));
        }
    }
    let gi = match builder.build() {
        Ok(gi) => gi,
        Err(err) => {
            errs.push(err);
            GitignoreBuilder::new(dir).build().unwrap()
        }
    };
    (gi, errs.into_error_option())
}

/// Find the GIT_COMMON_DIR for the given git worktree.
///
/// This is the directory that may contain a private ignore file
/// "info/exclude". Unlike git, this function does *not* read environment
/// variables GIT_DIR and GIT_COMMON_DIR, because it is not clear how to use
/// them when multiple repositories are searched.
///
/// Some I/O errors are ignored.
fn resolve_git_commondir(
    dir: &Path,
    git_type: Option<FileType>,
) -> Result<PathBuf, Option<Error>> {
    let git_dir_path = || dir.join(".git");
    let git_dir = git_dir_path();
    if !git_type.map_or(false, |ft| ft.is_file()) {
        return Ok(git_dir);
    }
    let file = match File::open(git_dir) {
        Ok(file) => io::BufReader::new(file),
        Err(err) => {
            return Err(Some(Error::Io(err).with_path(git_dir_path())));
        }
    };
    let dot_git_line = match file.lines().next() {
        Some(Ok(line)) => line,
        Some(Err(err)) => {
            return Err(Some(Error::Io(err).with_path(git_dir_path())));
        }
        None => return Err(None),
    };
    if !dot_git_line.starts_with("gitdir: ") {
        return Err(None);
    }
    let real_git_dir = PathBuf::from(&dot_git_line["gitdir: ".len()..]);
    let git_commondir_file = || real_git_dir.join("commondir");
    let file = match File::open(git_commondir_file()) {
        Ok(file) => io::BufReader::new(file),
        Err(_) => return Err(None),
    };
    let commondir_line = match file.lines().next() {
        Some(Ok(line)) => line,
        Some(Err(err)) => {
            return Err(Some(Error::Io(err).with_path(git_commondir_file())));
        }
        None => return Err(None),
    };
    let commondir_abs = if commondir_line.starts_with(".") {
        real_git_dir.join(commondir_line) // relative commondir
    } else {
        PathBuf::from(commondir_line)
    };
    Ok(commondir_abs)
}

/// Strips `prefix` from `path` if it's a prefix, otherwise returns `path`
/// unchanged.
fn strip_if_is_prefix<'a, P: AsRef<Path> + ?Sized>(
    prefix: &'a P,
    path: &'a Path,
) -> &'a Path {
    strip_prefix(prefix, path).map_or(path, |p| p)
}

#[cfg(test)]
mod tests {
    use std::{io::Write, path::Path, sync::Arc};

    use crate::{
        Error, dir::IgnoreBuilder, gitignore::Gitignore, tests::TempDir,
    };

    fn wfile<P: AsRef<Path>>(path: P, contents: &str) {
        let mut file = std::fs::File::create(path).unwrap();
        file.write_all(contents.as_bytes()).unwrap();
    }

    fn mkdirp<P: AsRef<Path>>(path: P) {
        std::fs::create_dir_all(path).unwrap();
    }

    fn partial(err: Error) -> Vec<Error> {
        match err {
            Error::Partial(errs) => errs,
            _ => panic!("expected partial error but got {:?}", err),
        }
    }

    fn tmpdir() -> TempDir {
        TempDir::new().unwrap()
    }

    #[test]
    fn explicit_ignore() {
        let td = tmpdir();
        wfile(td.path().join("not-an-ignore"), "foo\n!bar");

        let (gi, err) = Gitignore::new(td.path().join("not-an-ignore"));
        assert!(err.is_none());
        let (ig, err) =
            IgnoreBuilder::new().add_ignore(gi).build().add_child(td.path());
        assert!(err.is_none());
        assert!(ig.matched("foo", false).is_ignore());
        assert!(ig.matched("bar", false).is_whitelist());
        assert!(ig.matched("baz", false).is_none());
    }

    #[test]
    fn git_exclude() {
        let td = tmpdir();
        mkdirp(td.path().join(".git/info"));
        wfile(td.path().join(".git/info/exclude"), "foo\n!bar");

        let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
        assert!(err.is_none());
        assert!(ig.matched("foo", false).is_ignore());
        assert!(ig.matched("bar", false).is_whitelist());
        assert!(ig.matched("baz", false).is_none());
    }

    #[test]
    fn gitignore() {
        let td = tmpdir();
        mkdirp(td.path().join(".git"));
        wfile(td.path().join(".gitignore"), "foo\n!bar");

        let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
        assert!(err.is_none());
        assert!(ig.matched("foo", false).is_ignore());
        assert!(ig.matched("bar", false).is_whitelist());
        assert!(ig.matched("baz", false).is_none());
    }

    #[test]
    fn gitignore_with_jj() {
        let td = tmpdir();
        mkdirp(td.path().join(".jj"));
        wfile(td.path().join(".gitignore"), "foo\n!bar");

        let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
        assert!(err.is_none());
        assert!(ig.matched("foo", false).is_ignore());
        assert!(ig.matched("bar", false).is_whitelist());
        assert!(ig.matched("baz", false).is_none());
    }

    #[test]
    fn gitignore_no_git() {
        let td = tmpdir();
        wfile(td.path().join(".gitignore"), "foo\n!bar");

        let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
        assert!(err.is_none());
        assert!(ig.matched("foo", false).is_none());
        assert!(ig.matched("bar", false).is_none());
        assert!(ig.matched("baz", false).is_none());
    }

    #[test]
    fn gitignore_allowed_no_git() {
        let td = tmpdir();
        wfile(td.path().join(".gitignore"), "foo\n!bar");

        let (ig, err) = IgnoreBuilder::new()
            .require_git(false)
            .build()
            .add_child(td.path());
        assert!(err.is_none());
        assert!(ig.matched("foo", false).is_ignore());
        assert!(ig.matched("bar", false).is_whitelist());
        assert!(ig.matched("baz", false).is_none());
    }

    #[test]
    fn ignore() {
        let td = tmpdir();
        wfile(td.path().join(".ignore"), "foo\n!bar");

        let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
        assert!(err.is_none());
        assert!(ig.matched("foo", false).is_ignore());
        assert!(ig.matched("bar", false).is_whitelist());
        assert!(ig.matched("baz", false).is_none());
    }

    #[test]
    fn custom_ignore() {
        let td = tmpdir();
        let custom_ignore = ".customignore";
        wfile(td.path().join(custom_ignore), "foo\n!bar");

        let (ig, err) = IgnoreBuilder::new()
            .add_custom_ignore_filename(custom_ignore)
            .build()
            .add_child(td.path());
        assert!(err.is_none());
        assert!(ig.matched("foo", false).is_ignore());
        assert!(ig.matched("bar", false).is_whitelist());
        assert!(ig.matched("baz", false).is_none());
    }

    // Tests that a custom ignore file will override an .ignore.
    #[test]
    fn custom_ignore_over_ignore() {
        let td = tmpdir();
        let custom_ignore = ".customignore";
        wfile(td.path().join(".ignore"), "foo");
        wfile(td.path().join(custom_ignore), "!foo");

        let (ig, err) = IgnoreBuilder::new()
            .add_custom_ignore_filename(custom_ignore)
            .build()
            .add_child(td.path());
        assert!(err.is_none());
        assert!(ig.matched("foo", false).is_whitelist());
    }

    // Tests that earlier custom ignore files have lower precedence than later.
    #[test]
    fn custom_ignore_precedence() {
        let td = tmpdir();
        let custom_ignore1 = ".customignore1";
        let custom_ignore2 = ".customignore2";
        wfile(td.path().join(custom_ignore1), "foo");
        wfile(td.path().join(custom_ignore2), "!foo");

        let (ig, err) = IgnoreBuilder::new()
            .add_custom_ignore_filename(custom_ignore1)
            .add_custom_ignore_filename(custom_ignore2)
            .build()
            .add_child(td.path());
        assert!(err.is_none());
        assert!(ig.matched("foo", false).is_whitelist());
    }

    // Tests that an .ignore will override a .gitignore.
    #[test]
    fn ignore_over_gitignore() {
        let td = tmpdir();
        wfile(td.path().join(".gitignore"), "foo");
        wfile(td.path().join(".ignore"), "!foo");

        let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
        assert!(err.is_none());
        assert!(ig.matched("foo", false).is_whitelist());
    }

    // Tests that exclude has lower precedent than both .ignore and .gitignore.
    #[test]
    fn exclude_lowest() {
        let td = tmpdir();
        wfile(td.path().join(".gitignore"), "!foo");
        wfile(td.path().join(".ignore"), "!bar");
        mkdirp(td.path().join(".git/info"));
        wfile(td.path().join(".git/info/exclude"), "foo\nbar\nbaz");

        let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
        assert!(err.is_none());
        assert!(ig.matched("baz", false).is_ignore());
        assert!(ig.matched("foo", false).is_whitelist());
        assert!(ig.matched("bar", false).is_whitelist());
    }

    #[test]
    fn errored() {
        let td = tmpdir();
        wfile(td.path().join(".gitignore"), "{foo");

        let (_, err) = IgnoreBuilder::new().build().add_child(td.path());
        assert!(err.is_some());
    }

    #[test]
    fn errored_both() {
        let td = tmpdir();
        wfile(td.path().join(".gitignore"), "{foo");
        wfile(td.path().join(".ignore"), "{bar");

        let (_, err) = IgnoreBuilder::new().build().add_child(td.path());
        assert_eq!(2, partial(err.expect("an error")).len());
    }

    #[test]
    fn errored_partial() {
        let td = tmpdir();
        mkdirp(td.path().join(".git"));
        wfile(td.path().join(".gitignore"), "{foo\nbar");

        let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
        assert!(err.is_some());
        assert!(ig.matched("bar", false).is_ignore());
    }

    #[test]
    fn errored_partial_and_ignore() {
        let td = tmpdir();
        wfile(td.path().join(".gitignore"), "{foo\nbar");
        wfile(td.path().join(".ignore"), "!bar");

        let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
        assert!(err.is_some());
        assert!(ig.matched("bar", false).is_whitelist());
    }

    #[test]
    fn not_present_empty() {
        let td = tmpdir();

        let (_, err) = IgnoreBuilder::new().build().add_child(td.path());
        assert!(err.is_none());
    }

    #[test]
    fn stops_at_git_dir() {
        // This tests that .gitignore files beyond a .git barrier aren't
        // matched, but .ignore files are.
        let td = tmpdir();
        mkdirp(td.path().join(".git"));
        mkdirp(td.path().join("foo/.git"));
        wfile(td.path().join(".gitignore"), "foo");
        wfile(td.path().join(".ignore"), "bar");

        let ig0 = IgnoreBuilder::new().build();
        let (ig1, err) = ig0.add_child(td.path());
        assert!(err.is_none());
        let (ig2, err) = ig1.add_child(ig1.path().join("foo"));
        assert!(err.is_none());

        assert!(ig1.matched("foo", false).is_ignore());
        assert!(ig2.matched("foo", false).is_none());

        assert!(ig1.matched("bar", false).is_ignore());
        assert!(ig2.matched("bar", false).is_ignore());
    }

    #[test]
    fn absolute_parent() {
        let td = tmpdir();
        mkdirp(td.path().join(".git"));
        mkdirp(td.path().join("foo"));
        wfile(td.path().join(".gitignore"), "bar");

        // First, check that the parent gitignore file isn't detected if the
        // parent isn't added. This establishes a baseline.
        let ig0 = IgnoreBuilder::new().build();
        let (ig1, err) = ig0.add_child(td.path().join("foo"));
        assert!(err.is_none());
        assert!(ig1.matched("bar", false).is_none());

        // Second, check that adding a parent directory actually works.
        let ig0 = IgnoreBuilder::new().build();
        let (ig1, err) = ig0.add_parents(td.path().join("foo"));
        assert!(err.is_none());
        let (ig2, err) = ig1.add_child(td.path().join("foo"));
        assert!(err.is_none());
        assert!(ig2.matched("bar", false).is_ignore());
    }

    #[test]
    fn absolute_parent_anchored() {
        let td = tmpdir();
        mkdirp(td.path().join(".git"));
        mkdirp(td.path().join("src/llvm"));
        wfile(td.path().join(".gitignore"), "/llvm/\nfoo");

        let ig0 = IgnoreBuilder::new().build();
        let (ig1, err) = ig0.add_parents(td.path().join("src"));
        assert!(err.is_none());
        let (ig2, err) = ig1.add_child("src");
        assert!(err.is_none());

        assert!(ig1.matched("llvm", true).is_none());
        assert!(ig2.matched("llvm", true).is_none());
        assert!(ig2.matched("src/llvm", true).is_none());
        assert!(ig2.matched("foo", false).is_ignore());
        assert!(ig2.matched("src/foo", false).is_ignore());
    }

    #[test]
    fn absolute_parent_matchers_are_cached_across_roots() {
        let td = tmpdir();
        mkdirp(td.path().join(".git"));
        mkdirp(td.path().join("src/build"));
        mkdirp(td.path().join("tests/build"));
        wfile(td.path().join(".gitignore"), "tests/**/build/\n");

        let ig0 = IgnoreBuilder::new().build();
        let (src_parents, err) = ig0.add_parents(td.path().join("src"));
        assert!(err.is_none());
        let (src, err) = src_parents.add_child(td.path().join("src"));
        assert!(err.is_none());
        let (tests_parents, err) = ig0.add_parents(td.path().join("tests"));
        assert!(err.is_none());
        let (tests, err) = tests_parents.add_child(td.path().join("tests"));
        assert!(err.is_none());

        assert!(Arc::ptr_eq(&src_parents.inner, &tests_parents.inner));
        assert!(src.matched("build", true).is_none());
        assert!(tests.matched("build", true).is_ignore());
    }

    /// Parent matchers are shared across search roots, but path rewriting for
    /// absolute parents must use each root's own base path. Otherwise a rule
    /// like `src/invalid` is matched against the wrong absolute path when
    /// `src` is searched before a sibling root (e.g. `tests`).
    ///
    /// Paths passed to `matched` use the same relative layout as `Walk` when
    /// roots are given as relative directory names.
    ///
    /// Regression for: https://github.com/BurntSushi/ripgrep/issues/3376
    /// and https://github.com/BurntSushi/ripgrep/issues/3419
    #[test]
    fn multi_root_gitignore_order_independent() {
        let td = tmpdir();
        let cwd = std::env::current_dir().unwrap();
        // Use paths relative to CWD like the CLI walk does for `rg pat src tests`.
        let root = td.path().strip_prefix(&cwd).unwrap_or(td.path());
        let src_root = root.join("src");
        let tests_root = root.join("tests");

        mkdirp(td.path().join(".git"));
        mkdirp(td.path().join("src"));
        mkdirp(td.path().join("tests"));
        wfile(td.path().join(".gitignore"), "src/invalid\n");
        wfile(td.path().join("src/invalid"), "x");
        wfile(td.path().join("src/valid"), "x");
        wfile(td.path().join("tests/valid"), "x");

        let ig0 = IgnoreBuilder::new().build();

        // Historically buggy order: search `src` first, then `tests`.
        let (src_parents, err) = ig0.add_parents(&src_root);
        assert!(err.is_none());
        let (src, err) = src_parents.add_child(&src_root);
        assert!(err.is_none());
        let (tests_parents, err) = ig0.add_parents(&tests_root);
        assert!(err.is_none());
        let (tests, err) = tests_parents.add_child(&tests_root);
        assert!(err.is_none());

        assert!(Arc::ptr_eq(&src_parents.inner, &tests_parents.inner));
        // Each root must carry its own absolute_base even though inners are shared.
        assert_ne!(
            src.absolute_base.as_ref().unwrap().as_path(),
            tests.absolute_base.as_ref().unwrap().as_path()
        );
        assert!(
            src.matched(src_root.join("invalid"), false).is_ignore(),
            "parent .gitignore must apply for the src root even when \
             another root was prepared in the same process"
        );
        assert!(src.matched(src_root.join("valid"), false).is_none());
        assert!(tests.matched(tests_root.join("valid"), false).is_none());

        // Reverse order should behave the same way.
        let ig0 = IgnoreBuilder::new().build();
        let (tests_parents, err) = ig0.add_parents(&tests_root);
        assert!(err.is_none());
        let (tests, err) = tests_parents.add_child(&tests_root);
        assert!(err.is_none());
        let (src_parents, err) = ig0.add_parents(&src_root);
        assert!(err.is_none());
        let (src, err) = src_parents.add_child(&src_root);
        assert!(err.is_none());

        assert!(src.matched(src_root.join("invalid"), false).is_ignore());
        assert!(src.matched(src_root.join("valid"), false).is_none());
        assert!(tests.matched(tests_root.join("valid"), false).is_none());
    }

    /// Same multi-root / order issue for non-git ignore files (e.g. `.rgignore`
    /// via custom ignore names).
    ///
    /// Regression for: https://github.com/BurntSushi/ripgrep/issues/3320
    #[test]
    fn multi_root_custom_ignore_order_independent() {
        let td = tmpdir();
        let cwd = std::env::current_dir().unwrap();
        let root = td.path().strip_prefix(&cwd).unwrap_or(td.path());
        let alpha_root = root.join("alpha");
        let beta_root = root.join("beta");

        mkdirp(td.path().join("alpha"));
        mkdirp(td.path().join("beta"));
        wfile(td.path().join(".rgignore"), "beta/**/*.svg\n");
        wfile(td.path().join("alpha/a.txt"), "x");
        wfile(td.path().join("beta/x.svg"), "x");

        let ig0 = IgnoreBuilder::new()
            .add_custom_ignore_filename(".rgignore")
            .ignore(false)
            .git_ignore(false)
            .git_global(false)
            .git_exclude(false)
            .build();

        let (alpha_parents, err) = ig0.add_parents(&alpha_root);
        assert!(err.is_none());
        let (alpha, err) = alpha_parents.add_child(&alpha_root);
        assert!(err.is_none());
        let (beta_parents, err) = ig0.add_parents(&beta_root);
        assert!(err.is_none());
        let (beta, err) = beta_parents.add_child(&beta_root);
        assert!(err.is_none());

        assert_ne!(
            alpha.absolute_base.as_ref().unwrap().as_path(),
            beta.absolute_base.as_ref().unwrap().as_path()
        );
        assert!(alpha.matched(alpha_root.join("a.txt"), false).is_none());
        assert!(
            beta.matched(beta_root.join("x.svg"), false).is_ignore(),
            "parent .rgignore must apply for the beta root regardless of \
             which root was set up first"
        );
    }

    #[test]
    fn git_info_exclude_in_linked_worktree() {
        let td = tmpdir();
        let git_dir = td.path().join(".git");
        mkdirp(git_dir.join("info"));
        wfile(git_dir.join("info/exclude"), "ignore_me");
        mkdirp(git_dir.join("worktrees/linked-worktree"));
        let commondir_path =
            || git_dir.join("worktrees/linked-worktree/commondir");
        mkdirp(td.path().join("linked-worktree"));
        let worktree_git_dir_abs = format!(
            "gitdir: {}",
            git_dir.join("worktrees/linked-worktree").to_str().unwrap(),
        );
        wfile(td.path().join("linked-worktree/.git"), &worktree_git_dir_abs);

        // relative commondir
        wfile(commondir_path(), "../..");
        let ib = IgnoreBuilder::new().build();
        let (ignore, err) = ib.add_child(td.path().join("linked-worktree"));
        assert!(err.is_none());
        assert!(ignore.matched("ignore_me", false).is_ignore());

        // absolute commondir
        wfile(commondir_path(), git_dir.to_str().unwrap());
        let (ignore, err) = ib.add_child(td.path().join("linked-worktree"));
        assert!(err.is_none());
        assert!(ignore.matched("ignore_me", false).is_ignore());

        // missing commondir file
        assert!(std::fs::remove_file(commondir_path()).is_ok());
        let (_, err) = ib.add_child(td.path().join("linked-worktree"));
        // We squash the error in this case, because it occurs in repositories
        // that are not linked worktrees but have submodules.
        assert!(err.is_none());

        wfile(td.path().join("linked-worktree/.git"), "garbage");
        let (_, err) = ib.add_child(td.path().join("linked-worktree"));
        assert!(err.is_none());

        wfile(td.path().join("linked-worktree/.git"), "gitdir: garbage");
        let (_, err) = ib.add_child(td.path().join("linked-worktree"));
        assert!(err.is_none());
    }
}
