From e886ad061534bf28a74b05d8773241b98fe5b7c2 Mon Sep 17 00:00:00 2001 From: Zane Duffield Date: Sat, 9 Mar 2024 16:28:20 +1100 Subject: [PATCH] Add option to ignore nested git repositories This implements the suggestion made in #23 to provide an option to ignore nested git repositories. A nested git repository is identified by the presence of a .git file or directory. It's a directory in the regular case, but it's a file for git worktrees and git submodules. This option is disabled by default. --- crates/core/flags/complete/rg.zsh | 4 ++ crates/core/flags/defs.rs | 57 ++++++++++++++++++++++++++ crates/core/flags/hiargs.rs | 5 ++- crates/core/flags/lowargs.rs | 1 + crates/ignore/src/dir.rs | 66 +++++++++++++++++++++++++++++++ crates/ignore/src/walk.rs | 8 ++++ 6 files changed, 140 insertions(+), 1 deletion(-) diff --git a/crates/core/flags/complete/rg.zsh b/crates/core/flags/complete/rg.zsh index e9731a16..541cf816 100644 --- a/crates/core/flags/complete/rg.zsh +++ b/crates/core/flags/complete/rg.zsh @@ -146,6 +146,10 @@ _rg() { '--ignore-file-case-insensitive[process ignore files case insensitively]' $no'--no-ignore-file-case-insensitive[process ignore files case sensitively]' + + '(ignore-nested-git)' # Ignore nested git repository option + '--ignore-nested-git[ignore nested git repositories]' + $no"--no-ignore-nested-git[don't ignore nested git repositories]" + + '(ignore-exclude)' # Local exclude (ignore)-file options "--no-ignore-exclude[don't respect local exclude (ignore) files]" $no'--ignore-exclude[respect local exclude (ignore) files]' diff --git a/crates/core/flags/defs.rs b/crates/core/flags/defs.rs index b5114e28..c9603b16 100644 --- a/crates/core/flags/defs.rs +++ b/crates/core/flags/defs.rs @@ -83,6 +83,7 @@ pub(super) const FLAGS: &[&dyn Flag] = &[ &IgnoreCase, &IgnoreFile, &IgnoreFileCaseInsensitive, + &IgnoreNestedGit, &IncludeZero, &InvertMatch, &JSON, @@ -3238,6 +3239,62 @@ fn test_ignore_file_case_insensitive() { assert_eq!(true, args.ignore_file_case_insensitive); } +/// --ignore-nested-git +#[derive(Debug)] +struct IgnoreNestedGit; + +impl Flag for IgnoreNestedGit { + fn is_switch(&self) -> bool { + true + } + fn name_long(&self) -> &'static str { + "ignore-nested-git" + } + fn name_negated(&self) -> Option<&'static str> { + Some("no-ignore-nested-git") + } + fn doc_category(&self) -> Category { + Category::Filter + } + fn doc_short(&self) -> &'static str { + r"Ignore nested git repositories." + } + fn doc_long(&self) -> &'static str { + r" +Ignore any nested directory containing a \fB.git\fP file or directory. +This will prevent ripgrep from recursing into any git worktrees, submodules, +or regular repositories. +.sp +Note that this does not affect top-level directories. +" + } + + fn update(&self, v: FlagValue, args: &mut LowArgs) -> anyhow::Result<()> { + args.ignore_nested_git = v.unwrap_switch(); + Ok(()) + } +} + +#[cfg(test)] +#[test] +fn test_ignore_nested_git() { + let args = parse_low_raw(None::<&str>).unwrap(); + assert_eq!(false, args.ignore_nested_git); + + let args = parse_low_raw(["--ignore-nested-git"]).unwrap(); + assert_eq!(true, args.ignore_nested_git); + + let args = + parse_low_raw(["--ignore-nested-git", "--no-ignore-nested-git"]) + .unwrap(); + assert_eq!(false, args.ignore_nested_git); + + let args = + parse_low_raw(["--no-ignore-nested-git", "--ignore-nested-git"]) + .unwrap(); + assert_eq!(true, args.ignore_nested_git); +} + /// --include-zero #[derive(Debug)] struct IncludeZero; diff --git a/crates/core/flags/hiargs.rs b/crates/core/flags/hiargs.rs index df09dced..dadd9500 100644 --- a/crates/core/flags/hiargs.rs +++ b/crates/core/flags/hiargs.rs @@ -59,6 +59,7 @@ pub(crate) struct HiArgs { hyperlink_config: grep::printer::HyperlinkConfig, ignore_file_case_insensitive: bool, ignore_file: Vec, + ignore_nested_git: bool, include_zero: bool, invert_match: bool, is_terminal_stdout: bool, @@ -275,6 +276,7 @@ impl HiArgs { hyperlink_config, ignore_file: low.ignore_file, ignore_file_case_insensitive: low.ignore_file_case_insensitive, + ignore_nested_git: low.ignore_nested_git, include_zero: low.include_zero, invert_match: low.invert_match, is_terminal_stdout: state.is_terminal_stdout, @@ -893,7 +895,8 @@ impl HiArgs { .git_ignore(!self.no_ignore_vcs) .git_exclude(!self.no_ignore_vcs && !self.no_ignore_exclude) .require_git(!self.no_require_git) - .ignore_case_insensitive(self.ignore_file_case_insensitive); + .ignore_case_insensitive(self.ignore_file_case_insensitive) + .ignore_nested_git_repo(self.ignore_nested_git); if !self.no_ignore_dot { builder.add_custom_ignore_filename(".rgignore"); } diff --git a/crates/core/flags/lowargs.rs b/crates/core/flags/lowargs.rs index 184c96ae..c31b2b02 100644 --- a/crates/core/flags/lowargs.rs +++ b/crates/core/flags/lowargs.rs @@ -64,6 +64,7 @@ pub(crate) struct LowArgs { pub(crate) iglobs: Vec, pub(crate) ignore_file: Vec, pub(crate) ignore_file_case_insensitive: bool, + pub(crate) ignore_nested_git: bool, pub(crate) include_zero: bool, pub(crate) invert_match: bool, pub(crate) line_number: Option, diff --git a/crates/ignore/src/dir.rs b/crates/ignore/src/dir.rs index b302943a..968566a8 100644 --- a/crates/ignore/src/dir.rs +++ b/crates/ignore/src/dir.rs @@ -46,6 +46,7 @@ enum IgnoreMatchInner<'a> { Gitignore(&'a gitignore::Glob), Types(types::Glob<'a>), Hidden, + NestedRepo, } impl<'a> IgnoreMatch<'a> { @@ -64,6 +65,9 @@ impl<'a> IgnoreMatch<'a> { fn hidden() -> IgnoreMatch<'static> { IgnoreMatch(IgnoreMatchInner::Hidden) } + fn nested_repo() -> IgnoreMatch<'static> { + IgnoreMatch(IgnoreMatchInner::NestedRepo) + } } /// Options for the ignore matcher, shared between the matcher itself and the @@ -84,6 +88,8 @@ struct IgnoreOptions { git_exclude: bool, /// Whether to ignore files case insensitively ignore_case_insensitive: bool, + /// Whether to ignore nested git repositories. + ignore_nested_git_repo: bool, /// Whether a git repository must be present in order to apply any /// git-related ignore rules. require_git: bool, @@ -342,6 +348,7 @@ impl Ignore { || opts.git_global || opts.git_ignore || opts.git_exclude + || opts.ignore_nested_git_repo || has_custom_ignore_files || has_explicit_ignores } @@ -422,6 +429,14 @@ impl Ignore { mut m_gi_exclude, mut m_explicit, ) = (Match::None, Match::None, Match::None, Match::None, Match::None); + + if is_dir + && self.0.opts.ignore_nested_git_repo + && path.join(".git").exists() + { + return Match::Ignore(IgnoreMatch::nested_repo()); + } + let any_git = !self.0.opts.require_git || self.parents().any(|ig| ig.0.has_git); let mut saw_git = false; @@ -599,6 +614,7 @@ impl IgnoreBuilder { git_ignore: true, git_exclude: true, ignore_case_insensitive: false, + ignore_nested_git_repo: false, require_git: true, }, } @@ -773,6 +789,17 @@ impl IgnoreBuilder { self.opts.ignore_case_insensitive = yes; self } + + /// Enables ignoring nested git repositories. + /// + /// This is disabled by default. + pub(crate) fn ignore_nested_git_repo( + &mut self, + yes: bool, + ) -> &mut IgnoreBuilder { + self.opts.ignore_nested_git_repo = yes; + self + } } /// Creates a new gitignore matcher for the directory given. @@ -887,6 +914,10 @@ mod tests { file.write_all(contents.as_bytes()).unwrap(); } + fn rmfile>(path: P) { + std::fs::remove_file(path).unwrap(); + } + fn mkdirp>(path: P) { std::fs::create_dir_all(path).unwrap(); } @@ -1132,6 +1163,41 @@ mod tests { assert!(ig2.matched("bar", false).is_ignore()); } + // + #[test] + fn ignore_nested_git() { + let td = tmpdir(); + let repo = td.path().join("foo"); + mkdirp(&repo); + let dotgit_path = repo.join(".git"); + wfile(&dotgit_path, ""); + wfile(repo.join("bar"), ""); + + let (ig_default, err) = + IgnoreBuilder::new().build().add_child(td.path()); + assert!(err.is_none()); + + let (ig_git, err) = IgnoreBuilder::new() + .ignore_nested_git_repo(true) + .build() + .add_child(td.path()); + assert!(err.is_none()); + + // is_dir = false, so no check for .git child + assert!(ig_git.matched(&repo, false).is_none()); + // on the same level as .git; it's expected that the parent directory wouldn't be recursed in this case + assert!(ig_git.matched(repo.join("bar"), false).is_none()); + // is_dir = true and has .git child + assert!(ig_git.matched(&repo, true).is_ignore()); + // but by default, don't ignore dir with .git child + assert!(ig_default.matched(&repo, true).is_none()); + + // also test with .git as a directory + rmfile(&dotgit_path); + mkdirp(&dotgit_path); + assert!(ig_git.matched(&repo, true).is_ignore()); + } + #[test] fn absolute_parent() { let td = tmpdir(); diff --git a/crates/ignore/src/walk.rs b/crates/ignore/src/walk.rs index a8d17180..f9327ea2 100644 --- a/crates/ignore/src/walk.rs +++ b/crates/ignore/src/walk.rs @@ -811,6 +811,14 @@ impl WalkBuilder { self } + /// Enables ignoring nested git repositories, including submodules. + /// + /// This is disabled by default. + pub fn ignore_nested_git_repo(&mut self, yes: bool) -> &mut WalkBuilder { + self.ig_builder.ignore_nested_git_repo(yes); + self + } + /// Set a function for sorting directory entries by their path. /// /// If a compare function is set, the resulting iterator will return all