
# Hook

Gli hook Git sono script eseguibili in `.git/hooks/` che vengono eseguiti in momenti specifici delle operazioni git. Pitmaster rileva e invoca gli hook usando il controllo dei processi di PHP (`proc_open`).

## Gli hook vengono eseguiti per impostazione predefinita

Le operazioni sul repository eseguono automaticamente gli hook elencati in [Hook eseguiti da Pitmaster](#hooks-pitmaster-runs). Per disattivarli per un handle del repository, passate le opzioni all’apertura:

```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()` e `Pitmaster::clone()` accettano le stesse opzioni. Per eseguire un hook occorre `proc_open()`; sugli host in cui è disabilitato, aprite i repository con `['hooks' => false]`. CLI apre sempre il repository con gli hook abilitati.

Pitmaster cerca gli hook solo in `<gitDir>/hooks`. Ignora `core.hooksPath` e, in un worktree collegato, cerca in `.git/worktrees/<name>/hooks`, quindi gli script `.git/hooks` condivisi non vengono eseguiti ([#12](https://github.com/inline0/pitmaster/issues/12)).

`HookRunner`, descritto sotto, ignora queste opzioni: se chiamato direttamente, esegue sempre l’hook.

## HookRunner

```php
use Pitmaster\Hooks\HookRunner;

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

## Verificare se esiste un hook

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

Un hook esiste se il file è presente in `.git/hooks/<name>` ed è eseguibile.

## Eseguire un hook

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

### Con argomenti

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

### Con stdin

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

L’hook `pre-push` riceve informazioni sui ref tramite stdin.

### Verificare l’esito

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

`runAndCheck()` è una funzione di utilità che esegue l’hook e restituisce `true` se il codice di uscita è 0. Se l’hook non esiste, restituisce `true` (l’assenza di un hook equivale a nessuna obiezione).

## Elencare gli hook installati

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

Elenca tutti i file eseguibili in `.git/hooks/` (escludendo i file `.sample`).

`run()` restituisce il codice di uscita 0 e un output vuoto se l’hook manca o non è eseguibile. Se non è possibile avviare il processo, restituisce il codice di uscita 1 e invia `Failed to start hook` su stderr.

## Installare un hook

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

Scrive il contenuto dello script in `.git/hooks/<name>` e imposta il permesso di esecuzione (`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);
```

## Hook eseguiti da Pitmaster

`HookRunner` può eseguire qualsiasi hook per nome. `Repository` esegue automaticamente questi hook:

| Hook | Eseguito da | Argomenti | Può interrompere? |
|------|--------|-----------|-----------|
| `pre-commit` | `commit()` | nessuno | Sì (codice di uscita diverso da zero) |
| `prepare-commit-msg` | `commit()` | file del messaggio, poi `message` (oppure `merge` al completamento di un merge) | Sì |
| `commit-msg` | `commit()` | file del messaggio (`.git/COMMIT_EDITMSG`); vengono usate le modifiche al file | Sì (codice di uscita diverso da zero) |
| `post-commit` | `commit()` | nessuno | No (informativo) |
| `pre-rebase` | `rebase()` | revisione upstream | Sì |
| `post-checkout` | `checkout()` | HEAD precedente, nuovo HEAD, `1` | No (informativo) |
| `post-merge` | `merge()`, `mergeOctopus()`, `mergeContinue()` | `0` | No (informativo) |
| `pre-push` | `push()` e le relative varianti | nome del remote, URL; righe ref su stdin | Sì (codice di uscita diverso da zero) |

Un hook non riuscito fa generare all’operazione un’eccezione `RuntimeException('<name> hook failed')`; l’output dell’hook non viene incluso. Gli hook di commit vengono eseguiti solo da `commit()`: i merge commit creati da `merge()` non li eseguono. `post-checkout` non viene eseguito per `addWorktree()`, `reset()` o `restore()`.

Gli hook lato server (`pre-receive`, `update`, `post-receive`, `post-update`), `pre-auto-gc` e `post-rewrite` non vengono mai eseguiti automaticamente.

## Ambiente degli hook

Quando Pitmaster esegue un hook, gli passa questo ambiente:

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

Queste sono le sole variabili disponibili per l’hook: `PATH`, `HOME` e tutte le altre variabili del processo PHP non vengono passate ([#13](https://github.com/inline0/pitmaster/issues/13)). Usate percorsi assoluti negli script degli hook oppure impostate `PATH` nello script; uno shebang `#!/usr/bin/env php` trova gli interpreti solo nel percorso predefinito di sistema.

L’hook viene eseguito in `dirname($gitDir)`, che corrisponde all’albero di lavoro di una normale directory `.git`.

## Formato degli script hook

Gli hook possono essere scritti in qualsiasi linguaggio. La prima riga deve contenere uno shebang:

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

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

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

## Esempio: pre-commit con Pitmaster

`commit()` esegue già `pre-commit`, `prepare-commit-msg`, `commit-msg` e `post-commit`, quindi il codice dell’applicazione deve solo gestire l’errore:

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

Chiamate `HookRunner` direttamente solo su un handle aperto con `['hooks' => false]`, oppure per gli hook che Pitmaster non esegue.

## Esempi di hook

Git inizializza i repository con file hook `.sample`; `Pitmaster::init()` crea una directory `hooks/` vuota. Il file `listHooks()` di Pitmaster esclude i file `.sample`:

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