Skip to content

Commit

Permalink
Add option to ignore nested git repositories
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
zaneduffield committed Mar 9, 2024
1 parent e9abbc1 commit e886ad0
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 1 deletion.
4 changes: 4 additions & 0 deletions crates/core/flags/complete/rg.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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]'
Expand Down
57 changes: 57 additions & 0 deletions crates/core/flags/defs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub(super) const FLAGS: &[&dyn Flag] = &[
&IgnoreCase,
&IgnoreFile,
&IgnoreFileCaseInsensitive,
&IgnoreNestedGit,
&IncludeZero,
&InvertMatch,
&JSON,
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion crates/core/flags/hiargs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub(crate) struct HiArgs {
hyperlink_config: grep::printer::HyperlinkConfig,
ignore_file_case_insensitive: bool,
ignore_file: Vec<PathBuf>,
ignore_nested_git: bool,
include_zero: bool,
invert_match: bool,
is_terminal_stdout: bool,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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");
}
Expand Down
1 change: 1 addition & 0 deletions crates/core/flags/lowargs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub(crate) struct LowArgs {
pub(crate) iglobs: Vec<String>,
pub(crate) ignore_file: Vec<PathBuf>,
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<bool>,
Expand Down
66 changes: 66 additions & 0 deletions crates/ignore/src/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ enum IgnoreMatchInner<'a> {
Gitignore(&'a gitignore::Glob),
Types(types::Glob<'a>),
Hidden,
NestedRepo,
}

impl<'a> IgnoreMatch<'a> {
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -599,6 +614,7 @@ impl IgnoreBuilder {
git_ignore: true,
git_exclude: true,
ignore_case_insensitive: false,
ignore_nested_git_repo: false,
require_git: true,
},
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -887,6 +914,10 @@ mod tests {
file.write_all(contents.as_bytes()).unwrap();
}

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

fn mkdirp<P: AsRef<Path>>(path: P) {
std::fs::create_dir_all(path).unwrap();
}
Expand Down Expand Up @@ -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();
Expand Down
8 changes: 8 additions & 0 deletions crates/ignore/src/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit e886ad0

Please sign in to comment.