use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use crate::error::{self, Result};
use crate::rules::Gate;
use crate::{home, policy};
pub const COMPARISON_REMOTE_ENV: &str = "ONEVCS_COMPARISON_REMOTE";
pub const COMPARISON_BASE_ENV: &str = "ONEVCS_COMPARISON_BASE";
pub const PRESERVED_LOG_DIRNAME: &str = "gate-logs";
pub const PRESERVED_LOG_ATTEMPTS: usize = 10;
pub fn comparison_env(remote: &str, base: &str) -> Vec<(String, String)> {
vec![
(COMPARISON_REMOTE_ENV.to_owned(), remote.to_owned()),
(COMPARISON_BASE_ENV.to_owned(), base.to_owned()),
]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ruling {
Passed,
Rejected,
}
impl Ruling {
pub fn from_exit(succeeded: bool) -> Self {
if succeeded {
Ruling::Passed
} else {
Ruling::Rejected
}
}
pub fn passed(self) -> bool {
self == Ruling::Passed
}
pub fn describe(self) -> &'static str {
match self {
Ruling::Passed => "pass",
Ruling::Rejected => "fail",
}
}
}
#[derive(Debug, Clone)]
pub struct Verdict {
pub ruling: Ruling,
pub output: String,
pub command: String,
}
pub fn run(worktree: &Path, argv: &[String], env: &[(String, String)]) -> Verdict {
let command = argv.join(" ");
let Some((program, arguments)) = argv.split_first() else {
return Verdict {
ruling: Ruling::Rejected,
output: "the gate names no command, so it verified nothing\n".to_owned(),
command,
};
};
let mut child = match Command::new(program)
.args(arguments)
.current_dir(worktree)
.envs(env.iter().cloned())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(error) => {
return Verdict {
ruling: Ruling::Rejected,
output: format!("gate command not found: {program:?} ({error})\n"),
command,
}
}
};
let mut stdout = String::new();
let mut stderr = String::new();
if let Some(mut pipe) = child.stdout.take() {
let _ = pipe.read_to_string(&mut stdout);
}
if let Some(mut pipe) = child.stderr.take() {
let _ = pipe.read_to_string(&mut stderr);
}
let status = child.wait();
Verdict {
ruling: Ruling::from_exit(status.map(|status| status.success()).unwrap_or(false)),
output: format!("{stdout}{stderr}"),
command,
}
}
pub fn own_command(gate: &Gate) -> Option<&Vec<String>> {
match gate {
Gate::Command { command } => Some(command),
Gate::Kind { .. } => None,
}
}
pub fn preserve_log(run_root: &Path, branch: &str, contents: &str) -> Result<PathBuf> {
let directory = run_root
.join(PRESERVED_LOG_DIRNAME)
.join(policy::branch_slug(branch));
home::ensure_dir(&directory)?;
let mut next = highest(&directory) + 1;
while next < 10_000 {
let path = directory.join(format!("gate-{next:04}.log"));
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
Ok(_) => {
std::fs::write(&path, crate::stream::redact(contents))
.map_err(error::at("preserve the gate log at", &path))?;
prune(&directory)?;
return Ok(path);
}
Err(_) => next += 1,
}
}
Err(error::at(
"claim a preserved gate log number under",
&directory,
)("ten thousand attempts is not a branch's history"))
}
fn highest(directory: &Path) -> u32 {
let Ok(entries) = std::fs::read_dir(directory) else {
return 0;
};
entries
.flatten()
.filter_map(|entry| {
entry
.file_name()
.to_string_lossy()
.strip_prefix("gate-")?
.strip_suffix(".log")?
.parse::<u32>()
.ok()
})
.max()
.unwrap_or(0)
}
fn prune(directory: &Path) -> Result<()> {
let Ok(entries) = std::fs::read_dir(directory) else {
return Ok(());
};
let mut logs: Vec<PathBuf> = entries
.flatten()
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|value| value == "log"))
.collect();
logs.sort();
while logs.len() > PRESERVED_LOG_ATTEMPTS {
let oldest = logs.remove(0);
let _ = std::fs::remove_file(oldest);
}
Ok(())
}