Преглед изворни кода

Thread an ignore cache through the program

!
Benjamin Sago пре 8 година
родитељ
комит
b95446d834
8 измењених фајлова са 125 додато и 21 уклоњено
  1. 19 4
      src/exa.rs
  2. 8 4
      src/fs/dir.rs
  3. 69 0
      src/fs/feature/ignore.rs
  4. 1 3
      src/fs/feature/mod.rs
  5. 15 0
      src/fs/filter.rs
  6. 1 0
      src/options/filter.rs
  7. 7 6
      src/output/details.rs
  8. 5 4
      src/output/grid_details.rs

+ 19 - 4
src/exa.rs

@@ -30,6 +30,7 @@ use std::path::{Component, PathBuf};
 use ansi_term::{ANSIStrings, Style};
 
 use fs::{Dir, File};
+use fs::feature::ignore::IgnoreCache;
 use fs::feature::git::GitCache;
 use options::{Options, Vars};
 pub use options::Misfire;
@@ -61,6 +62,10 @@ pub struct Exa<'args, 'w, W: Write + 'w> {
     /// This has to last the lifetime of the program, because the user might
     /// want to list several directories in the same repository.
     pub git: Option<GitCache>,
+
+    /// A cache of git-ignored files.
+    /// This lasts the lifetime of the program too, for the same reason.
+    pub ignore: Option<IgnoreCache>,
 }
 
 /// The “real” environment variables type.
@@ -84,6 +89,15 @@ fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> {
     }
 }
 
+fn ignore_cache(options: &Options) -> Option<IgnoreCache> {
+    use fs::filter::GitIgnore;
+
+    match options.filter.git_ignore {
+        GitIgnore::CheckAndIgnore => Some(IgnoreCache::new()),
+        GitIgnore::Off            => None,
+    }
+}
+
 impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
     pub fn new<I>(args: I, writer: &'w mut W) -> Result<Exa<'args, 'w, W>, Misfire>
     where I: Iterator<Item=&'args OsString> {
@@ -99,7 +113,8 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
             }
 
             let git = git_options(&options, &args);
-            Exa { options, writer, args, git }
+            let ignore = ignore_cache(&options);
+            Exa { options, writer, args, git, ignore }
         })
     }
 
@@ -160,7 +175,7 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
             }
 
             let mut children = Vec::new();
-            for file in dir.files(self.options.filter.dot_filter) {
+            for file in dir.files(self.options.filter.dot_filter, self.ignore.as_ref()) {
                 match file {
                     Ok(file)       => children.push(file),
                     Err((path, e)) => writeln!(stderr(), "[{}: {}]", path.display(), e)?,
@@ -212,10 +227,10 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
                     grid::Render { files, colours, style, opts }.render(self.writer)
                 }
                 Mode::Details(ref opts) => {
-                    details::Render { dir, files, colours, style, opts, filter: &self.options.filter, recurse: self.options.dir_action.recurse_options() }.render(self.git.as_ref(), self.writer)
+                    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.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.ignore.as_ref(), self.writer)
                 }
             }
         }

+ 8 - 4
src/fs/dir.rs

@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
 use std::slice::Iter as SliceIter;
 
 use fs::File;
+use fs::feature::ignore::IgnoreCache;
 
 
 /// A **Dir** provides a cached list of the file paths in a directory that's
