use std::collections::{HashMap, HashSet};
use std::path::Path;
use crate::error::Error;
use crate::info::FileGitInfo;
use crate::parse::{parse_log, rel_under, set_from, split_nul};
use crate::runner::run_git_sync;
pub(crate) const ARGS_TOPLEVEL: &[&str] = &["rev-parse", "--show-toplevel"];
pub(crate) const ARGS_HEAD: &[&str] = &["rev-parse", "HEAD"];
pub(crate) const ARGS_LS_TRACKED: &[&str] = &["ls-files", "-z"];
pub(crate) const ARGS_LS_IGNORED: &[&str] = &[
"ls-files",
"--others",
"--ignored",
"--exclude-standard",
"-z",
];
pub(crate) const ARGS_LOG: &[&str] = &[
"-c",
"core.quotePath=false",
"log",
"--name-only",
"--no-renames",
"--format=COMMIT\t%H\t%at\t%an\t%s",
"HEAD",
];
#[derive(Debug)]
pub struct Cache {
repo_root: String,
repo_root_alt: Option<String>,
head_sha: String,
files: HashMap<String, FileGitInfo>,
tracked: HashSet<String>,
ignored: HashSet<String>,
}
impl Cache {
pub fn new(root: impl AsRef<Path>) -> Result<Option<Cache>, Error> {
let root = root.as_ref();
let repo_root = match run_git_sync(root, ARGS_TOPLEVEL) {
Ok(out) => trim_utf8(&out),
Err(_) => return Ok(None),
};
if repo_root.is_empty() {
return Ok(None);
}
let canonical = Path::new(&repo_root);
let repo_root_alt = alt_root(root, &repo_root);
let head_sha = match run_git_sync(canonical, ARGS_HEAD) {
Ok(out) => trim_utf8(&out),
Err(_) => {
let tracked = match run_git_sync(canonical, ARGS_LS_TRACKED) {
Ok(out) => out,
Err(_) => return Ok(None),
};
let ignored = run_git_sync(canonical, ARGS_LS_IGNORED).unwrap_or_default();
return Ok(Some(assemble_empty(repo_root, tracked, ignored)));
}
};
let tracked = run_git_sync(canonical, ARGS_LS_TRACKED)?;
let ignored = run_git_sync(canonical, ARGS_LS_IGNORED)?;
let log = run_git_sync(canonical, ARGS_LOG)?;
Ok(Some(assemble(
repo_root,
repo_root_alt,
head_sha,
tracked,
ignored,
log,
)))
}
pub fn repo_root(&self) -> &str {
&self.repo_root
}
pub fn head_sha(&self) -> &str {
&self.head_sha
}
pub fn lookup(&self, abs_path: impl AsRef<Path>) -> Option<&FileGitInfo> {
let rel = self.to_rel(abs_path.as_ref())?;
self.files.get(&rel)
}
pub fn is_tracked(&self, abs_path: impl AsRef<Path>) -> bool {
self.to_rel(abs_path.as_ref())
.is_some_and(|rel| self.tracked.contains(&rel))
}
pub fn is_ignored(&self, abs_path: impl AsRef<Path>) -> bool {
self.to_rel(abs_path.as_ref())
.is_some_and(|rel| self.ignored.contains(&rel))
}
fn to_rel(&self, abs_path: &Path) -> Option<String> {
if abs_path.as_os_str().is_empty() {
return None;
}
if let Some(rel) = rel_under(Path::new(&self.repo_root), abs_path) {
return Some(rel);
}
if let Some(alt) = &self.repo_root_alt {
if let Some(rel) = rel_under(Path::new(alt), abs_path) {
return Some(rel);
}
}
None
}
}
pub(crate) fn trim_utf8(out: &[u8]) -> String {
String::from_utf8_lossy(out).trim().to_string()
}
pub(crate) fn alt_root(user_root: &Path, canonical: &str) -> Option<String> {
let abs = std::path::absolute(user_root).ok()?;
let abs = abs.to_string_lossy().into_owned();
if abs == canonical {
None
} else {
Some(abs)
}
}
pub(crate) fn assemble(
repo_root: String,
repo_root_alt: Option<String>,
head_sha: String,
tracked_out: Vec<u8>,
ignored_out: Vec<u8>,
log_out: Vec<u8>,
) -> Cache {
Cache {
repo_root,
repo_root_alt,
head_sha,
files: parse_log(&String::from_utf8_lossy(&log_out)),
tracked: set_from(split_nul(&tracked_out)),
ignored: set_from(split_nul(&ignored_out)),
}
}
pub(crate) fn assemble_empty(
repo_root: String,
tracked_out: Vec<u8>,
ignored_out: Vec<u8>,
) -> Cache {
Cache {
repo_root,
repo_root_alt: None,
head_sha: String::new(),
files: HashMap::new(),
tracked: set_from(split_nul(&tracked_out)),
ignored: set_from(split_nul(&ignored_out)),
}
}