
# Hooks

Los hooks Git son scripts ejecutables en `.git/hooks/` que se ejecutan en puntos específicos de las operaciones de git. Pitmaster detecta e invoca hooks mediante el control de procesos de PHP (`proc_open`).

## Los hooks se ejecutan de forma predeterminada

Las operaciones del repositorio ejecutan los hooks enumerados en [Hooks que Pitmaster ejecuta](#hooks-pitmaster-runs) automáticamente. Para desactivarlos en un identificador de repositorio, pasa opciones al abrirlo:

```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()` y `Pitmaster::clone()` aceptan las mismas opciones. Para ejecutar un hook se necesita `proc_open()`; en los hosts donde está desactivado, abre los repositorios con `['hooks' => false]`. CLI siempre abre el repositorio con los hooks habilitados.

Pitmaster busca hooks solo en `<gitDir>/hooks`. Ignora `core.hooksPath` y, en un árbol de trabajo vinculado, busca en `.git/worktrees/<name>/hooks`, por lo que los scripts compartidos de `.git/hooks` no se ejecutan allí ([#12](https://github.com/inline0/pitmaster/issues/12)).

`HookRunner`, descrito más abajo, ignora estas opciones: si lo llamas directamente, siempre ejecuta el hook.

## HookRunner

```php
use Pitmaster\Hooks\HookRunner;

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

## Comprobar si existe un hook

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

Un hook existe si el archivo está presente en `.git/hooks/<name>` y es ejecutable.

## Ejecutar 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 argumentos

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

El hook `pre-push` recibe información de referencias por stdin.

### Comprobar el éxito

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

`runAndCheck()` es una función auxiliar que ejecuta el hook y devuelve `true` si el código de salida es 0. Si el hook no existe, devuelve `true` (si no hay hook, no hay objeción).

## Enumerar los hooks instalados

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

Enumera todos los archivos ejecutables en `.git/hooks/` (excluye los archivos `.sample`).

`run()` devuelve el código de salida 0 y una salida vacía si el hook no existe o no es ejecutable. Si no se puede iniciar el proceso, devuelve el código de salida 1 y `Failed to start hook` en stderr.

## Instalar un hook

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

Escribe el contenido del script en `.git/hooks/<name>` y establece el permiso de ejecución (`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 que ejecuta Pitmaster

`HookRunner` puede ejecutar cualquier hook por nombre. `Repository` ejecuta estos automáticamente:

| Hook | Lo ejecuta | Argumentos | ¿Puede abortar? |
|------|------------|-----------|----------------|
| `pre-commit` | `commit()` | ninguno | Sí (código de salida distinto de cero) |
| `prepare-commit-msg` | `commit()` | archivo de mensaje y luego `message` (o `merge` al completar una fusión) | Sí |
| `commit-msg` | `commit()` | archivo de mensaje (`.git/COMMIT_EDITMSG`); se usan los cambios en el archivo | Sí (código de salida distinto de cero) |
| `post-commit` | `commit()` | ninguno | No (informativo) |
| `pre-rebase` | `rebase()` | la revisión de origen | Sí |
| `post-checkout` | `checkout()` | HEAD anterior, HEAD nuevo, `1` | No (informativo) |
| `post-merge` | `merge()`, `mergeOctopus()`, `mergeContinue()` | `0` | No (informativo) |
| `pre-push` | `push()` y sus variantes | nombre del remoto, URL; líneas de referencia por stdin | Sí (código de salida distinto de cero) |

Si un hook falla, la operación genera `RuntimeException('<name> hook failed')`; su salida no se incluye. Los hooks de confirmación solo se ejecutan desde `commit()`: las fusiones creadas por `merge()` no los ejecutan. `post-checkout` no se ejecuta para `addWorktree()`, `reset()` ni `restore()`.

Los hooks del lado del servidor (`pre-receive`, `update`, `post-receive`, `post-update`), `pre-auto-gc` y `post-rewrite` nunca se ejecutan automáticamente.

## Entorno del hook

Cuando Pitmaster ejecuta un hook, le pasa este entorno:

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

Estas son las únicas variables que recibe el hook: `PATH`, `HOME` y todas las demás variables del proceso PHP no se pasan ([#13](https://github.com/inline0/pitmaster/issues/13)). Usa rutas absolutas en los scripts de hooks o establece `PATH` dentro del script; un shebang `#!/usr/bin/env php` solo encuentra intérpretes en la ruta predeterminada del sistema.

El hook se ejecuta en `dirname($gitDir)`, que es el árbol de trabajo de un directorio `.git` normal.

## Formato del script del hook

Los hooks pueden escribirse en cualquier lenguaje. La primera línea debe ser un shebang:

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

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

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

## Ejemplo: pre-commit con Pitmaster

`commit()` ya ejecuta `pre-commit`, `prepare-commit-msg`, `commit-msg` y `post-commit`, así que el código de la aplicación solo tiene que gestionar el error:

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

Llama a `HookRunner` por tu cuenta solo si el identificador se abrió con `['hooks' => false]` o para los hooks que Pitmaster no ejecuta.

## Hooks de ejemplo

Git inicializa repositorios con archivos de hook `.sample`; `Pitmaster::init()` crea un directorio `hooks/` vacío. El `listHooks()` de Pitmaster excluye los archivos `.sample`:

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