This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/scripts/pre-commit-check.py
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

259 lines
8.2 KiB
Python
Executable File

#!/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())