From 040dbb2414a9aae243f053bb7927a1d92b4684e8 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Mon, 28 Aug 2017 18:11:38 +0100 Subject: [PATCH] Use a global Git cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a cache for Git repositories based on the path being queried. Its only immediate effect is that when you query the same directory twice (such as /testcases/git /testcases/git), it won’t need to check that the second one is a Git directory the second time. So, a minuscule optimisation for something you’d never do anyway? Wrong! It’s going to let us combine multiple entries over the same repository later, letting us use --tree and --recurse, because now Git scanning is behind a factory. --- src/exa.rs | 20 +++++++++- src/fs/dir.rs | 10 +++-- src/fs/feature/git.rs | 86 ++++++++++++++++++++++++++++++++++--------- src/fs/feature/mod.rs | 41 ++++++++++++++------- src/fs/file.rs | 6 +-- src/fs/mod.rs | 2 +- src/output/details.rs | 3 +- 7 files changed, 127 insertions(+), 41 deletions(-) diff --git a/src/exa.rs b/src/exa.rs index a87fefd..07f8244 100644 --- a/src/exa.rs +++ b/src/exa.rs @@ -30,6 +30,7 @@ use std::path::{Component, PathBuf}; use ansi_term::{ANSIStrings, Style}; use fs::{Dir, File}; +use fs::feature::git::GitCache; use options::{Options, Vars}; pub use options::Misfire; use output::{escape, lines, grid, grid_details, details, View, Mode}; @@ -79,6 +80,8 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { } pub fn run(&mut self) -> IOResult { + use fs::DirOptions; + let mut files = Vec::new(); let mut dirs = Vec::new(); let mut exit_status = 0; @@ -88,6 +91,8 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { self.args = vec![ OsStr::new(".") ]; } + let git = self.git_options(&*self.args); + for file_path in &self.args { match File::new(PathBuf::from(file_path), None, None) { Err(e) => { @@ -96,7 +101,7 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { }, Ok(f) => { 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(DirOptions { git: git.as_ref() }) { Ok(d) => dirs.push(d), Err(e) => writeln!(stderr(), "{:?}: {}", file_path, e)?, } @@ -121,7 +126,18 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { self.print_dirs(dirs, no_files, is_only_dir, exit_status) } + fn git_options(&self, args: &[&OsStr]) -> Option { + if self.options.should_scan_for_git() { + Some(args.iter().map(|os| PathBuf::from(os)).collect()) + } + else { + None + } + } + fn print_dirs(&mut self, dir_files: Vec, mut first: bool, is_only_dir: bool, exit_status: i32) -> IOResult { + use fs::DirOptions; + for dir in dir_files { // Put a gap between directories, or between the list of files and @@ -156,7 +172,7 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { let mut child_dirs = Vec::new(); for child_dir in children.iter().filter(|f| f.is_directory()) { - match child_dir.to_dir(false) { + match child_dir.to_dir(DirOptions { git: None }) { Ok(d) => child_dirs.push(d), Err(e) => writeln!(stderr(), "{}: {}", child_dir.path.display(), e)?, } diff --git a/src/fs/dir.rs b/src/fs/dir.rs index 77345d9..d1abffb 100644 --- a/src/fs/dir.rs +++ b/src/fs/dir.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::{Path, PathBuf}; use std::slice::Iter as SliceIter; -use fs::feature::Git; +use fs::feature::git::{Git, GitCache}; use fs::{File, fields}; @@ -26,6 +26,10 @@ pub struct Dir { git: Option, } +pub struct DirOptions<'exa> { + pub git: Option<&'exa GitCache> +} + impl Dir { /// Create a new Dir object filled with all the files in the directory @@ -36,14 +40,14 @@ impl Dir { /// 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 /// ourselves after the files have been read. - pub fn read_dir(path: PathBuf, git: bool) -> IOResult { + pub fn read_dir(path: PathBuf, options: DirOptions) -> IOResult { info!("Reading directory {:?}", &path); let contents: Vec = try!(fs::read_dir(&path)? .map(|result| result.map(|entry| entry.path())) .collect()); - let git = if git { Git::scan(&path).ok() } else { None }; + let git = options.git.and_then(|cache| cache.get(&path)); Ok(Dir { contents, path, git }) } diff --git a/src/fs/feature/git.rs b/src/fs/feature/git.rs index 0bfc24e..011605d 100644 --- a/src/fs/feature/git.rs +++ b/src/fs/feature/git.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::{Path, PathBuf}; use git2; @@ -5,6 +6,73 @@ use git2; use fs::fields as f; +pub struct GitCache { + repos: HashMap>, +} + +pub struct GitRepo { + repo: git2::Repository, + workdir: PathBuf, +} + +impl GitRepo { + fn discover(path: &Path) -> Option { + info!("Searching for Git repository above {:?}", path); + if let Ok(repo) = git2::Repository::discover(&path) { + if let Some(workdir) = repo.workdir().map(|wd| wd.to_path_buf()) { + return Some(GitRepo { repo, workdir }); + } + } + + None + } +} + +use std::iter::FromIterator; +impl FromIterator for GitCache { + fn from_iter>(iter: I) -> Self { + let iter = iter.into_iter(); + let mut repos = HashMap::with_capacity(iter.size_hint().0); + + for path in iter { + if repos.contains_key(&path) { + debug!("Skipping {:?} because we already queried it", path); + } + else { + let repo = GitRepo::discover(&path); + let _ = repos.insert(path, repo); + } + } + + GitCache { repos } + } +} + +impl GitCache { + pub fn get(&self, index: &Path) -> Option { + let repo = match self.repos[index] { + Some(ref r) => r, + None => return None, + }; + + let iter = match repo.repo.statuses(None) { + Ok(es) => es, + Err(_) => return None, + }; + + let mut statuses = Vec::new(); + + for e in iter.iter() { + let path = repo.workdir.join(Path::new(e.path().unwrap())); + let elem = (path, e.status()); + statuses.push(elem); + } + + Some(Git { statuses }) + } +} + + /// Container of Git statuses for all the files in this folder's Git repository. pub struct Git { statuses: Vec<(PathBuf, git2::Status)>, @@ -12,24 +80,6 @@ pub struct Git { impl Git { - /// 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 { - info!("Scanning for Git repository under {:?}", path); - - let repo = git2::Repository::discover(path)?; - 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. pub fn status(&self, path: &Path) -> f::Git { let status = self.statuses.iter() diff --git a/src/fs/feature/mod.rs b/src/fs/feature/mod.rs index 72db8e1..e2f5d0c 100644 --- a/src/fs/feature/mod.rs +++ b/src/fs/feature/mod.rs @@ -3,24 +3,39 @@ pub mod xattr; // 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"))] -impl Git { - pub fn scan(_: &Path) -> Result { - Err(()) +pub mod git { + use std::iter::FromIterator; + use std::path::{Path, PathBuf}; + + use fs::fields; + + + pub struct GitCache; + + impl FromIterator for GitCache { + fn from_iter>(_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 { + 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) + } } } diff --git a/src/fs/file.rs b/src/fs/file.rs index c07f879..bebe690 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -6,7 +6,7 @@ use std::io::Result as IOResult; use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt}; use std::path::{Path, PathBuf}; -use fs::dir::Dir; +use fs::dir::{Dir, DirOptions}; use fs::fields as f; @@ -115,8 +115,8 @@ impl<'dir> File<'dir> { /// /// 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()`. - pub fn to_dir(&self, scan_for_git: bool) -> IOResult { - Dir::read_dir(self.path.clone(), scan_for_git) + pub fn to_dir(&self, options: DirOptions) -> IOResult { + Dir::read_dir(self.path.clone(), options) } /// Whether this file is a regular file on the filesystem - that is, not a diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 3275ccf..85e6eaf 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -1,5 +1,5 @@ mod dir; -pub use self::dir::{Dir, DotFilter}; +pub use self::dir::{Dir, DirOptions, DotFilter}; mod file; pub use self::file::{File, FileTarget}; diff --git a/src/output/details.rs b/src/output/details.rs index eef49e3..d4f315f 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -178,6 +178,7 @@ impl<'a> Render<'a> { use scoped_threadpool::Pool; use std::sync::{Arc, Mutex}; use fs::feature::xattr; + use fs::DirOptions; let mut pool = Pool::new(num_cpus::get() as u32); let mut file_eggs = Vec::new(); @@ -240,7 +241,7 @@ impl<'a> Render<'a> { if let Some(r) = self.recurse { if file.is_directory() && r.tree && !r.is_too_deep(depth.0) { - match file.to_dir(false) { + match file.to_dir(DirOptions { git: None }) { Ok(d) => { dir = Some(d); }, Err(e) => { errors.push((e, None)) }, }