@@ -43,12 +44,13 @@ impl Dir {
 
     /// Produce an iterator of IO results of trying to read all the files in
     /// this directory.
-    pub fn files(&self, dots: DotFilter) -> Files {
+    pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, ignore: Option<&'ig IgnoreCache>) -> Files<'dir, 'ig> {
         Files {
             inner:     self.contents.iter(),
             dir:       self,
             dotfiles:  dots.shows_dotfiles(),
             dots:      dots.dots(),
+            ignore:    ignore,
         }
     }
 
@@ -65,7 +67,7 @@ impl Dir {
 
 
 /// Iterator over reading the contents of a directory as `File` objects.
-pub struct Files<'dir> {
+pub struct Files<'dir, 'ig> {
 
     /// The internal iterator over the paths that have been read already.
     inner: SliceIter<'dir, PathBuf>,
@@ -79,9 +81,11 @@ pub struct Files<'dir> {
     /// Whether the `.` or `..` directories should be produced first, before
     /// any files have been listed.
     dots: Dots,
+
+    ignore: Option<&'ig IgnoreCache>,
 }
 
-impl<'dir> Files<'dir> {
+impl<'dir, 'ig> Files<'dir, 'ig> {
     fn parent(&self) -> PathBuf {
         // We can’t use `Path#parent` here because all it does is remove the
         // last path component, which is no good for us if the path is
@@ -124,7 +128,7 @@ enum Dots {
 }
 
 
-impl<'dir> Iterator for Files<'dir> {
+impl<'dir, 'ig> Iterator for Files<'dir, 'ig> {
     type Item = Result<File<'dir>, (PathBuf, io::Error)>;
 
     fn next(&mut self) -> Option<Self::Item> {

+ 69 - 0
src/fs/feature/ignore.rs

@@ -0,0 +1,69 @@
+//! 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};
+
+use fs::filter::IgnorePatterns;
+
+
+/// An **ignore cache** holds sets of glob patterns paired with the
+/// directories that they should be ignored underneath.
+#[derive(Default, Debug)]
+pub struct IgnoreCache {
+    entries: Vec<(PathBuf, IgnorePatterns)>
+}
+
+impl IgnoreCache {
+    pub fn new() -> IgnoreCache {
+        IgnoreCache::default()
+    }
+
+    #[allow(unused_results)]  // don’t do this
+    pub fn discover_underneath(&mut self, path: &Path) {
+        let mut path = Some(path);
+
+        while let Some(p) = path {
+            let ignore_file = p.join(".gitignore");
+            if ignore_file.is_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));
+                }
+            }
+
+            path = p.parent();
+        }
+    }
+
+    pub fn is_ignored(&self, suspect: &Path) -> bool {
+        self.entries.iter().any(|&(ref base_path, ref patterns)| {
+            if let Ok(suffix) = suspect.strip_prefix(&base_path) {
+                patterns.is_ignored_path(suffix)
+            }
+            else {
+                false
+            }
+        })
+    }
+}
+
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn empty() {
+        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")));
+    }
+}

+ 1 - 3
src/fs/feature/mod.rs

@@ -1,7 +1,5 @@
-// Extended attribute support
 pub mod xattr;
-
-// Git support
+pub mod ignore;
 
 #[cfg(feature="git")] pub mod git;
 

+ 15 - 0
src/fs/filter.rs

@@ -3,6 +3,7 @@
 use std::cmp::Ordering;
 use std::iter::FromIterator;
 use std::os::unix::fs::MetadataExt;
+use std::path::Path;
 
 use glob;
 use natord;
@@ -79,6 +80,12 @@ pub struct FileFilter {
     /// Glob patterns to ignore. Any file name that matches *any* of these
     /// patterns won’t be displayed in the list.
     pub ignore_patterns: IgnorePatterns,
+
+    /// Whether to ignore Git-ignored patterns.
+    /// This is implemented completely separately from the actual Git
+    /// repository scanning — a `.gitignore` file will still be scanned even
+    /// if there’s no `.git` folder present.
+    pub git_ignore: GitIgnore,
 }
 
 
@@ -302,6 +309,14 @@ impl IgnorePatterns {
     fn is_ignored(&self, file: &str) -> bool {
         self.patterns.iter().any(|p| p.matches(file))
     }
+
+    /// Test whether the given file should be hidden from the results.
+    pub fn is_ignored_path(&self, file: &Path) -> bool {
+        self.patterns.iter().any(|p| p.matches_path(file))
+    }
+
+    // TODO(ogham): The fact that `is_ignored_path` is pub while `is_ignored`
+    // isn’t probably means it’s in the wrong place
 }
 
 

+ 1 - 0
src/options/filter.rs

@@ -17,6 +17,7 @@ impl FileFilter {
             sort_field:      SortField::deduce(matches)?,
             dot_filter:      DotFilter::deduce(matches)?,
             ignore_patterns: IgnorePatterns::deduce(matches)?,
+            git_ignore:      GitIgnore::deduce(matches)?,
         })
     }
 }

+ 7 - 6
src/output/details.rs

@@ -69,6 +69,7 @@ use ansi_term::Style;
 use fs::{Dir, File};
 use fs::dir_action::RecurseOptions;
 use fs::filter::FileFilter;
