Browse Source

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.
Benjamin Sago 8 năm trước cách đây
mục cha
commit
265f93f7cd

+ 34 - 6
Vagrantfile

@@ -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
-        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
+        find "#{test_dir}/attributes" -exec touch {} -t #{some_date} \\;
 
 
         # 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
 
 
-
-        touch -t #{some_date} "#{test_dir}/git/"*/*
+        find "#{test_dir}/git" -exec touch {} -t #{some_date} \\;
         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.

+ 31 - 11
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)
                 }
                 }
             }
             }
         }
         }

+ 5 - 25
src/fs/dir.rs

@@ -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, fields};
+use fs::File;
 
 
 
 
 /// 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()))
-                                                 .collect());
+                                             .map(|result| result.map(|entry| entry.path()))
+                                             .collect());
 
 
-        let git = if git { Git::scan(&path).ok() } else { None };
-        Ok(Dir { contents, path, git })
+        Ok(Dir { contents, path })
     }
     }
 
 
     /// 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()
-        }
-    }
 }
 }
 
 
 
 

+ 246 - 27
src/fs/feature/git.rs

@@ -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.
-pub struct Git {
-    statuses: Vec<(PathBuf, git2::Status)>,
+/// A **Git cache** is assembled based on the user’s input arguments.
+///
+/// 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 Git {
+impl GitCache {
+    pub fn has_anything_for(&self, index: &Path) -> bool {
+        self.repos.iter().any(|e| e.has_path(index))
+    }
 
 
-    /// Discover a Git repository on or above this directory, scanning it for
-    /// the files' statuses if one is found.
-    pub fn scan(path: &Path) -> Result<Git, git2::Error> {
-        info!("Scanning for Git repository under {:?}", path);
+    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()
+    }
+}
 
 
-        let repo = git2::Repository::discover(path)?;
-        let workdir = match repo.workdir() {
-            Some(w) => w,
-            None => return Ok(Git { statuses: vec![] }),  // bare repo
+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(),
         };
         };
 
 
-        let statuses = repo.statuses(None)?.iter()
-										   .map(|e| (workdir.join(Path::new(e.path().unwrap())), e.status()))
-										   .collect();
+        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;
+                        }
 
 
-        Ok(Git { statuses: statuses })
+                        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>,
 
 
-    /// Get the status for the file at the given path, if present.
-    pub fn status(&self, path: &Path) -> f::Git {
-        let status = self.statuses.iter()
-                                  .find(|p| p.0.as_path() == path);
-        match status {
-            Some(&(_, s)) => f::Git { staged: index_status(s),           unstaged: working_tree_status(s) },
-            None          => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified }
+    /// 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)>,
+}
+
+impl Git {
+
+    /// Get either the file or directory status for the given path.
+    /// “Prefix lookup” means that it should report an aggregate status of all
+    /// paths starting with the given prefix (in other words, a directory).
+    fn status(&self, index: &Path, prefix_lookup: bool) -> f::Git {
+        if prefix_lookup { self.dir_status(index) }
+                    else { self.file_status(index) }
+    }
+
+    /// Get the status for the file at the given path.
+    fn file_status(&self, file: &Path) -> f::Git {
+        let path = reorient(file);
+        self.statuses.iter()
+            .find(|p| p.0.as_path() == path)
+            .map(|&(_, s)| f::Git { staged: index_status(s), unstaged: working_tree_status(s) })
+            .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.
-    pub fn dir_status(&self, dir: &Path) -> f::Git {
+    /// directories, which don’t really have an ‘official’ status.
+    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 {

+ 28 - 13
src/fs/feature/mod.rs

@@ -3,24 +3,39 @@ pub mod xattr;
 
 
 // Git support
 // Git support
 
 
-#[cfg(feature="git")] 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(feature="git")] pub mod git;
 
 
 #[cfg(not(feature="git"))]
 #[cfg(not(feature="git"))]
-impl Git {
-    pub fn scan(_: &Path) -> Result<Git, ()> {
-        Err(())
+pub mod git {
+    use std::iter::FromIterator;
+    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
+        }
     }
     }
 
 
-    pub fn status(&self, _: &Path) -> fields::Git {
-        panic!("Tried to access a Git repo without Git support!");
+    impl GitCache {
+        pub fn get(&self, _index: &Path) -> Option<Git> {
+            panic!("Tried to query a Git cache, but Git support is disabled")
+        }
     }
     }
 
 
-    pub fn dir_status(&self, path: &Path) -> fields::Git {
-        self.status(path)
+    pub struct Git;
+
+    impl Git {
+        pub fn status(&self, _: &Path) -> fields::Git {
+            panic!("Tried to get a Git status, but Git support is disabled")
+        }
+
+        pub fn dir_status(&self, path: &Path) -> fields::Git {
+            self.status(path)
+        }
     }
     }
 }
 }

+ 4 - 2
src/fs/fields.rs

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

+ 25 - 46
src/fs/file.rs

@@ -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 files 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
-    /// around, as certain operations involve looking up the file's absolute
-    /// location (such as the Git status, or searching for compiled files).
+    /// 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
+    /// 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
-    /// filesystem, we'll never need to look at it, so it'll be `None`.
+    /// parent directory reference  although they technically have one on the
+    /// 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 its 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 shouldnt 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> {
-        Dir::read_dir(self.path.clone(), scan_for_git)
+    pub fn to_dir(&self) -> IOResult<Dir> {
+        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
-    /// executable directories, and so should be highlighted differently.
+    /// current user. An executable file has a different purpose from an
+    /// 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 its 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
-        // we actually look up and turn into a `File` -- which needs to be
+        // this file — which could be absolute or relative — to the path
+        // 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 files 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())
-            },
-        }
-    }
 }
 }
 
 
 
 

