Workflow-Ereignisse per Skript auslösen Git-Automatisierung per Hook

Autor / Redakteur: Mirco Lang / Stephan Augsten

Mit Hooks bietet Git ein sowohl simples als auch mächtiges Konzept: Jede Statusänderung im Git-Workflow kann dabei beliebige Aktionen auslösen – oder auch den Workflow unterbrechen.

Firma zum Thema

In Verbindung mit klassischen Git-Kommandos lassen sich via Hooks zahlreiche Aktivitäten automatisieren.
In Verbindung mit klassischen Git-Kommandos lassen sich via Hooks zahlreiche Aktivitäten automatisieren.
(© ribkhan - stock.adobe.com)

Die Arbeit mit Git ist nicht ganz trivial. Selbst einfache Dinge wie das Zurücknehmen eines Arbeitsschritts gehen weit über ein einfaches Undo-Kürzel hinaus. Sobald aber ein ganzes Team in einem Repository werkelt, wachsen die Problemquellen in den Himmel.

Einfache „harte“ Probleme wie Merge-Konflikte oder fehlende Updates behandelt Git dabei selbst sehr ordentlich und hilft mit erstaunlich anschaulichen Fehlermeldungen. Bei komplexeren oder „weichen“ Problemen hingegen ist auch Git machtlos.

Ein Beispiel: Angenommen, das Team hat sich auf eine bestimmte Commit-Policy geeinigt, etwa um Commit-Nachrichten verständlich, aussagekräftig und mit vorgegebenen Formatierungen zu schreiben. Dann sind ist man zunächst auf Gedeih und Verderb auf die Zuverlässigkeit aller Teammitglieder angewiesen. Genau an dieser Stelle könnten zum Beispiel Hooks in Aktion treten und korrekte Commit-Nachrichten erzwingen – oder sagen wir besser dazu ermutigen, wie wir anhand eines konkreten Beispiels später zeigen.

Git Hooks sind im Grunde etwas sehr Simples: Skripte, die bei bestimmten Workflow-Ereignissen automatisch ausgeführt werden. Die Skripte können dabei in einer beliebigen Skriptsprache geschrieben werden, etwa Python für umfangreiche Manipulationen oder als Bash-Skript für kleinere Eingriffe. Mit Workflow-Ereignissen sind die einzelnen Schritte wie commit und push auf Client- oder auch Updates auf Server-Seite gemeint.

Der Einsatz ist so simpel wie das Konzept: Die Skripte liegen allesamt im Verzeichnis „./git/hooks“ unter fixen, sprechenden Namen wie „pre-push“. Und der Einfachheit halber finden sich für viele Skripte standardmäßig Beispieldateien im hooks-Ordner. Für einen simplen Test genügt esm einfach ein Kommando wie „echo foobar“ in das pre-push-Skript zu schreiben; schon zeigt der Terminal die Meldung „foobar“, bevor die push-Anweisung tatsächlich ausgeführt wird.

Hooks lassen sich für triviale wie komplexe Aufgaben einsetzen. Das beginnt bei simplen Statusmeldungen, geht über Vorgaben für automatisierte (Merge-)Commit-Nachrichten oder die Bereinigung von temporären Dateien und reicht bis zum Aufbau einer persönlichen Continuous-Integration-Lösung.

Die verschiedenen Hooks

Für die einzelnen Hooks sind drei Aspekte relevant: Wann genau werden sie ausgeführt, also von welchem git-Befehl getriggert? Welche Parameter werden übergeben? Und läuft das Skript auf Client-Seite oder im Remote-Repo? Im Folgenden zeigen wir die wichtigsten Hooks entlang des üblichen Git-Workflows. Insgesamt gibt es rund 28 Hooks, hier werden lediglich 15 behandelt, die regelmäßiger zum Einsatz kommen.

