|
|
@@ -1,4 +1,5 @@
|
|
|
-use std::collections::HashMap;
|
|
|
+//! Getting the Git status of files and directories.
|
|
|
+
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
use git2;
|
|
|
@@ -6,53 +7,116 @@ use git2;
|
|
|
use fs::fields as f;
|
|
|
|
|
|
|
|
|
+/// 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 {
|
|
|
- repos: HashMap<PathBuf, Option<GitRepo>>,
|
|
|
+
|
|
|
+ /// 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>,
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+/// 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
|
|
|
+ }
|
|
|
+
|
|
|
+ fn has_path(&self, path: &Path) -> bool {
|
|
|
+ self.original_path == path || self.extra_paths.iter().any(|e| e == path)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
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 repos = HashMap::with_capacity(iter.size_hint().0);
|
|
|
+ let mut git = GitCache {
|
|
|
+ repos: Vec::with_capacity(iter.size_hint().0),
|
|
|
+ misses: Vec::new(),
|
|
|
+ };
|
|
|
|
|
|
for path in iter {
|
|
|
- if repos.contains_key(&path) {
|
|
|
+ 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 {
|
|
|
- let repo = GitRepo::discover(&path);
|
|
|
- let _ = repos.insert(path, repo);
|
|
|
+ match GitRepo::discover(path) {
|
|
|
+ Ok(r) => {
|
|
|
+ if let Some(mut r2) = git.repos.iter_mut().find(|e| e.has_workdir(&r.workdir)) {
|
|
|
+ debug!("Adding to existing repo (workdir matches with {:?})", r2.workdir);
|
|
|
+ r2.extra_paths.push(r.original_path);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ debug!("Creating new repo in cache");
|
|
|
+ git.repos.push(r);
|
|
|
+ },
|
|
|
+ Err(miss) => git.misses.push(miss),
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- GitCache { repos }
|
|
|
+ git
|
|
|
}
|
|
|
}
|
|
|
|
|
|
impl GitRepo {
|
|
|
- fn discover(path: &Path) -> Option<GitRepo> {
|
|
|
+ fn discover(path: PathBuf) -> Result<GitRepo, PathBuf> {
|
|
|
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 });
|
|
|
+
|
|
|
+ let repo = match git2::Repository::discover(&path) {
|
|
|
+ Ok(r) => r,
|
|
|
+ Err(e) => {
|
|
|
+ error!("Error discovering Git repositories: {:?}", e);
|
|
|
+ return Err(path);
|
|
|
}
|
|
|
- }
|
|
|
+ };
|
|
|
|
|
|
- None
|
|
|
+ match repo.workdir().map(|wd| wd.to_path_buf()) {
|
|
|
+ Some(workdir) => Ok(GitRepo { repo, workdir, original_path: path, extra_paths: Vec::new() }),
|
|
|
+ None => {
|
|
|
+ warn!("Repository has no workdir?");
|
|
|
+ Err(path)
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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[index] {
|
|
|
- Some(ref r) => r,
|
|
|
- None => return None,
|
|
|
+ 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);
|
|
|
@@ -77,7 +141,7 @@ impl GitCache {
|
|
|
}
|
|
|
|
|
|
|
|
|
-/// Container of Git statuses for all the files in this folder's Git repository.
|
|
|
+/// Container of Git statuses for all the files in this folder’s Git repository.
|
|
|
pub struct Git {
|
|
|
statuses: Vec<(PathBuf, git2::Status)>,
|
|
|
}
|
|
|
@@ -96,7 +160,7 @@ impl Git {
|
|
|
|
|
|
/// 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.
|
|
|
+ /// directories, which don’t really have an ‘official’ status.
|
|
|
pub fn dir_status(&self, dir: &Path) -> f::Git {
|
|
|
let s = self.statuses.iter()
|
|
|
.filter(|p| p.0.starts_with(dir))
|
|
|
@@ -118,7 +182,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.
|
|
|
fn index_status(status: git2::Status) -> f::GitStatus {
|
|
|
match status {
|