Use git2 instead of parsing .gitignore for --git-ignore

Fix #636
This commit is contained in:
ariasuni 2020-04-19 05:52:35 +02:00
parent 78ba0b8973
commit 046af5cdd1
8 changed files with 26 additions and 236 deletions

View File

@ -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<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.
@ -71,15 +66,6 @@ fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> {
}
}
fn ignore_cache(options: &Options) -> Option<IgnoreCache> {
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<I>(args: I, writer: &'w mut W) -> Result<Exa<'args, 'w, W>, Misfire>
where I: Iterator<Item=&'args OsString> {
@ -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) => {

View File

@ -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)

View File

@ -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 were 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,
/// thats a valid English sentence.
#[derive(Default, Debug)]
pub struct IgnoreCache {
entries: RwLock<Vec<(PathBuf, IgnorePatterns)>>
}
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<Item=&'a str>
{
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")));
}
}

View File

@ -1,5 +1,4 @@
pub mod xattr;
pub mod ignore;
#[cfg(feature="git")] pub mod git;

View File

@ -177,6 +177,7 @@ pub struct Time {
/// A files 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 hasnt changed since the last commit.

View File

@ -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. Its 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,

View File

@ -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;
@ -149,7 +148,7 @@ impl<'a> AsRef<File<'a>> for Egg<'a> {
impl<'a> Render<'a> {
pub fn render<W: Write>(self, mut git: Option<&'a GitCache>, ignore: Option<&'a IgnoreCache>, w: &mut W) -> IOResult<()> {
pub fn render<W: Write>(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 cant 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<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], ignore: Option<&'ig IgnoreCache>, depth: TreeDepth) {
fn add_files_to_table<'dir, 'ig>(&self, pool: &mut Pool, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], git: Option<&'ig GitCache>, depth: TreeDepth) {
use std::sync::{Arc, Mutex};
use log::error;
use crate::fs::feature::xattr;
@ -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;
}
}

View File

@ -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)
}
}