diff options
author | Benjamin Sago <ogham@bsago.me> | 2017-09-01 19:13:47 +0100 |
---|---|---|
committer | Benjamin Sago <ogham@bsago.me> | 2017-09-01 19:13:47 +0100 |
commit | 45a807a14f617813c0b5ad9637f63dea673e9a23 (patch) | |
tree | 9f16f9e2e2c6c6bdb7e17ad73533b2008fadb89d /src/fs/feature/git.rs | |
parent | 3d4ddf8af6bd746b5d07b9ee62126fb404aa2c29 (diff) |
Redo Git implementation to allow --git --recurse
This is all a big commit because it took a lot more work than I thought it would! The commit basically moves Git repositories from being per-directory to living for the whole life of the program. This allows for several directories in the same repository to be listed in the same invocation; before, it would try to rediscover the repository each time! This is why two of the tests “broke”: it suddenly started working with --recurse.
The Dir type does now not use Git at all; because a Dir doesn’t have a Git, then a File doesn’t have one either, so the Git cache gets passed to the render functions which will put them in the Table to render them.
Diffstat (limited to 'src/fs/feature/git.rs')
-rw-r--r-- | src/fs/feature/git.rs | 221 |
1 files changed, 157 insertions, 64 deletions
diff --git a/src/fs/feature/git.rs b/src/fs/feature/git.rs index 75e2ef7..ffa2dc6 100644 --- a/src/fs/feature/git.rs +++ b/src/fs/feature/git.rs @@ -1,6 +1,7 @@ //! Getting the Git status of files and directories. use std::path::{Path, PathBuf}; +use std::sync::Mutex; use git2; @@ -20,34 +21,16 @@ pub struct GitCache { misses: Vec<PathBuf>, } - -/// A **Git repository** is one we’ve discovered somewhere on the filesystem. -pub struct GitRepo { - - /// Most of the interesting Git stuff goes through this. - repo: git2::Repository, - - /// 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>, -} - -impl GitRepo { - fn has_workdir(&self, path: &Path) -> bool { - self.workdir == path +impl GitCache { + pub fn has_anything_for(&self, index: &Path) -> bool { + true } - fn has_path(&self, path: &Path) -> bool { - self.original_path == path || self.extra_paths.iter().any(|e| e == 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() } } @@ -76,7 +59,7 @@ impl FromIterator<PathBuf> for GitCache { continue; } - debug!("Creating new repo in cache"); + debug!("Discovered new Git repo"); git.repos.push(r); }, Err(miss) => git.misses.push(miss), @@ -88,10 +71,89 @@ impl FromIterator<PathBuf> for GitCache { } } + + + +/// 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) => { @@ -101,7 +163,10 @@ impl GitRepo { }; match repo.workdir().map(|wd| wd.to_path_buf()) { - Some(workdir) => Ok(GitRepo { repo, workdir, original_path: path, extra_paths: Vec::new() }), + 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) @@ -110,66 +175,94 @@ impl GitRepo { } } -impl GitCache { - - /// Gets a repository from the cache and scans it to get all its files’ statuses. - pub fn get(&self, index: &Path) -> Option<Git> { - let repo = match self.repos.iter().find(|e| e.has_path(index)) { - Some(r) => r, - None => return None, - }; - - info!("Getting Git statuses for repo with workdir {:?}", &repo.workdir); - let iter = match repo.repo.statuses(None) { - Ok(es) => es, - Err(e) => { - error!("Error looking up Git statuses: {:?}", e); - 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); +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") } + } +} - Some(Git { statuses }) +/// 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 } } /// Container of Git statuses for all the files in this folder’s Git repository. -pub struct Git { +struct Git { statuses: Vec<(PathBuf, git2::Status)>, } impl Git { - /// 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 } - } + /// 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 /// 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 { + fn dir_status(&self, dir: &Path) -> f::Git { + let path = reorient(dir); 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); 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. fn working_tree_status(status: git2::Status) -> f::GitStatus { match status { |