From 297b428c8c92dca2c62d57dd41ad085aa94c4aa4 Mon Sep 17 00:00:00 2001
From: Naveen Nathan <naveen@lastninja.net>
Date: Thu, 7 Nov 2019 11:23:57 +1100
Subject: [PATCH] cli: add --no-ignore-exclude flag

This commit adds a new --no-ignore-exclude flag that permits disabling
the use of .git/info/exclude filtering. Local exclusions are manual
configurations to a repository and are not shared, so it is sometimes
useful to disable to get a consistent view of a repository.

This also adds a new section to the man page that describes automatic
filtering.

Closes #1420
---
 CHANGELOG.md     |  2 ++
 complete/_rg     |  4 +++
 doc/rg.1.txt.tpl | 70 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/app.rs       | 21 +++++++++++++++
 src/args.rs      |  7 ++++-
 tests/feature.rs | 12 +++++++++
 6 files changed, 115 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 203bd1c6..5a3e9f1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,8 @@ Feature enhancements:
   Add `--include-zero` flag that shows files searched without matches.
 * [FEATURE #1390](https://github.com/BurntSushi/ripgrep/pull/1390):
   Add `--no-context-separator` flag that always hides context separators.
+* [FEATURE #1420](https://github.com/BurntSushi/ripgrep/pull/1420):
+  Add `--no-ignore-exclude` to disregard rules in `.git/info/exclude` files.
 
 Bug fixes:
 
diff --git a/complete/_rg b/complete/_rg
index 5fa6e9c2..b90d1801 100644
--- a/complete/_rg
+++ b/complete/_rg
@@ -129,6 +129,10 @@ _rg() {
     '--ignore-file-case-insensitive[process ignore files case insensitively]'
     $no'--no-ignore-file-case-insensitive[process ignore files case sensitively]'
 
+    + '(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]'
+
     + '(ignore-global)' # Global ignore-file options
     "--no-ignore-global[don't respect global ignore files]"
     $no'--ignore-global[respect global ignore files]'
diff --git a/doc/rg.1.txt.tpl b/doc/rg.1.txt.tpl
index 3ce0881d..2d3a391f 100644
--- a/doc/rg.1.txt.tpl
+++ b/doc/rg.1.txt.tpl
@@ -104,6 +104,76 @@ was found. In summary:
   unable to read a file).
 
 
+AUTOMATIC FILTERING
+-------------------
+TL;DR - To disable automatic filtering, use 'rg -uuu'.
+
+One of ripgrep's most important features is its automatic smart filtering.
+It is the most apparent differentiating feature between ripgrep and other tools
+like 'grep'. As such, its behavior may be surprising to users that aren't
+expecting it.
+
+ripgrep does four types of filtering automatically:
+
+    1. Files and directories that match ignore rules are not searched.
+    2. Hidden files and directories are not searched.
+    3. Binary files (files with a 'NUL' byte) are not searched.
+    4. Symbolic links are not followed.
+
+The first type of filtering is the most sophisticated. ripgrep will attempt to
+respect your gitignore rules as faithfully as possible. In particular, this
+includes the following:
+
+    * Any global rules, e.g., in '$HOME/.config/git/ignore'.
+    * Any rules in '.gitignore'.
+    * Any local rules, e.g., in '.git/info/exclude'.
+
+In some cases, ripgrep and git will not always be in sync in terms of which
+files are ignored. For example, a file that is ignored via '.gitignore' but is
+tracked by git would not be searched by ripgrep even though git tracks it. This
+is unlikely to ever be fixed. Instead, you should either make sure your exclude
+rules match the files you track precisely, or otherwise use 'git grep' for
+search.
+
+Additional ignore rules can be provided outside of a git context:
+
+    * Any rules in '.ignore'.
+    * Any rules in '.rgignore'.
+    * Any rules in files specified with the '--ignore-file' flag.
+
+The precedence of ignore rules is as follows, with later items overriding
+earlier items:
+
+    * Files given by '--ignore-file'.
+    * Global gitignore rules, e.g., from '$HOME/.config/git/ignore'.
+    * Local rules from '.git/info/exclude'.
+    * Rules from '.gitignore'.
+    * Rules from '.ignore'.
+    * Rules from '.rgignore'.
+
+So for example, if 'foo' were in a '.gitignore' and '!foo' were in an
+'.rgignore', then 'foo' would not be ignored since '.rgignore' takes precedence
+over '.gitignore'.
+
+Each of the types of filtering can be configured via command line flags:
+
+    * There are several flags starting with '--no-ignore' that toggle which,
+      if any, ignore rules are respected. '--no-ignore' by itself will disable
+      all of them.
+    * '--hidden' will force ripgrep to search hidden files and directories.
+    * '--binary' will force ripgrep to search binary files.
+    * '-L/--follow' will force ripgrep to follow symlinks.
+
+As a special short hand, the `-u` flag can be specified up to three times. Each
+additional time incrementally decreases filtering:
+
+    * '-u' is equivalent to '--no-ignore'.
+    * '-uu' is equivalent to '--no-ignore --hidden'.
+    * '-uuu' is equivalent to '--no-ignore --hidden --binary'.
+
+In particular, 'rg -uuu' should search the same exact content as 'grep -r'.
+
+
 CONFIGURATION FILES
 -------------------
 ripgrep supports reading configuration files that change ripgrep's default
diff --git a/src/app.rs b/src/app.rs
index b72baf09..94e8a421 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -595,6 +595,7 @@ pub fn all_args_and_flags() -> Vec<RGArg> {
     flag_no_config(&mut args);
     flag_no_ignore(&mut args);
     flag_no_ignore_dot(&mut args);
+    flag_no_ignore_exclude(&mut args);
     flag_no_ignore_global(&mut args);
     flag_no_ignore_messages(&mut args);
     flag_no_ignore_parent(&mut args);
@@ -1769,6 +1770,26 @@ This flag can be disabled with the --ignore-dot flag.
     args.push(arg);
 }
 
+fn flag_no_ignore_exclude(args: &mut Vec<RGArg>) {
+    const SHORT: &str = "Don't respect local exclusion files.";
+    const LONG: &str = long!("\
+Don't respect ignore files that are manually configured for the repository
+such as git's '.git/info/exclude'.
+
+This flag can be disabled with the --ignore-exclude flag.
+");
+    let arg = RGArg::switch("no-ignore-exclude")
+        .help(SHORT).long_help(LONG)
+        .overrides("ignore-exclude");
+    args.push(arg);
+
+    let arg = RGArg::switch("ignore-exclude")
+        .hidden()
+        .overrides("no-ignore-exclude");
+    args.push(arg);
+}
+
+
 fn flag_no_ignore_global(args: &mut Vec<RGArg>) {
     const SHORT: &str = "Don't respect global ignore files.";
     const LONG: &str = long!("\
diff --git a/src/args.rs b/src/args.rs
index 32ea1b11..3dc4b16f 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -881,7 +881,7 @@ impl ArgMatches {
             .ignore(!self.no_ignore_dot())
             .git_global(!self.no_ignore_vcs() && !self.no_ignore_global())
             .git_ignore(!self.no_ignore_vcs())
-            .git_exclude(!self.no_ignore_vcs())
+            .git_exclude(!self.no_ignore_vcs() && !self.no_ignore_exclude())
             .ignore_case_insensitive(self.ignore_file_case_insensitive());
         if !self.no_ignore() {
             builder.add_custom_ignore_filename(".rgignore");
@@ -1231,6 +1231,11 @@ impl ArgMatches {
         self.is_present("no-ignore-dot") || self.no_ignore()
     }
 
+    /// Returns true if local exclude (ignore) files should be ignored.
+    fn no_ignore_exclude(&self) -> bool {
+        self.is_present("no-ignore-exclude") || self.no_ignore()
+    }
+
     /// Returns true if global ignore files should be ignored.
     fn no_ignore_global(&self) -> bool {
         self.is_present("no-ignore-global") || self.no_ignore()
diff --git a/tests/feature.rs b/tests/feature.rs
index 4d918163..33ab4e17 100644
--- a/tests/feature.rs
+++ b/tests/feature.rs
@@ -728,6 +728,18 @@ rgtest!(f1207_ignore_encoding, |dir: Dir, mut cmd: TestCommand| {
     eqnice!("\u{FFFD}\u{FFFD}\x00b\n", cmd.stdout());
 });
 
+// See: https://github.com/BurntSushi/ripgrep/pull/1420
+rgtest!(f1420_no_ignore_dot, |dir: Dir, mut cmd: TestCommand| {
+    dir.create_dir(".git/info");
+    dir.create(".git/info/exclude", "foo");
+    dir.create("bar", "");
+    dir.create("foo", "");
+
+    cmd.arg("--sort").arg("path").arg("--files");
+    eqnice!("bar\n", cmd.stdout());
+    eqnice!("bar\nfoo\n", cmd.arg("--no-ignore-exclude").stdout());
+});
+
 rgtest!(no_context_sep, |dir: Dir, mut cmd: TestCommand| {
     dir.create("test", "foo\nctx\nbar\nctx\nfoo\nctx");
     cmd.args(&[