Einstieg ins Pre-Commit-Framework Git-Hook-Management mit Pre-Commit

Von Mirco Lang

Anbieter zum Thema

Git liefert unter der Haube ein mächtiges Tool für Automatisierungen: Hooks. Mit dem freien Pre-Commit-Framework lässt sich dabei Ordnung und Struktur in die Skriptsammlung bringen.

Pre-Commit ist hilfreich, der Einstieg ist allerdings nicht selbsterklärend und verlangt nach etwas Geduld.
Pre-Commit ist hilfreich, der Einstieg ist allerdings nicht selbsterklärend und verlangt nach etwas Geduld.
(Bild: Lang / Canonical)

Über die vielfältigen Einsatzzwecke von Git-Hooks haben wir bereits berichtet; und auch über die Möglichkeit, Hooks mit einem kleinen Kniff über das Repository selbst mit anderen zu teilen. Das Ganze ist aber weder elegant noch komfortabel, die Hook-Skripte werden letztlich komplett händisch gepflegt und verteilt.

Und was taucht meistens auf, wenn Tools auf einem gegebenen System installiert werden, sagen wir unter GNU/Linux oder in einer Python-Umgebung? Genau, Paketmanager, zum Beispiel Apt und Pip. Und Pre-Commit (Eigenschreibweise pre-commit) ist im Grunde ein Paketmanager für Git-Hook-Skripte. Ganz wichtig dabei, der Name täuscht etwas: Pre-Commit kann durchaus andere Hooks als eben Pre-Commit-Hooks verwalten, zumindest mittlerweile.

Aber mal von vorne. Die Funktionsweise von Pre-Commit ist (nur) auf den ersten Blick ziemlich simpel: Das Framework wird in einem Git-Repository installiert und über den Befehl „git commit“ getriggert. Daraufhin führt es die installierten Hooks zu gegebener Zeit, genauer gesagt zu einem bestimmten Stage (commit, push etc.) aus.

Die Hooks selbst können dabei in nahezu beliebiger Sprache oder für verschiedene Umgebungen verfasst sein, unter anderem Conda, Docker, Dotnet, Node, Lua, Perl, Ruby, Rust, Pygrep und selbstverständlich auch Python und Shell- Code. Pre-Commit kümmert sich dabei in der Regel auch um etwaige Abhängigkeiten, die für eine Umgebung zum Ausführen der Skripte benötigt werden. Teils, etwa bei Shell-Script, müssen und können sich Nutzer selbst darum kümmern.

Noch mehr Flexibilität bekommen Sie durch die Ablage der Hooks: Diese können zwar lokal und direkt im betroffenen Repo abgelegt werden, standardmäßig aber werden sie aus einem eigenen Repository gezogen – wie man es von Paketmanagern ja auch gewohnt ist.

Die Verwaltung und Verteilung von Hooks ist die eine Seite. Die andere: Es gibt auch eine vernünftige Ausgabe der Hook-Verarbeitung im Terminal, so dass auf einen Blick schnell klar wird, welche Prüfungen bestanden wurden und welche nicht.

Pre-Commit ist wirklich hilfreich und lässt sich, einmal installiert und verstanden, auch recht simpel nutzen. Aber das impliziert gleichermaßen: Der Einstieg ist nicht ganz so selbsterklärend. Das liegt zum einen daran, dass Pre-Commit schon ein recht eigenständiges Projekt ist, hinter dem mehr steckt, als man anfangs vermutet.

Vor allem aber kommt man leider nicht umhin festzustellen, dass die Dokumentation schlichtweg sehr dünn ist. Aber das ist nun einmal auch nicht die Stärke von Entwicklern. Für reine Schreibkräfte dürfte das Open-Source-Projekt von Pre-Commit deutlich zu klein sein, fast alle Commits stammen von Anthony Sottile.

Etwas einfacher wird der Einstieg jedoch durch das zum Projekt gehörende GitHub-Repo Pre-Commit-Hooks, eine Sammlung mit gut 30 in Python geschriebenen Hooks, die sich ganz fix einsetzen lässt. Im Folgenden befassen wir uns zunächst einmal mit der Installation von Pre-Commit und Pre-Commit-Hooks. Dann zeigen wir, wie sich ein einfacher Shell-Script-Hook hinzufügen lässt. Der Einfachheit halber geschieht dies alles über einen Fork des Pre-Commit-Hooks-Repos.

