use crate::env;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone)]
pub struct GitInvoker {
program: PathBuf,
env_path: Option<OsString>,
}
impl Default for GitInvoker {
fn default() -> Self {
GitInvoker {
program: PathBuf::from("git"),
env_path: None,
}
}
}
impl GitInvoker {
pub fn resolved(program: PathBuf, env_path: OsString) -> GitInvoker {
GitInvoker {
program,
env_path: Some(env_path),
}
}
fn cmd(&self, dir: &Path) -> Command {
let mut cmd = Command::new(&self.program);
cmd.arg("-C").arg(dir);
if let Some(p) = &self.env_path {
cmd.env("PATH", p);
}
cmd.env(env::DISABLED, "1");
cmd
}
pub fn detect_repo_root(&self, cwd: &Path) -> PathBuf {
if let Ok(out) = self
.cmd(cwd)
.args(["rev-parse", "--show-toplevel"])
.output()
{
if out.status.success() {
let text = String::from_utf8_lossy(&out.stdout);
let trimmed = text.trim();
if !trimmed.is_empty() {
return PathBuf::from(trimmed);
}
}
}
std::fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf())
}
pub fn head(&self, repo_root: &Path) -> Option<String> {
let out = self
.cmd(repo_root)
.args(["rev-parse", "HEAD"])
.output()
.ok()?;
if out.status.success() {
let text = String::from_utf8_lossy(&out.stdout);
let trimmed = text.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
None
}
pub fn worktree_hash(&self, repo_root: &Path) -> Option<String> {
let out = self
.cmd(repo_root)
.args(["status", "--porcelain=v1", "-z"])
.output()
.ok()?;
if out.status.success() {
return Some(crate::util::sha256_hex(&out.stdout));
}
None
}
fn state(&self, repo_root: &Path) -> GitState {
GitState {
head: self.head(repo_root),
worktree_hash: self.worktree_hash(repo_root),
}
}
}
pub struct GitState {
pub head: Option<String>,
pub worktree_hash: Option<String>,
}
pub enum GitStatePrefetch {
Spawned(std::thread::JoinHandle<GitState>),
Inline {
invoker: GitInvoker,
repo_root: PathBuf,
},
}
impl GitStatePrefetch {
pub fn spawn(invoker: GitInvoker, repo_root: PathBuf) -> GitStatePrefetch {
let thread_invoker = invoker.clone();
let thread_root = repo_root.clone();
match std::thread::Builder::new()
.name("dejavu-git-state".to_string())
.spawn(move || thread_invoker.state(&thread_root))
{
Ok(handle) => GitStatePrefetch::Spawned(handle),
Err(_) => GitStatePrefetch::Inline { invoker, repo_root },
}
}
pub fn join(self) -> GitState {
match self {
GitStatePrefetch::Spawned(handle) => handle.join().unwrap_or(GitState {
head: None,
worktree_hash: None,
}),
GitStatePrefetch::Inline { invoker, repo_root } => invoker.state(&repo_root),
}
}
}
pub fn detect_repo_root(cwd: &Path) -> PathBuf {
GitInvoker::default().detect_repo_root(cwd)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prefetch_joins_to_nones_outside_a_repo() {
let tmp = tempfile::tempdir().unwrap();
let state = GitStatePrefetch::spawn(GitInvoker::default(), tmp.path().to_path_buf()).join();
assert!(state.head.is_none());
assert!(state.worktree_hash.is_none());
}
#[test]
fn prefetch_captures_state_in_a_repo() {
let tmp = tempfile::tempdir().unwrap();
let run = |args: &[&str]| {
std::process::Command::new("git")
.arg("-C")
.arg(tmp.path())
.args(args)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.unwrap()
};
run(&["init", "-q", "."]);
run(&["config", "user.email", "t@t"]);
run(&["config", "user.name", "t"]);
std::fs::write(tmp.path().join("f.txt"), "x").unwrap();
run(&["add", "-A"]);
run(&["-c", "commit.gpgsign=false", "commit", "-qm", "init"]);
let state = GitStatePrefetch::spawn(GitInvoker::default(), tmp.path().to_path_buf()).join();
assert!(state.head.is_some(), "HEAD should exist after a commit");
assert!(state.worktree_hash.is_some());
std::fs::write(tmp.path().join("g.txt"), "y").unwrap();
let dirty = GitStatePrefetch::spawn(GitInvoker::default(), tmp.path().to_path_buf()).join();
assert_ne!(state.worktree_hash, dirty.worktree_hash);
}
}