Repository: BurntSushi/ripgrep (rust)

Issue:
Behavior of ignoring files/folders in .gitignore does not align with git
This issue originally arose when using VS Code's search feature which actually uses `ripgrep`.  For specific details, you can see this issue:
https://github.com/Microsoft/vscode/issues/42174

The issue seems to be, `ripgrep` is not ignoring the same files and folders as `git`.  If this is intended behavior, it isn't really stated as the documentation simply states it will be "respecting your gitignore rules."  Currently, if I have a structure as the following:
```
- path1/
  - file_to_exclude_1
  - file_to_exclude_2
  - file_to_exclude_3
- path2/
  - file_to_include_1
  - path1/
    - file_to_include_2
```
If I also have a `.gitignore` which has the following entry:
```
path1/*
```
`git` will only ignore `./path1` in the Root of the project and not `./path2/path1`.  `ripgrep` seems to be ignoring both `./path1` and `./path2/path1` which is inconsistent with what `git` ignores when using these rules.

Pull request:
Make handling of patterns containing a `/` match actual git behaviour
Fixes #761

This is kind of a scary change, but it *seems* right...?

Per the unit tests, the `ignore` crate assumes that `bar/*.rs` should match both `bar/main.rs` and `foo/bar/main.rs`. However, the `gitignore` documentation seems to state very clearly that it should *not* behave this way:

>Otherwise [if the pattern contains a slash], Git treats the pattern as a shell glob suitable for consumption by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will not match a / in the pathname. **For example, "Documentation/*.html" matches "Documentation/git.html" but not "Documentation/ppc/ppc.html" or "tools/perf/Documentation/perf.html".**

The real-world behaviour of git 2.16.1 is consistent with this.

