Software-Entwicklung mit Git: Lokale Git-Hooks (Teil 1)

Bei Git-Hooks handelt es sich um Skripte, die automatisch ausgeführt werden, wenn in einem Git-Repository bestimmte Ereignisse eintreten, siehe die Einführung ins Thema. Dabei werden lokale und serverseitige Hooks unterschieden, denen wir uns nun im Rahmen einiger Tutorials widmen wollen. Zunächst zu lokalen Hooks.

Diese 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-Policy zu erzwingen. Allerdings erleichtern sie es Entwicklern, bestimmte Richtlinien zu wahren.

In diesem und in den Folgeartikeln diskutieren wir die sechs hilfreichsten lokalen Hooks:

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

Mit den ersten vier können wir an verschiedenen Punkten des Commit-Lebenszyklus' andocken, mit den letzten beiden können wir ein paar Extraaktionen oder Sicherheitschecks für die Befehle git checkout und git rebase durchführen.

Die pre-Hooks erlauben es, die Aktion, die stattfinden soll, zu modifizieren. Die post-Hooks dienen nur zu Benachrichtigungszwecken.

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

Wenn wir git commit nutzen, wird das pre-commit-Skript jedes Mal ausgeführt, bevor Git den Entwickler um eine Commit-Message bittet oder ein Commit-Objekt generiert. Wir können diesen Hook anwenden, um den Snapshot zu inspizieren, der commitet werden soll. Beispielsweise könnten wir ein paar automatisierte Tests laufen lassen, um sicherzustellen, dass der Commit keine bestehenden Funktionalitäten kaputt macht.

Mit dem pre-commit-Skript werden keine Argumente übergeben, und ein Exit mit einem Nicht-Null-Status verwirft den gesamten Commit. Sehen wir uns eine vereinfachte Version des nativen pre-commit-Hooks an. Dieses Skript verwirft den Commit, wenn es irgendwelche Leerraumfehler findet, wie sie vom Befehl git diff-index festgelegt sind. (Nachhängende Leerzeichen, Zeilen, die nur aus Leerraum bestehen, und ein Leerzeichen gefolgt von einem Tab im initialen Einzug einer Zeile werden standardmäßig berücksichtigt.)

#!/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

Um git diff-index zu nutzen, müssen wir herausfinden, mit welcher Commit-Referenz wir den Index abgleichen. Normalerweise ist das HEAD, doch HEAD existiert nicht, wenn wir den ersten Commit erstellen. Somit besteht unsere erste Aufgabe darin, diesen Grenzfall zu erfassen. Das tun wir mit git rev-parse –verify, womit einfach überprüft wird, ob das Argument (HEAD) eine valide Referenz ist. Der Teil >/dev/null 2>&1 schaltet jede Ausgabe von git rev-parse ab. Entweder HEAD oder ein leeres Commit-Objekt werden in der against-Variable für die Nutzung mit git diff-index gespeichert. Der Hash 4b825d... ist eine magische Commit-ID, die den leeren Commit repräsentiert.

Der Befehl git diff-index –cached gleicht einen Commit gegen den Index ab. Durch das Hinzufügen der Option --check definieren wir, dass wir gewarnt werden, wenn die Änderungen Leerraumfehler einführen würden. Wenn das der Fall ist, verwerfen wir den Commit durch die Eingabe des Exit-Status' 1. Wenn alles okay ist, kann der Commit-Workflow nach dem Exit 0 ganz normal seinen Lauf nehmen.

Das ist nur ein Beispiel des pre-commit-Hooks. Hier haben wir bestehende Git-Befehle genutzt, um Tests an den Änderungen vorzunehmen, die mit dem Commit eingeführt würden. Wir können mit diesem Hook aber auch andere Dinge tun – von der Ausführung weiterer Skripte oder einer Third-Party-Testsuite bis hin zur Prüfung des Codestils mit Lint.

Commit-Nachricht vorbereiten

