use std::io::Read as _;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Unchecked(pub String);
pub struct Out {
pub bytes: Vec<u8>,
pub ok: bool,
pub cut: bool,
}
pub struct Git {
dir: PathBuf,
deadline: Instant,
budget: Duration,
}
const GLOBAL: &[&str] = &[
"--no-pager",
"--no-optional-locks",
"-c",
"core.fsmonitor=false",
"-c",
"core.quotePath=false",
"-c",
"log.showSignature=false",
];
impl Git {
pub fn new(dir: impl Into<PathBuf>, deadline: Instant, budget: Duration) -> Self {
Self {
dir: dir.into(),
deadline,
budget,
}
}
fn timeout(&self) -> Unchecked {
Unchecked(format!(
"git did not answer within {} ms",
self.budget.as_millis()
))
}
pub fn run(&self, args: &[&str], limit: usize) -> Result<Out, Unchecked> {
if Instant::now() >= self.deadline {
return Err(self.timeout());
}
let mut child = Command::new("git")
.args(GLOBAL)
.args(args)
.current_dir(&self.dir)
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_OPTIONAL_LOCKS", "0")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.map_err(|e| Unchecked(format!("git could not be started: {e}")))?;
let Some(stdout) = child.stdout.take() else {
let _ = child.kill();
return Err(Unchecked("git's output could not be read".to_owned()));
};
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let mut bytes = Vec::new();
let read = stdout.take(limit as u64 + 1).read_to_end(&mut bytes);
let _ = tx.send(read.map(|_| bytes));
});
let left = self.deadline.saturating_duration_since(Instant::now());
let bytes = match rx.recv_timeout(left) {
Ok(Ok(bytes)) => bytes,
Ok(Err(e)) => {
let _ = child.kill();
let _ = child.wait();
return Err(Unchecked(format!("git's output could not be read: {e}")));
}
Err(_) => {
let _ = child.kill();
let _ = child.wait();
return Err(self.timeout());
}
};
if bytes.len() > limit {
let _ = child.kill();
let _ = child.wait();
let mut bytes = bytes;
bytes.truncate(limit);
return Ok(Out {
bytes,
ok: false,
cut: true,
});
}
loop {
match child.try_wait() {
Ok(Some(status)) => {
return Ok(Out {
bytes,
ok: status.success(),
cut: false,
});
}
Ok(None) if Instant::now() < self.deadline => {
std::thread::sleep(Duration::from_millis(2));
}
Ok(None) => {
let _ = child.kill();
let _ = child.wait();
return Err(self.timeout());
}
Err(e) => return Err(Unchecked(format!("git could not be waited for: {e}"))),
}
}
}
pub fn value(&self, args: &[&str]) -> Result<Option<String>, Unchecked> {
let out = self.run(args, 64 * 1024)?;
if !out.ok {
return Ok(None);
}
let text = String::from_utf8_lossy(&out.bytes);
Ok(text
.lines()
.next()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(str::to_owned))
}
}