use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
use anyhow::bail;
use clap::Subcommand;
use crate::verify::{CheckKind, CheckPlan};
#[derive(Debug, Subcommand)]
pub(crate) enum CheckCommand {
Quick {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Commit {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Gate {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
impl CheckCommand {
fn path(self) -> Option<PathBuf> {
match self {
CheckCommand::Quick { path }
| CheckCommand::Commit { path }
| CheckCommand::Gate { path } => path,
}
}
}
impl From<&CheckCommand> for CheckKind {
fn from(cmd: &CheckCommand) -> Self {
match cmd {
CheckCommand::Quick { .. } => CheckKind::Quick,
CheckCommand::Commit { .. } => CheckKind::Commit,
CheckCommand::Gate { .. } => CheckKind::Gate,
}
}
}
pub(crate) fn dispatch(cmd: CheckCommand) -> anyhow::Result<()> {
use std::io::Write;
let kind = CheckKind::from(&cmd);
let root = crate::root::find(cmd.path(), &crate::root::default_markers())?;
let cfg = crate::coverage_store::load_config(&root)?;
match crate::verify::resolve_check(&cfg, kind) {
CheckPlan::Noop(note) => {
writeln!(std::io::stdout(), "{note}")?;
#[expect(
clippy::disallowed_methods,
reason = "the check verb forwards a terminal exit status; an owned no-op exits 0 (design §5.4)"
)]
{
std::process::exit(0);
}
}
CheckPlan::Empty(k) => bail!(
"[verification].{} is empty — set a non-empty argv in {}",
k.key(),
crate::dtoml::DOCTRINE_TOML
),
CheckPlan::Run(argv) => run_proxy(&root, &argv, kind),
}
}
fn run_proxy(root: &Path, argv: &[String], kind: CheckKind) -> anyhow::Result<()> {
let Some((program, args)) = argv.split_first() else {
bail!("internal: empty check argv (resolve_check INV-2 violated)");
};
let status = Command::new(program)
.args(args)
.current_dir(root)
.status()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
anyhow::anyhow!(
"`{program}` not found — set [verification].{} in {}",
kind.key(),
crate::dtoml::DOCTRINE_TOML
)
} else {
anyhow::Error::new(e).context(format!("failed to spawn `{program}`"))
}
})?;
#[expect(
clippy::disallowed_methods,
reason = "true exit forwarding (CR-F5): the proxied child's code/signal is the verb's terminal status (design §5.4)"
)]
{
std::process::exit(exit_code(status));
}
}
fn exit_code(status: ExitStatus) -> i32 {
if let Some(code) = status.code() {
return code;
}
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
status.signal().map_or(1, |s| 128 + s)
}
#[cfg(not(unix))]
{
1
}
}