#!/usr/bin/env python3 """ BreakPilot Pre-Commit Check Script Prüft vor einem Commit: 1. Sind alle geänderten Dateien dokumentiert? 2. Haben alle geänderten Funktionen Tests? 3. Sind Security-relevante Änderungen markiert? 4. Sind ADRs für neue Module vorhanden? Verwendung: python3 scripts/pre-commit-check.py # Prüft staged files python3 scripts/pre-commit-check.py --all # Prüft alle uncommitted changes python3 scripts/pre-commit-check.py --fix # Versucht automatische Fixes Exit Codes: 0 - Alles OK 1 - Warnungen (nicht blockierend) 2 - Fehler (blockierend) """ import subprocess import sys import os import re from pathlib import Path from typing import List, Tuple, Dict # ============================================ # Konfiguration # ============================================ # Dateien die dokumentiert sein sollten DOC_REQUIRED_PATTERNS = { r"consent-service/internal/handlers/.*\.go$": "docs/api/consent-service-api.md", r"backend/.*_api\.py$": "docs/api/backend-api.md", r"website/app/api/.*/route\.ts$": "docs/api/frontend-api.md", } # Dateien die Tests haben sollten TEST_REQUIRED_PATTERNS = { r"consent-service/internal/services/([^/]+)\.go$": r"consent-service/internal/services/\1_test.go", r"backend/([^/]+)\.py$": r"backend/tests/test_\1.py", } # Neue Module die ADRs benötigen ADR_REQUIRED_PATTERNS = [ r"consent-service/internal/services/\w+_service\.go$", r"backend/[^/]+_service\.py$", r"website/app/admin/[^/]+/page\.tsx$", ] # Security-relevante Patterns SECURITY_PATTERNS = [ (r"password|secret|token|api_key|apikey", "Credentials"), (r"exec\(|eval\(|subprocess|os\.system", "Code Execution"), (r"sql|query.*\+|f['\"].*select|f['\"].*insert", "SQL Injection Risk"), ] # ============================================ # Git Helpers # ============================================ def get_staged_files() -> List[str]: """Gibt die für Commit vorgemerkten Dateien zurück.""" result = subprocess.run( ["git", "diff", "--cached", "--name-only", "--diff-filter=ACMR"], capture_output=True, text=True ) return [f.strip() for f in result.stdout.strip().split("\n") if f.strip()] def get_all_changed_files() -> List[str]: """Gibt alle geänderten Dateien zurück (staged + unstaged).""" result = subprocess.run( ["git", "diff", "--name-only", "--diff-filter=ACMR", "HEAD"], capture_output=True, text=True ) return [f.strip() for f in result.stdout.strip().split("\n") if f.strip()] def get_file_content(filepath: str) -> str: """Liest den Inhalt einer Datei.""" try: with open(filepath, 'r', encoding='utf-8') as f: return f.read() except Exception: return "" def file_exists(filepath: str) -> bool: """Prüft ob eine Datei existiert.""" return Path(filepath).exists() # ============================================ # Checks # ============================================ def check_documentation(files: List[str]) -> List[Tuple[str, str]]: """Prüft ob Dokumentation für geänderte Dateien existiert.""" issues = [] for filepath in files: for pattern, doc_file in DOC_REQUIRED_PATTERNS.items(): if re.match(pattern, filepath): if not file_exists(doc_file): issues.append((filepath, f"Dokumentation fehlt: {doc_file}")) break return issues def check_tests(files: List[str]) -> List[Tuple[str, str]]: """Prüft ob Tests für geänderte Dateien existieren.""" issues = [] for filepath in files: # Überspringe Test-Dateien selbst if "_test.go" in filepath or "test_" in filepath or "__tests__" in filepath: continue for pattern, test_pattern in TEST_REQUIRED_PATTERNS.items(): match = re.match(pattern, filepath) if match: test_file = re.sub(pattern, test_pattern, filepath) if not file_exists(test_file): issues.append((filepath, f"Test fehlt: {test_file}")) break return issues def check_adrs(files: List[str]) -> List[Tuple[str, str]]: """Prüft ob ADRs für neue Module vorhanden sind.""" issues = [] adr_dir = Path("docs/adr") for filepath in files: for pattern in ADR_REQUIRED_PATTERNS: if re.match(pattern, filepath): # Prüfe ob es ein neues File ist (nicht nur Änderung) result = subprocess.run( ["git", "diff", "--cached", "--name-status", filepath], capture_output=True, text=True ) if result.stdout.startswith("A"): # Added # Prüfe ob es einen ADR dafür gibt module_name = Path(filepath).stem adr_exists = any( adr_dir.glob(f"ADR-*{module_name}*.md") ) if adr_dir.exists() else False if not adr_exists: issues.append(( filepath, f"Neues Modul ohne ADR. Erstelle: docs/adr/ADR-NNNN-{module_name}.md" )) break return issues def check_security(files: List[str]) -> List[Tuple[str, str]]: """Prüft auf security-relevante Änderungen.""" warnings = [] for filepath in files: content = get_file_content(filepath) content_lower = content.lower() for pattern, category in SECURITY_PATTERNS: if re.search(pattern, content_lower): warnings.append((filepath, f"Security-Review empfohlen: {category}")) break # Nur eine Warnung pro Datei return warnings # ============================================ # Main # ============================================ def print_section(title: str, items: List[Tuple[str, str]], icon: str = "⚠️"): """Gibt eine Sektion aus.""" if items: print(f"\n{icon} {title}:") for filepath, message in items: print(f" {filepath}") print(f" → {message}") def main(): import argparse parser = argparse.ArgumentParser(description="BreakPilot Pre-Commit Check") parser.add_argument("--all", action="store_true", help="Check all changed files, not just staged") parser.add_argument("--fix", action="store_true", help="Attempt automatic fixes") parser.add_argument("--strict", action="store_true", help="Treat warnings as errors") args = parser.parse_args() print("=" * 60) print("BREAKPILOT PRE-COMMIT CHECK") print("=" * 60) # Dateien ermitteln files = get_all_changed_files() if args.all else get_staged_files() if not files: print("\n✅ Keine Dateien zu prüfen.") return 0 print(f"\nPrüfe {len(files)} Datei(en)...") # Checks ausführen doc_issues = check_documentation(files) test_issues = check_tests(files) adr_issues = check_adrs(files) security_warnings = check_security(files) # Ergebnisse ausgeben has_errors = False has_warnings = False if doc_issues: print_section("Fehlende Dokumentation", doc_issues, "📝") has_warnings = True if test_issues: print_section("Fehlende Tests", test_issues, "🧪") has_warnings = True if adr_issues: print_section("Fehlende ADRs", adr_issues, "📋") has_warnings = True if security_warnings: print_section("Security-Hinweise", security_warnings, "🔒") has_warnings = True # Zusammenfassung print("\n" + "=" * 60) if not has_errors and not has_warnings: print("✅ Alle Checks bestanden!") return 0 total_issues = len(doc_issues) + len(test_issues) + len(adr_issues) total_warnings = len(security_warnings) print(f"📊 Zusammenfassung:") print(f" Issues: {total_issues}") print(f" Warnings: {total_warnings}") if args.strict and (has_errors or has_warnings): print("\n❌ Commit blockiert (--strict mode)") return 2 if has_errors: print("\n❌ Commit blockiert wegen Fehlern") return 2 print("\n⚠️ Commit möglich, aber Warnings beachten!") return 1 if __name__ == "__main__": sys.exit(main())