#![cfg_attr(coverage_nightly, coverage(off))]
use super::hooks_command::HooksCommand;
use crate::services::configuration_service::{configuration, PmatConfig};
use anyhow::Result;
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)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub(crate) fn generate_hook_header(&self) -> String {
r#"#!/bin/bash
# Generated pre-commit hook (auto-managed by PMAT)
# DO NOT EDIT: This file is automatically generated
set -e
echo "🔍 PMAT Pre-commit Quality Gates"
echo "================================"
"#
.to_string()
}
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}}"
# GH-301: directories excluded from any hook filesystem scan.
# Override with: PMAT_PRECOMMIT_EXCLUDE_DIRS=".claude/worktrees target ..." git commit
export PMAT_PRECOMMIT_EXCLUDE_DIRS="${{PMAT_PRECOMMIT_EXCLUDE_DIRS:-.claude/worktrees .cursor/worktrees target node_modules .venv}}"
"#,
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
"#
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
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..."
# GH-301: helper that strips PMAT_PRECOMMIT_EXCLUDE_DIRS prefixes from a stream
# of git-tracked paths. Never call `find` here — `find` traverses .gitignore'd
# paths (like .claude/worktrees/*) which blows up pre-commit time on monorepos
# with many agent worktrees and can even invoke nested Makefiles.
_pmat_filter_excluded() {
if [ -z "$PMAT_PRECOMMIT_EXCLUDE_DIRS" ]; then cat; return; fi
awk -v excl="$PMAT_PRECOMMIT_EXCLUDE_DIRS" '
BEGIN { n = split(excl, a, " ") }
{
skip = 0
for (i = 1; i <= n; i++) {
if (a[i] != "" && index($0, a[i] "/") == 1) { skip = 1; break }
}
if (!skip) print
}
'
}
# 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.
# Uses `git ls-files` (GH-301) so .gitignore'd directories — including
# .claude/worktrees/, target/, node_modules/ — are never scanned. Outside a
# git worktree we fall back to a bounded find with explicit prunes.
if git rev-parse --git-dir > /dev/null 2>&1; then
HAS_SOURCE_FILES=$(git ls-files -- \
'*.rs' '*.py' '*.ts' '*.tsx' '*.js' '*.jsx' \
'*.go' '*.c' '*.cpp' '*.lua' '*.php' '*.swift' 2>/dev/null \
| _pmat_filter_excluded | head -n 1)
else
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/*' -not -path '*/.claude/worktrees/*' -not -path '*/.cursor/worktrees/*' -print -quit 2>/dev/null)
fi
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
}
}