+use fs::feature::ignore::IgnoreCache;
 use fs::feature::git::GitCache;
 use fs::feature::xattr::{Attribute, FileAttributes};
 use style::Colours;
@@ -140,7 +141,7 @@ impl<'a> AsRef<File<'a>> for Egg<'a> {
 
 
 impl<'a> Render<'a> {
-    pub fn render<W: Write>(self, mut git: Option<&'a GitCache>, w: &mut W) -> IOResult<()> {
+    pub fn render<W: Write>(self, mut git: Option<&'a GitCache>, ignore: Option<&'a IgnoreCache>, w: &mut W) -> IOResult<()> {
         let mut rows = Vec::new();
 
         if let Some(ref table) = self.opts.table {
@@ -161,14 +162,14 @@ impl<'a> Render<'a> {
             // This is weird, but I can’t find a way around it:
             // https://internals.rust-lang.org/t/should-option-mut-t-implement-copy/3715/6
             let mut table = Some(table);
-            self.add_files_to_table(&mut table, &mut rows, &self.files, TreeDepth::root());
+            self.add_files_to_table(&mut table, &mut rows, &self.files, ignore, TreeDepth::root());
 
             for row in self.iterate_with_table(table.unwrap(), rows) {
                 writeln!(w, "{}", row.strings())?
             }
         }
         else {
-            self.add_files_to_table(&mut None, &mut rows, &self.files, TreeDepth::root());
+            self.add_files_to_table(&mut None, &mut rows, &self.files, ignore, TreeDepth::root());
 
             for row in self.iterate(rows) {
                 writeln!(w, "{}", row.strings())?
@@ -180,7 +181,7 @@ impl<'a> Render<'a> {
 
     /// Adds files to the table, possibly recursively. This is easily
     /// parallelisable, and uses a pool of threads.
-    fn add_files_to_table<'dir>(&self, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &Vec<File<'dir>>, depth: TreeDepth) {
+    fn add_files_to_table<'dir, 'ig>(&self, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &Vec<File<'dir>>, ignore: Option<&'ig IgnoreCache>, depth: TreeDepth) {
         use num_cpus;
         use scoped_threadpool::Pool;
         use std::sync::{Arc, Mutex};
@@ -282,7 +283,7 @@ impl<'a> Render<'a> {
             rows.push(row);
 
             if let Some(ref dir) = egg.dir {
-                for file_to_add in dir.files(self.filter.dot_filter) {
+                for file_to_add in dir.files(self.filter.dot_filter, ignore) {
                     match file_to_add {
                         Ok(f)          => files.push(f),
                         Err((path, e)) => errors.push((e, Some(path)))
@@ -300,7 +301,7 @@ impl<'a> Render<'a> {
                         rows.push(self.render_error(&error, TreeParams::new(depth.deeper(), false), path));
                     }
 
-                    self.add_files_to_table(table, rows, &files, depth.deeper());
+                    self.add_files_to_table(table, rows, &files, ignore, depth.deeper());
                     continue;
                 }
             }

+ 5 - 4
src/output/grid_details.rs

@@ -6,6 +6,7 @@ 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;
@@ -111,16 +112,16 @@ impl<'a> Render<'a> {
         }
     }
 
-    pub fn render<W: Write>(self, git: Option<&GitCache>, w: &mut W) -> IOResult<()> {
-        if let Some((grid, width)) = self.find_fitting_grid(git) {
+    pub fn render<W: Write>(self, git: Option<&GitCache>, ignore: Option<&'a IgnoreCache>, w: &mut W) -> IOResult<()> {
+        if let Some((grid, width)) = self.find_fitting_grid(git, ignore) {
             write!(w, "{}", grid.fit_into_columns(width))
         }
         else {
-            self.give_up().render(git, w)
+            self.give_up().render(git, ignore, w)
         }
     }
 
-    pub fn find_fitting_grid(&self, git: Option<&GitCache>) -> Option<(grid::Grid, grid::Width)> {
+    pub fn find_fitting_grid(&self, git: Option<&GitCache>, ignore: Option<&'a IgnoreCache>) -> Option<(grid::Grid, grid::Width)> {
         let options = self.details.table.as_ref().expect("Details table options not given!");
 
         let drender = self.details();