Wir beginnen eher unüblich mit Hooks, die durch „git am“ ausgelöst werden, also beim Verarbeiten von Patches via Mail. Hier gibt es drei Hooks:

  • applypatch-msg: bekommt als Parameter den Namen der Datei mit der Commit-Nachricht, wird angewandt bevor die Commit-Nachricht gesetzt wird und kann sie entsprechend ändern.
  • pre-applypatch: wird nach dem Anwenden des Patches, aber vor dem Commit angewandt (etwa zum Prüfen von Status).
  • post-applypatch: kann nach dem Commit genutzt werden, um Benachrichtigungen zu verteilen.

Der Befehl „git commit“ ruft insgesamt vier Hooks auf. Es beginnt mit dem Skript …

  • pre-commit: läuft noch vor der Commit-Nachricht und ohne Parameter – beispielsweise zum Prüfen der gemachten Änderungen im Repo.
  • prepare-commit-msg: wird nach der Übergabe der Standard-Commit-Nachricht, vor dem Start des Commit-Nachricht-Editors gestartet und bekommt (bis zu) drei Parameter: Name der Datei mit der Nachricht, dessen Quelle sowie gegebenenfalls ihren Hash. Mit diesem Hook ist es zum Beispiel möglich, Dinge wie Issue-Bezeichner in Commit-Nachrichten einbauen.
  • commit-msg: wird nach dem Nachrichten-Editor aufgerufen und kann abermals die Nachricht prüfen/manipulieren – dieser Hook wird unten auch für eine Commit-Policy verwendet.
  • post-commit: ein Hook, der nach im Workflow nach dem abgeschlossenen Commit aufgerufen wird und entsprechend vor allem für Benachrichtigungen genutzt wird – etwa eine Mail an den Teamverteiler, dass etwas committed wurde.

Mit „git rebase“ wird nur ein Hook getriggert:

  • pre-rebase: erhält die Namen des Upstreams und des bearbeiteten Branches als Parameter und wird vor dem Rebase ausgeführt wird. An dieser Stelle lassen sich Rebases zum Beispiel komplett unterbinden, Warnungen ausgeben und so weiter.

Die Befehle „git checkout“ und „git clone“ lösen jeweils ein und denselben Hook aus:

  • post-checkout: kennt drei Parameter, nämlich letzter HEAD, aktueller HEAD und ob es ein Datei- oder Branch-Checkout war. An diesem Punkt ist es zum Beispiel möglich, temporäre Dateien löschen zu lassen, Tools für die Arbeit in bestimmten Branches zu starten und so weiter.

„git merge“ und „git pull“ starten das folgende Skript:

  • post-merge: läuft dem Namen gemäß nach dem Merge/Pull und kann den Workflow somit auch nicht unterbrechen. Es eignet sich beispielsweise, um Dinge zu regeln, die Git nicht packt, zum Beispiel das Setzen von Zugriffsberechtigungen.

Der folgende Hoos wird mittels „git push“ ausgelöst:

  • pre-push: kennt als Parameter Namen und Ort des entfernten Repos. Zusätzlich können Infos über die Kommandozeile übergeben werden, um etwa Hashes zu prüfen.

Nun gibt es einen Abstecher auf die Server-Seite: „git-receive-pack“, also das Empfangen eines Pushs, lässt sich mit insgesamt vier Hooks verknüpfen.

  • pre-receive: läuft vor jeglichem Update und könnte zum Beispiel Nutzerrechte prüfen.
  • update: läuft ebenfalls noch vor dem eigentlichen Update, kennt allerdings Namen von altem und neuem Objekt sowie die Referenz der Änderungen – und läuft für jede Änderung separat. So könnten also Prüfungen wie bei „pre-receive“ durchgeführt werden, allerdings für jeden gepushten Branch einzeln.
  • post-receive: startet dann nachdem tatsächlich alle Referenzen aktualisiert wurden – der passende Zeitpunkt, um zum Beispiel abermals Benachrichtigungen zu senden oder ein Continuous-Integration-System zu starten.
  • post-update: wird mit der Liste der aktualisieren Referenzen aufgerufen. Dies kann verwendet werden, um beliebige repositoryweite Bereinigungsaufgaben zu implementieren.

Zu den ausgelassenen Hooks gehören etwa solche für Preforced-Repos, Mail-Versand oder Cache-Operationen.

Commit-Policy aufbauen

