Vai al contenuto

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

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

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

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