use std::io::Read;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
const GIT_TIMEOUT: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GitContext {
pub root: Option<String>,
pub branch: Option<String>,
pub remote: Option<String>,
}
impl GitContext {
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.root.is_none() && self.branch.is_none() && self.remote.is_none()
}
}
pub fn detect(cwd: &Path) -> GitContext {
let root = run_git(cwd, &["rev-parse", "--show-toplevel"]);
if root.is_none() {
return GitContext::default();
}
GitContext {
root,
branch: run_git(cwd, &["branch", "--show-current"]),
remote: run_git(cwd, &["config", "--get", "remote.origin.url"]),
}
}
fn run_git(cwd: &Path, args: &[&str]) -> Option<String> {
let mut child = Command::new("git")
.arg("-C")
.arg(cwd)
.args(args)
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_OPTIONAL_LOCKS", "0")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?;
let deadline = Instant::now() + GIT_TIMEOUT;
loop {
match child.try_wait() {
Ok(Some(status)) => {
if !status.success() {
return None;
}
let mut out = String::new();
child.stdout.take()?.read_to_string(&mut out).ok()?;
let trimmed = out.trim();
return if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
};
}
Ok(None) => {
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
return None;
}
std::thread::sleep(Duration::from_millis(10));
}
Err(_) => return None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn hors_depot_renvoie_un_contexte_vide() {
let ctx = detect(&PathBuf::from("/"));
assert!(ctx.is_empty(), "racine systeme ne doit pas etre un depot");
}
#[test]
fn chemin_inexistant_ne_panique_pas() {
let ctx = detect(&PathBuf::from("/chemin/qui/n/existe/pas/12345"));
assert!(ctx.is_empty());
}
}