diff --git a/src/gitignore.rs b/src/gitignore.rs index 4d90be82..e05dc582 100644 --- a/src/gitignore.rs +++ b/src/gitignore.rs @@ -31,7 +31,7 @@ use std::path::{Path, PathBuf}; use regex; use glob; -use pathutil::strip_prefix; +use pathutil::{is_file_name, strip_prefix}; /// Represents an error that can occur when parsing a gitignore file. #[derive(Debug)] @@ -115,8 +115,15 @@ impl Gitignore { if let Some(p) = strip_prefix("./", path) { path = p; } - if let Some(p) = strip_prefix(&self.root, path) { - path = p; + // Strip any common prefix between the candidate path and the root + // of the gitignore, to make sure we get relative matching right. + // BUT, a file name might not have any directory components to it, + // in which case, we don't want to accidentally strip any part of the + // file name. + if !is_file_name(path) { + if let Some(p) = strip_prefix(&self.root, path) { + path = p; + } } if let Some(p) = strip_prefix("/", path) { path = p; diff --git a/src/ignore.rs b/src/ignore.rs index 9a33dde6..d24909c5 100644 --- a/src/ignore.rs +++ b/src/ignore.rs @@ -227,16 +227,10 @@ impl Ignore { if let Some(is_ignored) = self.ignore_match(path, mat) { return is_ignored; } - if self.ignore_hidden && is_hidden(&path) { - debug!("{} ignored because it is hidden", path.display()); - return true; - } + let mut whitelisted = false; if !self.no_ignore { - let mut whitelisted = false; for id in self.stack.iter().rev() { let mat = id.matched(path, is_dir); - // println!("path: {}, mat: {:?}, id: {:?}", - // path.display(), mat, id); if let Some(is_ignored) = self.ignore_match(path, mat) { if is_ignored { return true; @@ -264,6 +258,7 @@ impl Ignore { // If this path is whitelisted by an ignore, then // fallthrough and let the file type matcher have a // say. + whitelisted = true; break; } } @@ -271,7 +266,14 @@ impl Ignore { } let mat = self.types.matched(path, is_dir); if let Some(is_ignored) = self.ignore_match(path, mat) { - return is_ignored; + if is_ignored { + return true; + } + whitelisted = true; + } + if !whitelisted && self.ignore_hidden && is_hidden(&path) { + debug!("{} ignored because it is hidden", path.display()); + return true; } false } diff --git a/src/pathutil.rs b/src/pathutil.rs index ba4b17bd..3a020fa6 100644 --- a/src/pathutil.rs +++ b/src/pathutil.rs @@ -98,3 +98,21 @@ pub fn is_hidden>(path: P) -> bool { false } } + +/// Returns true if this file path is just a file name. i.e., Its parent is +/// the empty string. +#[cfg(unix)] +pub fn is_file_name>(path: P) -> bool { + use std::os::unix::ffi::OsStrExt; + use memchr::memchr; + + let path = path.as_ref().as_os_str().as_bytes(); + memchr(b'/', path).is_none() +} + +/// Returns true if this file path is just a file name. i.e., Its parent is +/// the empty string. +#[cfg(not(unix))] +pub fn is_file_name>(path: P) -> bool { + path.as_ref().parent().map(|p| p.is_empty()).unwrap_or(false) +} diff --git a/tests/tests.rs b/tests/tests.rs index a937959b..85ca1164 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -694,6 +694,15 @@ clean!(regression_67, "test", ".", |wd: WorkDir, mut cmd: Command| { assert_eq!(lines, path("dir/bar:test\n")); }); +// See: https://github.com/BurntSushi/ripgrep/issues/90 +clean!(regression_90, "test", ".", |wd: WorkDir, mut cmd: Command| { + wd.create(".gitignore", "!.foo"); + wd.create(".foo", "test"); + + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, ".foo:test\n"); +}); + // See: https://github.com/BurntSushi/ripgrep/issues/20 sherlock!(feature_20, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { cmd.arg("--no-filename");