Pre-Commit einsetzen

Pre-Commit selbst gibt zur Installation Pip, Brew und Conda vor – allerdings zeigen sich unter Ubuntu auch Snap- und Apt-Pakete, die allerdings nicht aktuell sind. Es empfiehlt sich also die Installation via Pip:

pip install pre-commit

Dabei gilt es, auf etwaige Fehlermeldungen zu achten, uns unserem Fall wurde etwa in das Verzeichnis „~/.local/“ installiert, was nicht im Pfad liegt. Die Apt-Version wiederum wurde zwar nur mit Version 2.18.0 statt 2.20.0 installiert, dafür aber wie gewohnt im Pfad. Wenn alles funktioniert, sollte „pre-commit --version“ die Versionsnummer anzeigen.

Wechseln Sie nun zum gewünschten Repo. Die gesamte Konfiguration findet in der Datei „pre-commit-config.yaml“ statt, die Sie im Hauptverzeichnis Ihres Repos ablegen können. Für einen einfachen Einstieg können Sie eine simple Beispielkonfiguration erzeugen, die ein paar Hooks aus dem Pre-Commit-Hooks-Repository verwendet:

pre-commit sample-config > .pre-commit-config.yaml

Die sample-config wird standardmäßig schlicht ausgegeben, es gilt, die YAML-Konfiguration wie oben selbst damit zu befüllen. Inhaltlich ist das erfreulich simpel, hier mal ein Auszug der Demo-Konfiguration:

Jetzt Newsletter abonnieren

Täglich die wichtigsten Infos zu Softwareentwicklung und DevOps

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung.

Aufklappen für Details zu Ihrer Einwilligung
repos:
-  repo: https://github.com/Pre-Commit/Pre-Commit-hooks
   rev: v3.2.0
   hooks:
   - id: trailing-whitespace

Es werden also drei Angaben benötigt: Das Repo mit den Hooks, das Tag für den gewünschten Release und die IDs der gewünschten Hooks – dazu später mehr. Der hier geladene Hook würde wenig überraschend überflüssige Leerzeichen an Zeilenenden löschen.

Nun steht die Konfiguration und wir können Pre-Commit installieren:

pre-commit install

Mit diesem Kommando wird schlicht die Datei „pre-commit“ im Verzeichnis „.git/hooks“ abgelegt – der Pre-Commit-Hook ist damit aktiviert und führt bei „git commit“ eben Pre-Commit mit der erstellten Konfiguration aus. Und ja, es ist etwas verwirrend, dass Framework, Git-Hook und die eigentliche Datei alle gleichermaßen auf den Namen „Pre-Commit“ hören.

Statt über einen neuen Commit lassen sich Hooks aber auch manuell triggern, für einzelne Dateien oder schlicht alle Dateien, was bei neuen Hooks freilich sinnvoll ist:

pre-commit run --all-files

Damit werden alle in der YAML-Konfiguration angegebenen Hooks ausgeführt und Dateien werden gegebenenfalls geändert – jeweils bestätigt durch eine hübsch formatierte Ausgabe in der Form:

Trim Trailing Whitespace.................................................Failed
- hook id: trailing-whitespace
exit code: 1
- files were modified by this hook

Beim ersten Durchlauf wird gefixt, beim zweiten werden alle Tests bestanden.
Beim ersten Durchlauf wird gefixt, beim zweiten werden alle Tests bestanden.
(Bild: Lang / Canonical)

Ein erneuter Aufruf würde dann entsprechend ein „Passed“ produzieren, schließlich wurde dieses Problem behoben. Noch sieht das Ganze verständlicherweise ein wenig nach Black Box aus, denn die Gegenseite mit den Hooks wurde ja schlicht aus dem Pre-Commit-Repo referenziert – Zeit für ein wenig Eigenleistung.

Hook-Repo forken

