diff --git a/src/gitignore_filter.rs b/src/gitignore_filter.rs index ba12921..980accb 100644 --- a/src/gitignore_filter.rs +++ b/src/gitignore_filter.rs @@ -1,5 +1,10 @@ use crate::rocket_watcher; -use std::path::{Path, PathBuf}; +use std::fs; +use std::path::Path; + +// todo: are these an overridable conventions we need to respect? +const GITIGNORE_FILENAME: &str = ".gitignore"; +const GIT_METADATA_DIR_NAME: &str = ".git"; pub struct GitignoreFilter { ignorers: Vec, @@ -10,45 +15,130 @@ impl GitignoreFilter { GitignoreFilter { ignorers } } - pub fn build(mut dir: PathBuf) -> GitignoreFilter { - // todo: is this an overridable convention we need to respect? - const GITIGNORE_FILENAME: &str = ".gitignore"; - - let mut ignorers = Vec::new(); + pub fn build(dir: &Path) -> GitignoreFilter { + let mut ignorers = get_gitignores_recursively(dir); + let mut ancestors = get_parent_gitignores(&dir); + ignorers.append(&mut ancestors); + return GitignoreFilter::new(ignorers); + } +} - while dir.parent() != None { - let mut builder = ignore::gitignore::GitignoreBuilder::new(dir.as_path()); +// todo: Handle errors better. +// Right now any error is (logged, then) treated like there is no .gitignore. We should probably +// return any non-not-found errors instead since we're not sure we're accurately representing the +// filters described by the .gitignores. - // Update the dir to have the file name instead of making a copy and we'll .pop() twice to - // traverse upward to the parent dir. - dir.push(GITIGNORE_FILENAME); - println!( - "looking for {}", - dir.to_str() - .expect("if you use an OS where paths aren't unicode, your mom's a hoe") - ); - let path = dir.as_path(); - println!("path = {:?}", path); +fn get_gitignore(dir: &Path) -> Option { + if path_in_git_metadata(dir) { + return None; + } + let mut builder = ignore::gitignore::GitignoreBuilder::new(dir); + let mut filepath = dir.to_path_buf(); + filepath.push(GITIGNORE_FILENAME); + match builder.add(filepath) { + None => match builder.build() { + Ok(ignorer) => return Some(ignorer), + Err(err) => { + println!("error building ignorer for {:?}: {:?}", dir, err); + return None; + } + }, + Some(err) => { + println!("error adding {:?}: {:?}", dir, err); + return None; + } + } +} - match builder.add(path) { - None => match builder.build() { - Ok(ignorer) => ignorers.push(ignorer), - Err(err) => println!("error building ignorer for {:?}: {:?}", path, err), - }, - Some(err) => { - println!("error adding {:?}: {:?}", path, err); - } +fn get_gitignores_recursively(dir: &Path) -> Vec { + let mut ignores = Vec::new(); + if !dir.is_dir() || path_in_git_metadata(dir) { + return ignores; + } + let readdir = match fs::read_dir(dir) { + Ok(readdir) => readdir, + Err(err) => { + println!("error reading filesystem entries for '{:?}': {}", dir, err); + return ignores; + } + }; + for entry in readdir { + let entry = match entry { + Ok(entry) => entry, + Err(err) => { + println!("error reading filesystem entry: {}", err); + continue; } - let _ = dir.pop(); - let _ = dir.pop(); + }; + ignores.append(&mut get_gitignores_recursively(&entry.path())); + } + match get_gitignore(dir) { + Some(ignorer) => ignores.push(ignorer), + None => {} + } + return ignores; +} + +fn get_parent_gitignores(dir: &Path) -> Vec { + // We only want ancestors up to and including the root of the containing repo. If we find that + // we're not in a git repo then we'll ignore all ancestors. + let mut ancestors = Vec::new(); + if is_git_repo_root(dir) { + return ancestors; + } + let mut dir = dir.to_owned(); + let mut found_root = false; + while dir.pop() { + match get_gitignore(&dir) { + Some(ignorer) => ancestors.push(ignorer), + None => {} } + if is_git_repo_root(&dir) { + found_root = true; + break; + } + } + if found_root { + return ancestors; + } + return Vec::new(); +} - return GitignoreFilter::new(ignorers); +fn path_in_git_metadata(path: &Path) -> bool { + return path.iter().any(|e| e == GIT_METADATA_DIR_NAME); +} + +fn is_git_repo_root(path: &Path) -> bool { + if !path.is_dir() { + return false; } + let readdir = match fs::read_dir(path) { + Ok(readdir) => readdir, + Err(err) => { + println!("error reading filesystem entries for '{:?}': {}", path, err); + return false; + } + }; + for entry in readdir { + let entry = match entry { + Ok(entry) => entry, + Err(err) => { + println!("error reading filesystem entry: {}", err); + return false; + } + }; + if entry.file_name() == GIT_METADATA_DIR_NAME { + return true; + } + } + return false; } impl rocket_watcher::PathFilter for GitignoreFilter { fn exclude(&self, path: &Path) -> bool { + if path_in_git_metadata(path) { + return true; + } for ignorer in &self.ignorers { // todo: figure out how to distinguish files from directories let resp = ignorer.matched(path, true); diff --git a/src/main.rs b/src/main.rs index e5db28f..a20ce3e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,7 @@ fn main() { } }; - let filter = GitignoreFilter::build(watch_dir.clone()); + let filter = GitignoreFilter::build(&watch_dir); let watchy = RocketWatch::new(filter); watchy.watch_directory(