2017-09-28 08:09:57 +00:00
|
|
|
|
//! Ignoring globs in `.gitignore` files.
|
|
|
|
|
//!
|
|
|
|
|
//! This uses a cache because the file with the globs in might not be the same
|
|
|
|
|
//! directory that we’re listing!
|
|
|
|
|
|
|
|
|
|
use std::fs::File;
|
|
|
|
|
use std::io::Read;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
2017-09-28 15:13:47 +00:00
|
|
|
|
use std::sync::RwLock;
|
2017-09-28 08:09:57 +00:00
|
|
|
|
|
|
|
|
|
use fs::filter::IgnorePatterns;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// An **ignore cache** holds sets of glob patterns paired with the
|
2017-09-28 15:13:47 +00:00
|
|
|
|
/// directories that they should be ignored underneath. Believe it or not,
|
|
|
|
|
/// that’s a valid English sentence.
|
2017-09-28 08:09:57 +00:00
|
|
|
|
#[derive(Default, Debug)]
|
|
|
|
|
pub struct IgnoreCache {
|
2017-09-28 15:13:47 +00:00
|
|
|
|
entries: RwLock<Vec<(PathBuf, IgnorePatterns)>>
|
2017-09-28 08:09:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl IgnoreCache {
|
|
|
|
|
pub fn new() -> IgnoreCache {
|
|
|
|
|
IgnoreCache::default()
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-28 15:13:47 +00:00
|
|
|
|
pub fn discover_underneath(&self, path: &Path) {
|
2017-09-28 08:09:57 +00:00
|
|
|
|
let mut path = Some(path);
|
2017-09-28 15:13:47 +00:00
|
|
|
|
let mut entries = self.entries.write().unwrap();
|
2017-09-28 08:09:57 +00:00
|
|
|
|
|
|
|
|
|
while let Some(p) = path {
|
2017-09-28 15:13:47 +00:00
|
|
|
|
if p.components().next().is_none() { break }
|
|
|
|
|
|
2017-09-28 08:09:57 +00:00
|
|
|
|
let ignore_file = p.join(".gitignore");
|
|
|
|
|
if ignore_file.is_file() {
|
2017-09-28 15:13:47 +00:00
|
|
|
|
debug!("Found a .gitignore file: {:?}", ignore_file);
|
2017-09-28 08:09:57 +00:00
|
|
|
|
if let Ok(mut file) = File::open(ignore_file) {
|
|
|
|
|
let mut contents = String::new();
|
|
|
|
|
|
2017-09-28 16:46:30 +00:00
|
|
|
|
match file.read_to_string(&mut contents) {
|
|
|
|
|
Ok(_) => {
|
2017-09-28 17:42:42 +00:00
|
|
|
|
let patterns = file_lines_to_patterns(contents.lines());
|
2017-09-28 16:46:30 +00:00
|
|
|
|
entries.push((p.into(), patterns));
|
|
|
|
|
}
|
|
|
|
|
Err(e) => debug!("Failed to read a .gitignore: {:?}", e)
|
|
|
|
|
}
|
2017-09-28 08:09:57 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-28 15:13:47 +00:00
|
|
|
|
else {
|
|
|
|
|
debug!("Found no .gitignore file at {:?}", ignore_file);
|
|
|
|
|
}
|
2017-09-28 08:09:57 +00:00
|
|
|
|
|
|
|
|
|
path = p.parent();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_ignored(&self, suspect: &Path) -> bool {
|
2017-09-28 15:13:47 +00:00
|
|
|
|
let entries = self.entries.read().unwrap();
|
|
|
|
|
entries.iter().any(|&(ref base_path, ref patterns)| {
|
2017-09-28 08:09:57 +00:00
|
|
|
|
if let Ok(suffix) = suspect.strip_prefix(&base_path) {
|
|
|
|
|
patterns.is_ignored_path(suffix)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2017-09-28 17:42:42 +00:00
|
|
|
|
fn file_lines_to_patterns<'a, I>(iter: I) -> IgnorePatterns
|
2017-09-28 17:48:56 +00:00
|
|
|
|
where I: Iterator<Item=&'a str>
|
|
|
|
|
{
|
|
|
|
|
let iter = iter.filter(|el| !el.is_empty());
|
2018-06-19 12:58:03 +00:00
|
|
|
|
let iter = iter.filter(|el| !el.starts_with('#'));
|
2017-09-28 17:48:56 +00:00
|
|
|
|
|
2017-09-28 17:59:51 +00:00
|
|
|
|
// TODO: Figure out if this should trim whitespace or not
|
|
|
|
|
|
2017-09-28 17:42:42 +00:00
|
|
|
|
// Errors are currently being ignored... not a good look
|
|
|
|
|
IgnorePatterns::parse_from_iter(iter).0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2017-09-28 08:09:57 +00:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod test {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
2017-09-28 17:42:42 +00:00
|
|
|
|
#[test]
|
|
|
|
|
fn parse_nothing() {
|
|
|
|
|
use std::iter::empty;
|
|
|
|
|
let (patterns, _) = IgnorePatterns::parse_from_iter(empty());
|
|
|
|
|
assert_eq!(patterns, file_lines_to_patterns(empty()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_some_globs() {
|
|
|
|
|
let stuff = vec![ "*.mp3", "README.md" ];
|
2017-09-28 17:48:56 +00:00
|
|
|
|
let reals = vec![ "*.mp3", "README.md" ];
|
|
|
|
|
let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
|
|
|
|
|
assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_some_comments() {
|
|
|
|
|
let stuff = vec![ "*.mp3", "# I am a comment!", "#", "README.md" ];
|
|
|
|
|
let reals = vec![ "*.mp3", "README.md" ];
|
|
|
|
|
let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
|
2017-09-28 17:42:42 +00:00
|
|
|
|
assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-28 17:48:56 +00:00
|
|
|
|
#[test]
|
|
|
|
|
fn parse_some_blank_lines() {
|
|
|
|
|
let stuff = vec![ "*.mp3", "", "", "README.md" ];
|
|
|
|
|
let reals = vec![ "*.mp3", "README.md" ];
|
|
|
|
|
let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
|
|
|
|
|
assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-28 17:59:51 +00:00
|
|
|
|
#[test]
|
|
|
|
|
fn parse_some_whitespacey_lines() {
|
|
|
|
|
let stuff = vec![ " *.mp3", " ", " a ", "README.md " ];
|
|
|
|
|
let reals = vec![ " *.mp3", " ", " a ", "README.md " ];
|
|
|
|
|
let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
|
|
|
|
|
assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
|
|
|
|
|
}
|
2017-09-28 17:48:56 +00:00
|
|
|
|
|
|
|
|
|
|
2017-09-28 18:00:01 +00:00
|
|
|
|
fn test_cache(dir: &'static str, pats: Vec<&str>) -> IgnoreCache {
|
|
|
|
|
IgnoreCache { entries: RwLock::new(vec![ (dir.into(), IgnorePatterns::parse_from_iter(pats.into_iter()).0) ]) }
|
|
|
|
|
}
|
2017-09-28 17:42:42 +00:00
|
|
|
|
|
2017-09-28 08:09:57 +00:00
|
|
|
|
#[test]
|
2017-09-28 17:48:56 +00:00
|
|
|
|
fn an_empty_cache_ignores_nothing() {
|
2017-09-28 08:09:57 +00:00
|
|
|
|
let ignores = IgnoreCache::default();
|
|
|
|
|
assert_eq!(false, ignores.is_ignored(Path::new("/usr/bin/drinking")));
|
|
|
|
|
assert_eq!(false, ignores.is_ignored(Path::new("target/debug/exa")));
|
|
|
|
|
}
|
2017-09-28 18:00:01 +00:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_nonempty_cache_ignores_some_things() {
|
|
|
|
|
let ignores = test_cache("/vagrant", vec![ "target" ]);
|
|
|
|
|
assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/src")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/target")));
|
|
|
|
|
}
|
2017-09-28 18:03:15 +00:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ignore_some_globs() {
|
|
|
|
|
let ignores = test_cache("/vagrant", vec![ "*.ipr", "*.iws", ".docker" ]);
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/exa.ipr")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/exa.iws")));
|
|
|
|
|
assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/exa.iwiwal")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/.docker")));
|
|
|
|
|
assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/exa.docker")));
|
|
|
|
|
|
|
|
|
|
assert_eq!(false, ignores.is_ignored(Path::new("/srcode/exa.ipr")));
|
|
|
|
|
assert_eq!(false, ignores.is_ignored(Path::new("/srcode/exa.iws")));
|
|
|
|
|
}
|
2017-09-30 07:17:05 +00:00
|
|
|
|
|
|
|
|
|
#[test] #[ignore]
|
|
|
|
|
fn ignore_relatively() {
|
|
|
|
|
let ignores = test_cache(".", vec![ "target" ]);
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./target")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
|
|
|
|
|
|
|
|
|
|
assert_eq!(false, ignores.is_ignored(Path::new("./.target")));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test] #[ignore]
|
|
|
|
|
fn ignore_relatively_sometimes() {
|
|
|
|
|
let ignores = test_cache(".", vec![ "project/target" ]);
|
|
|
|
|
assert_eq!(false, ignores.is_ignored(Path::new("./target")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test] #[ignore]
|
|
|
|
|
fn ignore_relatively_absolutely() {
|
|
|
|
|
let ignores = test_cache(".", vec![ "/project/target" ]);
|
|
|
|
|
assert_eq!(false, ignores.is_ignored(Path::new("./target")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test] #[ignore] // not 100% sure if dot works this way...
|
|
|
|
|
fn ignore_relatively_absolutely_dot() {
|
|
|
|
|
let ignores = test_cache(".", vec![ "./project/target" ]);
|
|
|
|
|
assert_eq!(false, ignores.is_ignored(Path::new("./target")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
|
|
|
|
|
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
|
|
|
|
|
}
|
2017-09-28 08:09:57 +00:00
|
|
|
|
}
|