
# Hooks

Git-hooks zijn uitvoerbare scripts in `.git/hooks/` die op specifieke momenten tijdens git-bewerkingen worden uitgevoerd. Pitmaster detecteert en start hooks met de procesbesturing van PHP (`proc_open`).

## Hooks worden standaard uitgevoerd

Bij repositorybewerkingen worden de hooks in [Hooks Pitmaster voert uit](#hooks-pitmaster-runs) automatisch uitgevoerd. U schakelt ze uit voor een repositoryhandle door opties mee te geven wanneer u deze opent:

```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()` en `Pitmaster::clone()` accepteren dezelfde opties. Voor het uitvoeren van een hook is `proc_open()` nodig; open repositories met `['hooks' => false]` op hosts waarop dit is uitgeschakeld. De CLI opent de repository altijd met hooks ingeschakeld.

Pitmaster zoekt hooks alleen in `<gitDir>/hooks`. Het negeert `core.hooksPath` en zoekt in een gekoppelde worktree in `.git/worktrees/<name>/hooks`, zodat gedeelde scripts in `.git/hooks` daar niet worden uitgevoerd ([#12](https://github.com/inline0/pitmaster/issues/12)).

`HookRunner`, hieronder beschreven, negeert deze opties: als u deze rechtstreeks aanroept, wordt de hook altijd uitgevoerd.

## HookRunner

```php
use Pitmaster\Hooks\HookRunner;

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

## Controleren of een hook bestaat

```php
if ($hooks->exists('pre-commit')) {
    echo "pre-commit hook is installed\n";
}
```

Een hook bestaat als het bestand aanwezig is op `.git/hooks/<name>` en uitvoerbaar is.

## Een hook uitvoeren

```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
```

### Met argumenten

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

### Met stdin

```php
$result = $hooks->run('pre-push', ['origin', 'https://github.com/user/repo.git'], $stdinData);
```

De `pre-push`-hook ontvangt ref-informatie via stdin.

### Succes controleren

```php
if ($hooks->runAndCheck('pre-commit')) {
    // Hook passed (exit code 0), proceed with commit
} else {
    // Hook rejected the operation
}
```

`runAndCheck()` is een hulpfunctie die de hook uitvoert en `true` retourneert als de afsluitcode 0 is. Als de hook niet bestaat, retourneert deze `true` (geen hook betekent geen bezwaar).

## Geïnstalleerde hooks weergeven

```php
$installed = $hooks->listHooks();
// ['pre-commit', 'commit-msg', 'post-checkout']
```

Geeft alle uitvoerbare bestanden in `.git/hooks/` weer (`.sample`-bestanden worden uitgesloten).

`run()` retourneert bij een ontbrekende of niet-uitvoerbare hook afsluitcode 0 en geen uitvoer. Als het proces niet kan worden gestart, retourneert deze afsluitcode 1 en `Failed to start hook` op stderr.

## Een hook installeren

```php
$hooks->install('pre-commit', "#!/bin/bash\nphp vendor/bin/phpcs --standard=PSR12 .\n");
```

Schrijft de scriptinhoud naar `.git/hooks/<name>` en stelt de uitvoerbare machtiging in (`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);
```

## Hooks die Pitmaster uitvoert

`HookRunner` kan elke hook op naam uitvoeren. `Repository` voert de volgende hooks automatisch uit:

| Hook | Uitgevoerd door | Argumenten | Kan afbreken? |
|------|--------|-----------|-----------|
| `pre-commit` | `commit()` | geen | Ja (niet-nul-afsluitcode) |
| `prepare-commit-msg` | `commit()` | berichtbestand, daarna `message` (of `merge` bij het voltooien van een merge) | Ja |
| `commit-msg` | `commit()` | berichtbestand (`.git/COMMIT_EDITMSG`); wijzigingen in het bestand worden gebruikt | Ja (niet-nul-afsluitcode) |
| `post-commit` | `commit()` | geen | Nee (informatief) |
| `pre-rebase` | `rebase()` | de upstream-revisie | Ja |
| `post-checkout` | `checkout()` | oude HEAD, nieuwe HEAD, `1` | Nee (informatief) |
| `post-merge` | `merge()`, `mergeOctopus()`, `mergeContinue()` | `0` | Nee (informatief) |
| `pre-push` | `push()` en de varianten daarvan | remote-naam, URL; ref-regels via stdin | Ja (niet-nul-afsluitcode) |

Een mislukte hook zorgt ervoor dat de bewerking `RuntimeException('<name> hook failed')` genereert; de uitvoer van de hook wordt niet opgenomen. De commit-hooks worden alleen uitgevoerd vanuit `commit()`: mergecommits die door `merge()` zijn aangemaakt, voeren ze niet uit. `post-checkout` wordt niet uitgevoerd voor `addWorktree()`, `reset()` of `restore()`.

Serverhooks (`pre-receive`, `update`, `post-receive`, `post-update`), `pre-auto-gc` en `post-rewrite` worden nooit automatisch uitgevoerd.

## Hookomgeving

Wanneer Pitmaster een hook uitvoert, geeft het deze omgevingsvariabelen mee:

```php
$env = [
    'GIT_DIR' => $this->gitDir,
    'GIT_WORK_TREE' => dirname($this->gitDir),
];
```

Dit zijn de enige variabelen die de hook meekrijgt: `PATH`, `HOME` en alle andere variabelen van het PHP-proces worden niet doorgegeven ([#13](https://github.com/inline0/pitmaster/issues/13)). Gebruik absolute paden in hookscripts of stel `PATH` in het script in; een `#!/usr/bin/env php`-shebang vindt alleen interpreters in het standaard systeempad.

De hook wordt uitgevoerd in `dirname($gitDir)`, de working tree voor een normale `.git`-directory.

## Hookscriptindeling

Hooks kunnen in elke taal worden geschreven. De eerste regel moet een shebang zijn:

```bash
#!/bin/bash
# Bash hook
```

```php
#!/usr/bin/env php
<?php
// PHP hook
```

```python
#!/usr/bin/env python3
# Python hook
```

## Voorbeeld: pre-commit met Pitmaster

`commit()` voert `pre-commit`, `prepare-commit-msg`, `commit-msg` en `post-commit` al uit, dus hoeft applicatiecode alleen de fout af te handelen:

```php
try {
    $commitId = $repo->commit('Add feature');
} catch (\RuntimeException $e) {
    echo $e->getMessage(); // "pre-commit hook failed"
}
```

Roep `HookRunner` zelf alleen aan bij een handle die is geopend met `['hooks' => false]`, of voor hooks die Pitmaster niet uitvoert.

## Voorbeeldhooks

Git initialiseert repositories met hookbestanden in `.sample`; `Pitmaster::init()` maakt een lege `hooks/`-directory aan. Pitmaster's `listHooks()` sluit `.sample`-bestanden uit:

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