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 finished = match Command::new(program)
.args(arguments)
.current_dir(worktree)
.envs(env.iter().cloned())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
{
Ok(finished) => finished,
Err(error) => {
return Verdict {
ruling: Ruling::Rejected,
output: format!("the gate command {program:?} could not be run: {error}\n"),
command,
}
}
};
let stdout = String::from_utf8_lossy(&finished.stdout);
let stderr = String::from_utf8_lossy(&finished.stderr);
Verdict {
ruling: Ruling::from_exit(finished.status.success()),
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"))
}
pub fn has_recorded_verdict(run_root: &Path) -> bool {
let Ok(branches) = std::fs::read_dir(run_root.join(PRESERVED_LOG_DIRNAME)) else {
return false;
};
branches.flatten().any(|branch| {
std::fs::read_dir(branch.path())
.into_iter()
.flatten()
.flatten()
.any(|log| {
let name = log.file_name();
let name = name.to_string_lossy();
name.starts_with("gate-")
&& name.ends_with(".log")
&& log.file_type().is_ok_and(|kind| kind.is_file())
})
})
}
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(())
}