use std::ffi::OsStr;
#[cfg(target_family = "unix")]
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use log::*;
use crate::fs::fields as f;
pub struct GitCache {
repos: Vec<GitRepo>,
misses: Vec<PathBuf>,
}
impl super::VcsCache for GitCache {
fn has_anything_for(&self, path: &Path) -> bool {
self.repos.iter().any(|e| e.has_path(path))
}
fn get(&self, path: &Path, prefix_lookup: bool) -> f::VcsFileStatus {
self.repos.iter()
.find(|e| e.has_path(path))
.map(|repo| repo.search(path, prefix_lookup))
.unwrap_or_default()
}
fn header_name(&self) -> &'static str { "Git" }
}
use std::iter::FromIterator;
impl FromIterator<PathBuf> for GitCache {
fn from_iter<I>(iter: I) -> Self
where I: IntoIterator<Item=PathBuf>
{
let iter = iter.into_iter();
let mut git = Self {
repos: Vec::with_capacity(iter.size_hint().0),
misses: Vec::new(),
};
for path in iter {
if git.misses.contains(&path) {
debug!("Skipping {} because it already came back Gitless", path.display());
}
else if git.repos.iter().any(|e| e.has_path(&path)) {
debug!("Skipping {} because we already queried it", path.display());
}
else {
match GitRepo::discover(path) {
Ok(r) => {
if let Some(r2) = git.repos.iter_mut().find(|e| e.has_workdir(&r.workdir)) {
debug!("Adding to existing repo (workdir matches with {})", r2.workdir.display());
r2.extra_paths.push(r.original_path);
continue;
}
debug!("Discovered new Git repo");
git.repos.push(r);
}
Err(miss) => {
git.misses.push(miss);
}
}
}
}
git
}
}
pub struct GitRepo {
contents: Mutex<GitContents>,
workdir: PathBuf,
original_path: PathBuf,
extra_paths: Vec<PathBuf>,
}
enum GitContents {
Before {
repo: git2::Repository,
},
Processing,
After {
statuses: Git,
},
}
impl GitRepo {
fn search(&self, index: &Path, prefix_lookup: bool) -> f::Git {
use std::mem::replace;
let mut contents = self.contents.lock().unwrap();
if let GitContents::After { ref statuses } = *contents {
debug!("Git repo {} has been found in cache", self.workdir.display());
return statuses.status(index, prefix_lookup);
}
debug!("Querying Git repo {} for the first time", self.workdir.display());
let repo = replace(&mut *contents, GitContents::Processing).inner_repo();
let statuses = repo_to_statuses(&repo, &self.workdir);
let result = statuses.status(index, prefix_lookup);
let _processing = replace(&mut *contents, GitContents::After { statuses });
result
}
fn has_workdir(&self, path: &Path) -> bool {
self.workdir == path
}
fn has_path(&self, path: &Path) -> bool {
path.starts_with(&self.original_path) || self.extra_paths.iter().any(|e| path.starts_with(e))
}
fn discover(path: PathBuf) -> Result<Self, PathBuf> {
info!("Searching for Git repository above {}", path.display());
let repo = match git2::Repository::discover(&path) {
Ok(r) => r,
Err(e) => {
error!("Error discovering Git repositories: {e}");
return Err(path);
}
};
if let Some(workdir) = repo.workdir() {
let workdir = workdir.to_path_buf();
let contents = Mutex::new(GitContents::Before { repo });
Ok(Self { contents, workdir, original_path: path, extra_paths: Vec::new() })
}
else {
warn!("Repository has no workdir?");
Err(path)
}
}
}
impl GitContents {
fn inner_repo(self) -> git2::Repository {
if let Self::Before { repo } = self {
repo
}
else {
unreachable!("Tried to extract a non-Repository")
}
}
}
fn repo_to_statuses(repo: &git2::Repository, workdir: &Path) -> Git {
let mut statuses = Vec::new();
info!("Getting Git statuses for repo with workdir {}", workdir.display());
match repo.statuses(None) {
Ok(es) => {
for e in es.iter() {
#[cfg(target_family = "unix")]
let path = workdir.join(Path::new(OsStr::from_bytes(e.path_bytes())));
#[cfg(not(target_family = "unix"))]
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 }
}
struct Git {
statuses: Vec<(PathBuf, git2::Status)>,
}
impl Git {
fn status(&self, index: &Path, prefix_lookup: bool) -> f::Git {
if prefix_lookup { self.dir_status(index) }
else { self.file_status(index) }
}
fn file_status(&self, file: &Path) -> f::Git {
let path = reorient(file);
let s = self.statuses.iter()
.filter(|p| if p.1 == git2::Status::IGNORED {
path.starts_with(&p.0)
} else {
p.0 == path
})
.fold(git2::Status::empty(), |a, b| a | b.1);
let staged = index_status(s);
let unstaged = working_tree_status(s);
f::Git { staged, unstaged }
}
fn dir_status(&self, dir: &Path) -> f::Git {
let path = reorient(dir);
let s = self.statuses.iter()
.filter(|p| if p.1 == git2::Status::IGNORED {
path.starts_with(&p.0)
} else {
p.0.starts_with(&path)
})
.fold(git2::Status::empty(), |a, b| a | b.1);
let staged = index_status(s);
let unstaged = working_tree_status(s);
f::Git { staged, unstaged }
}
}
#[cfg(unix)]
fn reorient(path: &Path) -> PathBuf {
use std::env::current_dir;
let path = match current_dir() {
Err(_) => Path::new(".").join(path),
Ok(dir) => dir.join(path),
};
path.canonicalize().unwrap_or(path)
}
#[cfg(windows)]
fn reorient(path: &Path) -> PathBuf {
let unc_path = path.canonicalize().unwrap();
let normal_path = unc_path.as_os_str().to_str().unwrap().trim_left_matches("\\\\?\\");
return PathBuf::from(normal_path);
}
fn working_tree_status(status: git2::Status) -> f::GitStatus {
match status {
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,
s if s.contains(git2::Status::IGNORED) => f::GitStatus::Ignored,
s if s.contains(git2::Status::CONFLICTED) => f::GitStatus::Conflicted,
_ => f::GitStatus::NotModified,
}
}
fn index_status(status: git2::Status) -> f::GitStatus {
match status {
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,
}
}