Skip to content

Commit e1b6eae

Browse files
committed
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 BurntSushi#1445
1 parent 8892bf6 commit e1b6eae

File tree

1 file changed

+116
-13
lines changed

1 file changed

+116
-13
lines changed

ignore/src/dir.rs

+116-13
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
use std::collections::HashMap;
1717
use std::ffi::{OsStr, OsString};
18+
use std::fs::File;
19+
use std::io::{self, BufRead};
1820
use std::path::{Path, PathBuf};
1921
use std::sync::{Arc, RwLock};
2022

@@ -225,6 +227,7 @@ impl Ignore {
225227
Gitignore::empty()
226228
} else {
227229
let (m, err) = create_gitignore(
230+
&dir,
228231
&dir,
229232
&self.0.custom_ignore_filenames,
230233
self.0.opts.ignore_case_insensitive,
@@ -236,28 +239,37 @@ impl Ignore {
236239
Gitignore::empty()
237240
} else {
238241
let (m, err) =
239-
create_gitignore(&dir, &[".ignore"], self.0.opts.ignore_case_insensitive);
242+
create_gitignore(&dir, &dir, &[".ignore"], self.0.opts.ignore_case_insensitive);
240243
errs.maybe_push(err);
241244
m
242245
};
243246
let gi_matcher = if !self.0.opts.git_ignore {
244247
Gitignore::empty()
245248
} else {
246249
let (m, err) =
247-
create_gitignore(&dir, &[".gitignore"], self.0.opts.ignore_case_insensitive);
250+
create_gitignore(&dir, &dir, &[".gitignore"], self.0.opts.ignore_case_insensitive);
248251
errs.maybe_push(err);
249252
m
250253
};
251254
let gi_exclude_matcher = if !self.0.opts.git_exclude {
252255
Gitignore::empty()
253256
} else {
254-
let (m, err) = create_gitignore(
255-
&dir,
256-
&[".git/info/exclude"],
257-
self.0.opts.ignore_case_insensitive,
258-
);
259-
errs.maybe_push(err);
260-
m
257+
match resolve_git_commondir(dir) {
258+
Ok(git_dir) => {
259+
let (m, err) = create_gitignore(
260+
&dir,
261+
&git_dir,
262+
&["info/exclude"],
263+
self.0.opts.ignore_case_insensitive,
264+
);
265+
errs.maybe_push(err);
266+
m
267+
}
268+
Err(err) => {
269+
errs.maybe_push(err);
270+
Gitignore::empty()
271+
}
272+
}
261273
};
262274
let has_git = if self.0.opts.git_ignore {
263275
dir.join(".git").exists()
@@ -675,20 +687,23 @@ impl IgnoreBuilder {
675687

676688
/// Creates a new gitignore matcher for the directory given.
677689
///
678-
/// Ignore globs are extracted from each of the file names in `dir` in the
679-
/// order given (earlier names have lower precedence than later names).
690+
/// The matcher is meant to match files below `dir`.
691+
/// Ignore globs are extracted from each of the file names relative to
692+
/// `dir_for_ignorefile` in the order given (earlier names have lower
693+
/// precedence than later names).
680694
///
681695
/// I/O errors are ignored.
682696
pub fn create_gitignore<T: AsRef<OsStr>>(
683697
dir: &Path,
698+
dir_for_ignorefile: &Path,
684699
names: &[T],
685700
case_insensitive: bool,
686701
) -> (Gitignore, Option<Error>) {
687702
let mut builder = GitignoreBuilder::new(dir);
688703
let mut errs = PartialErrorBuilder::default();
689704
builder.case_insensitive(case_insensitive).unwrap();
690705
for name in names {
691-
let gipath = dir.join(name.as_ref());
706+
let gipath = dir_for_ignorefile.join(name.as_ref());
692707
errs.maybe_push_ignore_io(builder.add(gipath));
693708
}
694709
let gi = match builder.build() {
@@ -701,10 +716,55 @@ pub fn create_gitignore<T: AsRef<OsStr>>(
701716
(gi, errs.into_error_option())
702717
}
703718

719+
/// Find the GIT_COMMON_DIR for the given git worktree.
720+
///
721+
/// This is the directory that may contain a private ignore file "info/exclude".
722+
/// Unlike git, this function does *not* read environment variables
723+
/// GIT_DIR and GIT_COMMON_DIR, because it is not clear how to use them when
724+
/// multiple repositories are searched.
725+
///
726+
/// Some I/O errors are ignored.
727+
fn resolve_git_commondir(dir: &Path) -> Result<PathBuf, Option<Error>> {
728+
let git_dir_path = || dir.join(".git");
729+
let git_dir = git_dir_path();
730+
if !git_dir.is_file() {
731+
return Ok(git_dir);
732+
}
733+
let file = match File::open(git_dir) {
734+
Ok(file) => io::BufReader::new(file),
735+
Err(err) => return Err(Some(Error::Io(err).with_path(git_dir_path()))),
736+
};
737+
let dot_git_line = match file.lines().next() {
738+
Some(Ok(line)) => line,
739+
Some(Err(err)) => return Err(Some(Error::Io(err).with_path(git_dir_path()))),
740+
None => return Err(None),
741+
};
742+
if !dot_git_line.starts_with("gitdir: ") {
743+
return Err(None);
744+
}
745+
let real_git_dir = PathBuf::from(&dot_git_line["gitdir: ".len()..]);
746+
let git_commondir_file = || real_git_dir.join("commondir");
747+
let file = match File::open(git_commondir_file()) {
748+
Ok(file) => io::BufReader::new(file),
749+
Err(err) => return Err(Some(Error::Io(err).with_path(git_commondir_file()))),
750+
};
751+
let commondir_line = match file.lines().next() {
752+
Some(Ok(line)) => line,
753+
Some(Err(err)) => return Err(Some(Error::Io(err).with_path(git_commondir_file()))),
754+
None => return Err(None),
755+
};
756+
let commondir_abs = if commondir_line.starts_with(".") {
757+
real_git_dir.join(commondir_line) // relative commondir
758+
} else {
759+
PathBuf::from(commondir_line)
760+
};
761+
Ok(commondir_abs)
762+
}
763+
704764
#[cfg(test)]
705765
mod tests {
706766
use std::fs::{self, File};
707-
use std::io::Write;
767+
use std::io::{self, Write};
708768
use std::path::Path;
709769

710770
use dir::IgnoreBuilder;
@@ -991,4 +1051,47 @@ mod tests {
9911051
assert!(ig2.matched("foo", false).is_ignore());
9921052
assert!(ig2.matched("src/foo", false).is_ignore());
9931053
}
1054+
1055+
#[test]
1056+
fn git_info_exclude_in_linked_worktree() {
1057+
let td = tmpdir();
1058+
let git_dir = td.path().join(".git");
1059+
mkdirp(git_dir.join("info"));
1060+
wfile(git_dir.join("info/exclude"), "ignore_me");
1061+
mkdirp(git_dir.join("worktrees/linked-worktree"));
1062+
let commondir_path = || git_dir.join("worktrees/linked-worktree/commondir");
1063+
mkdirp(td.path().join("linked-worktree"));
1064+
let worktree_git_dir_abs = format!("gitdir: {}", git_dir.join("worktrees/linked-worktree").to_str().unwrap());
1065+
wfile(td.path().join("linked-worktree/.git"), &worktree_git_dir_abs);
1066+
1067+
wfile(commondir_path(), "../.."); // relative commondir
1068+
let ib = IgnoreBuilder::new().build();
1069+
let (ignore, err) = ib.add_child(td.path().join("linked-worktree"));
1070+
assert!(err.is_none());
1071+
assert!(ignore.matched("ignore_me", false).is_ignore());
1072+
1073+
wfile(commondir_path(), git_dir.to_str().unwrap()); // absolute commondir
1074+
let (ignore, err) = ib.add_child(td.path().join("linked-worktree"));
1075+
assert!(err.is_none());
1076+
assert!(ignore.matched("ignore_me", false).is_ignore());
1077+
1078+
assert!(fs::remove_file(commondir_path()).is_ok()); // missing commondir file
1079+
let (_, err) = ib.add_child(td.path().join("linked-worktree"));
1080+
assert!(err.is_some());
1081+
assert!(match err {
1082+
Some(Error::WithPath { path, err }) => path == commondir_path() && match *err {
1083+
Error::Io(ioerr) => ioerr.kind() == io::ErrorKind::NotFound,
1084+
_ => false,
1085+
},
1086+
_ => false,
1087+
});
1088+
1089+
wfile(td.path().join("linked-worktree/.git"), "garbage");
1090+
let (_, err) = ib.add_child(td.path().join("linked-worktree"));
1091+
assert!(err.is_none());
1092+
1093+
wfile(td.path().join("linked-worktree/.git"), "gitdir: garbage");
1094+
let (_, err) = ib.add_child(td.path().join("linked-worktree"));
1095+
assert!(err.is_some());
1096+
}
9941097
}

0 commit comments

Comments
 (0)