Add pre-receive hook to prevent merging code with Python syntax errors. Features: - Checks all Python files (.py) in each push using python -m py_compile - Special protection for critical files: - run_agent.py - model_tools.py - hermes-agent/tools/nexus_architect.py - cli.py, batch_runner.py, hermes_state.py - Clear error messages showing file and line number - Rejects pushes containing syntax errors Files added: - .githooks/pre-receive (Bash implementation) - .githooks/pre-receive.py (Python implementation) - docs/GITEA_SYNTAX_GUARD.md (installation guide) - .githooks/pre-commit (existing secret detection hook) Closes #82
217 lines
6.0 KiB
Bash
Executable File
217 lines
6.0 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# Pre-receive hook for Gitea - Python Syntax Guard
|
|
#
|
|
# This hook validates Python files for syntax errors before allowing pushes.
|
|
# It uses `python -m py_compile` to check files for syntax errors.
|
|
#
|
|
# Installation in Gitea:
|
|
# 1. Go to Repository Settings → Git Hooks
|
|
# 2. Edit the "pre-receive" hook
|
|
# 3. Copy the contents of this file
|
|
# 4. Save and enable
|
|
#
|
|
# Or for system-wide Gitea hooks, place in:
|
|
# /path/to/gitea-repositories/<repo>.git/hooks/pre-receive
|
|
#
|
|
# Features:
|
|
# - Checks all Python files (.py) in the push
|
|
# - Focuses on critical files: run_agent.py, model_tools.py, nexus_architect.py
|
|
# - Provides detailed error messages with line numbers
|
|
# - Rejects pushes containing syntax errors
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
# Colors for output (may not work in all Gitea environments)
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Exit codes
|
|
EXIT_SUCCESS=0
|
|
EXIT_SYNTAX_ERROR=1
|
|
EXIT_INTERNAL_ERROR=2
|
|
|
|
# Temporary directory for file extraction
|
|
TEMP_DIR=$(mktemp -d)
|
|
trap "rm -rf $TEMP_DIR" EXIT
|
|
|
|
# Counters
|
|
ERRORS_FOUND=0
|
|
FILES_CHECKED=0
|
|
CRITICAL_FILES_CHECKED=0
|
|
|
|
# Critical files that must always be checked
|
|
CRITICAL_FILES=(
|
|
"run_agent.py"
|
|
"model_tools.py"
|
|
"hermes-agent/tools/nexus_architect.py"
|
|
"cli.py"
|
|
"batch_runner.py"
|
|
"hermes_state.py"
|
|
)
|
|
|
|
# ============================================================================
|
|
# HELPER FUNCTIONS
|
|
# ============================================================================
|
|
|
|
log_info() {
|
|
echo -e "${GREEN}[INFO]${NC} $1"
|
|
}
|
|
|
|
log_warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[ERROR]${NC} $1"
|
|
}
|
|
|
|
# Extract file content from git object
|
|
get_file_content() {
|
|
local ref="$1"
|
|
git show "$ref" 2>/dev/null || echo ""
|
|
}
|
|
|
|
# Check if file is a Python file
|
|
is_python_file() {
|
|
local filename="$1"
|
|
[[ "$filename" == *.py ]]
|
|
}
|
|
|
|
# Check if file is in the critical list
|
|
is_critical_file() {
|
|
local filename="$1"
|
|
for critical in "${CRITICAL_FILES[@]}"; do
|
|
if [[ "$filename" == *"$critical" ]]; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Check Python file for syntax errors
|
|
check_syntax() {
|
|
local filename="$1"
|
|
local content="$2"
|
|
local ref="$3"
|
|
|
|
# Write content to temp file
|
|
local temp_file="$TEMP_DIR/$(basename "$filename")"
|
|
echo "$content" > "$temp_file"
|
|
|
|
# Run py_compile
|
|
local output
|
|
if ! output=$(python3 -m py_compile "$temp_file" 2>&1); then
|
|
echo "SYNTAX_ERROR"
|
|
echo "$output"
|
|
return 1
|
|
fi
|
|
|
|
echo "OK"
|
|
return 0
|
|
}
|
|
|
|
# ============================================================================
|
|
# MAIN PROCESSING
|
|
# ============================================================================
|
|
|
|
echo "========================================"
|
|
echo " Python Syntax Guard - Pre-receive"
|
|
echo "========================================"
|
|
echo ""
|
|
|
|
# Read refs from stdin (provided by Git)
|
|
# Format: <oldrev> <newrev> <refname>
|
|
while read -r oldrev newrev refname; do
|
|
# Skip if this is a branch deletion (newrev is all zeros)
|
|
if [[ "$newrev" == "0000000000000000000000000000000000000000" ]]; then
|
|
log_info "Branch deletion detected, skipping syntax check"
|
|
continue
|
|
fi
|
|
|
|
# If this is a new branch (oldrev is all zeros), check all files
|
|
if [[ "$oldrev" == "0000000000000000000000000000000000000000" ]]; then
|
|
# List all files in the new commit
|
|
files=$(git ls-tree --name-only -r "$newrev" 2>/dev/null || echo "")
|
|
else
|
|
# Get list of changed files between old and new
|
|
files=$(git diff --name-only "$oldrev" "$newrev" 2>/dev/null || echo "")
|
|
fi
|
|
|
|
# Process each file
|
|
while IFS= read -r file; do
|
|
[ -z "$file" ] && continue
|
|
|
|
# Only check Python files
|
|
if ! is_python_file "$file"; then
|
|
continue
|
|
fi
|
|
|
|
FILES_CHECKED=$((FILES_CHECKED + 1))
|
|
|
|
# Check if critical file
|
|
local is_critical=false
|
|
if is_critical_file "$file"; then
|
|
is_critical=true
|
|
CRITICAL_FILES_CHECKED=$((CRITICAL_FILES_CHECKED + 1))
|
|
fi
|
|
|
|
# Get file content at the new revision
|
|
content=$(git show "$newrev:$file" 2>/dev/null || echo "")
|
|
|
|
if [ -z "$content" ]; then
|
|
# File might have been deleted
|
|
continue
|
|
fi
|
|
|
|
# Check syntax
|
|
result=$(check_syntax "$file" "$content" "$newrev")
|
|
status=$?
|
|
|
|
if [ $status -ne 0 ]; then
|
|
ERRORS_FOUND=$((ERRORS_FOUND + 1))
|
|
log_error "Syntax error in: $file"
|
|
|
|
if [ "$is_critical" = true ]; then
|
|
echo " ^^^ CRITICAL FILE - This file is essential for system operation"
|
|
fi
|
|
|
|
# Display the py_compile error
|
|
echo ""
|
|
echo "$result" | grep -v "^SYNTAX_ERROR$" | sed 's/^/ /'
|
|
echo ""
|
|
else
|
|
if [ "$is_critical" = true ]; then
|
|
log_info "✓ Critical file OK: $file"
|
|
fi
|
|
fi
|
|
|
|
done <<< "$files"
|
|
done
|
|
|
|
echo ""
|
|
echo "========================================"
|
|
echo " SUMMARY"
|
|
echo "========================================"
|
|
echo "Files checked: $FILES_CHECKED"
|
|
echo "Critical files checked: $CRITICAL_FILES_CHECKED"
|
|
echo "Errors found: $ERRORS_FOUND"
|
|
echo ""
|
|
|
|
# Exit with appropriate code
|
|
if [ $ERRORS_FOUND -gt 0 ]; then
|
|
log_error "╔════════════════════════════════════════════════════════════╗"
|
|
log_error "║ PUSH REJECTED: Syntax errors detected! ║"
|
|
log_error "║ ║"
|
|
log_error "║ Please fix the syntax errors above before pushing again. ║"
|
|
log_error "╚════════════════════════════════════════════════════════════╝"
|
|
echo ""
|
|
exit $EXIT_SYNTAX_ERROR
|
|
fi
|
|
|
|
log_info "✓ All Python files passed syntax check"
|
|
exit $EXIT_SUCCESS
|