#![cfg_attr(coverage_nightly, coverage(off))]
use super::hooks_command::HooksCommand;
use crate::services::configuration_service::{configuration, PmatConfig};
use anyhow::Result;
use chrono::Local;
use std::fs;
use std::path::Path;
impl HooksCommand {
pub(super) fn normalize_hook_content(content: &str) -> String {
content
.lines()
.filter(|line| !line.contains("# Generated at:"))
.collect::<Vec<_>>()
.join("\n")
}
pub(super) fn is_pmat_managed(&self, hook_path: &Path) -> Result<bool> {
if !hook_path.exists() {
return Ok(false);
}
let content = fs::read_to_string(hook_path)?;
Ok(content.contains("auto-managed by PMAT") && content.contains("DO NOT EDIT"))
}
pub(super) async fn generate_hook_content(&self) -> Result<String> {
let config_service = configuration();
let config = config_service.get_config()?;
let header = self.generate_hook_header();
let env_vars = self.generate_env_vars(&config);
let checks = self.generate_quality_checks();
let hook_content = format!("{header}\n{env_vars}\n{checks}");
Ok(hook_content)
}
pub(crate) fn generate_hook_header(&self) -> String {
format!(
r#"#!/bin/bash
# Generated pre-commit hook (auto-managed by PMAT)
# DO NOT EDIT: This file is automatically generated
# Generated at: {}
set -e
echo "🔍 PMAT Pre-commit Quality Gates"
echo "================================"
"#,
Local::now().format("%Y-%m-%d %H:%M:%S")
)
}
fn generate_env_vars(&self, config: &PmatConfig) -> String {
format!(
r#"# Load current configuration dynamically
export PMAT_MAX_CYCLOMATIC_COMPLEXITY={}
export PMAT_MAX_COGNITIVE_COMPLEXITY={}
export PMAT_MIN_TEST_COVERAGE={}
export PMAT_MAX_SATD_COMMENTS=5
export PMAT_TASK_ID_PATTERN="PMAT-[0-9]{{4}}"
"#,
config.quality.max_complexity,
config.quality.max_cognitive_complexity,
config.quality.min_coverage as u32
)
}
fn generate_format_check() -> &'static str {
r#"# 0. Format check (Rust only — cargo fmt --check on staged .rs files)
STAGED_RS=$(git diff --cached --name-only --diff-filter=ACMR -- '*.rs' 2>/dev/null)
if [ -n "$STAGED_RS" ] && command -v cargo &> /dev/null && [ -f Cargo.toml ]; then
echo -n " Format check... "
FMT_OUTPUT=$(cargo fmt -- --check 2>&1)
if [ $? -eq 0 ]; then
echo "✅"
else
echo "❌"
echo " Unformatted Rust code detected. Run 'cargo fmt' before committing."
echo "$FMT_OUTPUT" | grep "^Diff in" | head -5
exit 1
fi
fi
"#
}
pub(crate) fn generate_quality_checks(&self) -> String {
let mut hook = String::from(
r#"# Check if pmat is available
if ! command -v pmat &> /dev/null; then
echo "⚠️ Warning: pmat not found in PATH"
echo " Install with: cargo install pmat"
exit 0 # Allow commit but warn
fi
echo "📊 Running quality gate checks..."
# Detect if this repo has any source files at all (not just staged ones).
# Non-code repos (docs, configs, YAML, contracts) get a fast pass.
HAS_SOURCE_FILES=$(find . -maxdepth 4 -type f \( -name '*.rs' -o -name '*.py' -o -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' -o -name '*.go' -o -name '*.c' -o -name '*.cpp' -o -name '*.lua' -o -name '*.php' -o -name '*.swift' \) -not -path './.git/*' -not -path '*/target/*' -not -path '*/node_modules/*' -print -quit 2>/dev/null)
if [ -z "$HAS_SOURCE_FILES" ]; then
echo " Project type: non-code (docs/configs/YAML)"
echo " Complexity check... ⏭️ (no source files in repo)"
echo " SATD check... ⏭️ (no source files in repo)"
echo ""
echo "✅ All quality gates passed!"
echo ""
exit 0
fi
"#,
);
hook.push_str(Self::generate_format_check());
hook.push_str(r#"
# 1. Complexity analysis (only staged source files, not entire project)
# Supports: Rust, Python, TypeScript, JavaScript, Go, C, C++, Lua, PHP, Swift
STAGED_SRC=$(git diff --cached --name-only --diff-filter=ACMR -- '*.rs' '*.py' '*.ts' '*.tsx' '*.js' '*.jsx' '*.go' '*.c' '*.cpp' '*.lua' '*.php' '*.swift' 2>/dev/null | head -20)
if [ -n "$STAGED_SRC" ]; then
echo -n " Complexity check... "
COMPLEXITY_FAILED=0
COMPLEXITY_DETAILS=""
for SRC_FILE in $STAGED_SRC; do
if [ -f "$SRC_FILE" ]; then
FILE_OUTPUT=$(pmat analyze complexity --file "$SRC_FILE" --max-cyclomatic $PMAT_MAX_CYCLOMATIC_COMPLEXITY --max-cognitive $PMAT_MAX_COGNITIVE_COMPLEXITY 2>&1)
if echo "$FILE_OUTPUT" | grep -q 'Errors.*: [1-9]'; then
COMPLEXITY_FAILED=1
# Extract the offending file and functions
COMPLEXITY_DETAILS="${COMPLEXITY_DETAILS} ${SRC_FILE}:"$'\n'
FILE_VIOLATIONS=$(echo "$FILE_OUTPUT" | grep -E '^[0-9]+\. ' | grep -E 'Cyclomatic|Cognitive' | head -3)
COMPLEXITY_DETAILS="${COMPLEXITY_DETAILS}${FILE_VIOLATIONS}"$'\n'
fi
fi
done
if [ "$COMPLEXITY_FAILED" -eq 0 ]; then
echo "✅"
else
echo "❌"
echo "Issues Found:"
echo "$COMPLEXITY_DETAILS" | head -10
echo " Complexity exceeds thresholds (Cyclomatic: $PMAT_MAX_CYCLOMATIC_COMPLEXITY, Cognitive: $PMAT_MAX_COGNITIVE_COMPLEXITY)"
exit 1
fi
else
echo " Complexity check... ⏭️ (no source files staged)"
fi
# 2. SATD (Self-Admitted Quality Issues) check - informational only
echo -n " SATD check... "
SATD_OUTPUT=$(pmat analyze satd 2>&1)
SATD_COUNT=$(echo "$SATD_OUTPUT" | grep -oP 'Total SATD comments found: \K[0-9]+' || echo "0")
if [ "$SATD_COUNT" -le "$PMAT_MAX_SATD_COMMENTS" ] 2>/dev/null; then
echo "✅ ($SATD_COUNT SATD comments)"
else
echo "⚠️ ($SATD_COUNT SATD comments, threshold: $PMAT_MAX_SATD_COMMENTS)"
fi
# 3. Documentation synchronization (only if docs structure exists)
if [ -d "docs/execution" ] || [ -f "CHANGELOG.md" ]; then
echo -n " Documentation check... "
if [ -f "docs/execution/roadmap.md" ] && [ -f "CHANGELOG.md" ]; then
echo "✅"
else
echo "⚠️ (docs/execution/roadmap.md or CHANGELOG.md missing)"
fi
fi
# 4. Task ID validation (if commit message available)
if [ -n "$1" ]; then
echo -n " Task ID check... "
if echo "$1" | grep -qE "$PMAT_TASK_ID_PATTERN"; then
echo "✅"
else
echo "⚠️"
echo " Warning: Commit message should contain task ID matching $PMAT_TASK_ID_PATTERN"
fi
fi
echo ""
echo "✅ All quality gates passed!"
echo ""
# Success
exit 0
"#);
hook
}
}