Zum Inhalt springen

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:

PHP
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

PHP
use Pitmaster\Hooks\HookRunner;

$hooks = new HookRunner($repo->gitDir());

Prüfen, ob ein Hook vorhanden ist

PHP
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

PHP
$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

PHP
$result = $hooks->run('commit-msg', ['/path/to/COMMIT_EDITMSG']);

Über stdin

PHP
$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

PHP
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

PHP
$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

PHP
$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).

PHP
// 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:

PHP
$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:

Shell
#!/bin/bash
# Bash hook
PHP
#!/usr/bin/env php
<?php
// PHP hook
Python
#!/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:

PHP
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:

Code
.git/hooks/
  pre-commit.sample       (excluded from listHooks)
  commit-msg.sample       (excluded from listHooks)
  pre-commit              (included - installed by user)