Schnelle Prüfungen, bevor der Commit überhaupt entsteht
Ein pre-commit Hook läuft, bevor Git den Commit tatsächlich erstellt, und kann ihn bei Regelverstößen vollständig verhindern. Dieser Artikel zeigt, welche Prüfungen wie Linting, Formatierung und schnelle Unit-Tests dort sinnvoll sind, warum Geschwindigkeit entscheidend ist, und wie sich Hand-geschriebene Skripte gegenüber dem pre-commit Framework schlagen.
Inhaltsverzeichnis
- 1. Warum der pre-commit Hook der erste Kontrollpunkt ist
- 2. Wie der pre-commit Hook technisch abläuft
- 3. Linting als Basis-Prüfung im pre-commit Hook
- 4. Formatierung automatisch korrigieren statt nur melden
- 5. Schnelle Unit-Tests im pre-commit Hook
- 6. Geschwindigkeit: warum langsame Hooks Entwickler frustrieren
- 7. Hand-geschriebene Shell-Skripte: Flexibilität mit Wartungslast
- 8. Das pre-commit Framework als sprachunabhängiger Multi-Tool-Runner
- 9. Hand-geschriebene Skripte vs. pre-commit Framework im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum der pre-commit Hook der erste Kontrollpunkt ist
Ein pre-commit Hook läuft, nachdem Dateien mit git add gestaged wurden, aber bevor Git den eigentlichen Commit-Editor öffnet und das Commit-Objekt erzeugt. Genau dieser Zeitpunkt macht ihn zum frühesten sinnvollen Kontrollpunkt im gesamten Entwickler-Workflow: Ein Fehler wird erkannt, bevor er überhaupt Teil der lokalen Historie wird, nicht erst beim Push oder in der CI-Pipeline Minuten später. Bricht der Hook mit einem Nicht-Null-Exit-Code ab, verhindert Git den Commit vollständig, und der Entwickler landet zurück im Terminal mit einer klaren Fehlermeldung.
Der Vorteil gegenüber späteren Prüfungen liegt im Kontextwechsel: Ein Entwickler, der noch mitten in der Änderung steckt, kann einen Fehler sofort beheben, ohne erst zu einer alten Änderung zurückspringen zu müssen, die längst aus dem Kopf verschwunden ist. Ein pre-commit Hook ersetzt dabei keine vollständige CI-Pipeline, sondern ergänzt sie um eine schnelle, lokale Vorstufe, die offensichtliche Probleme abfängt, bevor sie überhaupt in die geteilte Historie gelangen.
2. Wie der pre-commit Hook technisch abläuft
Technisch erhält ein pre-commit Hook keine Kommandozeilenargumente, sondern muss selbst über git diff --cached --name-only --diff-filter=ACM ermitteln, welche Dateien tatsächlich gestaged sind. Wichtig ist, ausschließlich den gestagten Inhalt zu prüfen, nicht die Version im Arbeitsverzeichnis, denn bei teilweise gestagten Dateien mit git add -p können beide Versionen voneinander abweichen. Der zuverlässige Weg dafür ist git show :datei.php, das den Inhalt direkt aus dem Staging-Bereich liest.
Nach der Prüfung entscheidet ausschließlich der Exit-Code über das Ergebnis, nicht die Textausgabe. Ein Exit-Code von 0 lässt den Commit zu, jeder andere Wert bricht ihn ab. Aus diesem Grund sollten alle Teilprüfungen ihr Ergebnis in einer Sammelvariablen bündeln, statt beim ersten Fehler sofort abzubrechen, damit der Entwickler in einem Durchlauf alle Probleme sieht, statt sie nacheinander einzeln zu entdecken.
#!/usr/bin/env bash
# pre-commit hook: determine staged files and read their staged content
set -euo pipefail
# Only added, copied and modified files, deletions are irrelevant to lint
staged_files=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$staged_files" ]; then
echo "No staged files, skipping checks."
exit 0
fi
exit_code=0
for file in $staged_files; do
case "$file" in
*.php)
# Read the staged content directly, not the working-directory version
if ! git show ":$file" | php -l > /dev/null 2>&1; then
echo "Syntax error in staged version of: $file"
exit_code=1
fi
;;
esac
done
exit $exit_code
3. Linting als Basis-Prüfung im pre-commit Hook
Linting ist die naheliegendste und meist gewinnbringendste Prüfung in einem pre-commit Hook, weil sie rein statisch arbeitet und keine Laufzeitumgebung benötigt. Ein Linter erkennt unbenutzte Variablen, fehlerhafte Syntax, verbotene Konstrukte wie var_dump in produktivem PHP-Code oder Verstöße gegen einen vereinbarten Coding-Standard wie PSR-12. Weil Linting typischerweise nur Millisekunden bis wenige Sekunden pro Datei benötigt, lässt es sich problemlos bei jedem einzelnen Commit ausführen, ohne den Workflow spürbar zu verlangsamen.
Entscheidend ist, den Linter ausschließlich auf die gestagten Dateien anzuwenden, nicht auf das gesamte Repository. Ein vollständiger Lint-Lauf über tausende Dateien bei jedem Commit wäre selbst bei einem schnellen Linter unpraktikabel und würde die eigentliche Idee eines schnellen, lokalen Kontrollpunkts konterkarieren. Tools wie phpcs, eslint oder ruff unterstützen dafür alle eine gezielte Angabe einzelner Dateipfade als Argument.
4. Formatierung automatisch korrigieren statt nur melden
Formatierungsprüfungen unterscheiden sich von Linting durch einen entscheidenden Punkt: Sie lassen sich in den meisten Fällen automatisch beheben, statt nur gemeldet zu werden. Tools wie php-cs-fixer, prettier oder black formatieren den Code direkt um und geben ihn in korrigierter Form zurück, statt dem Entwickler lediglich eine Liste von Abweichungen zu präsentieren, die er von Hand nachbessern müsste. Ein gut konfigurierter pre-commit Hook nutzt diese Fähigkeit, formatiert die gestagten Dateien im Arbeitsverzeichnis neu und fügt die korrigierten Versionen mit git add erneut zum Staging-Bereich hinzu.
Diese Automatik nimmt Entwicklern die mühsame manuelle Formatierungsarbeit vollständig ab und beendet gleichzeitig endlose Diskussionen über Einrückung oder Anführungszeichen in Code-Reviews. Wichtig ist, dass der Hook nach dem automatischen Fix erneut prüft, ob noch Formatierungsfehler übrig sind, die das Tool nicht automatisch lösen konnte, und in diesem Fall den Commit weiterhin ablehnt.
#!/usr/bin/env bash
# pre-commit hook: auto-fix formatting, then re-stage the fixed files
set -euo pipefail
staged_php=$(git diff --cached --name-only --diff-filter=ACM -- '*.php')
if [ -z "$staged_php" ]; then
exit 0
fi
# Auto-fix formatting in place on the working-directory copies
php-cs-fixer fix --using-cache=no $staged_php
# Re-add the now-formatted files so the fix is part of this commit
git add $staged_php
# Fail only if something remains that the fixer could not resolve
if ! php-cs-fixer fix --dry-run --diff $staged_php > /dev/null 2>&1; then
echo "Formatting could not be fully auto-fixed, please review manually."
exit 1
fi
exit 0
5. Schnelle Unit-Tests im pre-commit Hook
Unit-Tests im pre-commit Hook sind sinnvoll, solange sie schnell genug bleiben, um den Commit-Vorgang nicht spürbar zu verzögern. Die praktikable Grenze liegt bei den meisten Teams bei wenigen Sekunden, keinesfalls bei mehreren Minuten. Statt die gesamte Testsuite auszuführen, beschränkt sich ein pre-commit Hook idealerweise auf Tests, die direkt zu den gestagten Dateien gehören, etwa über eine Namenskonvention wie ClassNameTest oder eine explizite Zuordnung in der Testkonfiguration.
Integrationstests, die eine Datenbank, einen externen Dienst oder einen vollständigen Applikations-Bootstrap benötigen, gehören in der Regel nicht in den pre-commit Hook, sondern bleiben Aufgabe der CI-Pipeline nach dem Push. Ein pre-commit Hook, der versucht, die komplette Testsuite abzudecken, wird über kurz oder lang deaktiviert oder mit --no-verify umgangen, weil er den Entwicklungsfluss zu stark unterbricht. Schnelle, gezielte Unit-Tests direkt an der Änderung liefern den besten Kompromiss zwischen Sicherheit und Geschwindigkeit.
6. Geschwindigkeit: warum langsame Hooks Entwickler frustrieren
Ein pre-commit Hook, der spürbar wartet, wird schnell zum größten Widerstand im täglichen Workflow, weil Commits typischerweise sehr häufig erstellt werden, oft mehrfach pro Stunde. Schon ein Hook, der zehn Sekunden benötigt, summiert sich über einen Arbeitstag zu erheblicher Wartezeit, und Entwickler beginnen fast zwangsläufig, nach Wegen zu suchen, ihn zu umgehen, etwa mit git commit --no-verify. Sobald dieser Reflex zur Gewohnheit wird, ist die eigentliche Schutzfunktion des Hooks faktisch außer Kraft gesetzt.
Die wirksamste Maßnahme gegen langsame Hooks ist, ausschließlich die gestagten Dateien statt des gesamten Repositorys zu prüfen, wie bereits bei Linting und Formatierung beschrieben. Zusätzlich hilft ein expliziter Timeout, der einen hängenden Prozess nach wenigen Sekunden abbricht, statt den Entwickler unbegrenzt warten zu lassen. Teure Prüfungen wie ein vollständiger statischer Analyzer über das gesamte Projekt gehören in den pre-push Hook oder die CI-Pipeline, nicht in den pre-commit Hook, der bei jedem einzelnen Commit läuft.
#!/usr/bin/env bash
# pre-commit hook: bound total runtime and only touch staged files
set -euo pipefail
readonly TIMEOUT_SECONDS=8
staged_files=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$staged_files" ]; then
exit 0
fi
# Wrap the whole check in a hard timeout so a hanging tool never blocks a commit
if ! timeout "$TIMEOUT_SECONDS" bash -c '
files="$1"
for f in $files; do
case "$f" in
*.php) php -l "$f" > /dev/null ;;
esac
done
' _ "$staged_files"; then
echo "pre-commit checks exceeded ${TIMEOUT_SECONDS}s or failed, aborting commit."
exit 1
fi
exit 0
7. Hand-geschriebene Shell-Skripte: Flexibilität mit Wartungslast
Ein selbst geschriebenes Shell-Skript als pre-commit Hook bietet uneingeschränkte Flexibilität: Jede beliebige Logik lässt sich direkt in Bash, Python oder einer anderen Sprache umsetzen, ohne fremde Abhängigkeiten. Für kleine Teams mit einem einzigen Tool-Stack, etwa ausschließlich PHP, ist ein kompaktes, selbst gepflegtes Skript oft schnell eingerichtet und leicht nachvollziehbar, weil der gesamte Ablauf in einer einzigen, lesbaren Datei steht.
Mit wachsender Zahl an Tools und Sprachen im Projekt wächst allerdings auch die Wartungslast: Jedes neue Tool muss von Hand in das Skript integriert werden, inklusive eigener Fehlerbehandlung, eigener Dateifilterung und eigener Versionsverwaltung der eingesetzten Linter. Läuft ein Team mit unterschiedlichen Betriebssystemen, treten zusätzlich subtile Unterschiede zwischen Bash-Versionen auf macOS und Linux auf. Genau an diesem Punkt lohnt sich der Blick auf ein etabliertes Framework, das diese Wiederholarbeit bereits gelöst hat.
8. Das pre-commit Framework als sprachunabhängiger Multi-Tool-Runner
Das pre-commit Framework, verfügbar über pre-commit.com, löst genau das Wartungsproblem hand-geschriebener Skripte: Es ist selbst sprachunabhängig, verwaltet aber die Umgebungen der einzelnen Tools automatisch, unabhängig davon, ob ein Tool in Python, Go, Rust oder Node.js geschrieben ist. Statt jedes Tool manuell zu installieren und im Skript aufzurufen, deklariert eine YAML-Datei .pre-commit-config.yaml lediglich, welche Hooks aus welchem Repository in welcher Version verwendet werden sollen.
Beim ersten Lauf lädt das Framework die passenden isolierten Umgebungen für jedes Tool automatisch herunter und cached sie, sodass spätere Läufe schnell bleiben. Jedes Team-Mitglied erhält so garantiert dieselbe Tool-Version, ohne dass eine globale Installation auf jedem Rechner nötig wäre. Ergänzend übernimmt das Framework auch das automatische Filtern auf gestagte Dateien, das Parallelisieren mehrerer Hooks und eine einheitliche, gut lesbare Ausgabe, Dinge, die bei einem hand-geschriebenen Skript jeweils selbst implementiert werden müssten.
# .pre-commit-config.yaml, versioned in the repository, shared by the whole team
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-merge-conflict
- repo: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer
rev: v3.57.0
hooks:
- id: php-cs-fixer
args: [--config=.php-cs-fixer.php]
- repo: local
hooks:
- id: php-lint
name: PHP syntax check
entry: php -l
language: system
files: \.php$
# Install once per clone, then it runs automatically on every commit
# pre-commit install
9. Hand-geschriebene Skripte vs. pre-commit Framework im Vergleich
Beide Ansätze lösen dasselbe Grundproblem, unterscheiden sich aber erheblich in Wartbarkeit, Portabilität und Startaufwand. Die folgende Übersicht fasst die wichtigsten Unterschiede zusammen.
| Dimension | Hand-geschriebenes Skript | pre-commit Framework |
|---|---|---|
| Sprachunabhängigkeit | Muss pro Tool manuell integriert werden | Ja, einheitlich über alle Sprachen |
| Tool-Versionierung | Abhängig von lokaler Installation | Fest in der YAML-Datei gepinnt |
| Einrichtungsaufwand | Gering bei einem einzigen Tool | Einmalige Installation, danach automatisch |
| Wartung bei neuen Tools | Manuelles Einbauen ins Skript nötig | Neuer Eintrag in der YAML-Datei genügt |
| Plattformübergreifend | Bash-Unterschiede zwischen macOS/Linux möglich | Isolierte Umgebungen pro Plattform |
| Volle Kontrolle über Logik | Uneingeschränkt frei | Über local-Hooks weiterhin möglich |
Für kleine, homogene Projekte mit einem einzigen Tool-Stack bleibt ein schlankes, hand-geschriebenes Skript eine legitime Wahl. Sobald mehrere Sprachen, mehrere Tools oder ein wachsendes Team im Spiel sind, reduziert das pre-commit Framework die Wartungslast erheblich und stellt sicher, dass jeder Entwickler dieselben Tool-Versionen nutzt, ohne dass jemand die Installation von Hand pflegen muss.
Mironsoft
Git-Workflows, Code-Qualität und CI/CD-Automatisierung für Entwicklerteams
Pre-Commit-Hooks, die Entwickler wirklich nutzen?
Wir richten schnelle, gezielte pre-commit Hooks für euer Team ein, von einem schlanken Shell-Skript bis zum vollständigen pre-commit Framework, mit Linting, Formatierung und schnellen Tests, die den Workflow nicht ausbremsen.
Hook-Einrichtung
pre-commit Framework oder Hand-geschriebene Skripte, passend zu eurem Stack
Linting & Formatierung
Automatische Prüfungen und Auto-Fix direkt vor dem Commit
Performance-Tuning
Hooks so schnell machen, dass niemand sie umgehen will
10. Zusammenfassung
Pre-Commit-Hooks sind der früheste sinnvolle Kontrollpunkt im Git-Workflow, weil sie laufen, bevor der Commit überhaupt entsteht, und ihn bei einem Fehler vollständig verhindern können. Linting, automatische Formatierung und schnelle, gezielte Unit-Tests decken die meisten praxisrelevanten Fälle ab, solange sie sich konsequent auf die gestagten Dateien beschränken. Geschwindigkeit ist dabei keine Nebensache: Ein Hook, der spürbar wartet, wird über kurz oder lang mit --no-verify umgangen und verliert damit seine eigentliche Schutzfunktion.
Hand-geschriebene Shell-Skripte bieten volle Kontrolle und eignen sich gut für kleine, homogene Projekte. Das pre-commit Framework von pre-commit.com reduziert dagegen die Wartungslast erheblich, sobald mehrere Sprachen und Tools im Spiel sind, weil es Umgebungen, Versionierung und Dateifilterung zentral über eine einzige YAML-Datei regelt. Die richtige Wahl hängt weniger vom persönlichen Geschmack ab als von der Größe und Heterogenität des Projekts.
Pre-Commit-Hooks für Code-Qualität, das Wichtigste auf einen Blick
Frühester Kontrollpunkt
Läuft vor der Commit-Erstellung und kann sie bei Regelverstößen vollständig verhindern.
Sinnvolle Prüfungen
Linting, automatische Formatierung mit Re-Add und schnelle, gezielte Unit-Tests an den gestagten Dateien.
Geschwindigkeit zählt
Nur gestagte Dateien prüfen, Timeouts setzen, teure Prüfungen in pre-push oder CI verschieben.
pre-commit Framework
Sprachunabhängiger Multi-Tool-Runner über .pre-commit-config.yaml, löst die Wartungslast hand-geschriebener Skripte.