use crate::cli::{Brief, Invocation};
use crate::git;
const LIST_ENV: &str = "GIT_AGENT_VERDICT_LIST";
pub struct Declaration {
pub gate: String,
pub read_only: bool,
pub standards: Vec<String>,
pub docs: Vec<String>,
pub rules: Vec<String>,
pub paths: Vec<String>,
pub model: Option<String>,
pub brief: Brief,
}
pub fn listing_requested() -> bool {
std::env::var_os(LIST_ENV).is_some()
}
pub fn emit_gate(inv: &Invocation) {
let mut fields = vec![inv.gate.clone()];
if inv.brief.simple {
fields.push("simple".to_string());
}
if inv.read_only {
fields.push("read-only".to_string());
}
if let Some(model) = &inv.model {
fields.push(format!("model={}", escaped(model)));
}
if let Some(path) = &inv.brief.prompt {
fields.push(format!("prompt={}", escaped(path)));
}
fields.extend(
inv.standards
.iter()
.map(|s| format!("standard={}", escaped(s))),
);
fields.extend(inv.docs.iter().map(|d| format!("doc={}", escaped(d))));
fields.extend(inv.rules.iter().map(|r| format!("rule={}", escaped(r))));
fields.extend(inv.paths.iter().map(|p| format!("path={}", escaped(p))));
println!("{}", fields.join("\t"));
}
fn escaped(text: &str) -> String {
text.replace('\\', "\\\\")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
fn unescaped(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut chars = text.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('t') => out.push('\t'),
Some(other) => out.push(other),
None => out.push('\\'),
}
}
out
}
fn read_gate(gate: &str, fields: std::str::Split<'_, char>) -> Option<Declaration> {
let mut declaration = Declaration {
gate: gate.to_string(),
read_only: false,
standards: Vec::new(),
docs: Vec::new(),
rules: Vec::new(),
paths: Vec::new(),
model: None,
brief: Brief::default(),
};
for field in fields {
if let Some(name) = field.strip_prefix("standard=") {
declaration.standards.push(unescaped(name));
} else if let Some(doc) = field.strip_prefix("doc=") {
declaration.docs.push(unescaped(doc));
} else if let Some(text) = field.strip_prefix("rule=") {
declaration.rules.push(unescaped(text));
} else if let Some(path) = field.strip_prefix("path=") {
declaration.paths.push(unescaped(path));
} else if let Some(name) = field.strip_prefix("model=") {
declaration.model = Some(unescaped(name));
} else if let Some(path) = field.strip_prefix("prompt=") {
declaration.brief.prompt = Some(unescaped(path));
} else if field == "read-only" {
declaration.read_only = true;
} else if field == "simple" {
declaration.brief.simple = true;
}
}
if declaration.docs.is_empty()
&& declaration.rules.is_empty()
&& declaration.standards.is_empty()
{
return None;
}
Some(declaration)
}
pub struct Hook {
pub path: String,
pub gates: Vec<Declaration>,
}
pub fn read() -> Result<Hook, String> {
let path = git::hook_path()?;
let out = std::process::Command::new(&path)
.arg("/dev/null")
.env(LIST_ENV, "1")
.output()
.map_err(|e| format!("cannot run {path}: {e}"))?;
let listing = String::from_utf8_lossy(&out.stdout).into_owned();
let mut hook = Hook {
path,
gates: Vec::new(),
};
for line in listing.lines() {
let mut fields = line.split('\t');
let Some(head) = fields.next() else { continue };
if let Some(gate) = read_gate(head, fields) {
hook.gates.push(gate);
}
}
let said = String::from_utf8_lossy(&out.stderr).trim().to_string();
if hook.gates.is_empty() {
if said.is_empty() {
return Err(format!("{} declared no gates", hook.path));
}
return Err(format!("{} declared no gates; it said: {said}", hook.path));
}
if !said.is_empty() {
return Err(format!(
"{}: a declaration in it was refused, so what it gates by cannot be read:\n{said}",
hook.path
));
}
Ok(hook)
}
pub fn find<'a>(hook: &'a Hook, want: &str) -> Result<&'a Declaration, String> {
hook.gates.iter().find(|d| d.gate == want).ok_or_else(|| {
let declared: Vec<&str> = hook.gates.iter().map(|d| d.gate.as_str()).collect();
format!(
"no gate '{want}' in {}; it declares: {}",
hook.path,
declared.join(", ")
)
})
}