From 827aa8bfc32eadf353f72b0ce41c88dc4ad411c1 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Thu, 28 Sep 2017 16:13:47 +0100 Subject: [PATCH] Ignore files matched in .gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This doesn’t *completely* work: it seems to have trouble with ignored paths beginning with slashes, possibly amongst others. Also, .gitignore scanning could be made more efficient. --- src/exa.rs | 2 +- src/fs/dir.rs | 6 ++++++ src/fs/feature/ignore.rs | 21 +++++++++++++++------ src/output/grid_details.rs | 12 +++++++----- xtests/git_2_ignore_recurse | 23 +++++++++++++++++++++++ xtests/git_2_ignore_tree | 12 ++++++++++++ xtests/run.sh | 5 +++++ 7 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 xtests/git_2_ignore_recurse create mode 100644 xtests/git_2_ignore_tree diff --git a/src/exa.rs b/src/exa.rs index 80401b6..5fc08c3 100644 --- a/src/exa.rs +++ b/src/exa.rs @@ -230,7 +230,7 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { details::Render { dir, files, colours, style, opts, filter: &self.options.filter, recurse: self.options.dir_action.recurse_options() }.render(self.git.as_ref(), self.ignore.as_ref(), self.writer) } Mode::GridDetails(ref opts) => { - grid_details::Render { dir, files, colours, style, grid: &opts.grid, details: &opts.details, filter: &self.options.filter, row_threshold: opts.row_threshold }.render(self.git.as_ref(), self.ignore.as_ref(), self.writer) + grid_details::Render { dir, files, colours, style, grid: &opts.grid, details: &opts.details, filter: &self.options.filter, row_threshold: opts.row_threshold }.render(self.git.as_ref(), self.writer) } } } diff --git a/src/fs/dir.rs b/src/fs/dir.rs index 72cd2ae..a9c0ccc 100644 --- a/src/fs/dir.rs +++ b/src/fs/dir.rs @@ -45,6 +45,8 @@ impl Dir { /// Produce an iterator of IO results of trying to read all the files in /// this directory. pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, ignore: Option<&'ig IgnoreCache>) -> Files<'dir, 'ig> { + if let Some(i) = ignore { i.discover_underneath(&self.path); } + Files { inner: self.contents.iter(), dir: self, @@ -103,6 +105,10 @@ impl<'dir, 'ig> Files<'dir, 'ig> { let filename = File::filename(path); if !self.dotfiles && filename.starts_with(".") { continue } + if let Some(i) = self.ignore { + if i.is_ignored(path) { continue } + } + return Some(File::new(path.clone(), self.dir, filename) .map_err(|e| (path.clone(), e))) } diff --git a/src/fs/feature/ignore.rs b/src/fs/feature/ignore.rs index 66a8caf..10c0d94 100644 --- a/src/fs/feature/ignore.rs +++ b/src/fs/feature/ignore.rs @@ -6,15 +6,17 @@ use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; +use std::sync::RwLock; use fs::filter::IgnorePatterns; /// An **ignore cache** holds sets of glob patterns paired with the -/// directories that they should be ignored underneath. +/// directories that they should be ignored underneath. Believe it or not, +/// that’s a valid English sentence. #[derive(Default, Debug)] pub struct IgnoreCache { - entries: Vec<(PathBuf, IgnorePatterns)> + entries: RwLock> } impl IgnoreCache { @@ -23,27 +25,35 @@ impl IgnoreCache { } #[allow(unused_results)] // don’t do this - pub fn discover_underneath(&mut self, path: &Path) { + pub fn discover_underneath(&self, path: &Path) { let mut path = Some(path); + let mut entries = self.entries.write().unwrap(); while let Some(p) = path { + if p.components().next().is_none() { break } + let ignore_file = p.join(".gitignore"); if ignore_file.is_file() { + debug!("Found a .gitignore file: {:?}", ignore_file); if let Ok(mut file) = File::open(ignore_file) { let mut contents = String::new(); file.read_to_string(&mut contents).expect("Reading gitignore failed"); let (patterns, mut _errors) = IgnorePatterns::parse_from_iter(contents.lines()); - self.entries.push((p.into(), patterns)); + entries.push((p.into(), patterns)); } } + else { + debug!("Found no .gitignore file at {:?}", ignore_file); + } path = p.parent(); } } pub fn is_ignored(&self, suspect: &Path) -> bool { - self.entries.iter().any(|&(ref base_path, ref patterns)| { + let entries = self.entries.read().unwrap(); + entries.iter().any(|&(ref base_path, ref patterns)| { if let Ok(suffix) = suspect.strip_prefix(&base_path) { patterns.is_ignored_path(suffix) } @@ -55,7 +65,6 @@ impl IgnoreCache { } - #[cfg(test)] mod test { use super::*; diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs index f4a7748..2327c43 100644 --- a/src/output/grid_details.rs +++ b/src/output/grid_details.rs @@ -6,7 +6,6 @@ use ansi_term::ANSIStrings; use term_grid as grid; use fs::{Dir, File}; -use fs::feature::ignore::IgnoreCache; use fs::feature::git::GitCache; use fs::feature::xattr::FileAttributes; use fs::filter::FileFilter; @@ -112,16 +111,19 @@ impl<'a> Render<'a> { } } - pub fn render(self, git: Option<&GitCache>, ignore: Option<&'a IgnoreCache>, w: &mut W) -> IOResult<()> { - if let Some((grid, width)) = self.find_fitting_grid(git, ignore) { + // This doesn’t take an IgnoreCache even though the details one does + // because grid-details has no tree view. + + pub fn render(self, git: Option<&GitCache>, w: &mut W) -> IOResult<()> { + if let Some((grid, width)) = self.find_fitting_grid(git) { write!(w, "{}", grid.fit_into_columns(width)) } else { - self.give_up().render(git, ignore, w) + self.give_up().render(git, None, w) } } - pub fn find_fitting_grid(&self, git: Option<&GitCache>, ignore: Option<&'a IgnoreCache>) -> Option<(grid::Grid, grid::Width)> { + pub fn find_fitting_grid(&self, git: Option<&GitCache>) -> Option<(grid::Grid, grid::Width)> { let options = self.details.table.as_ref().expect("Details table options not given!"); let drender = self.details(); diff --git a/xtests/git_2_ignore_recurse b/xtests/git_2_ignore_recurse new file mode 100644 index 0000000..5f98671 --- /dev/null +++ b/xtests/git_2_ignore_recurse @@ -0,0 +1,23 @@ +drwxrwxr-x - cassowary  1 Jan 12:34 deeply +drwxrwxr-x - cassowary  1 Jan 12:34 ignoreds + +/testcases/git2/deeply: +drwxrwxr-x - cassowary  1 Jan 12:34 nested + +/testcases/git2/deeply/nested: +drwxrwxr-x - cassowary  1 Jan 12:34 directory +drwxrwxr-x - cassowary  1 Jan 12:34 repository + +/testcases/git2/deeply/nested/directory: +.rw-rw-r-- 0 cassowary  1 Jan 12:34 l8st +.rw-rw-r-- 18 cassowary  1 Jan 12:34 upd8d + +/testcases/git2/deeply/nested/repository: +.rw-rw-r-- 0 cassowary  1 Jan 12:34 subfile + +/testcases/git2/ignoreds: +.rw-rw-r-- 0 cassowary  1 Jan 12:34 music.m4a +drwxrwxr-x - cassowary  1 Jan 12:34 nested + +/testcases/git2/ignoreds/nested: +.rw-rw-r-- 0 cassowary  1 Jan 12:34 funky chicken.m4a diff --git a/xtests/git_2_ignore_tree b/xtests/git_2_ignore_tree new file mode 100644 index 0000000..3513fea --- /dev/null +++ b/xtests/git_2_ignore_tree @@ -0,0 +1,12 @@ +drwxrwxr-x - cassowary  1 Jan 12:34 /testcases/git2 +drwxrwxr-x - cassowary  1 Jan 12:34 ├── deeply +drwxrwxr-x - cassowary  1 Jan 12:34 │ └── nested +drwxrwxr-x - cassowary  1 Jan 12:34 │ ├── directory +.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ │ ├── l8st +.rw-rw-r-- 18 cassowary  1 Jan 12:34 │ │ └── upd8d +drwxrwxr-x - cassowary  1 Jan 12:34 │ └── repository +.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ └── subfile +drwxrwxr-x - cassowary  1 Jan 12:34 └── ignoreds +.rw-rw-r-- 0 cassowary  1 Jan 12:34  ├── music.m4a +drwxrwxr-x - cassowary  1 Jan 12:34  └── nested +.rw-rw-r-- 0 cassowary  1 Jan 12:34  └── funky chicken.m4a diff --git a/xtests/run.sh b/xtests/run.sh index b88ae20..d6828ae 100755 --- a/xtests/run.sh +++ b/xtests/run.sh @@ -223,6 +223,11 @@ $exa $testcases/git2/deeply/nested/directory $testcases/git/edits \ COLUMNS=40 $exa $testcases/files -lG --git | diff -q - $results/files_lG_40 || exit 1 # that aren't under git +# .gitignore +$exa $testcases/git2 --recurse --long --git-ignore 2>&1 | diff - $results/git_2_ignore_recurse +$exa $testcases/git2 --tree --long --git-ignore 2>&1 | diff - $results/git_2_ignore_tree + + # Hidden files COLUMNS=80 $exa $testcases/hiddens 2>&1 | diff -q - $results/hiddens || exit 1 COLUMNS=80 $exa $testcases/hiddens -a 2>&1 | diff -q - $results/hiddens_a || exit 1