2017-08-28 22:45:15 +00:00
|
|
|
|
//! Getting the Git status of files and directories.
|
|
|
|
|
|
Use new io + path + fs libraries (LOTS OF CHANGES)
Exa now uses the new IO, Path, and Filesystem libraries that have been out for a while now.
Unfortunately, the new libraries don't *entirely* cover the range of the old libraries just yet: in particular, to become more cross-platform, the data in `UnstableFileStat` isn't available in the Unix `MetadataExt` yet. Much of this is contained in rust-lang/rfcs#1044 (which is due to be implemented in rust-lang/rust#14711), but it's not *entirely* there yet.
As such, this commits a serious loss of functionality: no symlink viewing, no hard links or blocks, or users or groups. Also, some of the code could now be optimised. I just wanted to commit this to sort out most of the 'teething problems' of having a different path system in advance.
Here's an example problem that took ages to fix for you, just because you read this far: when I first got exa to compile, it worked mostly fine, except calling `exa` by itself didn't list the current directory. I traced where the command-line options were being generated, to where files and directories were sorted, to where the threads were spawned... and the problem turned out to be that it was using the full path as the file name, rather than just the last component, and these paths happened to begin with `.`, so it thought they were dotfiles.
2015-04-23 12:00:34 +00:00
|
|
|
|
use std::path::{Path, PathBuf};
|
2015-03-26 00:37:12 +00:00
|
|
|
|
|
|
|
|
|
use git2;
|
|
|
|
|
|
2016-04-16 17:59:25 +00:00
|
|
|
|
use fs::fields as f;
|
2015-03-26 00:37:12 +00:00
|
|
|
|
|
2015-06-08 20:33:39 +00:00
|
|
|
|
|
2017-08-28 22:45:15 +00:00
|
|
|
|
/// 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...
|
2017-08-28 17:11:38 +00:00
|
|
|
|
pub struct GitCache {
|
2017-08-28 22:45:15 +00:00
|
|
|
|
|
|
|
|
|
/// 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>,
|
2015-03-26 00:37:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-28 22:45:15 +00:00
|
|
|
|
|
|
|
|
|
/// A **Git repository** is one we’ve discovered somewhere on the filesystem.
|
2017-08-28 17:11:38 +00:00
|
|
|
|
pub struct GitRepo {
|
2017-08-28 22:45:15 +00:00
|
|
|
|
|
|
|
|
|
/// Most of the interesting Git stuff goes through this.
|
2017-08-28 17:11:38 +00:00
|
|
|
|
repo: git2::Repository,
|
2017-08-28 22:45:15 +00:00
|
|
|
|
|
|
|
|
|
/// The working directory of this repository.
|
|
|
|
|
/// This is used to check whether two repositories are the same.
|
2017-08-28 17:11:38 +00:00
|
|
|
|
workdir: PathBuf,
|
2017-08-28 22:45:15 +00:00
|
|
|
|
|
|
|
|
|
/// 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)
|
|
|
|
|
}
|
2017-08-28 17:11:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
use std::iter::FromIterator;
|
|
|
|
|
impl FromIterator<PathBuf> for GitCache {
|
|
|
|
|
fn from_iter<I: IntoIterator<Item=PathBuf>>(iter: I) -> Self {
|
|
|
|
|
let iter = iter.into_iter();
|
2017-08-28 22:45:15 +00:00
|
|
|
|
let mut git = GitCache {
|
|
|
|
|
repos: Vec::with_capacity(iter.size_hint().0),
|
|
|
|
|
misses: Vec::new(),
|
|
|
|
|
};
|
2017-08-28 17:11:38 +00:00
|
|
|
|
|
|
|
|
|
for path in iter {
|
2017-08-28 22:45:15 +00:00
|
|
|
|
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)) {
|
2017-08-28 17:40:52 +00:00
|
|
|
|
debug!("Skipping {:?} because we already queried it", path);
|
2017-08-28 17:11:38 +00:00
|
|
|
|
}
|
|
|
|
|
else {
|
2017-08-28 22:45:15 +00:00
|
|
|
|
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),
|
|
|
|
|
}
|
2017-08-28 17:11:38 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-28 22:45:15 +00:00
|
|
|
|
git
|
2017-08-28 17:11:38 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-08-26 22:53:47 +00:00
|
|
|
|
|
2017-08-28 17:40:52 +00:00
|
|
|
|
impl GitRepo {
|
2017-08-28 22:45:15 +00:00
|
|
|
|
fn discover(path: PathBuf) -> Result<GitRepo, PathBuf> {
|
2017-08-28 17:40:52 +00:00
|
|
|
|
info!("Searching for Git repository above {:?}", path);
|
2017-08-28 22:45:15 +00:00
|
|
|
|
|
|
|
|
|
let repo = match git2::Repository::discover(&path) {
|
|
|
|
|
Ok(r) => r,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("Error discovering Git repositories: {:?}", e);
|
|
|
|
|
return Err(path);
|
2017-08-28 17:40:52 +00:00
|
|
|
|
}
|
2017-08-28 22:45:15 +00:00
|
|
|
|
};
|
2017-08-28 17:40:52 +00:00
|
|
|
|
|
2017-08-28 22:45:15 +00:00
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-08-28 17:40:52 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-28 17:11:38 +00:00
|
|
|
|
impl GitCache {
|
2017-08-28 22:45:15 +00:00
|
|
|
|
|
|
|
|
|
/// Gets a repository from the cache and scans it to get all its files’ statuses.
|
2017-08-28 17:11:38 +00:00
|
|
|
|
pub fn get(&self, index: &Path) -> Option<Git> {
|
2017-08-28 22:45:15 +00:00
|
|
|
|
let repo = match self.repos.iter().find(|e| e.has_path(index)) {
|
|
|
|
|
Some(r) => r,
|
|
|
|
|
None => return None,
|
2015-03-26 00:37:12 +00:00
|
|
|
|
};
|
|
|
|
|
|
2017-08-28 17:40:52 +00:00
|
|
|
|
info!("Getting Git statuses for repo with workdir {:?}", &repo.workdir);
|
2017-08-28 17:11:38 +00:00
|
|
|
|
let iter = match repo.repo.statuses(None) {
|
|
|
|
|
Ok(es) => es,
|
2017-08-28 17:40:52 +00:00
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("Error looking up Git statuses: {:?}", e);
|
|
|
|
|
return None;
|
|
|
|
|
}
|
2017-08-28 17:11:38 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
Use new io + path + fs libraries (LOTS OF CHANGES)
Exa now uses the new IO, Path, and Filesystem libraries that have been out for a while now.
Unfortunately, the new libraries don't *entirely* cover the range of the old libraries just yet: in particular, to become more cross-platform, the data in `UnstableFileStat` isn't available in the Unix `MetadataExt` yet. Much of this is contained in rust-lang/rfcs#1044 (which is due to be implemented in rust-lang/rust#14711), but it's not *entirely* there yet.
As such, this commits a serious loss of functionality: no symlink viewing, no hard links or blocks, or users or groups. Also, some of the code could now be optimised. I just wanted to commit this to sort out most of the 'teething problems' of having a different path system in advance.
Here's an example problem that took ages to fix for you, just because you read this far: when I first got exa to compile, it worked mostly fine, except calling `exa` by itself didn't list the current directory. I traced where the command-line options were being generated, to where files and directories were sorted, to where the threads were spawned... and the problem turned out to be that it was using the full path as the file name, rather than just the last component, and these paths happened to begin with `.`, so it thought they were dotfiles.
2015-04-23 12:00:34 +00:00
|
|
|
|
|
2017-08-28 17:11:38 +00:00
|
|
|
|
Some(Git { statuses })
|
2015-03-26 00:37:12 +00:00
|
|
|
|
}
|
2017-08-28 17:11:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2017-08-28 22:45:15 +00:00
|
|
|
|
/// Container of Git statuses for all the files in this folder’s Git repository.
|
2017-08-28 17:11:38 +00:00
|
|
|
|
pub struct Git {
|
|
|
|
|
statuses: Vec<(PathBuf, git2::Status)>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Git {
|
2015-03-26 00:37:12 +00:00
|
|
|
|
|
|
|
|
|
/// Get the status for the file at the given path, if present.
|
2016-04-16 17:59:25 +00:00
|
|
|
|
pub fn status(&self, path: &Path) -> f::Git {
|
2015-03-26 00:37:12 +00:00
|
|
|
|
let status = self.statuses.iter()
|
Use new io + path + fs libraries (LOTS OF CHANGES)
Exa now uses the new IO, Path, and Filesystem libraries that have been out for a while now.
Unfortunately, the new libraries don't *entirely* cover the range of the old libraries just yet: in particular, to become more cross-platform, the data in `UnstableFileStat` isn't available in the Unix `MetadataExt` yet. Much of this is contained in rust-lang/rfcs#1044 (which is due to be implemented in rust-lang/rust#14711), but it's not *entirely* there yet.
As such, this commits a serious loss of functionality: no symlink viewing, no hard links or blocks, or users or groups. Also, some of the code could now be optimised. I just wanted to commit this to sort out most of the 'teething problems' of having a different path system in advance.
Here's an example problem that took ages to fix for you, just because you read this far: when I first got exa to compile, it worked mostly fine, except calling `exa` by itself didn't list the current directory. I traced where the command-line options were being generated, to where files and directories were sorted, to where the threads were spawned... and the problem turned out to be that it was using the full path as the file name, rather than just the last component, and these paths happened to begin with `.`, so it thought they were dotfiles.
2015-04-23 12:00:34 +00:00
|
|
|
|
.find(|p| p.0.as_path() == path);
|
2015-03-26 00:37:12 +00:00
|
|
|
|
match status {
|
2016-04-16 17:59:25 +00:00
|
|
|
|
Some(&(_, s)) => f::Git { staged: index_status(s), unstaged: working_tree_status(s) },
|
|
|
|
|
None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified }
|
2015-03-26 00:37:12 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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
|
2017-08-28 22:45:15 +00:00
|
|
|
|
/// directories, which don’t really have an ‘official’ status.
|
2016-04-16 17:59:25 +00:00
|
|
|
|
pub fn dir_status(&self, dir: &Path) -> f::Git {
|
2015-03-26 00:37:12 +00:00
|
|
|
|
let s = self.statuses.iter()
|
Use new io + path + fs libraries (LOTS OF CHANGES)
Exa now uses the new IO, Path, and Filesystem libraries that have been out for a while now.
Unfortunately, the new libraries don't *entirely* cover the range of the old libraries just yet: in particular, to become more cross-platform, the data in `UnstableFileStat` isn't available in the Unix `MetadataExt` yet. Much of this is contained in rust-lang/rfcs#1044 (which is due to be implemented in rust-lang/rust#14711), but it's not *entirely* there yet.
As such, this commits a serious loss of functionality: no symlink viewing, no hard links or blocks, or users or groups. Also, some of the code could now be optimised. I just wanted to commit this to sort out most of the 'teething problems' of having a different path system in advance.
Here's an example problem that took ages to fix for you, just because you read this far: when I first got exa to compile, it worked mostly fine, except calling `exa` by itself didn't list the current directory. I traced where the command-line options were being generated, to where files and directories were sorted, to where the threads were spawned... and the problem turned out to be that it was using the full path as the file name, rather than just the last component, and these paths happened to begin with `.`, so it thought they were dotfiles.
2015-04-23 12:00:34 +00:00
|
|
|
|
.filter(|p| p.0.starts_with(dir))
|
2015-03-26 00:37:12 +00:00
|
|
|
|
.fold(git2::Status::empty(), |a, b| a | b.1);
|
|
|
|
|
|
2016-04-16 17:59:25 +00:00
|
|
|
|
f::Git { staged: index_status(s), unstaged: working_tree_status(s) }
|
2015-03-26 00:37:12 +00:00
|
|
|
|
}
|
2015-05-11 22:28:01 +00:00
|
|
|
|
}
|
2015-03-26 00:37:12 +00:00
|
|
|
|
|
2015-05-11 22:28:01 +00:00
|
|
|
|
/// The character to display if the file has been modified, but not staged.
|
2016-04-16 17:59:25 +00:00
|
|
|
|
fn working_tree_status(status: git2::Status) -> f::GitStatus {
|
2015-05-11 22:28:01 +00:00
|
|
|
|
match status {
|
2016-04-16 17:59:25 +00:00
|
|
|
|
s if s.contains(git2::STATUS_WT_NEW) => f::GitStatus::New,
|
|
|
|
|
s if s.contains(git2::STATUS_WT_MODIFIED) => f::GitStatus::Modified,
|
|
|
|
|
s if s.contains(git2::STATUS_WT_DELETED) => f::GitStatus::Deleted,
|
|
|
|
|
s if s.contains(git2::STATUS_WT_RENAMED) => f::GitStatus::Renamed,
|
|
|
|
|
s if s.contains(git2::STATUS_WT_TYPECHANGE) => f::GitStatus::TypeChange,
|
|
|
|
|
_ => f::GitStatus::NotModified,
|
2015-03-26 00:37:12 +00:00
|
|
|
|
}
|
2015-05-11 22:28:01 +00:00
|
|
|
|
}
|
2015-03-26 00:37:12 +00:00
|
|
|
|
|
2017-08-28 22:45:15 +00:00
|
|
|
|
/// The character to display if the file has been modified and the change
|
2015-05-11 22:28:01 +00:00
|
|
|
|
/// has been staged.
|
2016-04-16 17:59:25 +00:00
|
|
|
|
fn index_status(status: git2::Status) -> f::GitStatus {
|
2015-05-11 22:28:01 +00:00
|
|
|
|
match status {
|
2016-04-16 17:59:25 +00:00
|
|
|
|
s if s.contains(git2::STATUS_INDEX_NEW) => f::GitStatus::New,
|
|
|
|
|
s if s.contains(git2::STATUS_INDEX_MODIFIED) => f::GitStatus::Modified,
|
|
|
|
|
s if s.contains(git2::STATUS_INDEX_DELETED) => f::GitStatus::Deleted,
|
|
|
|
|
s if s.contains(git2::STATUS_INDEX_RENAMED) => f::GitStatus::Renamed,
|
|
|
|
|
s if s.contains(git2::STATUS_INDEX_TYPECHANGE) => f::GitStatus::TypeChange,
|
|
|
|
|
_ => f::GitStatus::NotModified,
|
2015-03-26 00:37:12 +00:00
|
|
|
|
}
|
|
|
|
|
}
|