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>
259 lines
8.2 KiB
Python
Executable File
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())
|