Der Hook prepare-commit-msg wird nach dem pre-commit-Hook aufgerufen, um den Texteditor mit einer Commit-Nachricht zu befüllen. Dies ist ein guter Ort, um die automatisch generierte Commit-Message anzupassen.

Mit prepare-commit-msg werden bis zu drei Argumente übergeben:

  1. Der Name einer temporären Datei, die die Nachricht enthält. Wir können die Commit-Message ändern, indem wir diese Datei anpassen.
  2. Den 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 zerdrückt) handeln.
  3. Der Hash SHA1 des relevanten Commits, sofern die Option -c, -C oder --amend gegeben war.

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 editiert wurde, aber schauen wir uns nun mal ein hilfreicheres Skript an. Wenn wir einen Issue-Tracker wie JIRA nutzen, ist es oft gebräuchlich, jeden Vorgang in einem separaten Branch zu behandeln. Wenn wir die Vorgangsnummer in den Branch-Namen einschließen, können wir einen prepare-commit-msg-Hook schreiben, um sie automatisch auch jeder Commit-Message 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: File: %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 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 bekommen, der mit HEAD übereinstimmt. Wenn der Branch-Name mit issue- beginnt, wird der Inhalt der Commit-Message-Datei so überschrieben, dass die Vorgangsnummer in der ersten Zeile steht. Wenn unser Branch-Name issue-224 lautet, wird die folgende Commit-Nachricht generiert:

ISSUE-224 

# Please enter the commit message for your changes. Lines starting 
# with '#' will be ignored, and an empty message aborts the commit. 
# On branch issue-224 
# Changes to be committed: 
#   modified:   test.txt

Eine Sache müssen wir im Hinterkopf behalten, wenn wir prepare-commit-msg nutzen: Der Hook wird auch ausgeführt, wenn der User eine Message mit der Option -m für git commit übergibt. Das bedeutet, dass das Skript oben den String ISSUE-[#] automatisch einfügt, ohne dass der Nutzer ihn editieren kann. Wir können das handhaben, indem wir nachsehen, ob der zweite Parameter (commit_type) mit message übereinstimmt.

Ohne die -m-Option erlaubt der prepare-commit-msg-Hook es dem User allerdings, die Message zu editieren, nachdem sie generiert wurde. Es handelt sich also eher um ein Bequemlichkeits-Skript und nicht um einen Weg, um eine Commit-Message-Policy durchzusetzen. Dafür brauchen wir den Hook commit-msg, den wir demnächst hier diskutieren werden.

Git und Bitbucket Server effektiv nutzen? Wir sind Ihr Partner!

Kennen Sie Bitbucket Server (vormals Stash), Atlassians Git-Repository-Managementsystem? Bitbucket Server bietet eine zentrale Lösung zum Management des gesamten distributierten Codes: Hier kommen alle Git-Repositories im Unternehmen zusammen, hier finden Entwickler immer die letzte offizielle Version eines Projekts, hier können Projektverantwortliche Berechtigungen kontrollieren, um sicherzustellen, dass die richtigen Nutzer Zugriff auf den richtigen Code haben. Möchten Sie mehr erfahren? Wir sind offizieller Vertriebspartner von Atlassian und einer der größten Atlassian Experts Partner weltweit. Gerne unterstützen wir Sie bei der Evaluierung, Lizenzierung und Adaption von Bitbucket Server.

Übrigens: //SEIBERT/MEDIA bietet auch professionelle Grundlagen- und Aufbau-Workshops zu Git und Bitbucket Server an. Sie möchten, dass wir Ihren gesamten Software-Entwicklungsprozess mit Ihnen neu aufsetzen oder modernisieren? Dann testen Sie kostenfrei und unverbindlich unseren CoderStack.

Weiterführende Infos

99 Argumente für Bitbucket Server (Stash) als Git-Repository-Manager
Branch-basierte Git-Workflows adaptieren
Echte Integration: Das Zusammenspiel von JIRA, Stash und Bamboo
Interview: Die Vorteile von Git in der Software-Entwicklung und die Möglichkeiten von Bitbucket Server (Stash)
So funktioniert die Lizenzierung von Atlassian-Produkten