Git-Hooks sind Skripte, die jedes Mal, wenn ein bestimmtes Ereignis in einem Git-Repository auftritt, automatisch ausgeführt werden. Mit ihnen kannst du das interne Verhalten von Git individuell anpassen und benutzerdefinierte Aktionen an wichtigen Punkten im Entwicklungszyklus auslösen.

Ausführung von Hooks während der Commit-Erstellung

Übliche Anwendungsfälle für Git-Hooks sind beispielsweise die Förderung der Einhaltung einer Commit-Richtlinie, die Änderung der Projektumgebung je nach Zustand des Repositorys und die Implementierung von Continuous Integration Workflows. Zumal die Skripte unbegrenzt anpassbar sind, kannst du Git-Hooks zur Automatisierung oder Optimierung nahezu jedes Aspekts deines Entwicklungs-Workflows nutzen.

In diesem Artikel erhältst du einen konzeptionellen Überblick über die Funktionsweise von Git-Hooks. Anschließend sehen wir uns einige der beliebtesten Hooks an, die sowohl in lokalen als auch serverseitigen Repositorys verwendet werden können.

Konzept-Überblick

Alle Git-Hooks sind gewöhnliche Skripte, die Git ausführt, wenn im Repository bestimmte Ereignisse auftreten. Aus diesem Grund sind sie äußerst einfach zu installieren und zu konfigurieren.

Hooks können sich entweder in lokalen oder serverseitigen Repositorys befinden und werden nur auf Aktionen in diesem Repository hin ausgeführt. Wir werden hier später noch einen genaueren Blick auf verschiedene Kategorien von Hooks werfen. Die Konfiguration, die wir im weiteren Verlauf des Abschnitts besprechen, ist sowohl für lokale als auch serverseitige Hooks anwendbar.

Installieren von Hooks

Hooks befinden sich in jedem Git-Repository im Verzeichnis .git/hooks. Beim Anlegen eines Repositorys legt Git in diesem Verzeichnis automatisch einige Beispielskripte ab. Wenn du einen Blick in .git/hooks wirfst, entdeckst du folgende Dateien:

applypatch-msg.sample pre-push.sample
commit-msg.sample pre-rebase.sample
post-update.sample prepare-commit-msg.sample
pre-applypatch.sample update.sample
pre-commit.sample

Diese Dateien repräsentieren die meisten der verfügbaren Hooks, die Erweiterung .sample beugt jedoch einer standardmäßigen Ausführung vor. Zur "Installation" eines Hooks musst du lediglich die Erweiterung .sample entfernen. Alternativ kannst du, wenn du ein von Grund auf neues Skript schreibst, eine neue Datei mit den obigen Dateinamen ohne die Erweiterung .sample hinzufügen.

Nehmen wir an, du möchtest einen einfachen prepare-commit-msg-Hook installieren. Entferne die Erweiterung .sample von diesem Skript und füge der Datei Folgendes hinzu:

#!/bin/sh
echo "# Please include a useful commit message!" > $1

Hooks müssen ausführbar sein. Wenn du den Hook neu erstellst, musst du daher eventuell die Dateiberechtigungen des Skripts anpassen. Um sicherzugehen, dass z. B. prepare-commit-msg ausführbar ist, würdest du folgenden Befehl ausführen:

chmod +x prepare-commit-msg

Jetzt sollte jedes Mal, wenn du git commit ausführst, diese Nachricht anstatt der standardmäßigen Commit-Nachricht angezeigt werden. Im Abschnitt zur Vorbereitung von Commit-Nachrichten sehen wir uns genauer an, wie dies funktioniert. Für den Moment genießen wir einfach erst einmal die Tatsache, dass wir einige interne Funktionen von Git individuell anpassen können.

Die nativen Sample-Skripte sind sehr nützliche Referenzen, denn sie dokumentieren die Parameter, die mit jedem Hook übergeben werden. (Sie variieren von Hook zu Hook.)

Skriptsprachen

