summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohannes Altmanninger <aclopte@gmail.com>2019-12-11 17:41:04 +0100
committerAndrew Gallant <jamslam@gmail.com>2020-02-17 17:16:28 -0500
commit6f2b79f5847f941aec44ed5813c0dae16c2b2946 (patch)
tree05efee20472972ee228d378f2a5450306af8dbbc
parent0c3b673e4c64c6e9c580b15ff8cf9c2a21c29cbd (diff)
ignore: use git commondir for sourcing .git/info/exclude
Git looks for this file in GIT_COMMON_DIR, which is usually the same as GIT_DIR (.git). However, when searching inside a linked worktree, .git is usually a file that contains the path of the actual git dir, which in turn contains a file "commondir" which references the directory where info/exclude may reside, alongside other configuration shared across all worktrees. This directory is usually the git dir of the main worktree. Unlike git this does *not* read environment variables GIT_DIR and GIT_COMMON_DIR, because it is not clear how to interpret them when searching multiple repositories. Fixes #1445, Closes #1446
-rw-r--r--CHANGELOG.md2
-rw-r--r--ignore/src/dir.rs176
-rw-r--r--tests/regression.rs16
3 files changed, 176 insertions, 18 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b6fd070..e841cc64 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,8 @@ Bug fixes:
Fixes a performance bug when searching plain text files with very long lines.
* [BUG #1344](https://github.com/BurntSushi/ripgrep/issues/1344):
Document usage of `--type all`.
+* [BUG #1445](https://github.com/BurntSushi/ripgrep/issues/1445):
+ ripgrep now respects ignore rules from .git/info/exclude in worktrees.
11.0.2 (2019-08-01)
diff --git a/ignore/src/dir.rs b/ignore/src/dir.rs
index 54e1f7be..cf957891 100644
--- a/ignore/src/dir.rs
+++ b/ignore/src/dir.rs
@@ -15,6 +15,8 @@
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
+use std::fs::{File, FileType};
+use std::io::{self, BufRead};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
@@ -220,12 +222,20 @@ impl Ignore {
/// Like add_child, but takes a full path and returns an IgnoreInner.
fn add_child_path(&self, dir: &Path) -> (IgnoreInner, Option<Error>) {
+ let git_type = if self.0.opts.git_ignore || self.0.opts.git_exclude {
+ dir.join(".git").metadata().ok().map(|md| md.file_type())
+ } else {
+ None
+ };
+ let has_git = git_type.map(|_| true).unwrap_or(false);
+
let mut errs = PartialErrorBuilder::default();
let custom_ig_matcher = if self.0.custom_ignore_filenames.is_empty() {
Gitignore::empty()
} else {
let (m, err) = create_gitignore(
&dir,
+ &dir,
&self.0.custom_ignore_filenames,
self.0.opts.ignore_case_insensitive,
);
@@ -235,34 +245,46 @@ impl Ignore {
let ig_matcher = if !self.0.opts.ignore {
Gitignore::empty()
} else {
- let (m, err) =
- create_gitignore(&dir, &[".ignore"], self.0.opts.ignore_case_insensitive);
+ let (m, err) = create_gitignore(
+ &dir,
+ &dir,
+ &[".ignore"],
+ self.0.opts.ignore_case_insensitive,
+ );
errs.maybe_push(err);
m
};
let gi_matcher = if !self.0.opts.git_ignore {
Gitignore::empty()
} else {
- let (m, err) =
- create_gitignore(&dir, &[".gitignore"], self.0.opts.ignore_case_insensitive);
- errs.maybe_push(err);
- m
- };
- let gi_exclude_matcher = if !self.0.opts.git_exclude {
- Gitignore::empty()
- } else {
let (m, err) = create_gitignore(
&dir,
- &[".git/info/exclude"],
+ &dir,
+ &[".gitignore"],
self.0.opts.ignore_case_insensitive,
);
errs.maybe_push(err);
m
};
- let has_git = if self.0.opts.git_ignore {
- dir.join(".git").exists()
+ let gi_exclude_matcher = if !self.0.opts.git_exclude {
+ Gitignore::empty()
} else {
- false
+ match resolve_git_commondir(dir, git_type) {
+ Ok(git_dir) => {
+ let (m, err) = create_gitignore(
+ &dir,
+ &git_dir,
+ &["info/exclude"],
+ self.0.opts.ignore_case_insensitive,
+ );
+ errs.maybe_push(err);
+ m
+ }
+ Err(err) => {
+ errs.maybe_push(err);
+ Gitignore::empty()
+ }
+ }
};
let ig = IgnoreInner {
compiled: self.0.compiled.clone(),
@@ -675,12 +697,15 @@ impl IgnoreBuilder {
/// Creates a new gitignore matcher for the directory given.
///
-/// Ignore globs are extracted from each of the file names in `dir` in the
-/// order given (earlier names have lower precedence than later names).
+/// The matcher is meant to match files below `dir`.
+/// Ignore globs are extracted from each of the file names relative to
+/// `dir_for_ignorefile` in the order given (earlier names have lower
+/// precedence than later names).
///
/// I/O errors are ignored.
pub fn create_gitignore<T: AsRef<OsStr>>(
dir: &Path,
+ dir_for_ignorefile: &Path,
names: &[T],
case_insensitive: bool,
) -> (Gitignore, Option<Error>) {
@@ -688,7 +713,7 @@ pub fn create_gitignore<T: AsRef<OsStr>>(
let mut errs = PartialErrorBuilder::default();
builder.case_insensitive(case_insensitive).unwrap();
for name in names {
- let gipath = dir.join(name.as_ref());
+ let gipath = dir_for_ignorefile.join(name.as_ref());
// This check is not necessary, but is added for performance. Namely,
// a simple stat call checking for existence can often be just a bit
// quicker than actually trying to open a file. Since the number of
@@ -715,10 +740,66 @@ pub fn create_gitignore<T: AsRef<OsStr>>(
(gi, errs.into_error_option())
}
+/// Find the GIT_COMMON_DIR for the given git worktree.
+///
+/// This is the directory that may contain a private ignore file
+/// "info/exclude". Unlike git, this function does *not* read environment
+/// variables GIT_DIR and GIT_COMMON_DIR, because it is not clear how to use
+/// them when multiple repositories are searched.
+///
+/// Some I/O errors are ignored.
+fn resolve_git_commondir(
+ dir: &Path,
+ git_type: Option<FileType>,
+) -> Result<PathBuf, Option<Error>> {
+ let git_dir_path = || dir.join(".git");
+ let git_dir = git_dir_path();
+ if !git_type.map_or(false, |ft| ft.is_file()) {
+ return Ok(git_dir);
+ }
+ let file = match File::open(git_dir) {
+ Ok(file) => io::BufReader::new(file),
+ Err(err) => {
+ return Err(Some(Error::Io(err).with_path(git_dir_path())));
+ }
+ };
+ let dot_git_line = match file.lines().next() {
+ Some(Ok(line)) => line,
+ Some(Err(err)) => {
+ return Err(Some(Error::Io(err).with_path(git_dir_path())));
+ }
+ None => return Err(None),
+ };
+ if !dot_git_line.starts_with("gitdir: ") {
+ return Err(None);
+ }
+ let real_git_dir = PathBuf::from(&dot_git_line["gitdir: ".len()..]);
+ let git_commondir_file = || real_git_dir.join("commondir");
+ let file = match File::open(git_commondir_file()) {
+ Ok(file) => io::BufReader::new(file),
+ Err(err) => {
+ return Err(Some(Error::Io(err).with_path(git_commondir_file())));
+ }
+ };
+ let commondir_line = match file.lines().next() {
+ Some(Ok(line)) => line,
+ Some(Err(err)) => {
+ return Err(Some(Error::Io(err).with_path(git_commondir_file())));
+ }
+ None => return Err(None),
+ };
+ let commondir_abs = if commondir_line.starts_with(".") {
+ real_git_dir.join(commondir_line) // relative commondir
+ } else {
+ PathBuf::from(commondir_line)
+ };
+ Ok(commondir_abs)
+}
+
#[cfg(test)]
mod tests {
use std::fs::{self, File};
- use std::io::Write;
+ use std::io::{self, Write};
use std::path::Path;
use dir::IgnoreBuilder;
@@ -1005,4 +1086,63 @@ mod tests {
assert!(ig2.matched("foo", false).is_ignore());
assert!(ig2.matched("src/foo", false).is_ignore());
}
+
+ #[test]
+ fn git_info_exclude_in_linked_worktree() {
+ let td = tmpdir();
+ let git_dir = td.path().join(".git");
+ mkdirp(git_dir.join("info"));
+ wfile(git_dir.join("info/exclude"), "ignore_me");
+ mkdirp(git_dir.join("worktrees/linked-worktree"));
+ let commondir_path = || {
+ git_dir.join("worktrees/linked-worktree/commondir")
+ };
+ mkdirp(td.path().join("linked-worktree"));
+ let worktree_git_dir_abs = format!(
+ "gitdir: {}",
+ git_dir.join("worktrees/linked-worktree").to_str().unwrap(),
+ );
+ wfile(td.path().join("linked-worktree/.git"), &worktree_git_dir_abs);
+
+ // relative commondir
+ wfile(commondir_path(), "../..");
+ let ib = IgnoreBuilder::new().build();
+ let (ignore, err) = ib.add_child(td.path().join("linked-worktree"));
+ assert!(err.is_none());
+ assert!(ignore.matched("ignore_me", false).is_ignore());
+
+ // absolute commondir
+ wfile(commondir_path(), git_dir.to_str().unwrap());
+ let (ignore, err) = ib.add_child(td.path().join("linked-worktree"));
+ assert!(err.is_none());
+ assert!(ignore.matched("ignore_me", false).is_ignore());
+
+ // missing commondir file
+ assert!(fs::remove_file(commondir_path()).is_ok());
+ let (_, err) = ib.add_child(td.path().join("linked-worktree"));
+ assert!(err.is_some());
+ assert!(match err {
+ Some(Error::WithPath { path, err }) => {
+ if path != commondir_path() {
+ false
+ } else {
+ match *err {
+ Error::Io(ioerr) => {
+ ioerr.kind() == io::ErrorKind::NotFound
+ }
+ _ => false,
+ }
+ }
+ }
+ _ => false,
+ });
+
+ wfile(td.path().join("linked-worktree/.git"), "garbage");
+ let (_, err) = ib.add_child(td.path().join("linked-worktree"));
+ assert!(err.is_none());
+
+ wfile(td.path().join("linked-worktree/.git"), "gitdir: garbage");
+ let (_, err) = ib.add_child(td.path().join("linked-worktree"));
+ assert!(err.is_some());
+ }
}
diff --git a/tests/regression.rs b/tests/regression.rs
index e8c915ae..6d925744 100644
--- a/tests/regression.rs
+++ b/tests/regression.rs
@@ -738,3 +738,19 @@ rgtest!(r1334_crazy_literals, |dir: Dir, mut cmd: TestCommand| {
cmd.arg("-Ff").arg("patterns").arg("corpus").stdout()
);
});
+
+// See: https://github.com/BurntSushi/ripgrep/pull/1446
+rgtest!(r1446_respect_excludes_in_worktree, |dir: Dir, mut cmd: TestCommand| {
+ dir.create_dir("repo/.git/info");
+ dir.create("repo/.git/info/exclude", "ignored");
+ dir.create_dir("repo/.git/worktrees/repotree");
+ dir.create("repo/.git/worktrees/repotree/commondir", "../..");
+
+ dir.create_dir("repotree");
+ dir.create("repotree/.git", "gitdir: repo/.git/worktrees/repotree");
+ dir.create("repotree/ignored", "");
+ dir.create("repotree/not-ignored", "");
+
+ cmd.arg("--sort").arg("path").arg("--files").arg("repotree");
+ eqnice!("repotree/not-ignored\n", cmd.stdout());
+});