+ 1 - 1
src/options/mod.rs

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

+ 11 - 4
src/output/details.rs

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

+ 18 - 11
src/output/grid_details.rs

@@ -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<()> {
-        if let Some((grid, width)) = self.find_fitting_grid() {
+    pub fn render<W: Write>(self, git: Option<&GitCache>, w: &mut W) -> IOResult<()> {
+        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>) {
-        let mut table = Table::new(options, self.dir, self.colours);
+    fn make_table<'t>(&'a self, options: &'a TableOptions, mut git: Option<&'a GitCache>, drender: &DetailsRender) -> (Table<'a>, Vec<DetailsRow>) {
+        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();

+ 30 - 31
src/output/table.rs

@@ -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
-}
-
-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>")
-    }
+    pub git: bool,
 }
 }
 
 
 impl Columns {
 impl Columns {
-    pub fn should_scan_for_git(&self) -> bool {
-        self.git
-    }
-
-    pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
-        let mut columns = vec![];
+    pub fn collect(&self, actually_enable_git: bool) -> Vec<Column> {
+        let mut columns = Vec::with_capacity(4);
 
 
         if self.inode {
         if self.inode {
             columns.push(Column::Inode);
             columns.push(Column::Inode);
@@ -89,12 +86,8 @@ impl Columns {
             columns.push(Column::Timestamp(TimeType::Accessed));
             columns.push(Column::Timestamp(TimeType::Accessed));
         }
         }
 
 
-        if cfg!(feature="git") {
-            if let Some(d) = dir {
-                if self.should_scan_for_git() && d.has_git_repo() {
-                    columns.push(Column::GitStatus);
-                }
-            }
+        if cfg!(feature="git") && self.git && actually_enable_git {
+            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> {
-        let colz = options.extra_columns.for_dir(dir);
-        let widths = TableWidths::zero(colz.len());
-        Table { colours, widths,
-            columns: colz,
+    pub fn new(options: &'a Options, git: Option<&'a GitCache>, colours: &'a Colours) -> Table<'a> {
+        let columns = options.extra_columns.collect(git.is_some());
+        let widths = TableWidths::zero(columns.len());
+
+        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 - 0
xtests/git_12

@@ -0,0 +1,9 @@
+/testcases/git:
+drwxrwxr-x - cassowary  1 Jan 12:34 NN additions
+drwxrwxr-x - cassowary  1 Jan 12:34 MM edits
+drwxrwxr-x - cassowary  1 Jan 12:34 N- moves
+
+/testcases/git2:
+drwxrwxr-x - cassowary  1 Jan 12:34 -N deeply
+drwxrwxr-x - cassowary  1 Jan 12:34 -N ignoreds
+drwxrwxr-x - cassowary  1 Jan 12:34 -- target

+ 16 - 0
xtests/git_1212

@@ -0,0 +1,16 @@
+/testcases/git/additions:
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 NM edited
+.rw-rw-r--  0 cassowary  1 Jan 12:34 N- staged
+.rw-rw-r--  0 cassowary  1 Jan 12:34 -N unstaged
+
+/testcases/git2/deeply:
+drwxrwxr-x - cassowary  1 Jan 12:34 -N nested
+
+/testcases/git/edits:
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 MM both
+.rw-rw-r-- 15 cassowary  1 Jan 12:34 M- staged
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 -M unstaged
+
+/testcases/git2/deeply/nested:
+drwxrwxr-x - cassowary  1 Jan 12:34 -N directory
+drwxrwxr-x - cassowary  1 Jan 12:34 -N repository

+ 0 - 0
xtests/git_additions → xtests/git_1_additions


+ 9 - 0
xtests/git_1_both

@@ -0,0 +1,9 @@
+/testcases/git/additions:
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 NM edited
+.rw-rw-r--  0 cassowary  1 Jan 12:34 N- staged
+.rw-rw-r--  0 cassowary  1 Jan 12:34 -N unstaged
+
+/testcases/git/edits:
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 MM both
+.rw-rw-r-- 15 cassowary  1 Jan 12:34 M- staged
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 -M unstaged

+ 0 - 0
xtests/git_edits → xtests/git_1_edits


+ 1 - 0
xtests/git_1_file

@@ -0,0 +1 @@
+.rw-rw-r-- 21 cassowary  1 Jan 12:34 N- /testcases/git/moves/thither

+ 4 - 0
xtests/git_1_files

@@ -0,0 +1,4 @@
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 NM /testcases/git/additions/edited      .rw-rw-r-- 15 cassowary  1 Jan 12:34 M- /testcases/git/edits/staged
+.rw-rw-r--  0 cassowary  1 Jan 12:34 N- /testcases/git/additions/staged      .rw-rw-r-- 20 cassowary  1 Jan 12:34 -M /testcases/git/edits/unstaged
+.rw-rw-r--  0 cassowary  1 Jan 12:34 -N /testcases/git/additions/unstaged    .rw-rw-r-- 21 cassowary  1 Jan 12:34 N- /testcases/git/moves/thither
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 MM /testcases/git/edits/both            drwxr-xr-x  - cassowary  1 Jan 12:34 -- /testcases

+ 3 - 0
xtests/git_1_long

@@ -0,0 +1,3 @@
+drwxrwxr-x - cassowary  1 Jan 12:34 NN additions
+drwxrwxr-x - cassowary  1 Jan 12:34 MM edits
+drwxrwxr-x - cassowary  1 Jan 12:34 N- moves

+ 3 - 0
xtests/git_1_nogit

@@ -0,0 +1,3 @@
+drwxrwxr-x - cassowary  1 Jan 12:34 additions
+drwxrwxr-x - cassowary  1 Jan 12:34 edits
+drwxrwxr-x - cassowary  1 Jan 12:34 moves

+ 16 - 0
xtests/git_1_recurse

@@ -0,0 +1,16 @@
+drwxrwxr-x - cassowary  1 Jan 12:34 NN additions
+drwxrwxr-x - cassowary  1 Jan 12:34 MM edits
+drwxrwxr-x - cassowary  1 Jan 12:34 N- moves
+
+/testcases/git/additions:
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 NM edited
+.rw-rw-r--  0 cassowary  1 Jan 12:34 N- staged
+.rw-rw-r--  0 cassowary  1 Jan 12:34 -N unstaged
+
+/testcases/git/edits:
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 MM both
+.rw-rw-r-- 15 cassowary  1 Jan 12:34 M- staged
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 -M unstaged
+
+/testcases/git/moves:
+.rw-rw-r-- 21 cassowary  1 Jan 12:34 N- thither

+ 11 - 0
xtests/git_1_tree

@@ -0,0 +1,11 @@
+drwxrwxr-x  - cassowary  1 Jan 12:34 NN /testcases/git
+drwxrwxr-x  - cassowary  1 Jan 12:34 NN ├── additions
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 NM │  ├── edited
+.rw-rw-r--  0 cassowary  1 Jan 12:34 N- │  ├── staged
+.rw-rw-r--  0 cassowary  1 Jan 12:34 -N │  └── unstaged
+drwxrwxr-x  - cassowary  1 Jan 12:34 MM ├── edits
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 MM │  ├── both
+.rw-rw-r-- 15 cassowary  1 Jan 12:34 M- │  ├── staged
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 -M │  └── unstaged
+drwxrwxr-x  - cassowary  1 Jan 12:34 N- └── moves
+.rw-rw-r-- 21 cassowary  1 Jan 12:34 N-    └── thither

+ 19 - 0
xtests/git_21221

@@ -0,0 +1,19 @@
+/testcases/git2/deeply/nested/directory:
+.rw-rw-r--  0 cassowary  1 Jan 12:34 -N l8st
+.rw-rw-r-- 18 cassowary  1 Jan 12:34 -M upd8d
+
+/testcases/git/edits:
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 MM both
+.rw-rw-r-- 15 cassowary  1 Jan 12:34 M- staged
+.rw-rw-r-- 20 cassowary  1 Jan 12:34 -M unstaged
+
+/testcases/git2/target:
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -- another ignored file
+
+/testcases/git2/deeply:
+drwxrwxr-x - cassowary  1 Jan 12:34 -N nested
+
+/testcases/git:
+drwxrwxr-x - cassowary  1 Jan 12:34 NN additions
+drwxrwxr-x - cassowary  1 Jan 12:34 MM edits
+drwxrwxr-x - cassowary  1 Jan 12:34 N- moves

+ 9 - 0
xtests/git_2_all

@@ -0,0 +1,9 @@
+/testcases/git2/deeply:
+drwxrwxr-x - cassowary  1 Jan 12:34 -N nested
+
+/testcases/git2/ignoreds:
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -N music.m4a
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -- music.mp3
+
+/testcases/git2/target:
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -- another ignored file

+ 2 - 0
xtests/git_2_ignoreds

@@ -0,0 +1,2 @@
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -N music.m4a
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -- music.mp3

+ 3 - 0
xtests/git_2_long

@@ -0,0 +1,3 @@
+drwxrwxr-x - cassowary  1 Jan 12:34 -N deeply
+drwxrwxr-x - cassowary  1 Jan 12:34 -N ignoreds
+drwxrwxr-x - cassowary  1 Jan 12:34 -- target

+ 3 - 0
xtests/git_2_nogit

@@ -0,0 +1,3 @@
+drwxrwxr-x - cassowary  1 Jan 12:34 deeply
+drwxrwxr-x - cassowary  1 Jan 12:34 ignoreds
+drwxrwxr-x - cassowary  1 Jan 12:34 target

+ 24 - 0
xtests/git_2_recurse

@@ -0,0 +1,24 @@
+drwxrwxr-x - cassowary  1 Jan 12:34 -N deeply
+drwxrwxr-x - cassowary  1 Jan 12:34 -N ignoreds
+drwxrwxr-x - cassowary  1 Jan 12:34 -- target
+
+/testcases/git2/deeply:
+drwxrwxr-x - cassowary  1 Jan 12:34 -N nested
+
+/testcases/git2/deeply/nested:
+drwxrwxr-x - cassowary  1 Jan 12:34 -N directory
+drwxrwxr-x - cassowary  1 Jan 12:34 -N repository
+
+/testcases/git2/deeply/nested/directory:
+.rw-rw-r--  0 cassowary  1 Jan 12:34 -N l8st
+.rw-rw-r-- 18 cassowary  1 Jan 12:34 -M upd8d
+
+/testcases/git2/deeply/nested/repository:
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -- subfile
+
+/testcases/git2/ignoreds:
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -N music.m4a
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -- music.mp3
+
+/testcases/git2/target:
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -- another ignored file

+ 1 - 0
xtests/git_2_repository

@@ -0,0 +1 @@
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -N subfile

+ 1 - 0
xtests/git_2_target

@@ -0,0 +1 @@
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -- another ignored file

+ 13 - 0
xtests/git_2_tree

@@ -0,0 +1,13 @@
+drwxrwxr-x  - cassowary  1 Jan 12:34 -N /testcases/git2
+drwxrwxr-x  - cassowary  1 Jan 12:34 -N ├── deeply
+drwxrwxr-x  - cassowary  1 Jan 12:34 -N │  └── nested
+drwxrwxr-x  - cassowary  1 Jan 12:34 -N │     ├── directory
+.rw-rw-r--  0 cassowary  1 Jan 12:34 -N │     │  ├── l8st
+.rw-rw-r-- 18 cassowary  1 Jan 12:34 -M │     │  └── upd8d
+drwxrwxr-x  - cassowary  1 Jan 12:34 -N │     └── repository
+.rw-rw-r--  0 cassowary  1 Jan 12:34 -- │        └── subfile
+drwxrwxr-x  - cassowary  1 Jan 12:34 -N ├── ignoreds
+.rw-rw-r--  0 cassowary  1 Jan 12:34 -N │  ├── music.m4a
+.rw-rw-r--  0 cassowary  1 Jan 12:34 -- │  └── music.mp3
+drwxrwxr-x  - cassowary  1 Jan 12:34 -- └── target
+.rw-rw-r--  0 cassowary  1 Jan 12:34 --    └── another ignored file

+ 30 - 2
xtests/run.sh

@@ -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/edits     -l --git 2>&1 | diff -q - $results/git_edits      || exit 1
+$exa $testcases/git                      -l --git 2>&1 | diff -q - $results/git_1_long       || 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