Hier folgt ein stark vereinfachtes Beispiel aus der Praxis, basierend auf einem Repo mit englischen und deutschen Varianten von Texten. Darin sollen Commit-Nachrichten mit „nurdeutsch“ anfangen, sofern eben nur die de-Variante verändert wurde (weil ansonsten ein Übersetzungsworkflow gestartet wird).

Ist das nurdeutsch-Tag gesetzt, wird committed. Falls nicht, soll geprüft werden, ob es sich um eine gerade Anzahl an Dateien handelt – als stark vereinfachtes Indiz dafür, dass zu jeder de-Datei auch eine en-Datei verändert wurde. Falls ja, wird ebenfalls commited. Zur Veranschaulichung der Möglichkeiten werden zudem PNG-Dateien aus der Prüfung ausgenommen (im gedachten Repo existieren lediglich de- und en-Textdateien sowie PNG-Bilder), was ein Schlaglicht auf das wichtige Cache-Thema wirft.

Zudem ist noch eine Nutzerinteraktion eingebaut: Wenn das nurdeutsch-Tag fehlt und es sich um eine ungerade Anzahl an (Text-)Dateien handelt, muss der User entscheiden, ob der Commit dennoch durchgeführt werden soll – schließlich wäre auch das ein legitimes Anliegen, nämlich um den Übersetzungsworkflow anzustoßen.

Hier der Inhalt des „commit-msg“-Hooks:

# Nutzereingaben erlauben
exec < /dev/tty
# WENN Commit-Nachricht mit nurdeutsch beginnt.
if $(grep -q '^nurdeutsch' $1); then
   echo Es beginnt mit nurdeutsch - scheint okay, es wird committed
   exit
else
# WENN Anzahl der Nicht-PNG-Dateien ungerade ist.
if (( $(git diff --cached --name-only 2>/dev/null | grep -v *.png | sed 's/^.*\///' | wc -l) % 2 )); then
   echo "Kein nurdeutsch am Anfang UND ungerade Anzahl der Dateien!"
   read -e -p "Abbrechen (a) oder Commit ausführen (c)? " antwort
   case $antwort in
      a) echo Abbruch && exit 1;;
      c) echo Okay, fortfahren && exit;;
   esac
else
   echo Gerade Anzahl an Dateien, nurdeutsch am Anfang – es wird committed.
   exit
fi
exit
fi

Hier gibt es einige wichtige Aspekte neben Standard-Bash-Code: Zunächst müssen Nutzereingaben erlaubt werden, da git-Hooks nicht interaktiv sind – der „exec“-Befehl leitet die Standardeingabe entsprechend um. Ohne diese Umleitung würde die read-Interaktion später im Skript schlicht ignoriert.

In der ersten if-Abfrage findet sich die Variable „$1“, in der wie oben bereits erwähnt der Name der Datei mit der Commit-Nachricht gespeichert ist. Via grep wird dann auf ein „nurdeutsch“-Tag am Anfang der Nachricht geprüft. Analog kann man bei anderen Hooks auf weitere Parameter zugreifen.

Die zweite if-Abfrage enthält ein „git diff“ mit der cached-Option: Das ist hier wichtig, weil eben noch nicht committed wurde und die gewünschten Diff-Informationen entsprechend nur im Cache liegen. Auf solche, eventuell zwischengespeicherte Daten gilt es bei allen Hooks penibel zu achten.

Abgebrochen wird der Commit hier nur in einem Fall: Wenn der Nutzer dies in der read-Interaktion wünscht und mit „exit 1“ abgebrochen wird (also sobald das Skript mit einem Nicht-Null-Exit-Code endet).

Sobald einmal verstanden wurde wo die Hooks liegen, wann sie getriggert werden und auf einen Parameter zugegriffen werden kann, darf man der Kreativität freien Lauf lassen – kleine Stolpersteinchen wie Cache oder Interaktivität tauchen zwar durchaus auf, eigentlich sind Hooks aber ziemlich trivial.

(ID:47069055)

Über den Autor

 Mirco Lang

Mirco Lang

Freier Journalist & BSIler