#![cfg_attr(coverage_nightly, coverage(off))]
use crate::cli::colors as c;
use crate::cli::OutputFormat;
use anyhow::Result;
use std::path::PathBuf;
const PMAT_STACK_MARKER: &str = "# PMAT Standardized Pre-Commit Hook (auto-installed)";
const DEFAULT_STACK_REPOS: &[&str] = &[
"trueno",
"trueno-graph",
"trueno-db",
"trueno-rag",
"trueno-viz",
"trueno-zram-core",
"aprender",
"paiml-mcp-agent-toolkit",
"certeza",
"bashrs",
"probar",
"renacer",
"presentar",
"pmcp",
];
#[derive(Debug, Clone)]
struct StackRepo {
name: String,
path: PathBuf,
exists: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum HookState {
Installed,
Missing,
NonPmat,
Outdated,
}
impl std::fmt::Display for HookState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HookState::Installed => write!(f, "installed"),
HookState::Missing => write!(f, "missing"),
HookState::NonPmat => write!(f, "non-pmat"),
HookState::Outdated => write!(f, "outdated"),
}
}
}
fn discover_stack_repos() -> Vec<StackRepo> {
let home = std::env::var("HOME").unwrap_or_default();
let src_dir = PathBuf::from(&home).join("src");
DEFAULT_STACK_REPOS
.iter()
.map(|name| {
let path = src_dir.join(name);
let exists = path.join(".git").exists();
StackRepo {
name: name.to_string(),
path,
exists,
}
})
.collect()
}
fn generate_hook_content() -> String {
format!(
r#"#!/bin/sh
{PMAT_STACK_MARKER}
set -e
# 1. Cargo fmt check (Rust projects only)
if [ -f Cargo.toml ]; then
cargo fmt --all -- --check 2>/dev/null || {{
echo "cargo fmt failed. Run: cargo fmt --all"
exit 1
}}
fi
# 2. Orphan test detection (check for #[test] in files not in module tree)
if command -v pmat >/dev/null 2>&1; then
pmat comply check --check orphan-tests --quiet 2>/dev/null || true
fi
# 3. TDG baseline update (fast, O(1) check)
if command -v pmat >/dev/null 2>&1 && [ -f .pmat/baseline.json ]; then
pmat baseline update --quiet 2>/dev/null || true
fi
"#
)
}
fn detect_hook_state(repo_path: &PathBuf) -> HookState {
let hook_path = repo_path.join(".git/hooks/pre-commit");
if !hook_path.exists() {
return HookState::Missing;
}
match std::fs::read_to_string(&hook_path) {
Ok(content) => {
if !content.contains(PMAT_STACK_MARKER) {
HookState::NonPmat
} else if content.trim() == generate_hook_content().trim() {
HookState::Installed
} else {
HookState::Outdated
}
}
Err(_) => HookState::NonPmat,
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn handle_hooks_install_stack(update: bool) -> Result<()> {
let repos = discover_stack_repos();
let hook_content = generate_hook_content();
println!("{}", c::header("Installing PMAT hooks across stack repos"));
println!();
let mut installed = 0u32;
let mut skipped = 0u32;
let mut missing_repos = 0u32;
let mut errors = 0u32;
for repo in &repos {
if !repo.exists {
missing_repos += 1;
println!(
" {} {} {}",
c::dim(&repo.name),
c::dim("-"),
c::dim("repo not found")
);
continue;
}
let hook_path = repo.path.join(".git/hooks/pre-commit");
let state = detect_hook_state(&repo.path);
match state {
HookState::Missing => match write_hook(&hook_path, &hook_content) {
Ok(()) => {
installed += 1;
println!(
" {} {} {}",
c::pass("installed"),
c::label(&repo.name),
c::path(hook_path.to_string_lossy().as_ref())
);
}
Err(e) => {
errors += 1;
println!(" {} {} - {}", c::fail("error"), c::label(&repo.name), e);
}
},
HookState::NonPmat => {
skipped += 1;
println!(
" {} {} - non-PMAT hook exists, skipping",
c::warn("skipped"),
c::label(&repo.name),
);
}
HookState::Installed => {
skipped += 1;
println!(
" {} {} - already up to date",
c::skip("skipped"),
c::label(&repo.name),
);
}
HookState::Outdated => {
if update {
match write_hook(&hook_path, &hook_content) {
Ok(()) => {
installed += 1;
println!(
" {} {} {}",
c::pass("updated"),
c::label(&repo.name),
c::path(hook_path.to_string_lossy().as_ref())
);
}
Err(e) => {
errors += 1;
println!(" {} {} - {}", c::fail("error"), c::label(&repo.name), e);
}
}
} else {
skipped += 1;
println!(
" {} {} - outdated, use --update to overwrite",
c::warn("skipped"),
c::label(&repo.name),
);
}
}
}
}
println!();
println!("{}", c::separator());
println!(
" {}: {} {}: {} {}: {} {}: {}",
c::dim("Installed"),
c::number(&installed.to_string()),
c::dim("Skipped"),
c::number(&skipped.to_string()),
c::dim("Missing repos"),
c::number(&missing_repos.to_string()),
c::dim("Errors"),
if errors > 0 {
format!("{}{}{}", c::RED, errors, c::RESET)
} else {
c::number(&errors.to_string())
},
);
if errors > 0 {
Err(anyhow::anyhow!("{} hook(s) failed to install", errors))
} else {
Ok(())
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn handle_hooks_status_stack(format: &OutputFormat) -> Result<()> {
let repos = discover_stack_repos();
match format {
OutputFormat::Json => print_status_json(&repos),
_ => print_status_table(&repos),
}
Ok(())
}
fn print_status_table(repos: &[StackRepo]) {
println!("{}", c::header("Stack Hook Status"));
println!();
println!(
" {:<28} {:<12} {}",
c::subheader("Repository"),
c::subheader("Status"),
c::subheader("Path"),
);
println!(" {}", c::separator());
let mut installed_count = 0u32;
let mut total_present = 0u32;
for repo in repos {
if !repo.exists {
println!(
" {:<28} {:<12} {}",
c::dim(&repo.name),
c::dim("not found"),
c::dim("-"),
);
continue;
}
total_present += 1;
let state = detect_hook_state(&repo.path);
let status_str = match &state {
HookState::Installed => {
installed_count += 1;
c::pass("installed")
}
HookState::Missing => c::fail("missing"),
HookState::NonPmat => format!("{}{}non-pmat{}", c::YELLOW, "", c::RESET),
HookState::Outdated => c::warn("outdated"),
};
let hook_path = repo.path.join(".git/hooks/pre-commit");
let path_str = if hook_path.exists() {
c::path(hook_path.to_string_lossy().as_ref())
} else {
c::dim("-")
};
println!(" {:<28} {} {}", &repo.name, status_str, path_str,);
}
println!();
println!(
" {}/{} stack repos have PMAT hooks installed",
c::number(&installed_count.to_string()),
c::number(&total_present.to_string()),
);
}
fn print_status_json(repos: &[StackRepo]) {
let mut entries = Vec::new();
for repo in repos {
let (status, path_str) = if !repo.exists {
("not_found".to_string(), String::new())
} else {
let state = detect_hook_state(&repo.path);
let hook_path = repo.path.join(".git/hooks/pre-commit");
(
state.to_string(),
if hook_path.exists() {
hook_path.to_string_lossy().into_owned()
} else {
String::new()
},
)
};
entries.push(format!(
r#" {{"name": "{}", "status": "{}", "path": "{}"}}"#,
repo.name, status, path_str
));
}
println!("[\n{}\n]", entries.join(",\n"));
}
fn write_hook(hook_path: &PathBuf, content: &str) -> Result<()> {
if let Some(parent) = hook_path.parent() {
std::fs::create_dir_all(parent)?;
}
let tmp_path = hook_path.with_extension("tmp");
std::fs::write(&tmp_path, content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o755);
std::fs::set_permissions(&tmp_path, perms)?;
}
std::fs::rename(&tmp_path, hook_path)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_discover_stack_repos() {
let repos = discover_stack_repos();
assert!(!repos.is_empty());
for repo in &repos {
assert!(!repo.name.is_empty());
assert!(!repo.path.as_os_str().is_empty());
}
}
#[test]
fn test_default_repos_count() {
assert_eq!(DEFAULT_STACK_REPOS.len(), 14);
assert!(DEFAULT_STACK_REPOS.contains(&"trueno"));
assert!(DEFAULT_STACK_REPOS.contains(&"paiml-mcp-agent-toolkit"));
assert!(DEFAULT_STACK_REPOS.contains(&"aprender"));
}
#[test]
fn test_generate_hook_content() {
let content = generate_hook_content();
assert!(content.starts_with("#!/bin/sh"));
assert!(content.contains(PMAT_STACK_MARKER));
assert!(content.contains("cargo fmt --all -- --check"));
assert!(content.contains("orphan-tests"));
assert!(content.contains("pmat baseline update"));
}
#[test]
fn test_hook_marker_detection() {
let content = generate_hook_content();
assert!(content.contains(PMAT_STACK_MARKER));
let other = "#!/bin/sh\necho hello\n";
assert!(!other.contains(PMAT_STACK_MARKER));
}
#[test]
fn test_detect_hook_state_missing() {
let fake = PathBuf::from("/tmp/pmat-test-nonexistent-repo-xyz");
assert_eq!(detect_hook_state(&fake), HookState::Missing);
}
#[test]
fn test_hook_state_display() {
assert_eq!(HookState::Installed.to_string(), "installed");
assert_eq!(HookState::Missing.to_string(), "missing");
assert_eq!(HookState::NonPmat.to_string(), "non-pmat");
assert_eq!(HookState::Outdated.to_string(), "outdated");
}
#[test]
fn test_write_and_detect_hook() {
let tmp = std::env::temp_dir().join("pmat-hook-test");
let git_hooks = tmp.join(".git/hooks");
let _ = std::fs::create_dir_all(&git_hooks);
let hook_path = git_hooks.join("pre-commit");
let content = generate_hook_content();
write_hook(&hook_path, &content).unwrap();
assert_eq!(detect_hook_state(&tmp), HookState::Installed);
std::fs::write(&hook_path, format!("{}\n# extra", content)).unwrap();
assert_eq!(detect_hook_state(&tmp), HookState::Outdated);
std::fs::write(&hook_path, "#!/bin/sh\necho hello\n").unwrap();
assert_eq!(detect_hook_state(&tmp), HookState::NonPmat);
let _ = std::fs::remove_dir_all(&tmp);
}
}