Die integrierten Skripte sind hauptsächlich Shell- und Perl-Skripte, du kannst aber jede beliebige Skriptsprache verwenden, solange sie als Executable ausgeführt werden kann. Die Shebang-Zeile (#!/bin/sh) bestimmt im jeweiligen Skript, wie deine Datei interpretiert werden soll. Wenn du also eine andere Sprache verwenden möchtest, musst du hier lediglich den Pfad zu deinem Interpreter angeben.

Wir können zum Beispiel ein ausführbares Python-Skript in der Datei prepare-commit-msg erstellen, statt Shell-Befehle zu verwenden. Mit dem folgenden Hook kommen wir zum selben Ergebnis wie mit dem Shell-Skript aus dem letzten Abschnitt.

#!/usr/bin/env python
import sys, os
commit_msg_filepath = sys.argv[1]
with open(commit_msg_filepath, 'w') as f:
f.write("# Please include a useful commit message!")

Achte auf die erste Zeile, die sich verändert hat, um auf den Python-Interpreter zu verweisen. Und anstelle von $1, um auf das Argument zuzugreifen, das zuerst auf das Skript angewendet wurde, haben wir sys.argv[1] genommen (wie versprochen dazu später mehr).

Dieses Feature von Git-Hooks ist unglaublich nützlich, da du in der Sprache arbeiten kannst, die dir am liebsten ist.

Anwendungsbereich von Hooks

Hooks sind im jeweiligen Git-Repository lokal und werden mit dem Befehl git clone nicht mit in das neue Repository kopiert. Und da Hooks lokal sind, können sie von jedem, der Zugriff auf das Repository hat, geändert werden.

Dies bringt bedeutende Konsequenzen für die Konfiguration von Hooks für ein Entwicklerteam mit sich. Erstens musst du dafür sorgen, dass deine Hooks bei allen Teammitgliedern immer aktuell sind. Und zweitens kannst du die Entwickler nicht dazu zwingen, die Commits auf eine bestimmte Weise zu erstellen – du kannst sie nur dazu ermutigen.

Die Pflege von Hooks für ein Entwicklungsteam kann ein bisschen kniffelig sein, weil das Verzeichnis .git/hooks nicht mit dem Rest des Projekts geklont wird, noch der Versionskontrolle unterliegt. Eine einfache Lösung für beide Probleme besteht darin, unsere Hooks im tatsächlichen Projektverzeichnis zu speichern (oberhalb des .git-Verzeichnisses). Dann können wir sie editieren wie alle anderen versionskontrollierten Dateien. Um einen Hook zu installieren, können wir entweder in .git/hooks einen Symlink auf ihn erstellen, oder wir kopieren den Hook einfach jedes Mal ins Verzeichnis .git/hooks, wenn er aktualisiert wurde.

Ausführung von Hooks während der Commit-Erstellung

Als Alternative bietet Git einen Vorlagenverzeichnis-Mechanismus, der es vereinfacht, Hooks automatisch zu installieren. Alle Dateien und Verzeichnisse, die in diesem Vorlagenverzeichnis enthalten sind, werden jedes Mal in das .git-Verzeichnis kopiert, wenn wir git init oder git clone nutzen.

Alle lokalen Hooks, die im Folgenden beschrieben werden, können vom Verantwortlichen eines Repositorys angepasst oder auch komplett gelöscht werden. Es liegt bei jedem Teammitglied selbst, ob es einen Hook tatsächlich nutzt oder nicht. Mit diesem Gedanken im Hinterkopf betrachten wir Git-Hooks am besten als ein bequemes Entwickler-Tool statt als eine strikt vorgegebene Entwicklungsrichtlinie.

Trotzdem ist es möglich, mithilfe von serverseitigen Hooks Commits abzulehnen, die nicht dem festgelegten Standard entsprechen. Wir gehen später in diesem Artikel noch genauer darauf ein.

Lokale Hooks

Lokale Hooks beeinflussen nur das Repository, in dem sie sich befinden. Hier sollten wir uns in Erinnerung rufen, dass jeder Entwickler seine eigenen lokalen Hooks anpassen kann. Sie können also nicht als Möglichkeit genutzt werden, eine Commit-Richtlinie zu erzwingen. Allerdings erleichtern sie es Entwicklern, bestimmte Richtlinien zu wahren.

In diesem Abschnitt betrachten wir die sechs hilfreichsten lokalen Hooks näher:

  • pre-commit
  • prepare-commit-msg
  • commit-msg
  • post-commit
  • post-checkout
  • pre-rebase

Mit den ersten vier Hooks kannst du an verschiedenen Punkten des Commit-Lebenszyklus andocken, die letzten beiden ermöglichen einige zusätzliche Aktionen oder Sicherheitschecks für die Befehle git checkout und git rebase.

Mit allen pre-Hooks kannst du den bevorstehenden Vorgang beeinflussen. Die post-Hooks dienen nur als Benachrichtigungen.

Wir werden darüber hinaus auch einige nützliche Techniken für das Parsen von Hook-Argumenten und für die Abfrage von Informationen über das Repository mithilfe von Low-Level-Git-Befehlen kennenlernen.

Vor dem Commit

Das Skript pre-commit wird immer ausgeführt, wenn du den Befehl git commit angibst, bevor Git den Entwickler zur Eingabe einer Commit-Nachricht auffordert oder ein Commit-Objekt erzeugt. Mit diesem Hook kannst du dir den Snapshot, der gerade committet werden soll, genauer ansehen. Nehmen wir einmal an, du willst einige automatisierte Tests durchführen, um sicherzustellen, dass der Commit bei bestehenden Funktionen keine Fehler verursacht.

Auf das pre-commit-Skript werden keine Argumente angewendet und bei einem Nicht-Null-Status am Ende wird der gesamte Commit abgebrochen. Werfen wir einen Blick auf eine vereinfachte (und ausführlichere) Version des integrierten pre-commit-Hooks. Mit diesem Skript werden alle Commits mit Leerraum-Fehlern, wie für git diff-index definiert, abgebrochen. (Nachfolgende Leerräume, Zeilen mit nur einem Leerraum sowie einem Leerzeichen gefolgt von einem Tab innerhalb der ersten Einrückung einer Zeile werden standardmäßig als Fehler gezählt.)

#!/bin/sh
# Check if this is the initial commit
if git rev-parse --verify HEAD >/dev/null 2>&1
then
echo "pre-commit: About to create a new commit..."
against=HEAD
else
echo "pre-commit: About to create the first commit..."
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
# Use git diff-index to check for whitespace errors
echo "pre-commit: Testing for whitespace errors..."
if ! git diff-index --check --cached $against
then
echo "pre-commit: Aborting commit due to whitespace errors"
exit 1
else
echo "pre-commit: No whitespace errors :)"
exit 0
fi

Zur Verwendung von git diff-indexmüssen wir klären, mit welcher Commit-Referenz den Index vergleichen. Normalerweise ist dies HEAD, doch HEAD existiert noch nicht, wenn der erste Commit erstellt wird, sodass wir zunächst einmal diesen Grenzfall erfassen müssen. Dies geschieht mit git rev-parse --verify, womit einfach überprüft wird, ob das Argument (HEAD) eine gültige Referenz ist. Der Teil >/dev/null 2>&1 schaltet jede Ausgabe von git rev-parse ab. Entweder wird HEAD oder ein leeres Commit-Objekt in der against-Variable für die Nutzung mit git diff-index gespeichert. Der Hash 4b825d... ist eine magische Commit-ID, die für einen leeren Commit steht.

Mit dem Befehl git diff-index --cached vergleichst du einen Commit mit dem entsprechenden Index. Bei Angabe der Option --check erhältst du eine Warnmeldung, wenn durch Änderungen Leerraumfehler auftreten. Bei einer solchen Warnmeldung wird der Commit abgebrochen und der Beendigungsstatus 1 ausgegeben. Ohne Fehler lautet der Beendigungsstatus 0 und der Commit-Workflow wird ganz normal fortgesetzt.

Dies ist nur ein Beispiel für den pre-commit-Hook. Darin haben wir bestehende Git-Befehle genutzt, um Tests für die Änderungen auszuführen, die mit dem Commit eingeführt würden. Hooks in pre-commit sind aber auch anderweitig einsetzbar, z. B. für die Ausführung weiterer Skripte oder einer Testsuite von Drittanbietern oder zur Prüfung des Codestils mit Lint.

Vorbereiten der Commit-Nachricht

Der Hook prepare-commit-msg wird nach dem Hook pre-commit aufgerufen und gibt eine Commit-Nachricht in den Texteditor aus. An dieser Stelle bietet es sich an, automatisch erzeugte Commit-Nachrichten für gesquashte oder gemergte Commits zu ändern.

Ein bis drei Argumente werden auf das prepare-commit-msg-Skript angewendet:

  1. Der Name einer temporären Datei, die die Nachricht enthält. Du kannst die Commit-Nachricht ändern, indem du diese Datei anpasst.
  2. Der Typ des Commits. Dabei kann es sich um message (Option -m oder -F), template (Option -t), merge (wenn es sich um einen Merge-Commit handelt) oder squash ((wenn der Commit andere Commits squasht) handeln.
  3. Der SHA1-Hash des jeweiligen Commits. Nur, wenn -c, -C oder die Option --amend angewendet wird.

Wie beim pre-commit-Hook führt ein Exit mit Nicht-Null-Status zum Verwerfen des Commits.

Wir haben bereits ein einfaches Beispiel gesehen, in dem die Commit-Nachricht bearbeitet wurde, aber schauen wir uns nun ein hilfreicheres Skript an. Wenn wir einen Issue Tracker nutzen, ist es oft üblich, jedes Issue in einem separaten Branch zu behandeln. Wenn der Branch-Name die Issue-Nummer enthält, kannst du einen prepare-commit-msg-Hook schreiben, um die Nummer automatisch auch jeder Commit-Nachricht in diesem Branch hinzuzufügen.

#!/usr/bin/env python
import sys, os, re
from subprocess import check_output
# Collect the parameters
commit_msg_filepath = sys.argv[1]
if len(sys.argv) > 2:
commit_type = sys.argv[2]
else:
commit_type = ''
if len(sys.argv) > 3:
commit_hash = sys.argv[3]
else:
commit_hash = ''
print "prepare-commit-msg: Datei: %s\nType: %s\nHash: %s" % (commit_msg_filepath, commit_type, commit_hash)
# Figure out which branch we're on
branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip()
print "prepare-commit-msg: On branch '%s'" % branch
# Populate the commit message with the issue #, if there is one
if branch.startswith('issue-'):
print "prepare-commit-msg: Oh hey, it's an issue branch."
result = re.match('issue-(.*)', branch)
issue_number = result.group(1)
with open(commit_msg_filepath, 'r+') as f:
content = f.read()
f.seek(0, 0)
f.write("ISSUE-%s %s" % (issue_number, content))

Dieser prepare-commit-msg-Hook zeigt uns zunächst, wie wir all die Parameter einsammeln, die dem Skript übergeben werden. Dann ruft er git symbolic-ref --short HEAD auf, um den Branch-Namen zu erhalten, der mit HEAD übereinstimmt. Wenn der Branch-Name mit issue- beginnt, wird der Inhalt der Commit-Nachrichtendatei so überschrieben, dass die Issue-Nummer in der ersten Zeile steht. Wenn der Branch-Name z. B. issue-224 lautet, wird die folgende Commit-Nachricht generiert:

ISSUE-224
# Please enter the commit message for your changes. Zeilen, die
# mit '#' beginnen, werden ignoriert. Durch leere Nachrichten werden Commits abgebrochen.
# On branch issue-224
# Changes to be committed:
# modified: test.txt

Beachte, dass prepare-commit-msg auch dann ausgeführt wird, wenn der Benutzer eine Nachricht mit der Option -m für den Befehl git commit eingibt. Das Skript oben fügt also automatisch den String ISSUE-[#] ein, ohne dass der Benutzer ihn bearbeiten kann. In diesem Fall kannst du überprüfen, ob der zweite Parameter (commit_type) mit message übereinstimmt.

Jedoch können Benutzer ohne die Option -m die Nachricht mit dem Hook prepare-commit-msg bearbeiten, nachdem sie erstellt wurde. Daher soll dieser Hook vielmehr ein Hilfsmittel sein und kein Skript zum Durchsetzen von Richtlinien für Commit-Nachrichten. Dazu brauchst du den Hook commit-msg, der im nächsten Abschnitt erklärt wird.

Commit-Nachricht

Die Hooks commit-msg und prepare-commit-msg haben viele Gemeinsamkeiten. Der letztere wird jedoch aufgerufen, nachdem der Benutzer die Commit-Nachricht eingegeben hat. Das ist ein geeigneter Zeitpunkt, um einem Entwickler darauf hinzuweisen, dass seine Nachricht nicht den Standards des Teams entspricht.

Das einzige Argument, das diesem Hook übergeben wird, ist der Name der Datei, die die Nachricht enthält. Wenn dem Hook die Commit-Nachricht nicht gefällt, kann er diese Datei abändern (genau wie bei prepare-commit-msg) oder den Commit durch einen Exit mit einem Nicht-Null-Status verwerfen.

Beispielsweise prüft das folgende Skript, ob der Benutzer den String ISSUE-[#] gelöscht hat, der vom Hook prepare-commit-msg im vorherigen Abschnitt automatisch generiert wurde.

#!/usr/bin/env python
import sys, os, re
from subprocess import check_output
# Collect the parameters
commit_msg_filepath = sys.argv[1]
# Figure out which branch we're on
branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip()
print "commit-msg: On branch '%s'" % branch
# Check the commit message if we're on an issue branch
if branch.startswith('issue-'):
print "commit-msg: Oh hey, it's an issue branch."
result = re.match('issue-(.*)', branch)
issue_number = result.group(1)
required_message = "ISSUE-%s" % issue_number
with open(commit_msg_filepath, 'r') as f:
content = f.read()
if not content.startswith(required_message):
print "commit-msg: ERROR! The commit message must start with '%s'" % required_message
sys.exit(1)

Da dieses Skript immer dann aufgerufen wird, wenn der Benutzer einen Commit erstellt, solltest du dich auf das Lesen der Commit-Nachricht beschränken. Wenn du willst, dass andere Services Benachrichtigungen über das Commiten eines Snapshots erhalten, benutzt du besser den Hook post-commit.

Nach dem Commit

Der Hook post-commit wird direkt nach dem Hook commit-msg aufgerufen. Dadurch kann das Ergebnis des git commit-Vorgangs nicht geändert werden. Der Hook dient also vor allem zur Benachrichtigung.

Das Skript holt keine Parameter und sein Exit-Status beeinflusst den Commit in keiner Weise. Bei den meisten post-commit-Skripten möchten wir auf den Commit zugreifen, der gerade erstellt wurde. Dazu kannst du git rev-parse HEAD verwenden, um den SHA1-Hash des neuen Commit zu erhalten oder mit git log -1 HEAD alle seine Informationen abrufen.

Wenn du etwa deinem Chef beim jedem Snapshot-Commit eine E-Mail senden willst (was bei den meisten Workflows wahrscheinlich nicht gerade die beste Idee ist), könntest du folgenden post-commit-Hook hinzufügen.

#!/usr/bin/env python
import smtplib
from email.mime.text import MIMEText
from subprocess import check_output
# Get the git log --stat entry of the new commit
log = check_output(['git', 'log', '-1', '--stat', 'HEAD'])
# Create a plaintext email message
msg = MIMEText("Look, I'm actually doing some work:\n\n%s" % log)
msg['Subject'] = 'Git post-commit hook notification'
msg['From'] = 'mary@example.com'
msg['To'] = 'boss@example.com'
# Send the message
SMTP_SERVER = 'smtp.example.com'
SMTP_PORT = 587
session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
session.ehlo()
session.starttls()
session.ehlo()
session.login(msg['From'], 'secretPassword')
session.sendmail(msg['From'], msg['To'], msg.as_string())
session.quit()

Auch mit post-commit kann ein lokales Continuous Integration-System ausgelöst werden. In den meisten Fällen bietet sich dafür jedoch der Hook post-receive an. Der Hook wird nicht auf der lokalen Maschine des Benutzers, sondern auf dem Server ausgeführt und auch immer dann, wenn irgendein Entwickler Code pusht. Das ist ein viel passender Ort für deine Continuous Integration.

Nach dem Checkout

Der post-checkout-Hook gleicht in vielen Belangen dem post-commit-Hook, doch er wird immer dann aufgerufen, wenn du eine Referenz mit git checkout auscheckst. Das ist hilfreich, um das Arbeitsverzeichnis von generierten Dateien zu bereinigen, die sonst Verwirrung stiften würden.

Dieser Hook lässt drei Parameter zu, deren Beendigungsstatus sich nicht auf den git checkout-Befehl auswirken.

  1. Die Referenz des vorangehenden HEAD
  2. Die Referenz des neuen HEAD
  3. Ein Flag, der angibt, ob es sich um einen Branch-Checkout oder einen Datei-Checkout handelt. Der Flag zeigt entweder 1 oder 0 an.

Python-Entwickler haben oft beim Erstellen von .pyc-Dateien mit dem Problem zu kämpfen, dass diese Dateien nach dem Wechseln von Branches bestehen bleiben. Der Interpreter verwendet diese .pyc-Dateien manchmal anstelle der .py-Quelldatei. Um keine Verwirrungen aufkommen zu lassen, kannst du sämtliche .pyc-Dateien jedes Mal löschen, wenn du einen neuen Branch auscheckst. Das funktioniert mithilfe des folgenden post-checkout-Skripts:

#!/usr/bin/env python
import sys, os, re
from subprocess import check_output
# Collect the parameters
previous_head = sys.argv[1]
new_head = sys.argv[2]
is_branch_checkout = sys.argv[3]
if is_branch_checkout == "0":
print "post-checkout: Dies ist ein Datei-Checkout. Nothing to do."
sys.exit(0)
print "post-checkout: Deleting all '.pyc' files in working directory"
for root, dirs, files in os.walk('.'):
for filename in files:
ext = os.path.splitext(filename)[1]
if ext == '.pyc':
os.unlink(os.path.join(root, filename))

Das aktuelle Arbeitsverzeichnis für Hook-Skripte ist immer als Root des Repositorys festgelegt. Daher kannst du os.walk('.') für alle Dateien des Repositorys ausführen. Überprüfe dann die Dateierweiterung und lösche alle .pyc-Dateien.

Alternativ kannst du basierend auf dem Branch, den du ausgecheckt hast, mit dem Hook post-checkout dein Arbeitsverzeichnis ändern. Nehmen wir an, du speicherst alle deine Plug-ins außerhalb der Haupt-Codebasis in einem plugins-Branch. Wenn für diese Plug-ins viele Binärdateien erforderlich sind, die andere Branches nicht benötigen, kannst du diese Dateien selektiv nur im plugins-Branch erstellen.

Vor dem Rebasing

Der pre-rebase-Hook wird aufgerufen, bevor git rebase Änderungen vornimmt. Damit ist er eine gute Möglichkeit sicherzustellen, dass nichts Verheerendes geschieht.

Dieser Hook nimmt zwei Parameter: den Upstream-Branch, von dem geforkt wurde, und den Branch für das Rebasing. Beim Rebasen des aktuellen Branch ist der zweite Parameter leer. Ein Exit mit Nicht-Null-Status bricht das Rebasing ab.

Z. B. kannst du Rebasing in deinem Repository mit folgendem pre-rebase-Skript vollständig unterbinden:

#!/bin/sh
# Disallow all rebasing
echo "pre-rebase: Rebasing is dangerous. Don't do it."
exit 1

Jedes Mal, wenn du nun git rebase ausführst, wird dir die folgende Nachricht angezeigt:

Pre-Rebase: Rebasing ist gefährlich. Lass es bleiben.
Der Pre-Rebase-Hook hat das Rebasing verweigert.

Ein detaillierteres Beispiel ist das native pre-rebase.sample-Skript. Dieses Skript geht mit dem Unterbinden von Rebasing etwas intelligenter um. Es prüft, ob der Themen-Branch, den wir rebasen wollen, bereits in den next-Branch gemergt wurde (in diesem Fall der Mainline Branch). Falls dem so ist, würdest du wahrscheinlich in Schwierigkeiten kommen, wenn du trotzdem ein Rebasing durchführst, deshalb verwirft das Skript den Rebase.

Serverseitige Hooks

Serverseitige Hooks funktionieren genauso wie lokale, außer dass sie in serverseitigen Repositorys liegen (z. B. im zentralen Repository oder im öffentlichen Repository eines Entwicklers). Wenn sie an das offizielle Repository angehängt sind, können einige von ihnen als Möglichkeit dienen, eine Richtlinie durchzusetzen, indem bestimmte Commits abgelehnt werden.

Es gibt drei serverseitige Hooks, die wir in diesem Artikel besprechen möchten:

  • pre-receive
  • update
  • post-receive

Mit all diesen Hooks kannst auf verschiedene Phasen des git push-Prozesses reagieren.

Die Ausgabe der serverseitigen Hooks wird an die Client-Konsole geleitet, sodass das Senden von Nachrichten an den Entwickler sehr einfach ist. Du solltest jedoch bedenken, dass diese Skripte die Kontrolle über das Terminal erst wieder nach ihrer Ausführung abgeben. Deshalb solltest du vorsichtig bei der Durchführung lange andauernder Vorgänge sein.

Pre-Receive

Der Hook pre-receive wird jedes Mal ausgeführt, wenn jemand mit git push Commits in das Repository pusht. Er sollte sich nicht im Ausgangs-Repository, sondern immer in dem Remote-Repository befinden, das das Ziel des Push-Vorgangs ist.

Der Hook wird ausgeführt, noch bevor die Referenzen aktualisiert werden, und ist daher ideal geeignet, um alle möglichen Entwicklungsrichtlinien durchzusetzen. Wenn du nicht mehr möchtest, dass eine bestimmte Person Code pusht, oder wenn du die Commit-Nachricht anders formatieren willst oder nicht mit den Änderungen im Commit einverstanden bist, kannst du all das einfach ablehnen. Zwar kannst du gegen schlecht aufgebaute Commits von Entwickler nichts tun, aber du kannst verhindern, dass diese Commits in die offizielle Codebasis gelangen, indem du sie mit pre-receive ablehnst.

Das Skript akzeptiert keine Parameter, aber jede Ref, die gepusht wird, wird dem Skript auf einer separaten Zeile (Standard-Input) im folgenden Format übergeben:

<old-value> <new-value> <ref-name>

So funktioniert dieser Hook mit einem sehr einfachen pre-receive-Skript, das die gepushten Refs einfach einliest und ausgibt.

#!/usr/bin/env python
import sys
import fileinput
# Read in each ref that the user is trying to update
for line in fileinput.input():
print "pre-receive: Pushen von Ref wird versucht: %s" % line
# Abort the push
# sys.exit(1)

Zur Erinnerung: Im Gegensatz zu anderen Hooks werden die Informationen hier als Standardeingabe mit dem Skript übermittelt und nicht als Argumente in Befehlszeilen. Lege das obige Skript in dem .git/hooks-Verzeichnis eines Remote-Repositorys ab und pushe den Master Branch. Anschließend zeigt deine Konsole in etwa Folgendes:

b6b36c697eb2d24302f89aa22d9170dfe609855b 85baa88c22b52ddd24d71f05db31f4e46d579095 refs/heads/master

Du kannst diese SHA1-Hashes zusammen mit einigen Lower-Level-Git-Befehlen nutzen, um die Änderungen zu inspizieren, die eingeführt werden sollen. Einige Anwendungsfälle hierfür:

  • Ablehnen von Änderungen, die ein Upstream-Rebasing beinhalten
  • Verhindern von Nicht-Fast-Forward-Merges
  • Prüfen, ob der Benutzer die korrekten Zugriffsrechte hat, um die Änderungen vorzunehmen (relevant v. a. in zentralisierten Git-Workflows)

Werden mehrere Refs gepusht, wird von pre-receive ein Nicht-Null-Status zurückgegeben und alle Änderungen werden abgebrochen. Wenn du Branches einzeln akzeptieren oder ablehnen willst, solltest du stattdessen den Hook update verwenden.

Update

Der update-Hook wird nach dem pre-receive-Hook aufgerufen und funktioniert auch ganz ähnlich wie dieser. Er wird ausgeführt, bevor tatsächlich etwas geändert wird, aber der Hook wird für jede gepushte Ref separat aufgerufen. Wenn ein Benutzer also beispielsweise vier Branches pusht, wird update viermal ausgeführt. Im Gegensatz zu pre-receive ist dieser Hook nicht auf die Standardeingabe angewiesen. Er akzeptiert stattdessen die folgenden drei Argumente:

  1. Den Namen der Ref, die aktualisiert wird
  2. Den alten Objektnamen, der in der Ref gespeichert ist
  3. Den neuen Objektnamen, der in der Ref gespeichert ist

Dieselben Informationen werden auch an pre-receive übergeben, aber da update für jede Ref separat ausgeführt wird, kannst du bestimmte Refs ablehnen und andere erlauben.

#!/usr/bin/env python
import sys
branch = sys.argv[1]
old_commit = sys.argv[2]
new_commit = sys.argv[3]
print "Moving '%s' from %s to %s" % (branch, old_commit, new_commit)
# Abort pushing only this branch
# sys.exit(1)

Mit dem Hook update oben werden lediglich der Branch und die alten/neuen Commit-Hashes ausgegeben. Wenn du mehr als einen Branch in das Remote-Repository pushst, wird die Anweisung print (ausgeben) für jeden Branch ausgeführt.

Post-Receive

Der post-receive-Hook wird nach einer erfolgreichen Push-Operation aufgerufen und eignet sich hervorragend zum Ausführen von Benachrichtigungen. In vielen Workflows ist dieser Hook besser für das Anstoßen von Benachrichtigungen geeignet als post-commit, da die Änderungen auf einem öffentlichen Server verfügbar sind statt nur auf dem lokalen Rechner des Benutzers. E-Mails an andere Entwickler und das Auslösen eines Continuous Integration-Systems sind häufige Anwendungsfälle für post-receive.

Das Skript akzeptiert keine Parameter, erhält aber dieselben Informationen wie pre-receive über die Standardeingabe.

Summary

In diesem Artikel haben wir gezeigt, wie man internes Verhalten und den Erhalt von Benachrichtigungen mit Git-Hooks beeinflussen kann, wenn in einem Repository bestimmte Ereignisse auftreten. Hooks sind gewöhnliche Skripte, die sich im .git/hooks-Repository befinden. Daher kannst du sie ganz leicht installieren und anpassen.

Darüber hinaus haben wir uns die gebräuchlichsten lokalen und serverseitigen Hooks angesehen, mit denen wir uns an beliebigen Stellen im Entwicklungslebenszyklus einklinken können. Wir wissen nun, wie wir in jedem Stadium der Commit-Erstellung und des git push-Prozesses individuelle Aktionen durchführen können. Mit ein wenig Skriptingkenntnissen stehen uns alle Türen offen, in unserem Git-Repo praktisch alles zu machen, was wir uns vorstellen können.