
# 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](#hooks-pitmaster-runs) 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](https://github.com/inline0/pitmaster/issues/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](https://github.com/inline0/pitmaster/issues/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:

```bash
#!/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:

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