summaryrefslogtreecommitdiffstats
path: root/src/fs/feature/git.rs
diff options
context:
space:
mode:
authorBenjamin Sago <ogham@bsago.me>2017-09-01 19:13:47 +0100
committerBenjamin Sago <ogham@bsago.me>2017-09-01 19:13:47 +0100
commit45a807a14f617813c0b5ad9637f63dea673e9a23 (patch)
tree9f16f9e2e2c6c6bdb7e17ad73533b2008fadb89d /src/fs/feature/git.rs
parent3d4ddf8af6bd746b5d07b9ee62126fb404aa2c29 (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.rs221
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 {