Skip to content

Commit

Permalink
ignore: use git commondir for sourcing .git/info/exclude
Browse files Browse the repository at this point in the history
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
  • Loading branch information
krobelus authored and BurntSushi committed Feb 17, 2020
1 parent 0c3b673 commit 6f2b79f
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 18 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
176 changes: 158 additions & 18 deletions ignore/src/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -220,11 +222,19 @@ 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,
Expand All @@ -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(),
Expand Down Expand Up @@ -675,20 +697,23 @@ 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>) {
let mut builder = GitignoreBuilder::new(dir);
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
Expand All @@ -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;
Expand Down Expand Up @@ -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());
}
}
16 changes: 16 additions & 0 deletions tests/regression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});

0 comments on commit 6f2b79f

Please sign in to comment.