mirror of
				https://github.com/BurntSushi/ripgrep.git
				synced 2025-10-30 23:17:47 +02:00 
			
		
		
		
	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
This commit is contained in:
		
				
					committed by
					
						 Andrew Gallant
						Andrew Gallant
					
				
			
			
				
	
			
			
			
						parent
						
							0c3b673e4c
						
					
				
				
					commit
					6f2b79f584
				
			| @@ -34,6 +34,8 @@ Bug fixes: | |||||||
|   Fixes a performance bug when searching plain text files with very long lines. |   Fixes a performance bug when searching plain text files with very long lines. | ||||||
| * [BUG #1344](https://github.com/BurntSushi/ripgrep/issues/1344): | * [BUG #1344](https://github.com/BurntSushi/ripgrep/issues/1344): | ||||||
|   Document usage of `--type all`. |   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) | 11.0.2 (2019-08-01) | ||||||
|   | |||||||
| @@ -15,6 +15,8 @@ | |||||||
|  |  | ||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
| use std::ffi::{OsStr, OsString}; | use std::ffi::{OsStr, OsString}; | ||||||
|  | use std::fs::{File, FileType}; | ||||||
|  | use std::io::{self, BufRead}; | ||||||
| use std::path::{Path, PathBuf}; | use std::path::{Path, PathBuf}; | ||||||
| use std::sync::{Arc, RwLock}; | use std::sync::{Arc, RwLock}; | ||||||
|  |  | ||||||
| @@ -220,11 +222,19 @@ impl Ignore { | |||||||
|  |  | ||||||
|     /// Like add_child, but takes a full path and returns an IgnoreInner. |     /// Like add_child, but takes a full path and returns an IgnoreInner. | ||||||
|     fn add_child_path(&self, dir: &Path) -> (IgnoreInner, Option<Error>) { |     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 mut errs = PartialErrorBuilder::default(); | ||||||
|         let custom_ig_matcher = if self.0.custom_ignore_filenames.is_empty() { |         let custom_ig_matcher = if self.0.custom_ignore_filenames.is_empty() { | ||||||
|             Gitignore::empty() |             Gitignore::empty() | ||||||
|         } else { |         } else { | ||||||
|             let (m, err) = create_gitignore( |             let (m, err) = create_gitignore( | ||||||
|  |                 &dir, | ||||||
|                 &dir, |                 &dir, | ||||||
|                 &self.0.custom_ignore_filenames, |                 &self.0.custom_ignore_filenames, | ||||||
|                 self.0.opts.ignore_case_insensitive, |                 self.0.opts.ignore_case_insensitive, | ||||||
| @@ -235,34 +245,46 @@ impl Ignore { | |||||||
|         let ig_matcher = if !self.0.opts.ignore { |         let ig_matcher = if !self.0.opts.ignore { | ||||||
|             Gitignore::empty() |             Gitignore::empty() | ||||||
|         } else { |         } else { | ||||||
|             let (m, err) = |             let (m, err) = create_gitignore( | ||||||
|                 create_gitignore(&dir, &[".ignore"], self.0.opts.ignore_case_insensitive); |                 &dir, | ||||||
|  |                 &dir, | ||||||
|  |                 &[".ignore"], | ||||||
|  |                 self.0.opts.ignore_case_insensitive, | ||||||
|  |             ); | ||||||
|             errs.maybe_push(err); |             errs.maybe_push(err); | ||||||
|             m |             m | ||||||
|         }; |         }; | ||||||
|         let gi_matcher = if !self.0.opts.git_ignore { |         let gi_matcher = if !self.0.opts.git_ignore { | ||||||
|             Gitignore::empty() |             Gitignore::empty() | ||||||
|         } else { |         } else { | ||||||
|             let (m, err) = |             let (m, err) = create_gitignore( | ||||||
|                 create_gitignore(&dir, &[".gitignore"], self.0.opts.ignore_case_insensitive); |                 &dir, | ||||||
|  |                 &dir, | ||||||
|  |                 &[".gitignore"], | ||||||
|  |                 self.0.opts.ignore_case_insensitive, | ||||||
|  |             ); | ||||||
|             errs.maybe_push(err); |             errs.maybe_push(err); | ||||||
|             m |             m | ||||||
|         }; |         }; | ||||||
|         let gi_exclude_matcher = if !self.0.opts.git_exclude { |         let gi_exclude_matcher = if !self.0.opts.git_exclude { | ||||||
|             Gitignore::empty() |             Gitignore::empty() | ||||||
|         } else { |         } else { | ||||||
|             let (m, err) = create_gitignore( |             match resolve_git_commondir(dir, git_type) { | ||||||
|                 &dir, |                 Ok(git_dir) => { | ||||||
|                 &[".git/info/exclude"], |                     let (m, err) = create_gitignore( | ||||||
|                 self.0.opts.ignore_case_insensitive, |                         &dir, | ||||||
|             ); |                         &git_dir, | ||||||
|             errs.maybe_push(err); |                         &["info/exclude"], | ||||||
|             m |                         self.0.opts.ignore_case_insensitive, | ||||||
|         }; |                     ); | ||||||
|         let has_git = if self.0.opts.git_ignore { |                     errs.maybe_push(err); | ||||||
|             dir.join(".git").exists() |                     m | ||||||
|         } else { |                 } | ||||||
|             false |                 Err(err) => { | ||||||
|  |                     errs.maybe_push(err); | ||||||
|  |                     Gitignore::empty() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         }; |         }; | ||||||
|         let ig = IgnoreInner { |         let ig = IgnoreInner { | ||||||
|             compiled: self.0.compiled.clone(), |             compiled: self.0.compiled.clone(), | ||||||
| @@ -675,12 +697,15 @@ impl IgnoreBuilder { | |||||||
|  |  | ||||||
| /// Creates a new gitignore matcher for the directory given. | /// Creates a new gitignore matcher for the directory given. | ||||||
| /// | /// | ||||||
| /// Ignore globs are extracted from each of the file names in `dir` in the | /// The matcher is meant to match files below `dir`. | ||||||
| /// order given (earlier names have lower precedence than later names). | /// 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. | /// I/O errors are ignored. | ||||||
| pub fn create_gitignore<T: AsRef<OsStr>>( | pub fn create_gitignore<T: AsRef<OsStr>>( | ||||||
|     dir: &Path, |     dir: &Path, | ||||||
|  |     dir_for_ignorefile: &Path, | ||||||
|     names: &[T], |     names: &[T], | ||||||
|     case_insensitive: bool, |     case_insensitive: bool, | ||||||
| ) -> (Gitignore, Option<Error>) { | ) -> (Gitignore, Option<Error>) { | ||||||
| @@ -688,7 +713,7 @@ pub fn create_gitignore<T: AsRef<OsStr>>( | |||||||
|     let mut errs = PartialErrorBuilder::default(); |     let mut errs = PartialErrorBuilder::default(); | ||||||
|     builder.case_insensitive(case_insensitive).unwrap(); |     builder.case_insensitive(case_insensitive).unwrap(); | ||||||
|     for name in names { |     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, |         // This check is not necessary, but is added for performance. Namely, | ||||||
|         // a simple stat call checking for existence can often be just a bit |         // 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 |         // 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()) |     (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)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use std::fs::{self, File}; |     use std::fs::{self, File}; | ||||||
|     use std::io::Write; |     use std::io::{self, Write}; | ||||||
|     use std::path::Path; |     use std::path::Path; | ||||||
|  |  | ||||||
|     use dir::IgnoreBuilder; |     use dir::IgnoreBuilder; | ||||||
| @@ -1005,4 +1086,63 @@ mod tests { | |||||||
|         assert!(ig2.matched("foo", false).is_ignore()); |         assert!(ig2.matched("foo", false).is_ignore()); | ||||||
|         assert!(ig2.matched("src/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()); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -738,3 +738,19 @@ rgtest!(r1334_crazy_literals, |dir: Dir, mut cmd: TestCommand| { | |||||||
|         cmd.arg("-Ff").arg("patterns").arg("corpus").stdout() |         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()); | ||||||
|  | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user