Saltar al contenido

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 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).

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). 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:

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

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