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.


Discussion:
As a note, this rule seems to be explicitly stated on the `git` documentation page:
https://git-scm.com/docs/gitignore

> If the pattern does not contain a slash /, Git treats it as a shell glob pattern and checks for a match against the pathname relative to the location of the .gitignore file (relative to the toplevel of the work tree if not from a .gitignore file).

> Otherwise, 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".

In this case, it "contains a /" and so will ignore `./path1/some_file_or_folder` but not `./path2/path1/some_file_or_folder`.  I also found this exact same statement in the `man gitignore` pages on my git version:
```
git version 2.14.0
hub version 2.2.9
```
>For example, "Documentation/*.html" matches "Documentation/git.html" but not "Documentation/ppc/ppc.html" or "tools/perf/Documentation/perf.html".

If i'm looking at it right, it seems like the `ignore` crate deliberately rejects this — there is a test asserting that `src/*.rs` should ignore `src/grep/src/main.rs`, which contradicts both the quoted text and git's actual behaviour:

```
% echo 'path1/*.rs' > .gitignore
% touch path1/foo.rs path2/path1/foo.rs
% git check-ignore -v path1/foo.rs; echo $?
.gitignore:1:path1/*.rs	path1/foo.rs
0
% git check-ignore -v path2/path1/foo.rs; echo $?
1
```

What causes this behaviour (and the issue with `path1/*`) is the implicit prepending of `**/` to a rule even when it contains a `/`.



Patch:
diff --git a/ignore/src/gitignore.rs b/ignore/src/gitignore.rs
index e7b0007e2..a21afa554 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 03fd39c4e..955dc5add 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 c76ee2d1f..4de7cf3a0 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());