From 046af5cdd19b0255c4de39153ed789d4f94d8d39 Mon Sep 17 00:00:00 2001 From: ariasuni Date: Sun, 19 Apr 2020 05:52:35 +0200 Subject: [PATCH] Use git2 instead of parsing .gitignore for --git-ignore Fix #636 --- src/exa.rs | 21 +--- src/fs/dir.rs | 16 +-- src/fs/feature/ignore.rs | 198 ------------------------------------- src/fs/feature/mod.rs | 1 - src/fs/fields.rs | 1 + src/options/mod.rs | 6 +- src/output/details.rs | 17 ++-- src/output/grid_details.rs | 2 +- 8 files changed, 26 insertions(+), 236 deletions(-) delete mode 100644 src/fs/feature/ignore.rs diff --git a/src/exa.rs b/src/exa.rs index 53e02a1..c8b26bb 100644 --- a/src/exa.rs +++ b/src/exa.rs @@ -11,7 +11,6 @@ use ansi_term::{ANSIStrings, Style}; use log::debug; use crate::fs::{Dir, File}; -use crate::fs::feature::ignore::IgnoreCache; use crate::fs::feature::git::GitCache; use crate::options::{Options, Vars}; pub use crate::options::vars; @@ -44,10 +43,6 @@ 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, - - /// A cache of git-ignored files. - /// This lasts the lifetime of the program too, for the same reason. - pub ignore: Option, } /// The “real” environment variables type. @@ -71,15 +66,6 @@ fn git_options(options: &Options, args: &[&OsStr]) -> Option { } } -fn ignore_cache(options: &Options) -> Option { - use crate::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 from_args(args: I, writer: &'w mut W) -> Result, Misfire> where I: Iterator { @@ -95,8 +81,7 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { } let git = git_options(&options, &args); - let ignore = ignore_cache(&options); - Exa { options, writer, args, git, ignore } + Exa { options, writer, args, git } }) } @@ -157,7 +142,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, self.ignore.as_ref()) { + for file in dir.files(self.options.filter.dot_filter, self.git.as_ref()) { match file { Ok(file) => children.push(file), Err((path, e)) => writeln!(stderr(), "[{}: {}]", path.display(), e)?, @@ -217,7 +202,7 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { let recurse = self.options.dir_action.recurse_options(); let r = details::Render { dir, files, colours, style, opts, filter, recurse }; - r.render(self.git.as_ref(), self.ignore.as_ref(), self.writer) + r.render(self.git.as_ref(), self.writer) } Mode::GridDetails(ref opts) => { diff --git a/src/fs/dir.rs b/src/fs/dir.rs index 4d5cfbb..8024a51 100644 --- a/src/fs/dir.rs +++ b/src/fs/dir.rs @@ -1,3 +1,5 @@ +use crate::fs::feature::git::GitCache; +use crate::fs::fields::GitStatus; use std::io::{self, Result as IOResult}; use std::fs; use std::path::{Path, PathBuf}; @@ -6,7 +8,6 @@ use std::slice::Iter as SliceIter; use log::info; use crate::fs::File; -use crate::fs::feature::ignore::IgnoreCache; /// A **Dir** provides a cached list of the file paths in a directory that's @@ -46,15 +47,13 @@ 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); } - + pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, git: Option<&'ig GitCache>) -> Files<'dir, 'ig> { Files { inner: self.contents.iter(), dir: self, dotfiles: dots.shows_dotfiles(), dots: dots.dots(), - ignore, + git, } } @@ -86,7 +85,7 @@ pub struct Files<'dir, 'ig> { /// any files have been listed. dots: DotsNext, - ignore: Option<&'ig IgnoreCache>, + git: Option<&'ig GitCache>, } impl<'dir, 'ig> Files<'dir, 'ig> { @@ -107,8 +106,9 @@ 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 } + let git_status = self.git.map(|g| g.get(path, false)).unwrap_or_default(); + if git_status.unstaged == GitStatus::Ignored { + continue; } return Some(File::from_args(path.clone(), self.dir, filename) diff --git a/src/fs/feature/ignore.rs b/src/fs/feature/ignore.rs deleted file mode 100644 index 160940a..0000000 --- a/src/fs/feature/ignore.rs +++ /dev/null @@ -1,198 +0,0 @@ -//! 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 std::sync::RwLock; - -use log::debug; - -use crate::fs::filter::IgnorePatterns; - - -/// An **ignore cache** holds sets of glob patterns paired with the -/// directories that they should be ignored underneath. Believe it or not, -/// that’s a valid English sentence. -#[derive(Default, Debug)] -pub struct IgnoreCache { - entries: RwLock> -} - -impl IgnoreCache { - pub fn new() -> IgnoreCache { - IgnoreCache::default() - } - - 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(); - - match file.read_to_string(&mut contents) { - Ok(_) => { - let patterns = file_lines_to_patterns(contents.lines()); - entries.push((p.into(), patterns)); - } - Err(e) => debug!("Failed to read a .gitignore: {:?}", e) - } - } - } - else { - debug!("Found no .gitignore file at {:?}", ignore_file); - } - - path = p.parent(); - } - } - - pub fn is_ignored(&self, suspect: &Path) -> bool { - 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) - } - else { - false - } - }) - } -} - - -fn file_lines_to_patterns<'a, I>(iter: I) -> IgnorePatterns -where I: Iterator -{ - let iter = iter.filter(|el| !el.is_empty()); - let iter = iter.filter(|el| !el.starts_with('#')); - - // TODO: Figure out if this should trim whitespace or not - - // Errors are currently being ignored... not a good look - IgnorePatterns::parse_from_iter(iter).0 -} - - -#[cfg(test)] -mod test { - use super::*; - - #[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" ]; - 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()); - assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter())); - } - - #[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())); - } - - #[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())); - } - - - 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) ]) } - } - - #[test] - fn an_empty_cache_ignores_nothing() { - 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"))); - } - - #[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"))); - } - - #[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"))); - } - - #[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"))); - } -} diff --git a/src/fs/feature/mod.rs b/src/fs/feature/mod.rs index 7c230d1..62c76c1 100644 --- a/src/fs/feature/mod.rs +++ b/src/fs/feature/mod.rs @@ -1,5 +1,4 @@ pub mod xattr; -pub mod ignore; #[cfg(feature="git")] pub mod git; diff --git a/src/fs/fields.rs b/src/fs/fields.rs index 87cebfc..e7d4fd4 100644 --- a/src/fs/fields.rs +++ b/src/fs/fields.rs @@ -177,6 +177,7 @@ pub struct Time { /// A file’s status in a Git repository. Whether a file is in a repository or /// not is handled by the Git module, rather than having a “null” variant in /// this enum. +#[derive(PartialEq)] pub enum GitStatus { /// This file hasn’t changed since the last commit. diff --git a/src/options/mod.rs b/src/options/mod.rs index a01ca3e..620a5a9 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -72,7 +72,7 @@ use std::ffi::{OsStr, OsString}; use crate::fs::dir_action::DirAction; -use crate::fs::filter::FileFilter; +use crate::fs::filter::{FileFilter,GitIgnore}; use crate::output::{View, Mode, details, grid_details}; mod style; @@ -146,6 +146,10 @@ impl Options { /// status column. It’s only worth trying to discover a repository if the /// results will end up being displayed. pub fn should_scan_for_git(&self) -> bool { + if self.filter.git_ignore == GitIgnore::CheckAndIgnore { + return true; + } + match self.view.mode { Mode::Details(details::Options { table: Some(ref table), .. }) | Mode::GridDetails(grid_details::Options { details: details::Options { table: Some(ref table), .. }, .. }) => table.columns.git, diff --git a/src/output/details.rs b/src/output/details.rs index 217b401..5279fa5 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -70,7 +70,6 @@ use ansi_term::{ANSIGenericString, Style}; use crate::fs::{Dir, File}; use crate::fs::dir_action::RecurseOptions; use crate::fs::filter::FileFilter; -use crate::fs::feature::ignore::IgnoreCache; use crate::fs::feature::git::GitCache; use crate::fs::feature::xattr::{Attribute, FileAttributes}; use crate::style::Colours; @@ -138,7 +137,7 @@ struct Egg<'a> { errors: Vec<(IOError, Option)>, dir: Option, file: &'a File<'a>, - icon: Option, + icon: Option, } impl<'a> AsRef> for Egg<'a> { @@ -149,7 +148,7 @@ impl<'a> AsRef> for Egg<'a> { impl<'a> Render<'a> { - pub fn render(self, mut git: Option<&'a GitCache>, ignore: Option<&'a IgnoreCache>, w: &mut W) -> IOResult<()> { + pub fn render(self, mut git: Option<&'a GitCache>, w: &mut W) -> IOResult<()> { let mut pool = Pool::new(num_cpus::get() as u32); let mut rows = Vec::new(); @@ -171,14 +170,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 pool, &mut table, &mut rows, &self.files, ignore, TreeDepth::root()); + self.add_files_to_table(&mut pool, &mut table, &mut rows, &self.files, git, TreeDepth::root()); for row in self.iterate_with_table(table.unwrap(), rows) { writeln!(w, "{}", row.strings())? } } else { - self.add_files_to_table(&mut pool, &mut None, &mut rows, &self.files, ignore, TreeDepth::root()); + self.add_files_to_table(&mut pool, &mut None, &mut rows, &self.files, git, TreeDepth::root()); for row in self.iterate(rows) { writeln!(w, "{}", row.strings())? @@ -190,7 +189,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, 'ig>(&self, pool: &mut Pool, table: &mut Option>, rows: &mut Vec, src: &[File<'dir>], ignore: Option<&'ig IgnoreCache>, depth: TreeDepth) { + fn add_files_to_table<'dir, 'ig>(&self, pool: &mut Pool, table: &mut Option>, rows: &mut Vec, src: &[File<'dir>], git: Option<&'ig GitCache>, depth: TreeDepth) { use std::sync::{Arc, Mutex}; use log::error; use crate::fs::feature::xattr; @@ -263,7 +262,7 @@ impl<'a> Render<'a> { } }; - let icon = if self.opts.icons { + let icon = if self.opts.icons { Some(painted_icon(&file, &self.style)) } else { None }; @@ -304,7 +303,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, ignore) { + for file_to_add in dir.files(self.filter.dot_filter, git) { match file_to_add { Ok(f) => files.push(f), Err((path, e)) => errors.push((e, Some(path))) @@ -322,7 +321,7 @@ impl<'a> Render<'a> { rows.push(self.render_error(&error, TreeParams::new(depth.deeper(), false), path)); } - self.add_files_to_table(pool, table, rows, &files, ignore, depth.deeper()); + self.add_files_to_table(pool, table, rows, &files, git, depth.deeper()); continue; } } diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs index 5c9f849..6e9504e 100644 --- a/src/output/grid_details.rs +++ b/src/output/grid_details.rs @@ -120,7 +120,7 @@ impl<'a> Render<'a> { write!(w, "{}", grid.fit_into_columns(width)) } else { - self.give_up().render(git, None, w) + self.give_up().render(git, w) } }