Hooks
Git-Hooks sind ausführbare Skripte in .git/hooks/, die an bestimmten Punkten in git-Operationen laufen. Pitmaster erkennt Hooks und führt sie mithilfe der Prozesssteuerung von PHP (proc_open) aus.
Hooks werden standardmäßig ausgeführt
Repository-Operationen führen die unter Hooks Pitmaster ausgeführten Hooks automatisch aus. Um sie für ein Repository-Handle zu deaktivieren, übergib beim Öffnen Optionen:
use Pitmaster\Pitmaster;
// Skip .git/hooks scripts for this handle
$repo = Pitmaster::open('/path/to/project', ['hooks' => false]);
// Start no processes at all: hooks, the fsmonitor hook, clone/fetch/push, and SSH are disabled
$repo = Pitmaster::open('/path/to/project', Pitmaster::processFreeOptions());
$repo->hooksEnabled(); // false
$repo->processesEnabled(); // false for the second handle
Pitmaster::init() und Pitmaster::clone() akzeptieren dieselben Optionen. Zum Ausführen eines Hooks wird proc_open() benötigt; auf Hosts, die es deaktivieren, öffne Repositories mit ['hooks' => false]. CLI öffnet das Repository immer mit aktivierten Hooks.
Pitmaster sucht Hooks ausschließlich in <gitDir>/hooks. core.hooksPath wird ignoriert; in einem verknüpften Worktree sucht es in .git/worktrees/<name>/hooks, daher werden die gemeinsamen .git/hooks-Skripte dort nicht ausgeführt (#12).
HookRunner, weiter unten beschrieben, ignoriert diese Optionen: Ein direkter Aufruf führt den Hook immer aus.
HookRunner
use Pitmaster\Hooks\HookRunner;
$hooks = new HookRunner($repo->gitDir());
Prüfen, ob ein Hook vorhanden ist
if ($hooks->exists('pre-commit')) {
echo "pre-commit hook is installed\n";
}
Ein Hook ist vorhanden, wenn die Datei unter .git/hooks/<name> existiert und ausführbar ist.
Einen Hook ausführen
$result = $hooks->run('pre-commit');
echo $result['exitCode']; // 0 = success
echo $result['stdout']; // Hook's standard output
echo $result['stderr']; // Hook's standard error
Mit Argumenten
$result = $hooks->run('commit-msg', ['/path/to/COMMIT_EDITMSG']);
Über stdin
$result = $hooks->run('pre-push', ['origin', 'https://github.com/user/repo.git'], $stdinData);
Der pre-push-Hook erhält Ref-Informationen über stdin.
Erfolg prüfen
if ($hooks->runAndCheck('pre-commit')) {
// Hook passed (exit code 0), proceed with commit
} else {
// Hook rejected the operation
}
runAndCheck() ist eine praktische Funktion, die den Hook ausführt und true zurückgibt, wenn der Exit-Code 0 ist. Ist der Hook nicht vorhanden, gibt sie true zurück (kein Hook bedeutet keinen Einwand).
Installierte Hooks auflisten
$installed = $hooks->listHooks();
// ['pre-commit', 'commit-msg', 'post-checkout']
Listet alle ausführbaren Dateien in .git/hooks/ auf (Dateien vom Typ .sample werden ausgeschlossen).
run() gibt bei einem fehlenden oder nicht ausführbaren Hook Exit-Code 0 und eine leere Ausgabe zurück. Kann der Prozess nicht gestartet werden, gibt es Exit-Code 1 zurück und schreibt Failed to start hook nach stderr.
Einen Hook installieren
$hooks->install('pre-commit', "#!/bin/bash\nphp vendor/bin/phpcs --standard=PSR12 .\n");
Schreibt den Skriptinhalt nach .git/hooks/<name> und setzt die Ausführungsberechtigung (chmod 0755).
// Install a PHP hook
$hooks->install('pre-commit', <<<'HOOK'
#!/usr/bin/env php
<?php
// Run static analysis before commit
$output = [];
$returnCode = 0;
exec('php vendor/bin/phpstan analyse --no-progress 2>&1', $output, $returnCode);
if ($returnCode !== 0) {
echo "PHPStan found errors:\n";
echo implode("\n", $output) . "\n";
exit(1);
}
HOOK);
Von Pitmaster ausgeführte Hooks
HookRunner kann jeden Hook anhand seines Namens ausführen. Repository führt die folgenden automatisch aus:
| Hook | Ausgeführt von | Argumente | Kann abbrechen? |
|---|---|---|---|
pre-commit |
commit() |
keine | Ja (Exit-Code ungleich null) |
prepare-commit-msg |
commit() |
Nachrichtendatei, dann message (oder merge beim Abschließen eines Merge-Vorgangs) |
Ja |
commit-msg |
commit() |
Nachrichtendatei (.git/COMMIT_EDITMSG); Änderungen an der Datei werden übernommen |
Ja (Exit-Code ungleich null) |
post-commit |
commit() |
keine | Nein (nur informativ) |
pre-rebase |
rebase() |
Upstream-Revision | Ja |
post-checkout |
checkout() |
alter HEAD, neuer HEAD, 1 |
Nein (nur informativ) |
post-merge |
merge(), mergeOctopus(), mergeContinue() |
0 |
Nein (nur informativ) |
pre-push |
push() und seine Varianten |
Remote-Name, URL; Ref-Zeilen über stdin | Ja (Exit-Code ungleich null) |
Ein fehlschlagender Hook lässt die Operation RuntimeException('<name> hook failed') auslösen; die Hook-Ausgabe ist nicht enthalten. Commit-Hooks laufen nur bei commit(): Merge-Commits, die von merge() erstellt werden, führen sie nicht aus. post-checkout wird bei addWorktree(), reset() oder restore() nicht ausgeführt.
Serverseitige Hooks (pre-receive, update, post-receive, post-update), pre-auto-gc und post-rewrite werden nie automatisch ausgeführt.
Hook-Umgebung
Wenn Pitmaster einen Hook ausführt, übergibt es diese Umgebung:
$env = [
'GIT_DIR' => $this->gitDir,
'GIT_WORK_TREE' => dirname($this->gitDir),
];
Der Hook erhält ausschließlich diese Variablen: PATH, HOME und alle weiteren Variablen des PHP-Prozesses werden nicht weitergegeben (#13). Verwende absolute Pfade in Hook-Skripten oder setze PATH im Skript; ein #!/usr/bin/env php-Shebang findet Interpreter nur im systemweiten Standardpfad.
Der Hook läuft in dirname($gitDir), dem Arbeitsverzeichnis eines normalen .git-Verzeichnisses.
Format von Hook-Skripten
Hooks können in jeder Sprache geschrieben werden. Die erste Zeile muss ein Shebang sein:
#!/bin/bash
# Bash hook
#!/usr/bin/env php
<?php
// PHP hook
#!/usr/bin/env python3
# Python hook
Beispiel: pre-commit mit Pitmaster
commit() führt pre-commit, prepare-commit-msg, commit-msg und post-commit bereits aus; der Anwendungscode muss daher nur den Fehler behandeln:
try {
$commitId = $repo->commit('Add feature');
} catch (\RuntimeException $e) {
echo $e->getMessage(); // "pre-commit hook failed"
}
Rufe HookRunner selbst nur für ein Handle auf, das mit ['hooks' => false] geöffnet wurde, oder für Hooks, die Pitmaster nicht ausführt.
Beispiel-Hooks
Git initialisiert Repositories mit .sample-Hook-Dateien; Pitmaster::init() erstellt ein leeres hooks/-Verzeichnis. Pitmaster schließt mit listHooks() .sample-Dateien aus:
.git/hooks/
pre-commit.sample (excluded from listHooks)
commit-msg.sample (excluded from listHooks)
pre-commit (included - installed by user)