
# Hooks

Les hooks Git sont des scripts exécutables dans `.git/hooks/` qui s’exécutent à des moments précis des opérations git. Pitmaster les détecte et les appelle à l’aide du contrôle des processus de PHP (`proc_open`).

## Les hooks s’exécutent par défaut

Les opérations du dépôt exécutent automatiquement les hooks répertoriés sous [Hooks exécutés par Pitmaster](#hooks-pitmaster-runs). Pour les désactiver pour un handle de dépôt, transmettez des options à son ouverture :

```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()` et `Pitmaster::clone()` acceptent les mêmes options. L’exécution d’un hook nécessite `proc_open()` ; sur les hôtes où cette fonctionnalité est désactivée, ouvrez les dépôts avec `['hooks' => false]`. Le CLI ouvre toujours le dépôt avec les hooks activés.

Pitmaster recherche les hooks uniquement dans `<gitDir>/hooks`. Il ignore `core.hooksPath` et, dans un worktree lié, les recherche dans `.git/worktrees/<name>/hooks` ; les scripts partagés de `.git/hooks` ne s’y exécutent donc pas ([#12](https://github.com/inline0/pitmaster/issues/12)).

`HookRunner`, décrit ci-dessous, ignore ces options : appelé directement, il exécute toujours le hook.

## HookRunner

```php
use Pitmaster\Hooks\HookRunner;

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

## Vérifier si un hook existe

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

Un hook existe si le fichier est présent à `.git/hooks/<name>` et est exécutable.

## Exécuter 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
```

### Avec des arguments

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

### Avec stdin

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

Le hook `pre-push` reçoit les informations de référence via stdin.

### Vérifier la réussite

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

`runAndCheck()` est une méthode pratique qui exécute le hook et renvoie `true` si le code de sortie est 0. Si le hook n’existe pas, elle renvoie `true` (l’absence de hook ne vaut pas objection).

## Lister les hooks installés

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

Répertorie tous les fichiers exécutables de `.git/hooks/` (à l’exclusion des fichiers `.sample`).

`run()` renvoie le code de sortie 0 et une sortie vide si le hook est absent ou non exécutable. Si le processus ne peut pas démarrer, il renvoie le code de sortie 1 et écrit `Failed to start hook` sur stderr.

## Installer un hook

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

Écrit le contenu du script dans `.git/hooks/<name>` et définit le droit d’exécution (`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 exécutés par Pitmaster

`HookRunner` peut exécuter n’importe quel hook par son nom. `Repository` exécute automatiquement les hooks suivants :

| Hook | Exécuté par | Arguments | Peut interrompre ? |
|------|------------|-----------|--------------------|
| `pre-commit` | `commit()` | aucun | Oui (code de sortie non nul) |
| `prepare-commit-msg` | `commit()` | fichier de message, puis `message` (ou `merge` lors de la finalisation d’une fusion) | Oui |
| `commit-msg` | `commit()` | fichier de message (`.git/COMMIT_EDITMSG`) ; les modifications du fichier sont utilisées | Oui (code de sortie non nul) |
| `post-commit` | `commit()` | aucun | Non (informatif) |
| `pre-rebase` | `rebase()` | révision amont | Oui |
| `post-checkout` | `checkout()` | ancien HEAD, nouveau HEAD, `1` | Non (informatif) |
| `post-merge` | `merge()`, `mergeOctopus()`, `mergeContinue()` | `0` | Non (informatif) |
| `pre-push` | `push()` et ses variantes | nom de la télécommande, URL ; lignes de référence via stdin | Oui (code de sortie non nul) |

Un hook qui échoue fait lever `RuntimeException('<name> hook failed')` par l’opération ; la sortie du hook n’est pas incluse. Les hooks de commit ne s’exécutent que depuis `commit()` : les commits de fusion créés par `merge()` ne les exécutent pas. `post-checkout` ne s’exécute pas pour `addWorktree()`, `reset()` ou `restore()`.

Les hooks côté serveur (`pre-receive`, `update`, `post-receive`, `post-update`), `pre-auto-gc` et `post-rewrite` ne sont jamais exécutés automatiquement.

## Environnement des hooks

Lorsque Pitmaster exécute un hook, il lui transmet cet environnement :

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

Ce sont les seules variables accessibles au hook : `PATH`, `HOME` et toutes les autres variables du processus PHP ne sont pas transmises ([#13](https://github.com/inline0/pitmaster/issues/13)). Utilisez des chemins absolus dans les scripts de hook, ou définissez `PATH` dans le script ; un shebang `#!/usr/bin/env php` ne trouve les interpréteurs que dans le chemin système par défaut.

Le hook s’exécute dans `dirname($gitDir)`, qui correspond au répertoire de travail d’un répertoire `.git` normal.

## Format des scripts de hook

Les hooks peuvent être écrits dans n’importe quel langage. La première ligne doit être un shebang :

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

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

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

## Exemple : pre-commit avec Pitmaster

`commit()` exécute déjà `pre-commit`, `prepare-commit-msg`, `commit-msg` et `post-commit` ; le code de l’application doit donc uniquement gérer l’échec :

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

Appelez vous-même `HookRunner` uniquement sur un handle ouvert avec `['hooks' => false]`, ou pour les hooks que Pitmaster n’exécute pas.

## Exemples de hooks

Git initialise les dépôts avec des fichiers de hook `.sample` ; `Pitmaster::init()` crée un répertoire `hooks/` vide. Le Pitmaster de `listHooks()` exclut les fichiers `.sample` :

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