Das Hooks-Repository muss grundsätzlich nur zwei Dinge enthalten: Die Hook-Skripte selbst sowie abermals eine YAML-Konfiguration, hier die Datei „pre-commit-hooks.yaml“. Am besten forkt man dazu einfach das bereits genutzte Hook-Repo aus dem Pre-Commit-Projekt – direkt über die GitHub-Oberfläche.

Anschließend müssen wir natürlich die Pre-Commit.yaml von oben anpassen, beispielsweise so:

repos:
-  repo: https://github.com/IHR-REPO/pre-commit-hooks
   rev: v1.0.0
   hooks:
   - id: trailing-whitespace

Die Pre-Commit-Konfiguration im Arbeits-Repo
Die Pre-Commit-Konfiguration im Arbeits-Repo
(Bild: Lang / Canonical)

Die URL will selbstredend angepasst sein. Wichtig ist aber auch die Angabe unter „rev“ – denn natürlich finden sich im neuen Fork noch keine Releases. Diesen müssen wir zunächst im Hook-Repository erstellen. Und das sollten in der Form „v1.0.0“ geschehen (ein nicht nachvollziehbarer Tag wie „foobar“ würde zu einer Warnmeldung führen).

Anschließend benötigt die Konfiguration im Arbeits-Repo ein Update:

pre-commit autoupdate

Damit wird der rev-Eintrag in der pre-commit.yaml auf das letzte Release gesetzt. Wenn der Einsatz der vorgegebenen und geforkten Hooks aus dem eigenen Hook-Repo funktioniert, lassen sich eigene Skripte testen.

Eigene Hooks nutzen

Als Hook soll hier ein super einfaches Shell-Skript dienen, das schlicht „foo“ gegen „bar“ austauscht – in einer Datei „test.sh“ im Hauptverzeichnis des Hook-Repos:

#!/bin/bash
sed -i ' s/foo/bar/ ' *

Anschließend muss dieser Hook der „pre-commit-hooks.yaml“ bekannt gemacht werden – hierfür kopieren wir einfach den Part des oben gesehenen Hooks „trailing-whitespace“ und passen entsprechend an:

-  id: test
   name: test
   description: testing stuff
   entry: ./test.sh
   language: script
   types: [text]
   stages: [commit, push, manual]

Hook-Konfiguration im Hook-Repo
Hook-Konfiguration im Hook-Repo
(Bild: Lang / Canonical)

Neben Name und ID sind hier zwei Einträge zu ändern: Als „language“ wird freilich „script“ angegeben und bei „entry“ muss der Pfad zum Skript gesetzt werden, relativ zum Hauptverzeichnis des Hook-Repos; hier liegt test.sh zum Beispiel selbst im Hauptverzeichnis.

Die weiteren Einträge zu „types“ und „stages“ sind optional; an dieser Stelle lässt sich festlegen, dass nur Textdateien vom Hook berücksichtigt werden und auf welche Stages dies beschränkt werden soll. Weitere Optionen finden sich in der Plug-ins-Doku zu Pre-Commit.

Abermals ist es nun notwendig, ein neues Release im Hooks-Repo zu erstellen. Ein letzter Schritt fehlt nun noch: Der neue, eigene Hook muss in der pre-commit-config.yaml im Arbeits-Repo nachgetragen werden:

repos:
-  repo: https://github.com/IHR-REPO/pre-commit-hooks
   rev: v1.0.1
   hooks:
   - id: trailing-whitespace
   - id: test

Nach einem finalen …

pre-commit autoupdate

… besteht Zugriff auf den Test-Hook.

Ganz wichtig, wenn auch nicht Pre-Commit-spezifisch: Das Skript muss ausführbar sein – und das gilt es freilich auch git mitzuteilen. Das funktioniert bereits beim Hinzufügen des Skripts per „git add“, aber auch nachträglich:

git update-index --chmod=+x test.sh

Zwei Repositories, zwei YAML-Konfigurationen, Releases in dem einen, Updates in dem anderen Repository, eine Doku, die eigentlich nur Python als Beispiel nutzt – Pre-Commit ist nicht gerade intuitiv und die erste Stunde kann durchaus frustrieren, aber danach ist es ein tolles Werkzeug.

(ID:48925098)