Wie nutze ich Git-Hooks?

Heute geht es um ein etwas fortgeschritteneres Thema mit Git: das Schreiben von Git-Hooks. Ich empfehle diesen Beitrag erst zu lesen, wenn ihr zumindest die Grundlagen von Git verstanden habt und einfache Befehle ausführen könnt. Falls ihr eine Auffrischung braucht, was Git angeht, ich habe bereits einen Beitrag für Git Anfänger geschrieben: 

Was sind Git-Hooks?

Git-Hooks sind Skripte, die Git automatisch vor bestimmten Ereignissen ausführt. Diese Ereignisse können zum Beispiel commit, push oder receive sein. Git-Hooks können somit individualisiertes Verhalten und Aktionen an bestimmten Punkten des Entwicklungszyklus von Git triggern.
Mithilfe dieser Skripte können zum Beispiel Fehler entdeckt werden oder ein bestimmter Coding-Standard eingehalten werden.
Ein Coding-Standard erklärt dem Entwickler, wie der Code aussehen soll. Dazu werden verschiedene Regeln angelegt, diese können beispielsweise vorschreiben, wie viele Zeichen in einer Zeile sein dürfen, welche Kommentare verwendet werden dürfen oder wie die Leerzeichen platziert werden sollen.

Warum solltest du Git-Hooks nutzen?

Git-Hooks sind für zwei große Anwendungsfälle sinnvoll zu nutzen:

Um deinen Codeinhalt zu kontrollieren

Gerade wenn du im Team arbeitest, ist es sinnvoll, wenn der Code auf einem bestimmten Standard basiert, in diesem die Platzierung von Leerzeichen, Klammern und Variablenbenennungen etc. festgelegt sind. Es kann, wenn jeder seinen Code unterschiedlich schreibt, bestenfalls zu nervigen Unterschieden beim Mergen kommen und schlimmstenfalls zu Fehlern führen.
Aber auch für dich selbst ist es sinnvoll deinen Code nach einheitlichen Regeln zu schreiben, damit dieser gut übersichtlich strukturiert ist.

Um deine Committ-Nachrichten zu kontrollieren

Bei Committ-Nachrichten ist es nicht ganz so gravierend, wenn bestimmte Standards nicht eingehalten werden. Trotzdem ist es auch hier sinnvoll, wenn zum Beispiel festgelegt werden kann, dass Commit-Nachrichten eine Mindestanzahl an Zeichen besitzen müssen und dass diese einheitlich in Englisch geschrieben sein sollen. 

Wie sind Git-Hooks aufgebaut?

Die Hooks befinden sich in dem .git/hooks Ordner deines Git Repositorys. Standardmäßig stehen in diesem Verzeichnis schon mehrere verschiedene Beispielskripte drin.