Looking at the `ignore` unit tests, the test patterns borrowed from [behnam/gitignore-test](https://github.com/behnam/gitignore-test) seem to have made this assumption as well:

```
# NO_MATCH
dir_deep_02/*

# NO_MATCH
dir_deep_03/**
```

Both patterns say `NO_MATCH`, but the unit tests designed for them *do* in fact expect a match. I don't see any comments anywhere that explain why it should be different, so i'm guessing it wasn't an intentional design decision or anything.

Anyway, *without* this change, if you check out the `gitignore-test` repo, build the file tree, and then compare `rg --files` to `git add . && git status`, you'll see that `rg` is missing the two matches for those directories. *With* the change, they both match exactly the same files.

Patch:
diff --git a/ignore/src/gitignore.rs b/ignore/src/gitignore.rs
index e7b0007..a21afa5 100644
--- a/ignore/src/gitignore.rs
+++ b/ignore/src/gitignore.rs
@@ -416,7 +416,6 @@ impl GitignoreBuilder {
             is_only_dir: false,
         };
         let mut literal_separator = false;
-        let has_slash = line.chars().any(|c| c == '/');
         let mut is_absolute = false;
         if line.starts_with("\\!") || line.starts_with("\\#") {
             line = &line[1..];
@@ -447,13 +446,13 @@ impl GitignoreBuilder {
         // If there is a literal slash, then we note that so that globbing
         // doesn't let wildcards match slashes.
         glob.actual = line.to_string();
-        if has_slash {
+        if is_absolute || line.chars().any(|c| c == '/') {
             literal_separator = true;
         }
-        // If there was a leading slash, then this is a glob that must
-        // match the entire path name. Otherwise, we should let it match
-        // anywhere, so use a **/ prefix.
-        if !is_absolute {
+        // If there was a slash, then this is a glob that must match the entire
+        // path name. Otherwise, we should let it match anywhere, so use a **/
+        // prefix.
+        if !literal_separator {
             // ... but only if we don't already have a **/ prefix.
             if !(glob.actual.starts_with("**/") || (glob.actual == "**" && glob.is_only_dir)) {
                 glob.actual = format!("**/{}", glob.actual);
@@ -617,10 +616,10 @@ mod tests {
     ignored!(ig25, ROOT, "Cargo.lock", "./tabwriter-bin/Cargo.lock");
     ignored!(ig26, ROOT, "/foo/bar/baz", "./foo/bar/baz");
     ignored!(ig27, ROOT, "foo/", "xyz/foo", true);
-    ignored!(ig28, ROOT, "src/*.rs", "src/grep/src/main.rs");
-    ignored!(ig29, "./src", "/llvm/", "./src/llvm", true);
-    ignored!(ig30, ROOT, "node_modules/ ", "node_modules", true);
-    ignored!(ig31, ROOT, "**/", "foo/bar", true);
+    ignored!(ig28, "./src", "/llvm/", "./src/llvm", true);
+    ignored!(ig29, ROOT, "node_modules/ ", "node_modules", true);
+    ignored!(ig30, ROOT, "**/", "foo/bar", true);
+    ignored!(ig31, ROOT, "path1/*", "path1/foo");
 
     not_ignored!(ignot1, ROOT, "amonths", "months");
     not_ignored!(ignot2, ROOT, "monthsa", "months");
@@ -640,6 +639,8 @@ mod tests {
         "./third_party/protobuf/csharp/src/packages/repositories.config");
     not_ignored!(ignot15, ROOT, "!/bar", "foo/bar");
     not_ignored!(ignot16, ROOT, "*\n!**/", "foo", true);
+    not_ignored!(ignot17, ROOT, "src/*.rs", "src/grep/src/main.rs");
+    not_ignored!(ignot18, ROOT, "path1/*", "path2/path1/foo");
 
     fn bytes(s: &str) -> Vec<u8> {
         s.to_string().into_bytes()
diff --git a/ignore/src/overrides.rs b/ignore/src/overrides.rs
index 03fd39c..955dc5a 100644
--- a/ignore/src/overrides.rs
+++ b/ignore/src/overrides.rs
@@ -202,8 +202,9 @@ mod tests {
     #[test]
     fn gitignore() {
         let ov = ov(&["/foo", "bar/*.rs", "baz/**"]);
+        assert!(ov.matched("bar/lib.rs", false).is_whitelist());
         assert!(ov.matched("bar/wat/lib.rs", false).is_ignore());
-        assert!(ov.matched("wat/bar/lib.rs", false).is_whitelist());
+        assert!(ov.matched("wat/bar/lib.rs", false).is_ignore());
         assert!(ov.matched("foo", false).is_whitelist());
         assert!(ov.matched("wat/foo", false).is_ignore());
         assert!(ov.matched("baz", false).is_ignore());


Test patch:
diff --git a/ignore/tests/gitignore_matched_path_or_any_parents_tests.rs b/ignore/tests/gitignore_matched_path_or_any_parents_tests.rs
index c76ee2d..4de7cf3 100644
--- a/ignore/tests/gitignore_matched_path_or_any_parents_tests.rs
+++ b/ignore/tests/gitignore_matched_path_or_any_parents_tests.rs
@@ -212,16 +212,16 @@ fn test_dirs_in_deep() {
     assert!(m("ROOT/parent_dir/dir_deep_01/child_dir/file", false).is_ignore());
 
     // 02
-    assert!(m("ROOT/parent_dir/dir_deep_02", true).is_none()); // dir itself doesn't match
-    assert!(m("ROOT/parent_dir/dir_deep_02/file", false).is_ignore());
-    assert!(m("ROOT/parent_dir/dir_deep_02/child_dir", true).is_ignore());
-    assert!(m("ROOT/parent_dir/dir_deep_02/child_dir/file", false).is_ignore());
+    assert!(m("ROOT/parent_dir/dir_deep_02", true).is_none());
+    assert!(m("ROOT/parent_dir/dir_deep_02/file", false).is_none());
+    assert!(m("ROOT/parent_dir/dir_deep_02/child_dir", true).is_none());
+    assert!(m("ROOT/parent_dir/dir_deep_02/child_dir/file", false).is_none());
 
     // 03
-    assert!(m("ROOT/parent_dir/dir_deep_03", true).is_none()); // dir itself doesn't match
-    assert!(m("ROOT/parent_dir/dir_deep_03/file", false).is_ignore());
-    assert!(m("ROOT/parent_dir/dir_deep_03/child_dir", true).is_ignore());
-    assert!(m("ROOT/parent_dir/dir_deep_03/child_dir/file", false).is_ignore());
+    assert!(m("ROOT/parent_dir/dir_deep_03", true).is_none());
+    assert!(m("ROOT/parent_dir/dir_deep_03/file", false).is_none());
+    assert!(m("ROOT/parent_dir/dir_deep_03/child_dir", true).is_none());
+    assert!(m("ROOT/parent_dir/dir_deep_03/child_dir/file", false).is_none());
 
     // 10
     assert!(m("ROOT/parent_dir/dir_deep_10", true).is_none());