Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ignore: use git commondir for sourcing .git/info/exclude #1446

Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 116 additions & 13 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;
use std::io::{self, BufRead};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};

Expand Down Expand Up @@ -225,6 +227,7 @@ impl Ignore {
Gitignore::empty()
} else {
let (m, err) = create_gitignore(
&dir,
&dir,
&self.0.custom_ignore_filenames,
self.0.opts.ignore_case_insensitive,
Expand All @@ -236,28 +239,37 @@ impl Ignore {
Gitignore::empty()
} else {
let (m, err) =
create_gitignore(&dir, &[".ignore"], self.0.opts.ignore_case_insensitive);
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);
create_gitignore(&dir, &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"],
self.0.opts.ignore_case_insensitive,
);
errs.maybe_push(err);
m
match resolve_git_commondir(dir) {
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 has_git = if self.0.opts.git_ignore {
dir.join(".git").exists()
Expand Down Expand Up @@ -675,20 +687,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());
errs.maybe_push_ignore_io(builder.add(gipath));
}
let gi = match builder.build() {
Expand All @@ -701,10 +716,55 @@ 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) -> Result<PathBuf, Option<Error>> {
let git_dir_path = || dir.join(".git");
let git_dir = git_dir_path();
if !git_dir.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 @@ -991,4 +1051,47 @@ 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);

wfile(commondir_path(), "../.."); // relative commondir
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());

wfile(commondir_path(), git_dir.to_str().unwrap()); // absolute commondir
let (ignore, err) = ib.add_child(td.path().join("linked-worktree"));
assert!(err.is_none());
assert!(ignore.matched("ignore_me", false).is_ignore());

assert!(fs::remove_file(commondir_path()).is_ok()); // missing commondir file
let (_, err) = ib.add_child(td.path().join("linked-worktree"));
assert!(err.is_some());
assert!(match err {
Some(Error::WithPath { path, err }) => path == commondir_path() && 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());
}
}