Um ein Hook benutzen zu können, musst du die .sample-Endung löschen oder einfach ein neues Skript mit einem der Dateinamen ohne Endung anlegen. Die .sample-Endung verhindert, dass die Skripte standardmäßig ausgeführt werden.
Die standardmäßig vorhandenen Skripts sind größtenteils in Shell oder Perl geschrieben, aber im Grunde kannst du für dein eigenes Skript jede Sprache benutzen, die ausführbar ist. Die Shebang Zeile (#!/bin/sh) in jedem Skript definiert wie die Datei interpretiert werden soll. Zum Beispiel, wenn du dein Skript in Python schreiben willst, kannst du schreiben: #!/usr/bin/env python.
Möchtest du zum Beispiel deine Default-Committ-Nachricht von Git ändern, kannst du folgendes Skript ausführen. Es wird jedes Mal, wenn die eigentliche Committ-Nachricht ausgegeben wird, der folgende Hook aufgerufen und die dort hinterlegte Nachricht ausgegeben, bevor die richtige Committ-Nachricht folgt.

prepare-commit-msg Datei

Wichtig zu wissen ist, dass Hooks lokal in deinem Repository liegen und nicht auf andere Repositories rüber kopiert werden, falls du dieses klonst. Auch wird das .git/hooks Verzeichnis nicht in der Versionskontrolle angezeigt. Dies bringt leider einige Probleme mit sich, wenn man im Team arbeitet und ein bestimmtes Hook genutzt werden soll. Jedes Teammitglied muss selbstständig darauf achten, dass seine Hooks dem aktuellen Stand entsprechen.
Eine mögliche Lösung für dieses Problem ist, deine Hooks einfach in deinem normalen Projektverzeichnis unterzubringen, sodass diese wie jede andere Datei mit in der Versionskontrolle erscheinen. Dafür kannst du entweder ein symlink zum ursprünglichen Verzeichnis erstellen oder einfach jedes Mal, wenn dein Hook aktualisiert wird die neue Version an die andere Stelle in deinem Projektverzeichnis kopieren.

Welche Arten von Git-Hooks gibt es?

Du kannst Hooks lokal oder serverseitig anlegen.
Im Folgenden werde ich einige der am häufig genutzten Hooks vorstellen. Natürlich gibt es noch viel mehr Hooks und man könnte auch diese viel genauer beschreiben, aber zur Vereinfachung wird dies weggelassen.

Lokale Hooks

Pre-Commit:

Wird jedes Mal ausgeführt, wenn du git commit ausführst und bevor Git nach einer Committ-Nachricht fragt.
Der Hook ist zum Beispiel gut geeignet um den vorhandenen Code nach Coding-Standards zu kontrollieren, wie zum Beispiel Leerzeichen zu bemerken und zu beheben. Prinzipiell kannst du hier alles was du willst ausführen lassen.

Prepare-commit-msg:

Kann genutzt werden, um eine andere Committ-Nachricht anzeigen zulassen als standardmäßig eingestellt ist. Dies kann beispielsweise bei einer bestimmten Art von Committs z.B. gemergten Committs sinnvoll sein, um separat eine automatische Committ-Nachricht sich ausgeben zu lassen.

Commit-msg:

Ist ähnlich dem prepare-commit-msg-Hook, nur dass dieser statt vor der Committ-Nachricht erst nachdem der Benutzer eine Committ-Nachricht eingegeben hat, aufgerufen wird. Dies kann zum Beispiel dafür genutzt werden den Nutzer zu warnen, dass seine Nachricht nicht den Teamstandards entspricht.

Post-commit:

Wird direkt nach dem commit-msg-Hook aufgerufen. Es wird hauptsächlich nur für Benachrichtigungszwecke aufgerufen, da es keinen Einfluss auf die git commit-Operation hat.

Post-checkout:

Funktioniert ähnlich wie der post-commit-Hook, wird aber stattdessen aufgerufen, immer, nachdem eine Referenz erfolgreich ausgecheckt wurde. Dies ist sinnvoll, um dein Verzeichnis vor generierten Dateien zu leeren, um Verwirrungen zu vermeiden.

Pre-rebase:

Wird aufgerufen, bevor git rebase Änderungen ausführt und ist somit gut geeignet, um nicht gewollte schlechte Ereignisse zu verhindern. Es kann zum Beispiel eine Warnbenachrichtigung vorm Rebasen ausgegeben und das Rebasing verhindern werden.

Serverseitige Hooks

Alle drei folgenden Hooks interagierten mit verschiedenen Stufen des git push Prozesses.

Pre-receive:

Wird jedes Mal ausgeführt, wenn jemand git push benutzt, um Committs zum Repository zu pushen. Dies ist nützlich, wenn geprüft werden soll, ob ein Nutzer die richtige Erlaubnis besitzt, um Änderungen zu pushen oder um Änderungen abzulehnen.

Update:

Wird nach pre-receive aufgerufen und funktioniert auf ähnliche Weise. Es wird aufgerufen, bevor geupdated wird. Dabei wird update für jeden Branch separat ausgeführt. Der Vorteil von update gegenüber pre-receive ist, dass für jeden Commit separat entschieden werden kann, ob dieser abgelehnt wird oder nicht.

Post-receive:

Wird nach einem erfolgreichen Push aufgerufen und ist ebenfalls sinnvoll, um Benachrichtigungen auszugeben.

Kurz zusammengefasst, erklärt euch die folgende Grafik nochmal, wie die Abgrenzung von lokal-serverseitig und pre- und post-Hooks funktioniert.

Bild von: https://wilsonmar.github.io/git-hooks/ by Sarah Goff-Dupont of Atlassian

Wie führe ich Coding-Standards als Git-Hook in PHP aus?

1. Schreiben des Hooks

Zuerst musst du dir überlegen, welche Art von Hook du eigentlich schreiben willst. Wenn du wie hier im Beispiel Fehler im Code angemerkt und gefixt bekommen möchtest, ist ein pre-commit-Hook sinnvoll. Somit werden die Änderungen im Code ausgeführt, bevor committet wird, und es kann selbstständig eine eigene Nachricht noch mitgegeben werden. Von dem Fixen selbst merkt man fast nichts.
Damit Fehler und Verstöße erkannt werden können, kann entweder direkt im Hook eine Regel geschrieben werden, oder die etwas ordentlichere Variante, wenn du vorhast mehrere Fehler zu finden, du benutzt einen Coding-Standard.


Um mit einen bestimmten Coding-Standard Fehler zu erkennen und zum Teil auch fixen zu können, können verschiedene Tools verwendet werden. Die Wahl des Tools hängt natürlich sehr davon ab, für welchen Verwendungszweck du es brauchst und für welche Programmiersprache.
Ich habe im Fall dieses Beispiels den PHP CodeSniffer verwendet, dieser in PHP-Dateien Fehler erkennen und fixen kann.
Der PHP CodeSniffer besteht aus zwei Teilen: phpcs, dem Code-Sniffer, der die Verstöße entdeckt, und phpcbf, dem Code-Beautifier, der einige Fehler automatisch korrigieren kann.

pre-commit Datei

Ich habe mich im Beispiel dazu entschieden, meinen Hook in Shell zu schreiben. Bash spezifiziert hier die Art der Shell.
Da der pre-commit-Hook ja erst beim Stagen der Dateien aufgerufen wird und dann die Fehler vorgemerkt und gefixt werden, sind die Dateien noch in der ursprünglichen Form vorgemerkt. Erst durch das erneute Vormerken aller Dateien, die zuvor in der Variable FILES gemerkt wurden, werden auch die Änderungen mit vorgemerkt.
Um unseren Hook aufrufen zu können, ist es notwendig einfach nur die Dateien über die Kommandozeile zu stagen und zu committen.

2. Festlegen des Coding-Standards

Der PHP CodeSniffer hat im Hintergrund bereits mehrere PHP Coding-Standards hinterlegt, von diesen einer immer ausgewählt ist.
Alle installierten Coding-Standards kannst du dir übrigens über phpcs -i in der Kommandozeile anzeigen lassen.
Den Standard kannst du über phpcs -config-set <default_standard> wechseln.
Ein Coding-Standard besteht aus einem oder mehreren Sniffs, in diesem die Regeln für den jeweiligen Coding-Standard definiert sind. Die Sniffs deines gerade aktuell ausgewählten Standards kannst du dir mit phpcs -e anzeigen lassen.


Falls bereits ein Coding-Standard existiert, der zur Lösung deines Problems genügt, kannst du gerne einen der vorgegeben Coding-Standards nutzen.
Wenn es keinen passenden gibt, musst du dir einen eigenen Coding-Standard schreiben. Aber keine Angst, dies ist nicht so schwer, wie es sich anhört.

Zum Schreiben eines eigenen Standards benötigst du zwei Dinge:

1. Eine „ruleset.xml"-Datei, diese den Coding-Standard beschreibt

ruleset.xml Datei

Unter „Name" kannst du den Namen deines Coding-Standards eintragen und unter „Rule" ist es möglich weitere Sniffs, die zu anderen Standards gehören zu inkludieren, damit diese mit ausgeführt werden. Wie bereits erwähnt, ist es nur möglich gleichzeitig einen Coding-Standard ausgewählt zu haben. Wenn du also Sniffs, die zu einem anderen Standard gehören, ausführen möchtest, musst du diese separat einfügen.

2. Eine oder mehrere Dateien in der das Sniff steht

Unter deinem neu angelegten Standard kannst du einen Ordner „Sniffs" anlegen, unter diesem du deine Dateien mit den Sniffs ablegen kannst.

Wichtig ist, dass du deiner Datei mit dem Sniff die Endung „Sniff" geben musst, also z.B. BeispielSniff.php, damit diese als Sniff erkannt wird.

Dein Sniff besteht aus einer Klasse, die zwei Funktionen beinhaltet:

register()
in dieser festlegt wird an welcher Art von Tokentyp der Sniff interessiert ist.
Du kannst dir vorstellen, dass dein Programmcode aus verschiedenen Tokens mit jeweils dazugehörigen Typen zusammengesetzt ist. Es gibt welche für Variablen, für Leerzeichen, für Kommentare etc.. Je nachdem was dein Sniff prüfen soll muss ein anderer Typ angegeben werden.

process()
in dieser sich das eigentliche Programm befindet, das die Tokens durchläuft und die jeweiligen Tokentypen, die wir in register() festgelegt haben, überprüft, sich Fehlerfälle vormerkt und auch verbessert.

Ich werde an dieser Stelle auf das Schreiben von Sniffs nicht genauer eingehen. Falls du Interesse hast, selbst einen eigenen Coding-Standard mithilfe des PHP CodeSniffers zu schreiben, kannst du zum Beispiel hier eine Anleitung dazu finden.

Fazit

Git-Hooks können dir beim Programmieren mit Git helfen deinen Code und deine Nachrichten beim Committen sauber und einheitlich zu halten. Es gibt viele verschiedene Arten von Hooks, die dir je nach Anwendungsfall anders behilflich sein können.
Aber eines ist sicher, das Nutzen von Hooks ist nie ein Nachteil! Deswegen, wenn du schon etwas erfahrener im Umgang mit Git bist, probiere doch ruhig mal das Schreiben von einfachen Git-Hooks aus.

Wenn du mehr über Git-Hooks erfahren möchtest, empfehle ich folgende Artikel:

By accepting you will be accessing a service provided by a third-party external to https://www.nrml.de/