use crate::git;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
pub struct Held {
path: PathBuf,
}
impl Drop for Held {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
fn base(path: &str) -> &str {
path.rsplit('/').next().unwrap_or(path)
}
fn mine() -> String {
std::env::current_exe().map_or_else(
|_| "git-agent-verdict".to_string(),
|p| base(&p.to_string_lossy()).to_string(),
)
}
fn running(pid: &str, exe: &str) -> bool {
let Ok(out) = std::process::Command::new("ps")
.args(["-p", pid, "-o", "args="])
.output()
else {
return true;
};
if !out.status.success() {
return false;
}
let text = String::from_utf8_lossy(&out.stdout);
text.split_whitespace()
.next()
.is_some_and(|argv0| base(argv0) == exe)
}
fn holder(text: &str) -> Option<(String, u64, String)> {
let mut fields = text.trim().split('\t');
let pid = fields.next()?.to_string();
let since = fields.next()?.parse().ok()?;
let exe = fields.next()?.to_string();
Some((pid, since, exe))
}
fn refusal(text: &str, path: &std::path::Path) -> Option<String> {
let (pid, since, exe) = holder(text)?;
if !running(&pid, &exe) {
return None;
}
let held = now().saturating_sub(since);
Some(format!(
"another attest is already running in this repo — pid {pid}, {held}s so far.\nOne at a time: two runs review the same gate, pay for it twice, and the second to finish drops the first's verdict.\nWait for it, or kill it. The claim is {}.",
path.display()
))
}
pub fn take() -> Result<Held, String> {
let path = git::git_path("agent-verdict.lock")?;
let claim = format!("{}\t{}\t{}", std::process::id(), now(), mine());
for _ in 0..2 {
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
Ok(mut file) => {
use std::io::Write;
let _ = file.write_all(claim.as_bytes());
return Ok(Held { path });
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
let text = std::fs::read_to_string(&path).unwrap_or_default();
if let Some(said) = refusal(&text, &path) {
return Err(said);
}
let _ = std::fs::remove_file(&path);
}
Err(e) => return Err(format!("cannot claim {}: {e}", path.display())),
}
}
Err(format!(
"cannot claim {}: it is being taken and dropped faster than this run can read it",
path.display()
))
}