mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-11-26 05:47:32 +00:00
parent
78ba0b8973
commit
046af5cdd1
21
src/exa.rs
21
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<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) => {
|
||||
|
@ -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)
|
||||
|
@ -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<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")));
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
pub mod xattr;
|
||||
pub mod ignore;
|
||||
|
||||
#[cfg(feature="git")] pub mod git;
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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<PathBuf>)>,
|
||||
dir: Option<Dir>,
|
||||
file: &'a File<'a>,
|
||||
icon: Option<String>,
|
||||
icon: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> AsRef<File<'a>> for Egg<'a> {
|
||||
@ -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 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<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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user