use error;
use pattern;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct File<'a> {
patterns: Vec<pattern::Pattern<'a>>,
root: &'a Path
}
impl<'b> File<'b> {
pub fn new(gitignore_path: &'b Path) -> Result<File<'b>, error::Error> {
let root = gitignore_path.parent().unwrap();
let patterns = File::patterns(gitignore_path, root)?;
Ok(File { patterns, root })
}
pub fn is_excluded(&self, path: &'b Path) -> Result<bool, error::Error> {
self.included_files().map(|files| !files.contains(&path.to_path_buf()))
}
pub fn included_files(&self) -> Result<Vec<PathBuf>, error::Error> {
let mut files: Vec<PathBuf> = vec![];
let mut roots = vec![self.root.to_path_buf()];
while let Some(root) = roots.pop() {
let entries = fs::read_dir(root)?;
for entry in entries {
let path = entry?.path();
if path.ends_with(".git") {
continue;
}
let matches = self.file_is_excluded(&path);
if matches.is_err() || matches? {
continue;
}
files.push(path.to_path_buf());
let metadata = fs::metadata(&path);
if metadata.is_ok() && metadata?.is_dir() {
roots.push(path);
}
}
}
Ok(files)
}
fn file_is_excluded(&self, path: &'b Path) -> Result<bool, error::Error> {
let abs_path = self.abs_path(path);
let directory = fs::metadata(&abs_path)?.is_dir();
Ok(self.patterns.iter().fold(false, |acc, pattern| {
let matches = pattern.is_excluded(&abs_path, directory);
if !matches {
acc
} else {
!pattern.negation
}
}))
}
fn patterns(path: &'b Path, root: &'b Path) -> Result<Vec<pattern::Pattern<'b>>, error::Error> {
let mut file = fs::File::open(path)?;
let mut s = String::new();
file.read_to_string(&mut s)?;
Ok(s.lines().filter_map(|line| {
if !line.trim().is_empty() {
pattern::Pattern::new(line, root).ok()
} else {
None
}
}).collect())
}
fn abs_path(&self, path: &'b Path) -> PathBuf {
if path.is_absolute() {
path.to_owned()
} else {
self.root.join(path)
}
}
}
#[cfg(test)]
mod tests {
extern crate glob;
extern crate tempdir;
use super::File;
use std::fs;
use std::io::Write;
use std::path::{Path,PathBuf};
#[cfg(feature = "nightly")]
use test::Bencher;
struct TestEnv<'a> {
gitignore: &'a Path,
paths: Vec<PathBuf>
}
#[test]
fn test_new_file_with_empty() {
with_fake_repo("", vec!["bar.foo"], |test_env| {
let file = File::new(test_env.gitignore).unwrap();
for path in test_env.paths.iter() {
assert!(!file.is_excluded(path.as_path()).unwrap());
}
})
}
#[test]
fn test_new_file_with_unanchored_wildcard() {
with_fake_repo("*.foo", vec!["bar.foo"], |test_env| {
let file = File::new(test_env.gitignore).unwrap();
for path in test_env.paths.iter() {
assert!(file.is_excluded(path.as_path()).unwrap());
}
})
}
#[test]
fn test_new_file_with_anchored() {
with_fake_repo("/out", vec!["out"], |test_env| {
let file = File::new(test_env.gitignore).unwrap();
for path in test_env.paths.iter() {
assert!(file.is_excluded(path.as_path()).unwrap());
}
})
}
#[test]
fn test_included_files() {
with_fake_repo("*.foo", vec!["bar.foo", "foo", "bar"], |test_env| {
let file = File::new(test_env.gitignore).unwrap();
let files: Vec<String> = file.included_files().unwrap().iter().map(|path|
path.file_name().unwrap().to_str().unwrap().to_string()
).collect();
assert!(files.len() == 3);
assert!(files.contains(&".gitignore".to_string()));
assert!(files.contains(&"bar".to_string()));
assert!(files.contains(&"foo".to_string()));
})
}
#[test]
fn test_nested_files() {
with_fake_repo("woo", vec!["win", "woo/hoo", "woo/boo/shoo"], |test_env| {
let file = File::new(test_env.gitignore).unwrap();
let files: Vec<String> = file.included_files().unwrap().iter().map(|path|
path.file_name().unwrap().to_str().unwrap().to_string()
).collect();
assert!(files.len() == 2);
assert!(files.contains(&".gitignore".to_string()));
assert!(files.contains(&"win".to_string()));
})
}
#[test]
fn test_included_by_ignore_pattern() {
with_fake_repo("*\n!assets/\n!assets/**\n!.git*", vec!["assets/foo/bar.html"], |test_env| {
let file = File::new(test_env.gitignore).unwrap();
let files: Vec<String> = file.included_files().unwrap().iter().map(|path|
path.file_name().unwrap().to_str().unwrap().to_string()
).collect();
assert!(files.len() == 4);
assert!(files.contains(&".gitignore".to_string()));
assert!(files.contains(&"assets".to_string()));
assert!(files.contains(&"foo".to_string()));
assert!(files.contains(&"bar.html".to_string()));
})
}
#[cfg(feature = "nightly")]
#[bench]
fn bench_new_file(b: &mut Bencher) {
let path = Path::new(".gitignore");
b.iter(|| {
File::new(path).unwrap();
})
}
#[cfg(feature = "nightly")]
#[bench]
fn bench_file_match(b: &mut Bencher) {
let file = File::new(Path::new(".gitignore")).unwrap();
let path = Path::new("/dev/null");
b.iter(|| {
file.is_excluded(path).unwrap();
})
}
fn with_fake_repo<F>(ignore_contents: &str, files: Vec<&str>, callback: F)
where F: Fn(&TestEnv) {
let dir = tempdir::TempDir::new("gitignore_tests").unwrap();
let paths = files.iter().map(|file| {
let path = dir.path().join(file);
path.parent().map(|parent| fs::create_dir_all(&parent));
write_to_file(&path, "");
path
}).collect();
let gitignore= dir.path().join(".gitignore");
write_to_file(gitignore.as_path(), ignore_contents);
let test_env = TestEnv {
gitignore: gitignore.as_path(),
paths
};
callback(&test_env);
dir.close().unwrap();
}
fn write_to_file(path: &Path, contents: &str) {
let mut file = fs::File::create(path).unwrap();
file.write_all(contents.as_bytes()).unwrap();
}
}