mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-11-26 13:56:27 +00:00
Merge branch 'late-git-discovery'
This merges in the new Git code, which now uses a global cache rather than being per-repository. This lets exa keep the Git column when listing files outside of a directory and when in recursive or tree views. Fixes #24 and #183.
This commit is contained in:
commit
265f93f7cd
40
Vagrantfile
vendored
40
Vagrantfile
vendored
@ -480,10 +480,7 @@ Vagrant.configure(2) do |config|
|
|||||||
touch $dir/that-file
|
touch $dir/that-file
|
||||||
done
|
done
|
||||||
|
|
||||||
touch -t #{some_date} "#{test_dir}/attributes" # there's probably
|
find "#{test_dir}/attributes" -exec touch {} -t #{some_date} \\;
|
||||||
touch -t #{some_date} "#{test_dir}/attributes"/* # a better
|
|
||||||
touch -t #{some_date} "#{test_dir}/attributes"/*/* # way to
|
|
||||||
touch -t #{some_date} "#{test_dir}/attributes"/*/*/* # do this
|
|
||||||
|
|
||||||
# I want to use the following to test,
|
# I want to use the following to test,
|
||||||
# but it only works on macos:
|
# but it only works on macos:
|
||||||
@ -519,12 +516,43 @@ Vagrant.configure(2) do |config|
|
|||||||
echo "more modifications!" | tee edits/unstaged edits/both additions/edited
|
echo "more modifications!" | tee edits/unstaged edits/both additions/edited
|
||||||
touch additions/unstaged
|
touch additions/unstaged
|
||||||
|
|
||||||
|
find "#{test_dir}/git" -exec touch {} -t #{some_date} \\;
|
||||||
touch -t #{some_date} "#{test_dir}/git/"*/*
|
|
||||||
sudo chown #{user}:#{user} -R "#{test_dir}/git"
|
sudo chown #{user}:#{user} -R "#{test_dir}/git"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|
||||||
|
# A second Git repository
|
||||||
|
# for testing two at once
|
||||||
|
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||||
|
set -xe
|
||||||
|
mkdir -p "#{test_dir}/git2/deeply/nested/directory"
|
||||||
|
cd "#{test_dir}/git2"
|
||||||
|
git init
|
||||||
|
|
||||||
|
touch "deeply/nested/directory/upd8d"
|
||||||
|
git add "deeply/nested/directory/upd8d"
|
||||||
|
git commit -m "Automated test commit"
|
||||||
|
|
||||||
|
echo "Now with contents" > "deeply/nested/directory/upd8d"
|
||||||
|
touch "deeply/nested/directory/l8st"
|
||||||
|
|
||||||
|
echo -e "target\n*.mp3" > ".gitignore"
|
||||||
|
mkdir "ignoreds"
|
||||||
|
touch "ignoreds/music.mp3"
|
||||||
|
touch "ignoreds/music.m4a"
|
||||||
|
|
||||||
|
mkdir "target"
|
||||||
|
touch "target/another ignored file"
|
||||||
|
|
||||||
|
mkdir "deeply/nested/repository"
|
||||||
|
cd "deeply/nested/repository"
|
||||||
|
git init
|
||||||
|
touch subfile
|
||||||
|
|
||||||
|
find "#{test_dir}/git2" -exec touch {} -t #{some_date} \\;
|
||||||
|
sudo chown #{user}:#{user} -R "#{test_dir}/git2"
|
||||||
|
EOF
|
||||||
|
|
||||||
# Hidden and dot file testcases.
|
# Hidden and dot file testcases.
|
||||||
# We need to set the permissions of `.` and `..` because they actually
|
# We need to set the permissions of `.` and `..` because they actually
|
||||||
# get displayed in the output here, so this has to come last.
|
# get displayed in the output here, so this has to come last.
|
||||||
|
42
src/exa.rs
42
src/exa.rs
@ -30,6 +30,7 @@ use std::path::{Component, PathBuf};
|
|||||||
use ansi_term::{ANSIStrings, Style};
|
use ansi_term::{ANSIStrings, Style};
|
||||||
|
|
||||||
use fs::{Dir, File};
|
use fs::{Dir, File};
|
||||||
|
use fs::feature::git::GitCache;
|
||||||
use options::{Options, Vars};
|
use options::{Options, Vars};
|
||||||
pub use options::Misfire;
|
pub use options::Misfire;
|
||||||
use output::{escape, lines, grid, grid_details, details, View, Mode};
|
use output::{escape, lines, grid, grid_details, details, View, Mode};
|
||||||
@ -55,6 +56,11 @@ pub struct Exa<'args, 'w, W: Write + 'w> {
|
|||||||
/// List of the free command-line arguments that should correspond to file
|
/// List of the free command-line arguments that should correspond to file
|
||||||
/// names (anything that isn’t an option).
|
/// names (anything that isn’t an option).
|
||||||
pub args: Vec<&'args OsStr>,
|
pub args: Vec<&'args OsStr>,
|
||||||
|
|
||||||
|
/// A global Git cache, if the option was passed in.
|
||||||
|
/// 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The “real” environment variables type.
|
/// The “real” environment variables type.
|
||||||
@ -67,14 +73,33 @@ impl Vars for LiveVars {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a Git cache populated with the arguments that are going to be
|
||||||
|
/// listed before they’re actually listed, if the options demand it.
|
||||||
|
fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> {
|
||||||
|
if options.should_scan_for_git() {
|
||||||
|
Some(args.iter().map(|os| PathBuf::from(os)).collect())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
|
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>
|
pub fn new<I>(args: I, writer: &'w mut W) -> Result<Exa<'args, 'w, W>, Misfire>
|
||||||
where I: Iterator<Item=&'args OsString> {
|
where I: Iterator<Item=&'args OsString> {
|
||||||
Options::parse(args, &LiveVars).map(move |(options, args)| {
|
Options::parse(args, &LiveVars).map(move |(options, mut args)| {
|
||||||
debug!("Dir action from arguments: {:#?}", options.dir_action);
|
debug!("Dir action from arguments: {:#?}", options.dir_action);
|
||||||
debug!("Filter from arguments: {:#?}", options.filter);
|
debug!("Filter from arguments: {:#?}", options.filter);
|
||||||
debug!("View from arguments: {:#?}", options.view.mode);
|
debug!("View from arguments: {:#?}", options.view.mode);
|
||||||
Exa { options, writer, args }
|
|
||||||
|
// List the current directory by default, like ls.
|
||||||
|
// This has to be done here, otherwise git_options won’t see it.
|
||||||
|
if args.is_empty() {
|
||||||
|
args = vec![ OsStr::new(".") ];
|
||||||
|
}
|
||||||
|
|
||||||
|
let git = git_options(&options, &args);
|
||||||
|
Exa { options, writer, args, git }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,11 +108,6 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
|
|||||||
let mut dirs = Vec::new();
|
let mut dirs = Vec::new();
|
||||||
let mut exit_status = 0;
|
let mut exit_status = 0;
|
||||||
|
|
||||||
// List the current directory by default, like ls.
|
|
||||||
if self.args.is_empty() {
|
|
||||||
self.args = vec![ OsStr::new(".") ];
|
|
||||||
}
|
|
||||||
|
|
||||||
for file_path in &self.args {
|
for file_path in &self.args {
|
||||||
match File::new(PathBuf::from(file_path), None, None) {
|
match File::new(PathBuf::from(file_path), None, None) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -96,7 +116,7 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
|
|||||||
},
|
},
|
||||||
Ok(f) => {
|
Ok(f) => {
|
||||||
if f.is_directory() && !self.options.dir_action.treat_dirs_as_files() {
|
if f.is_directory() && !self.options.dir_action.treat_dirs_as_files() {
|
||||||
match f.to_dir(self.options.should_scan_for_git()) {
|
match f.to_dir() {
|
||||||
Ok(d) => dirs.push(d),
|
Ok(d) => dirs.push(d),
|
||||||
Err(e) => writeln!(stderr(), "{:?}: {}", file_path, e)?,
|
Err(e) => writeln!(stderr(), "{:?}: {}", file_path, e)?,
|
||||||
}
|
}
|
||||||
@ -156,7 +176,7 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
|
|||||||
|
|
||||||
let mut child_dirs = Vec::new();
|
let mut child_dirs = Vec::new();
|
||||||
for child_dir in children.iter().filter(|f| f.is_directory()) {
|
for child_dir in children.iter().filter(|f| f.is_directory()) {
|
||||||
match child_dir.to_dir(false) {
|
match child_dir.to_dir() {
|
||||||
Ok(d) => child_dirs.push(d),
|
Ok(d) => child_dirs.push(d),
|
||||||
Err(e) => writeln!(stderr(), "{}: {}", child_dir.path.display(), e)?,
|
Err(e) => writeln!(stderr(), "{}: {}", child_dir.path.display(), e)?,
|
||||||
}
|
}
|
||||||
@ -192,10 +212,10 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
|
|||||||
grid::Render { files, colours, style, opts }.render(self.writer)
|
grid::Render { files, colours, style, opts }.render(self.writer)
|
||||||
}
|
}
|
||||||
Mode::Details(ref opts) => {
|
Mode::Details(ref opts) => {
|
||||||
details::Render { dir, files, colours, style, opts, filter: &self.options.filter, recurse: self.options.dir_action.recurse_options() }.render(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.writer)
|
||||||
}
|
}
|
||||||
Mode::GridDetails(ref opts) => {
|
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.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,7 @@ use std::fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::slice::Iter as SliceIter;
|
use std::slice::Iter as SliceIter;
|
||||||
|
|
||||||
use fs::feature::Git;
|
use fs::File;
|
||||||
use fs::{File, fields};
|
|
||||||
|
|
||||||
|
|
||||||
/// A **Dir** provides a cached list of the file paths in a directory that's
|
/// A **Dir** provides a cached list of the file paths in a directory that's
|
||||||
@ -20,10 +19,6 @@ pub struct Dir {
|
|||||||
|
|
||||||
/// The path that was read.
|
/// The path that was read.
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
|
||||||
/// Holds a `Git` object if scanning for Git repositories is switched on,
|
|
||||||
/// and this directory happens to contain one.
|
|
||||||
git: Option<Git>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dir {
|
impl Dir {
|
||||||
@ -36,15 +31,14 @@ impl Dir {
|
|||||||
/// The `read_dir` iterator doesn’t actually yield the `.` and `..`
|
/// The `read_dir` iterator doesn’t actually yield the `.` and `..`
|
||||||
/// entries, so if the user wants to see them, we’ll have to add them
|
/// entries, so if the user wants to see them, we’ll have to add them
|
||||||
/// ourselves after the files have been read.
|
/// ourselves after the files have been read.
|
||||||
pub fn read_dir(path: PathBuf, git: bool) -> IOResult<Dir> {
|
pub fn read_dir(path: PathBuf) -> IOResult<Dir> {
|
||||||
info!("Reading directory {:?}", &path);
|
info!("Reading directory {:?}", &path);
|
||||||
|
|
||||||
let contents: Vec<PathBuf> = try!(fs::read_dir(&path)?
|
let contents: Vec<PathBuf> = try!(fs::read_dir(&path)?
|
||||||
.map(|result| result.map(|entry| entry.path()))
|
.map(|result| result.map(|entry| entry.path()))
|
||||||
.collect());
|
.collect());
|
||||||
|
|
||||||
let git = if git { Git::scan(&path).ok() } else { None };
|
Ok(Dir { contents, path })
|
||||||
Ok(Dir { contents, path, git })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produce an iterator of IO results of trying to read all the files in
|
/// Produce an iterator of IO results of trying to read all the files in
|
||||||
@ -67,20 +61,6 @@ impl Dir {
|
|||||||
pub fn join(&self, child: &Path) -> PathBuf {
|
pub fn join(&self, child: &Path) -> PathBuf {
|
||||||
self.path.join(child)
|
self.path.join(child)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return whether there's a Git repository on or above this directory.
|
|
||||||
pub fn has_git_repo(&self) -> bool {
|
|
||||||
self.git.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a string describing the Git status of the given file.
|
|
||||||
pub fn git_status(&self, path: &Path, prefix_lookup: bool) -> fields::Git {
|
|
||||||
match (&self.git, prefix_lookup) {
|
|
||||||
(&Some(ref git), false) => git.status(path),
|
|
||||||
(&Some(ref git), true) => git.dir_status(path),
|
|
||||||
(&None, _) => fields::Git::empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,57 +1,276 @@
|
|||||||
|
//! Getting the Git status of files and directories.
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use git2;
|
use git2;
|
||||||
|
|
||||||
use fs::fields as f;
|
use fs::fields as f;
|
||||||
|
|
||||||
|
|
||||||
/// Container of Git statuses for all the files in this folder's Git repository.
|
/// A **Git cache** is assembled based on the user’s input arguments.
|
||||||
pub struct Git {
|
///
|
||||||
|
/// This uses vectors to avoid the overhead of hashing: it’s not worth it when the
|
||||||
|
/// expected number of Git repositories per exa invocation is 0 or 1...
|
||||||
|
pub struct GitCache {
|
||||||
|
|
||||||
|
/// A list of discovered Git repositories and their paths.
|
||||||
|
repos: Vec<GitRepo>,
|
||||||
|
|
||||||
|
/// Paths that we’ve confirmed do not have Git repositories underneath them.
|
||||||
|
misses: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitCache {
|
||||||
|
pub fn has_anything_for(&self, index: &Path) -> bool {
|
||||||
|
self.repos.iter().any(|e| e.has_path(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, index: &Path, prefix_lookup: bool) -> f::Git {
|
||||||
|
self.repos.iter()
|
||||||
|
.find(|e| e.has_path(index))
|
||||||
|
.map(|repo| repo.search(index, prefix_lookup))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
impl FromIterator<PathBuf> for GitCache {
|
||||||
|
fn from_iter<I: IntoIterator<Item=PathBuf>>(iter: I) -> Self {
|
||||||
|
let iter = iter.into_iter();
|
||||||
|
let mut git = GitCache {
|
||||||
|
repos: Vec::with_capacity(iter.size_hint().0),
|
||||||
|
misses: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for path in iter {
|
||||||
|
if git.misses.contains(&path) {
|
||||||
|
debug!("Skipping {:?} because it already came back Gitless", path);
|
||||||
|
}
|
||||||
|
else if git.repos.iter().any(|e| e.has_path(&path)) {
|
||||||
|
debug!("Skipping {:?} because we already queried it", path);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
match GitRepo::discover(path) {
|
||||||
|
Ok(r) => {
|
||||||
|
if let Some(mut r2) = git.repos.iter_mut().find(|e| e.has_workdir(&r.workdir)) {
|
||||||
|
debug!("Adding to existing repo (workdir matches with {:?})", r2.workdir);
|
||||||
|
r2.extra_paths.push(r.original_path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Discovered new Git repo");
|
||||||
|
git.repos.push(r);
|
||||||
|
},
|
||||||
|
Err(miss) => git.misses.push(miss),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
git
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// A **Git repository** is one we’ve discovered somewhere on the filesystem.
|
||||||
|
pub struct GitRepo {
|
||||||
|
|
||||||
|
/// The queryable contents of the repository: either a `git2` repo, or the
|
||||||
|
/// cached results from when we queried it last time.
|
||||||
|
contents: Mutex<GitContents>,
|
||||||
|
|
||||||
|
/// The working directory of this repository.
|
||||||
|
/// This is used to check whether two repositories are the same.
|
||||||
|
workdir: PathBuf,
|
||||||
|
|
||||||
|
/// The path that was originally checked to discover this repository.
|
||||||
|
/// This is as important as the extra_paths (it gets checked first), but
|
||||||
|
/// is separate to avoid having to deal with a non-empty Vec.
|
||||||
|
original_path: PathBuf,
|
||||||
|
|
||||||
|
/// Any other paths that were checked only to result in this same
|
||||||
|
/// repository.
|
||||||
|
extra_paths: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A repository’s queried state.
|
||||||
|
enum GitContents {
|
||||||
|
|
||||||
|
/// All the interesting Git stuff goes through this.
|
||||||
|
Before { repo: git2::Repository },
|
||||||
|
|
||||||
|
/// Temporary value used in `repo_to_statuses` so we can move the
|
||||||
|
/// repository out of the `Before` variant.
|
||||||
|
Processing,
|
||||||
|
|
||||||
|
/// The data we’ve extracted from the repository, but only after we’ve
|
||||||
|
/// actually done so.
|
||||||
|
After { statuses: Git }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitRepo {
|
||||||
|
|
||||||
|
/// Searches through this repository for a path (to a file or directory,
|
||||||
|
/// depending on the prefix-lookup flag) and returns its Git status.
|
||||||
|
///
|
||||||
|
/// Actually querying the `git2` repository for the mapping of paths to
|
||||||
|
/// Git statuses is only done once, and gets cached so we don't need to
|
||||||
|
/// re-query the entire repository the times after that.
|
||||||
|
///
|
||||||
|
/// The temporary `Processing` enum variant is used after the `git2`
|
||||||
|
/// repository is moved out, but before the results have been moved in!
|
||||||
|
/// See https://stackoverflow.com/q/45985827/3484614
|
||||||
|
fn search(&self, index: &Path, prefix_lookup: bool) -> f::Git {
|
||||||
|
use self::GitContents::*;
|
||||||
|
use std::mem::replace;
|
||||||
|
|
||||||
|
let mut contents = self.contents.lock().unwrap();
|
||||||
|
if let After { ref statuses } = *contents {
|
||||||
|
debug!("Git repo {:?} has been found in cache", &self.workdir);
|
||||||
|
return statuses.status(index, prefix_lookup);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Querying Git repo {:?} for the first time", &self.workdir);
|
||||||
|
let repo = replace(&mut *contents, Processing).inner_repo();
|
||||||
|
let statuses = repo_to_statuses(repo, &self.workdir);
|
||||||
|
let result = statuses.status(index, prefix_lookup);
|
||||||
|
let _processing = replace(&mut *contents, After { statuses });
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this repository has the given working directory.
|
||||||
|
fn has_workdir(&self, path: &Path) -> bool {
|
||||||
|
self.workdir == path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this repository cares about the given path at all.
|
||||||
|
fn has_path(&self, path: &Path) -> bool {
|
||||||
|
path.starts_with(&self.original_path) || self.extra_paths.iter().any(|e| path.starts_with(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Searches for a Git repository at any point above the given path.
|
||||||
|
/// Returns the original buffer if none is found.
|
||||||
|
fn discover(path: PathBuf) -> Result<GitRepo, PathBuf> {
|
||||||
|
info!("Searching for Git repository above {:?}", path);
|
||||||
|
let repo = match git2::Repository::discover(&path) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error discovering Git repositories: {:?}", e);
|
||||||
|
return Err(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match repo.workdir().map(|wd| wd.to_path_buf()) {
|
||||||
|
Some(workdir) => {
|
||||||
|
let contents = Mutex::new(GitContents::Before { repo });
|
||||||
|
Ok(GitRepo { contents, workdir, original_path: path, extra_paths: Vec::new() })
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
warn!("Repository has no workdir?");
|
||||||
|
Err(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl GitContents {
|
||||||
|
/// Assumes that the repository hasn’t been queried, and extracts it
|
||||||
|
/// (consuming the value) if it has. This is needed because the entire
|
||||||
|
/// enum variant gets replaced when a repo is queried (see above).
|
||||||
|
fn inner_repo(self) -> git2::Repository {
|
||||||
|
if let GitContents::Before { repo } = self {
|
||||||
|
repo
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
unreachable!("Tried to extract a non-Repository")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterates through a repository’s statuses, consuming it and returning the
|
||||||
|
/// mapping of files to their Git status.
|
||||||
|
/// We will have already used the working directory at this point, so it gets
|
||||||
|
/// passed in rather than deriving it from the `Repository` again.
|
||||||
|
fn repo_to_statuses(repo: git2::Repository, workdir: &Path) -> Git {
|
||||||
|
let mut statuses = Vec::new();
|
||||||
|
|
||||||
|
info!("Getting Git statuses for repo with workdir {:?}", workdir);
|
||||||
|
match repo.statuses(None) {
|
||||||
|
Ok(es) => {
|
||||||
|
for e in es.iter() {
|
||||||
|
let path = workdir.join(Path::new(e.path().unwrap()));
|
||||||
|
let elem = (path, e.status());
|
||||||
|
statuses.push(elem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => error!("Error looking up Git statuses: {:?}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Git { statuses }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The `repo.statuses` call above takes a long time. exa debug output:
|
||||||
|
//
|
||||||
|
// 20.311276 INFO:exa::fs::feature::git: Getting Git statuses for repo with workdir "/vagrant/"
|
||||||
|
// 20.799610 DEBUG:exa::output::table: Getting Git status for file "./Cargo.toml"
|
||||||
|
//
|
||||||
|
// Even inserting another logging line immediately afterwards doesn't make it
|
||||||
|
// look any faster.
|
||||||
|
|
||||||
|
|
||||||
|
/// Container of Git statuses for all the files in this folder’s Git repository.
|
||||||
|
struct Git {
|
||||||
statuses: Vec<(PathBuf, git2::Status)>,
|
statuses: Vec<(PathBuf, git2::Status)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Git {
|
impl Git {
|
||||||
|
|
||||||
/// Discover a Git repository on or above this directory, scanning it for
|
/// Get either the file or directory status for the given path.
|
||||||
/// the files' statuses if one is found.
|
/// “Prefix lookup” means that it should report an aggregate status of all
|
||||||
pub fn scan(path: &Path) -> Result<Git, git2::Error> {
|
/// paths starting with the given prefix (in other words, a directory).
|
||||||
info!("Scanning for Git repository under {:?}", path);
|
fn status(&self, index: &Path, prefix_lookup: bool) -> f::Git {
|
||||||
|
if prefix_lookup { self.dir_status(index) }
|
||||||
let repo = git2::Repository::discover(path)?;
|
else { self.file_status(index) }
|
||||||
let workdir = match repo.workdir() {
|
|
||||||
Some(w) => w,
|
|
||||||
None => return Ok(Git { statuses: vec![] }), // bare repo
|
|
||||||
};
|
|
||||||
|
|
||||||
let statuses = repo.statuses(None)?.iter()
|
|
||||||
.map(|e| (workdir.join(Path::new(e.path().unwrap())), e.status()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(Git { statuses: statuses })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the status for the file at the given path, if present.
|
/// Get the status for the file at the given path.
|
||||||
pub fn status(&self, path: &Path) -> f::Git {
|
fn file_status(&self, file: &Path) -> f::Git {
|
||||||
let status = self.statuses.iter()
|
let path = reorient(file);
|
||||||
.find(|p| p.0.as_path() == path);
|
self.statuses.iter()
|
||||||
match status {
|
.find(|p| p.0.as_path() == path)
|
||||||
Some(&(_, s)) => f::Git { staged: index_status(s), unstaged: working_tree_status(s) },
|
.map(|&(_, s)| f::Git { staged: index_status(s), unstaged: working_tree_status(s) })
|
||||||
None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified }
|
.unwrap_or_default()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the combined status for all the files whose paths begin with the
|
/// Get the combined status for all the files whose paths begin with the
|
||||||
/// path that gets passed in. This is used for getting the status of
|
/// path that gets passed in. This is used for getting the status of
|
||||||
/// directories, which don't really have an 'official' status.
|
/// directories, which don’t really have an ‘official’ status.
|
||||||
pub fn dir_status(&self, dir: &Path) -> f::Git {
|
fn dir_status(&self, dir: &Path) -> f::Git {
|
||||||
|
let path = reorient(dir);
|
||||||
let s = self.statuses.iter()
|
let s = self.statuses.iter()
|
||||||
.filter(|p| p.0.starts_with(dir))
|
.filter(|p| p.0.starts_with(&path))
|
||||||
.fold(git2::Status::empty(), |a, b| a | b.1);
|
.fold(git2::Status::empty(), |a, b| a | b.1);
|
||||||
|
|
||||||
f::Git { staged: index_status(s), unstaged: working_tree_status(s) }
|
f::Git { staged: index_status(s), unstaged: working_tree_status(s) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a path to an absolute path based on the current directory.
|
||||||
|
/// Paths need to be absolute for them to be compared properly, otherwise
|
||||||
|
/// you’d ask a repo about “./README.md” but it only knows about
|
||||||
|
/// “/vagrant/REAMDE.md”, prefixed by the workdir.
|
||||||
|
fn reorient(path: &Path) -> PathBuf {
|
||||||
|
use std::env::current_dir;
|
||||||
|
// I’m not 100% on this func tbh
|
||||||
|
match current_dir() {
|
||||||
|
Err(_) => Path::new(".").join(&path),
|
||||||
|
Ok(dir) => dir.join(&path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The character to display if the file has been modified, but not staged.
|
/// The character to display if the file has been modified, but not staged.
|
||||||
fn working_tree_status(status: git2::Status) -> f::GitStatus {
|
fn working_tree_status(status: git2::Status) -> f::GitStatus {
|
||||||
match status {
|
match status {
|
||||||
@ -64,7 +283,7 @@ fn working_tree_status(status: git2::Status) -> f::GitStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The character to display if the file has been modified, and the change
|
/// The character to display if the file has been modified and the change
|
||||||
/// has been staged.
|
/// has been staged.
|
||||||
fn index_status(status: git2::Status) -> f::GitStatus {
|
fn index_status(status: git2::Status) -> f::GitStatus {
|
||||||
match status {
|
match status {
|
||||||
|
@ -3,24 +3,39 @@ pub mod xattr;
|
|||||||
|
|
||||||
// Git support
|
// Git support
|
||||||
|
|
||||||
#[cfg(feature="git")] mod git;
|
#[cfg(feature="git")] pub mod git;
|
||||||
#[cfg(feature="git")] pub use self::git::Git;
|
|
||||||
|
|
||||||
#[cfg(not(feature="git"))] pub struct Git;
|
|
||||||
#[cfg(not(feature="git"))] use std::path::Path;
|
|
||||||
#[cfg(not(feature="git"))] use fs::fields;
|
|
||||||
|
|
||||||
#[cfg(not(feature="git"))]
|
#[cfg(not(feature="git"))]
|
||||||
impl Git {
|
pub mod git {
|
||||||
pub fn scan(_: &Path) -> Result<Git, ()> {
|
use std::iter::FromIterator;
|
||||||
Err(())
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use fs::fields;
|
||||||
|
|
||||||
|
|
||||||
|
pub struct GitCache;
|
||||||
|
|
||||||
|
impl FromIterator<PathBuf> for GitCache {
|
||||||
|
fn from_iter<I: IntoIterator<Item=PathBuf>>(_iter: I) -> Self {
|
||||||
|
GitCache
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GitCache {
|
||||||
|
pub fn get(&self, _index: &Path) -> Option<Git> {
|
||||||
|
panic!("Tried to query a Git cache, but Git support is disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Git;
|
||||||
|
|
||||||
|
impl Git {
|
||||||
pub fn status(&self, _: &Path) -> fields::Git {
|
pub fn status(&self, _: &Path) -> fields::Git {
|
||||||
panic!("Tried to access a Git repo without Git support!");
|
panic!("Tried to get a Git status, but Git support is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dir_status(&self, path: &Path) -> fields::Git {
|
pub fn dir_status(&self, path: &Path) -> fields::Git {
|
||||||
self.status(path)
|
self.status(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
//! Wrapper types for the values returned from `File`s.
|
//! Wrapper types for the values returned from `File`s.
|
||||||
//!
|
//!
|
||||||
//! The methods of `File` that return information about the entry on the
|
//! The methods of `File` that return information about the entry on the
|
||||||
@ -206,10 +207,11 @@ pub struct Git {
|
|||||||
pub unstaged: GitStatus,
|
pub unstaged: GitStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Git {
|
use std::default::Default;
|
||||||
|
impl Default for Git {
|
||||||
|
|
||||||
/// Create a Git status for a file with nothing done to it.
|
/// Create a Git status for a file with nothing done to it.
|
||||||
pub fn empty() -> Git {
|
fn default() -> Git {
|
||||||
Git { staged: GitStatus::NotModified, unstaged: GitStatus::NotModified }
|
Git { staged: GitStatus::NotModified, unstaged: GitStatus::NotModified }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ use fs::fields as f;
|
|||||||
/// start and hold on to all the information.
|
/// start and hold on to all the information.
|
||||||
pub struct File<'dir> {
|
pub struct File<'dir> {
|
||||||
|
|
||||||
/// The filename portion of this file's path, including the extension.
|
/// The filename portion of this file’s path, including the extension.
|
||||||
///
|
///
|
||||||
/// This is used to compare against certain filenames (such as checking if
|
/// This is used to compare against certain filenames (such as checking if
|
||||||
/// it’s “Makefile” or something) and to highlight only the filename in
|
/// it’s “Makefile” or something) and to highlight only the filename in
|
||||||
@ -33,26 +33,27 @@ pub struct File<'dir> {
|
|||||||
|
|
||||||
/// The path that begat this file.
|
/// The path that begat this file.
|
||||||
///
|
///
|
||||||
/// Even though the file's name is extracted, the path needs to be kept
|
/// Even though the file’s name is extracted, the path needs to be kept
|
||||||
/// around, as certain operations involve looking up the file's absolute
|
/// around, as certain operations involve looking up the file’s absolute
|
||||||
/// location (such as the Git status, or searching for compiled files).
|
/// location (such as searching for compiled files) or using its original
|
||||||
|
/// path (following a symlink).
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
|
||||||
/// A cached `metadata` call for this file.
|
/// A cached `metadata` (`stat`) call for this file.
|
||||||
///
|
///
|
||||||
/// This too is queried multiple times, and is *not* cached by the OS, as
|
/// This too is queried multiple times, and is *not* cached by the OS, as
|
||||||
/// it could easily change between invocations - but exa is so short-lived
|
/// it could easily change between invocations — but exa is so short-lived
|
||||||
/// it's better to just cache it.
|
/// it's better to just cache it.
|
||||||
pub metadata: fs::Metadata,
|
pub metadata: fs::Metadata,
|
||||||
|
|
||||||
/// A reference to the directory that contains this file, if present.
|
/// A reference to the directory that contains this file, if any.
|
||||||
///
|
///
|
||||||
/// Filenames that get passed in on the command-line directly will have no
|
/// Filenames that get passed in on the command-line directly will have no
|
||||||
/// parent directory reference - although they technically have one on the
|
/// parent directory reference — although they technically have one on the
|
||||||
/// filesystem, we'll never need to look at it, so it'll be `None`.
|
/// filesystem, we’ll never need to look at it, so it’ll be `None`.
|
||||||
/// However, *directories* that get passed in will produce files that
|
/// However, *directories* that get passed in will produce files that
|
||||||
/// contain a reference to it, which is used in certain operations (such
|
/// contain a reference to it, which is used in certain operations (such
|
||||||
/// as looking up a file's Git status).
|
/// as looking up compiled files).
|
||||||
pub parent_dir: Option<&'dir Dir>,
|
pub parent_dir: Option<&'dir Dir>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,11 +89,11 @@ impl<'dir> File<'dir> {
|
|||||||
/// Extract an extension from a file path, if one is present, in lowercase.
|
/// Extract an extension from a file path, if one is present, in lowercase.
|
||||||
///
|
///
|
||||||
/// The extension is the series of characters after the last dot. This
|
/// The extension is the series of characters after the last dot. This
|
||||||
/// deliberately counts dotfiles, so the ".git" folder has the extension "git".
|
/// deliberately counts dotfiles, so the “.git” folder has the extension “git”.
|
||||||
///
|
///
|
||||||
/// ASCII lowercasing is used because these extensions are only compared
|
/// ASCII lowercasing is used because these extensions are only compared
|
||||||
/// against a pre-compiled list of extensions which are known to only exist
|
/// against a pre-compiled list of extensions which are known to only exist
|
||||||
/// within ASCII, so it's alright.
|
/// within ASCII, so it’s alright.
|
||||||
fn ext(path: &Path) -> Option<String> {
|
fn ext(path: &Path) -> Option<String> {
|
||||||
use std::ascii::AsciiExt;
|
use std::ascii::AsciiExt;
|
||||||
|
|
||||||
@ -110,24 +111,24 @@ impl<'dir> File<'dir> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// If this file is a directory on the filesystem, then clone its
|
/// If this file is a directory on the filesystem, then clone its
|
||||||
/// `PathBuf` for use in one of our own `Dir` objects, and read a list of
|
/// `PathBuf` for use in one of our own `Dir` values, and read a list of
|
||||||
/// its contents.
|
/// its contents.
|
||||||
///
|
///
|
||||||
/// Returns an IO error upon failure, but this shouldn't be used to check
|
/// Returns an IO error upon failure, but this shouldn’t be used to check
|
||||||
/// if a `File` is a directory or not! For that, just use `is_directory()`.
|
/// if a `File` is a directory or not! For that, just use `is_directory()`.
|
||||||
pub fn to_dir(&self, scan_for_git: bool) -> IOResult<Dir> {
|
pub fn to_dir(&self) -> IOResult<Dir> {
|
||||||
Dir::read_dir(self.path.clone(), scan_for_git)
|
Dir::read_dir(self.path.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this file is a regular file on the filesystem - that is, not a
|
/// Whether this file is a regular file on the filesystem — that is, not a
|
||||||
/// directory, a link, or anything else treated specially.
|
/// directory, a link, or anything else treated specially.
|
||||||
pub fn is_file(&self) -> bool {
|
pub fn is_file(&self) -> bool {
|
||||||
self.metadata.is_file()
|
self.metadata.is_file()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this file is both a regular file *and* executable for the
|
/// Whether this file is both a regular file *and* executable for the
|
||||||
/// current user. Executable files have different semantics than
|
/// current user. An executable file has a different purpose from an
|
||||||
/// executable directories, and so should be highlighted differently.
|
/// executable directory, so they should be highlighted differently.
|
||||||
pub fn is_executable_file(&self) -> bool {
|
pub fn is_executable_file(&self) -> bool {
|
||||||
let bit = modes::USER_EXECUTE;
|
let bit = modes::USER_EXECUTE;
|
||||||
self.is_file() && (self.metadata.permissions().mode() & bit) == bit
|
self.is_file() && (self.metadata.permissions().mode() & bit) == bit
|
||||||
@ -159,7 +160,7 @@ impl<'dir> File<'dir> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Re-prefixes the path pointed to by this file, if it's a symlink, to
|
/// Re-prefixes the path pointed to by this file, if it’s a symlink, to
|
||||||
/// make it an absolute path that can be accessed from whichever
|
/// make it an absolute path that can be accessed from whichever
|
||||||
/// directory exa is being run from.
|
/// directory exa is being run from.
|
||||||
fn reorient_target_path(&self, path: &Path) -> PathBuf {
|
fn reorient_target_path(&self, path: &Path) -> PathBuf {
|
||||||
@ -190,8 +191,8 @@ impl<'dir> File<'dir> {
|
|||||||
pub fn link_target(&self) -> FileTarget<'dir> {
|
pub fn link_target(&self) -> FileTarget<'dir> {
|
||||||
|
|
||||||
// We need to be careful to treat the path actually pointed to by
|
// We need to be careful to treat the path actually pointed to by
|
||||||
// this file -- which could be absolute or relative -- to the path
|
// this file — which could be absolute or relative — to the path
|
||||||
// we actually look up and turn into a `File` -- which needs to be
|
// we actually look up and turn into a `File` — which needs to be
|
||||||
// absolute to be accessible from any directory.
|
// absolute to be accessible from any directory.
|
||||||
debug!("Reading link {:?}", &self.path);
|
debug!("Reading link {:?}", &self.path);
|
||||||
let path = match fs::read_link(&self.path) {
|
let path = match fs::read_link(&self.path) {
|
||||||
@ -216,11 +217,11 @@ impl<'dir> File<'dir> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This file's number of hard links.
|
/// This file’s number of hard links.
|
||||||
///
|
///
|
||||||
/// It also reports whether this is both a regular file, and a file with
|
/// It also reports whether this is both a regular file, and a file with
|
||||||
/// multiple links. This is important, because a file with multiple links
|
/// multiple links. This is important, because a file with multiple links
|
||||||
/// is uncommon, while you can come across directories and other types
|
/// is uncommon, while you come across directories and other types
|
||||||
/// with multiple links much more often. Thus, it should get highlighted
|
/// with multiple links much more often. Thus, it should get highlighted
|
||||||
/// more attentively.
|
/// more attentively.
|
||||||
pub fn links(&self) -> f::Links {
|
pub fn links(&self) -> f::Links {
|
||||||
@ -378,28 +379,6 @@ impl<'dir> File<'dir> {
|
|||||||
pub fn name_is_one_of(&self, choices: &[&str]) -> bool {
|
pub fn name_is_one_of(&self, choices: &[&str]) -> bool {
|
||||||
choices.contains(&&self.name[..])
|
choices.contains(&&self.name[..])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This file's Git status as two flags: one for staged changes, and the
|
|
||||||
/// other for unstaged changes.
|
|
||||||
///
|
|
||||||
/// This requires looking at the `git` field of this file's parent
|
|
||||||
/// directory, so will not work if this file has just been passed in on
|
|
||||||
/// the command line.
|
|
||||||
pub fn git_status(&self) -> f::Git {
|
|
||||||
use std::env::current_dir;
|
|
||||||
|
|
||||||
match self.parent_dir {
|
|
||||||
None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
|
|
||||||
Some(d) => {
|
|
||||||
let cwd = match current_dir() {
|
|
||||||
Err(_) => Path::new(".").join(&self.path),
|
|
||||||
Ok(dir) => dir.join(&self.path),
|
|
||||||
};
|
|
||||||
|
|
||||||
d.git_status(&cwd, self.is_directory())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ impl Options {
|
|||||||
pub fn should_scan_for_git(&self) -> bool {
|
pub fn should_scan_for_git(&self) -> bool {
|
||||||
match self.view.mode {
|
match self.view.mode {
|
||||||
Mode::Details(details::Options { table: Some(ref table), .. }) |
|
Mode::Details(details::Options { table: Some(ref table), .. }) |
|
||||||
Mode::GridDetails(grid_details::Options { details: details::Options { table: Some(ref table), .. }, .. }) => table.extra_columns.should_scan_for_git(),
|
Mode::GridDetails(grid_details::Options { details: details::Options { table: Some(ref table), .. }, .. }) => table.extra_columns.git,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,6 +69,7 @@ use ansi_term::Style;
|
|||||||
use fs::{Dir, File};
|
use fs::{Dir, File};
|
||||||
use fs::dir_action::RecurseOptions;
|
use fs::dir_action::RecurseOptions;
|
||||||
use fs::filter::FileFilter;
|
use fs::filter::FileFilter;
|
||||||
|
use fs::feature::git::GitCache;
|
||||||
use fs::feature::xattr::{Attribute, FileAttributes};
|
use fs::feature::xattr::{Attribute, FileAttributes};
|
||||||
use style::Colours;
|
use style::Colours;
|
||||||
use output::cell::TextCell;
|
use output::cell::TextCell;
|
||||||
@ -139,11 +140,17 @@ impl<'a> AsRef<File<'a>> for Egg<'a> {
|
|||||||
|
|
||||||
|
|
||||||
impl<'a> Render<'a> {
|
impl<'a> Render<'a> {
|
||||||
pub fn render<W: Write>(self, w: &mut W) -> IOResult<()> {
|
pub fn render<W: Write>(self, mut git: Option<&'a GitCache>, w: &mut W) -> IOResult<()> {
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
|
|
||||||
if let Some(ref table) = self.opts.table {
|
if let Some(ref table) = self.opts.table {
|
||||||
let mut table = Table::new(&table, self.dir, &self.colours);
|
match (git, self.dir) {
|
||||||
|
(Some(g), Some(d)) => if !g.has_anything_for(&d.path) { git = None },
|
||||||
|
(Some(g), None) => if !self.files.iter().any(|f| g.has_anything_for(&f.path)) { git = None },
|
||||||
|
(None, _) => {/* Keep Git how it is */},
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut table = Table::new(&table, git, &self.colours);
|
||||||
|
|
||||||
if self.opts.header {
|
if self.opts.header {
|
||||||
let header = table.header_row();
|
let header = table.header_row();
|
||||||
@ -151,7 +158,7 @@ impl<'a> Render<'a> {
|
|||||||
rows.push(self.render_header(header));
|
rows.push(self.render_header(header));
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is weird, but I can't find a way around it:
|
// 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
|
// https://internals.rust-lang.org/t/should-option-mut-t-implement-copy/3715/6
|
||||||
let mut table = Some(table);
|
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, TreeDepth::root());
|
||||||
@ -240,7 +247,7 @@ impl<'a> Render<'a> {
|
|||||||
|
|
||||||
if let Some(r) = self.recurse {
|
if let Some(r) = self.recurse {
|
||||||
if file.is_directory() && r.tree && !r.is_too_deep(depth.0) {
|
if file.is_directory() && r.tree && !r.is_too_deep(depth.0) {
|
||||||
match file.to_dir(false) {
|
match file.to_dir() {
|
||||||
Ok(d) => { dir = Some(d); },
|
Ok(d) => { dir = Some(d); },
|
||||||
Err(e) => { errors.push((e, None)) },
|
Err(e) => { errors.push((e, None)) },
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ use ansi_term::ANSIStrings;
|
|||||||
use term_grid as grid;
|
use term_grid as grid;
|
||||||
|
|
||||||
use fs::{Dir, File};
|
use fs::{Dir, File};
|
||||||
|
use fs::feature::git::GitCache;
|
||||||
use fs::feature::xattr::FileAttributes;
|
use fs::feature::xattr::FileAttributes;
|
||||||
use fs::filter::FileFilter;
|
use fs::filter::FileFilter;
|
||||||
|
|
||||||
@ -110,21 +111,21 @@ impl<'a> Render<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render<W: Write>(self, w: &mut W) -> IOResult<()> {
|
pub fn render<W: Write>(self, git: Option<&GitCache>, w: &mut W) -> IOResult<()> {
|
||||||
if let Some((grid, width)) = self.find_fitting_grid() {
|
if let Some((grid, width)) = self.find_fitting_grid(git) {
|
||||||
write!(w, "{}", grid.fit_into_columns(width))
|
write!(w, "{}", grid.fit_into_columns(width))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
self.give_up().render(w)
|
self.give_up().render(git, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_fitting_grid(&self) -> 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 options = self.details.table.as_ref().expect("Details table options not given!");
|
||||||
|
|
||||||
let drender = self.details();
|
let drender = self.details();
|
||||||
|
|
||||||
let (first_table, _) = self.make_table(options, &drender);
|
let (first_table, _) = self.make_table(options, git, &drender);
|
||||||
|
|
||||||
let rows = self.files.iter()
|
let rows = self.files.iter()
|
||||||
.map(|file| first_table.row_for_file(file, file_has_xattrs(file)))
|
.map(|file| first_table.row_for_file(file, file_has_xattrs(file)))
|
||||||
@ -134,12 +135,12 @@ impl<'a> Render<'a> {
|
|||||||
.map(|file| self.style.for_file(file, self.colours).paint().promote())
|
.map(|file| self.style.for_file(file, self.colours).paint().promote())
|
||||||
.collect::<Vec<TextCell>>();
|
.collect::<Vec<TextCell>>();
|
||||||
|
|
||||||
let mut last_working_table = self.make_grid(1, options, &file_names, rows.clone(), &drender);
|
let mut last_working_table = self.make_grid(1, options, git, &file_names, rows.clone(), &drender);
|
||||||
|
|
||||||
// If we can’t fit everything in a grid 100 columns wide, then
|
// If we can’t fit everything in a grid 100 columns wide, then
|
||||||
// something has gone seriously awry
|
// something has gone seriously awry
|
||||||
for column_count in 2..100 {
|
for column_count in 2..100 {
|
||||||
let grid = self.make_grid(column_count, options, &file_names, rows.clone(), &drender);
|
let grid = self.make_grid(column_count, options, git, &file_names, rows.clone(), &drender);
|
||||||
|
|
||||||
let the_grid_fits = {
|
let the_grid_fits = {
|
||||||
let d = grid.fit_into_columns(column_count);
|
let d = grid.fit_into_columns(column_count);
|
||||||
@ -166,8 +167,14 @@ impl<'a> Render<'a> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_table<'t>(&'a self, options: &'a TableOptions, drender: &DetailsRender) -> (Table<'a>, Vec<DetailsRow>) {
|
fn make_table<'t>(&'a self, options: &'a TableOptions, mut git: Option<&'a GitCache>, drender: &DetailsRender) -> (Table<'a>, Vec<DetailsRow>) {
|
||||||
let mut table = Table::new(options, self.dir, self.colours);
|
match (git, self.dir) {
|
||||||
|
(Some(g), Some(d)) => if !g.has_anything_for(&d.path) { git = None },
|
||||||
|
(Some(g), None) => if !self.files.iter().any(|f| g.has_anything_for(&f.path)) { git = None },
|
||||||
|
(None, _) => {/* Keep Git how it is */},
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut table = Table::new(options, git, self.colours);
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
|
|
||||||
if self.details.header {
|
if self.details.header {
|
||||||
@ -179,11 +186,11 @@ impl<'a> Render<'a> {
|
|||||||
(table, rows)
|
(table, rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_grid(&'a self, column_count: usize, options: &'a TableOptions, file_names: &[TextCell], rows: Vec<TableRow>, drender: &DetailsRender) -> grid::Grid {
|
fn make_grid(&'a self, column_count: usize, options: &'a TableOptions, git: Option<&GitCache>, file_names: &[TextCell], rows: Vec<TableRow>, drender: &DetailsRender) -> grid::Grid {
|
||||||
|
|
||||||
let mut tables = Vec::new();
|
let mut tables = Vec::new();
|
||||||
for _ in 0 .. column_count {
|
for _ in 0 .. column_count {
|
||||||
tables.push(self.make_table(options, drender));
|
tables.push(self.make_table(options, git, drender));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut num_cells = rows.len();
|
let mut num_cells = rows.len();
|
||||||
|
@ -13,7 +13,8 @@ use users::UsersCache;
|
|||||||
use style::Colours;
|
use style::Colours;
|
||||||
use output::cell::TextCell;
|
use output::cell::TextCell;
|
||||||
use output::time::TimeFormat;
|
use output::time::TimeFormat;
|
||||||
use fs::{File, Dir, fields as f};
|
use fs::{File, fields as f};
|
||||||
|
use fs::feature::git::GitCache;
|
||||||
|
|
||||||
|
|
||||||
/// Options for displaying a table.
|
/// Options for displaying a table.
|
||||||
@ -24,6 +25,14 @@ pub struct Options {
|
|||||||
pub extra_columns: Columns,
|
pub extra_columns: Columns,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I had to make other types derive Debug,
|
||||||
|
// and Mutex<UsersCache> is not that!
|
||||||
|
impl fmt::Debug for Options {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
write!(f, "Table({:#?})", self.extra_columns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extra columns to display in the table.
|
/// Extra columns to display in the table.
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
pub struct Columns {
|
pub struct Columns {
|
||||||
@ -36,24 +45,12 @@ pub struct Columns {
|
|||||||
pub links: bool,
|
pub links: bool,
|
||||||
pub blocks: bool,
|
pub blocks: bool,
|
||||||
pub group: bool,
|
pub group: bool,
|
||||||
pub git: bool
|
pub git: bool,
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Options {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
|
||||||
// I had to make other types derive Debug,
|
|
||||||
// and Mutex<UsersCache> is not that!
|
|
||||||
writeln!(f, "<table options>")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Columns {
|
impl Columns {
|
||||||
pub fn should_scan_for_git(&self) -> bool {
|
pub fn collect(&self, actually_enable_git: bool) -> Vec<Column> {
|
||||||
self.git
|
let mut columns = Vec::with_capacity(4);
|
||||||
}
|
|
||||||
|
|
||||||
pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
|
|
||||||
let mut columns = vec![];
|
|
||||||
|
|
||||||
if self.inode {
|
if self.inode {
|
||||||
columns.push(Column::Inode);
|
columns.push(Column::Inode);
|
||||||
@ -89,13 +86,9 @@ impl Columns {
|
|||||||
columns.push(Column::Timestamp(TimeType::Accessed));
|
columns.push(Column::Timestamp(TimeType::Accessed));
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg!(feature="git") {
|
if cfg!(feature="git") && self.git && actually_enable_git {
|
||||||
if let Some(d) = dir {
|
|
||||||
if self.should_scan_for_git() && d.has_git_repo() {
|
|
||||||
columns.push(Column::GitStatus);
|
columns.push(Column::GitStatus);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
columns
|
columns
|
||||||
}
|
}
|
||||||
@ -275,9 +268,6 @@ fn determine_time_zone() -> TZResult<TimeZone> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub struct Table<'a> {
|
pub struct Table<'a> {
|
||||||
columns: Vec<Column>,
|
columns: Vec<Column>,
|
||||||
colours: &'a Colours,
|
colours: &'a Colours,
|
||||||
@ -285,6 +275,7 @@ pub struct Table<'a> {
|
|||||||
widths: TableWidths,
|
widths: TableWidths,
|
||||||
time_format: &'a TimeFormat,
|
time_format: &'a TimeFormat,
|
||||||
size_format: SizeFormat,
|
size_format: SizeFormat,
|
||||||
|
git: Option<&'a GitCache>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -293,11 +284,12 @@ pub struct Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'f> Table<'a> {
|
impl<'a, 'f> Table<'a> {
|
||||||
pub fn new(options: &'a Options, dir: Option<&'a Dir>, colours: &'a Colours) -> Table<'a> {
|
pub fn new(options: &'a Options, git: Option<&'a GitCache>, colours: &'a Colours) -> Table<'a> {
|
||||||
let colz = options.extra_columns.for_dir(dir);
|
let columns = options.extra_columns.collect(git.is_some());
|
||||||
let widths = TableWidths::zero(colz.len());
|
let widths = TableWidths::zero(columns.len());
|
||||||
Table { colours, widths,
|
|
||||||
columns: colz,
|
Table {
|
||||||
|
colours, widths, columns, git,
|
||||||
env: &options.env,
|
env: &options.env,
|
||||||
time_format: &options.time_format,
|
time_format: &options.time_format,
|
||||||
size_format: options.size_format,
|
size_format: options.size_format,
|
||||||
@ -347,7 +339,7 @@ impl<'a, 'f> Table<'a> {
|
|||||||
Column::Blocks => file.blocks().render(self.colours),
|
Column::Blocks => file.blocks().render(self.colours),
|
||||||
Column::User => file.user().render(self.colours, &*self.env.lock_users()),
|
Column::User => file.user().render(self.colours, &*self.env.lock_users()),
|
||||||
Column::Group => file.group().render(self.colours, &*self.env.lock_users()),
|
Column::Group => file.group().render(self.colours, &*self.env.lock_users()),
|
||||||
Column::GitStatus => file.git_status().render(self.colours),
|
Column::GitStatus => self.git_status(file).render(self.colours),
|
||||||
|
|
||||||
Column::Timestamp(Modified) => file.modified_time().render(self.colours.date, &self.env.tz, &self.time_format),
|
Column::Timestamp(Modified) => file.modified_time().render(self.colours.date, &self.env.tz, &self.time_format),
|
||||||
Column::Timestamp(Created) => file.created_time() .render(self.colours.date, &self.env.tz, &self.time_format),
|
Column::Timestamp(Created) => file.created_time() .render(self.colours.date, &self.env.tz, &self.time_format),
|
||||||
@ -355,6 +347,13 @@ impl<'a, 'f> Table<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn git_status(&self, file: &File) -> f::Git {
|
||||||
|
debug!("Getting Git status for file {:?}", file.path);
|
||||||
|
self.git
|
||||||
|
.map(|g| g.get(&file.path, file.is_directory()))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render(&self, row: Row) -> TextCell {
|
pub fn render(&self, row: Row) -> TextCell {
|
||||||
let mut cell = TextCell::default();
|
let mut cell = TextCell::default();
|
||||||
|
|
||||||
|
9
xtests/git_12
Normal file
9
xtests/git_12
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/testcases/git:
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [32mNN[0m [1;34madditions[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [34mMM[0m [1;34medits[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m [1;34mmoves[0m
|
||||||
|
|
||||||
|
/testcases/git2:
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mdeeply[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mignoreds[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m [1;34mtarget[0m
|
16
xtests/git_1212
Normal file
16
xtests/git_1212
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/testcases/git/additions:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [32mN[34mM[0m edited
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m staged
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m unstaged
|
||||||
|
|
||||||
|
/testcases/git2/deeply:
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mnested[0m
|
||||||
|
|
||||||
|
/testcases/git/edits:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [34mMM[0m both
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m15[0m cassowary [34m 1 Jan 12:34[0m [34mM[38;5;244m-[0m staged
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[34mM[0m unstaged
|
||||||
|
|
||||||
|
/testcases/git2/deeply/nested:
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mdirectory[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mrepository[0m
|
9
xtests/git_1_both
Normal file
9
xtests/git_1_both
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/testcases/git/additions:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [32mN[34mM[0m edited
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m staged
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m unstaged
|
||||||
|
|
||||||
|
/testcases/git/edits:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [34mMM[0m both
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m15[0m cassowary [34m 1 Jan 12:34[0m [34mM[38;5;244m-[0m staged
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[34mM[0m unstaged
|
1
xtests/git_1_file
Normal file
1
xtests/git_1_file
Normal file
@ -0,0 +1 @@
|
|||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m21[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m [36m/testcases/git/moves/[0mthither
|
4
xtests/git_1_files
Normal file
4
xtests/git_1_files
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [32mN[34mM[0m [36m/testcases/git/additions/[0medited .[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m15[0m cassowary [34m 1 Jan 12:34[0m [34mM[38;5;244m-[0m [36m/testcases/git/edits/[0mstaged
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m [36m/testcases/git/additions/[0mstaged .[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[34mM[0m [36m/testcases/git/edits/[0munstaged
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [36m/testcases/git/additions/[0munstaged .[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m21[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m [36m/testcases/git/moves/[0mthither
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [34mMM[0m [36m/testcases/git/edits/[0mboth [1;34md[33mr[31mw[32mx[0m[33mr[38;5;244m-[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m [36m/[1;34mtestcases[0m
|
3
xtests/git_1_long
Normal file
3
xtests/git_1_long
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [32mNN[0m [1;34madditions[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [34mMM[0m [1;34medits[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m [1;34mmoves[0m
|
3
xtests/git_1_nogit
Normal file
3
xtests/git_1_nogit
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [1;34madditions[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [1;34medits[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [1;34mmoves[0m
|
16
xtests/git_1_recurse
Normal file
16
xtests/git_1_recurse
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [32mNN[0m [1;34madditions[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [34mMM[0m [1;34medits[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m [1;34mmoves[0m
|
||||||
|
|
||||||
|
/testcases/git/additions:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [32mN[34mM[0m edited
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m staged
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m unstaged
|
||||||
|
|
||||||
|
/testcases/git/edits:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [34mMM[0m both
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m15[0m cassowary [34m 1 Jan 12:34[0m [34mM[38;5;244m-[0m staged
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[34mM[0m unstaged
|
||||||
|
|
||||||
|
/testcases/git/moves:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m21[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m thither
|
11
xtests/git_1_tree
Normal file
11
xtests/git_1_tree
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [32mNN[0m [36m/testcases/[1;34mgit[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [32mNN[0m [38;5;244m├──[0m [1;34madditions[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [32mN[34mM[0m [38;5;244m│ ├──[0m edited
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m [38;5;244m│ ├──[0m staged
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [38;5;244m│ └──[0m unstaged
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [34mMM[0m [38;5;244m├──[0m [1;34medits[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [34mMM[0m [38;5;244m│ ├──[0m both
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m15[0m cassowary [34m 1 Jan 12:34[0m [34mM[38;5;244m-[0m [38;5;244m│ ├──[0m staged
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[34mM[0m [38;5;244m│ └──[0m unstaged
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m [38;5;244m└──[0m [1;34mmoves[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m21[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m [38;5;244m └──[0m thither
|
19
xtests/git_21221
Normal file
19
xtests/git_21221
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/testcases/git2/deeply/nested/directory:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m l8st
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m18[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[34mM[0m upd8d
|
||||||
|
|
||||||
|
/testcases/git/edits:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [34mMM[0m both
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m15[0m cassowary [34m 1 Jan 12:34[0m [34mM[38;5;244m-[0m staged
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m20[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[34mM[0m unstaged
|
||||||
|
|
||||||
|
/testcases/git2/target:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m another ignored file
|
||||||
|
|
||||||
|
/testcases/git2/deeply:
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mnested[0m
|
||||||
|
|
||||||
|
/testcases/git:
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [32mNN[0m [1;34madditions[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [34mMM[0m [1;34medits[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [32mN[38;5;244m-[0m [1;34mmoves[0m
|
9
xtests/git_2_all
Normal file
9
xtests/git_2_all
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/testcases/git2/deeply:
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mnested[0m
|
||||||
|
|
||||||
|
/testcases/git2/ignoreds:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [38;5;92mmusic.m4a[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m [38;5;92mmusic.mp3[0m
|
||||||
|
|
||||||
|
/testcases/git2/target:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m another ignored file
|
2
xtests/git_2_ignoreds
Normal file
2
xtests/git_2_ignoreds
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [38;5;92mmusic.m4a[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m [38;5;92mmusic.mp3[0m
|
3
xtests/git_2_long
Normal file
3
xtests/git_2_long
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mdeeply[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mignoreds[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m [1;34mtarget[0m
|
3
xtests/git_2_nogit
Normal file
3
xtests/git_2_nogit
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [1;34mdeeply[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [1;34mignoreds[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [1;34mtarget[0m
|
24
xtests/git_2_recurse
Normal file
24
xtests/git_2_recurse
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mdeeply[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mignoreds[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m [1;34mtarget[0m
|
||||||
|
|
||||||
|
/testcases/git2/deeply:
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mnested[0m
|
||||||
|
|
||||||
|
/testcases/git2/deeply/nested:
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mdirectory[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [1;34mrepository[0m
|
||||||
|
|
||||||
|
/testcases/git2/deeply/nested/directory:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m l8st
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m18[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[34mM[0m upd8d
|
||||||
|
|
||||||
|
/testcases/git2/deeply/nested/repository:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m subfile
|
||||||
|
|
||||||
|
/testcases/git2/ignoreds:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [38;5;92mmusic.m4a[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m [38;5;92mmusic.mp3[0m
|
||||||
|
|
||||||
|
/testcases/git2/target:
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m another ignored file
|
1
xtests/git_2_repository
Normal file
1
xtests/git_2_repository
Normal file
@ -0,0 +1 @@
|
|||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m subfile
|
1
xtests/git_2_target
Normal file
1
xtests/git_2_target
Normal file
@ -0,0 +1 @@
|
|||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m another ignored file
|
13
xtests/git_2_tree
Normal file
13
xtests/git_2_tree
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [36m/testcases/[1;34mgit2[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [38;5;244m├──[0m [1;34mdeeply[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [38;5;244m│ └──[0m [1;34mnested[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [38;5;244m│ ├──[0m [1;34mdirectory[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [38;5;244m│ │ ├──[0m l8st
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m18[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[34mM[0m [38;5;244m│ │ └──[0m upd8d
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [38;5;244m│ └──[0m [1;34mrepository[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m [38;5;244m│ └──[0m subfile
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [38;5;244m├──[0m [1;34mignoreds[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m [38;5;244m│ ├──[0m [38;5;92mmusic.m4a[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m [38;5;244m│ └──[0m [38;5;92mmusic.mp3[0m
|
||||||
|
[1;34md[33mr[31mw[32mx[0m[33mr[31mw[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m [38;5;244m└──[0m [1;34mtarget[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m--[0m [38;5;244m └──[0m another ignored file
|
@ -188,8 +188,36 @@ COLUMNS=80 $exa_binary --colour=never $testcases/file-names-exts | diff -q -
|
|||||||
|
|
||||||
|
|
||||||
# Git
|
# Git
|
||||||
$exa $testcases/git/additions -l --git 2>&1 | diff -q - $results/git_additions || exit 1
|
$exa $testcases/git -l --git 2>&1 | diff -q - $results/git_1_long || exit 1
|
||||||
$exa $testcases/git/edits -l --git 2>&1 | diff -q - $results/git_edits || exit 1
|
$exa $testcases/git -l 2>&1 | diff -q - $results/git_1_nogit || exit 1
|
||||||
|
$exa $testcases/git --recurse -l --git 2>&1 | diff -q - $results/git_1_recurse || exit 1
|
||||||
|
$exa $testcases/git --tree -l --git 2>&1 | diff -q - $results/git_1_tree || exit 1
|
||||||
|
$exa $testcases/git/moves/thither --tree -l --git 2>&1 | diff -q - $results/git_1_file || exit 1
|
||||||
|
$exa $testcases/git/additions -l --git 2>&1 | diff -q - $results/git_1_additions || exit 1
|
||||||
|
$exa $testcases/git/edits -l --git 2>&1 | diff -q - $results/git_1_edits || exit 1
|
||||||
|
$exa $testcases/git/{additions,edits} -l --git 2>&1 | diff -q - $results/git_1_both || exit 1
|
||||||
|
|
||||||
|
$exa $testcases/git2 -l --git 2>&1 | diff -q - $results/git_2_long || exit 1
|
||||||
|
$exa $testcases/git2 -l 2>&1 | diff -q - $results/git_2_nogit || exit 1
|
||||||
|
$exa $testcases/git2 --tree -l --git 2>&1 | diff -q - $results/git_2_tree || exit 1
|
||||||
|
$exa $testcases/git2 --recurse -l --git 2>&1 | diff -q - $results/git_2_recurse || exit 1
|
||||||
|
$exa $testcases/git2/ignoreds -l --git 2>&1 | diff -q - $results/git_2_ignoreds || exit 1
|
||||||
|
$exa $testcases/git2/target -l --git 2>&1 | diff -q - $results/git_2_target || exit 1
|
||||||
|
$exa $testcases/git2/deeply/nested/repository -l --git 2>&1 | diff -q - $results/git_2_repository || exit 1
|
||||||
|
$exa $testcases/git2/{deeply,ignoreds,target} -l --git 2>&1 | diff -q - $results/git_2_all || exit 1
|
||||||
|
|
||||||
|
COLUMNS=150 $exa $testcases/git/**/* $testcases --git --long --grid -d | diff -q - $results/git_1_files || exit 1
|
||||||
|
|
||||||
|
$exa $testcases/git $testcases/git2 --git --long | diff -q - $results/git_12 || exit 1
|
||||||
|
|
||||||
|
$exa $testcases/git/additions $testcases/git2/deeply \
|
||||||
|
$testcases/git/edits $testcases/git2/deeply/nested --git --long | diff -q - $results/git_1212 || exit 1
|
||||||
|
|
||||||
|
$exa $testcases/git2/deeply/nested/directory $testcases/git/edits \
|
||||||
|
$testcases/git2/target $testcases/git2/deeply $testcases/git --git --long | diff -q - $results/git_21221 || exit 1
|
||||||
|
|
||||||
|
$exa $testcases/files -l --git | diff -q - $results/files_l || exit 1 # no git status for dirs
|
||||||
|
COLUMNS=40 $exa $testcases/files -lG --git | diff -q - $results/files_lG_40 || exit 1 # that aren't under git
|
||||||
|
|
||||||
|
|
||||||
# Hidden files
|
# Hidden files
|
||||||
|
Loading…
Reference in New Issue
Block a user