
# Ganchos

Os hooks Git são scripts executáveis em `.git/hooks/`, acionados em pontos específicos das operações de git. Pitmaster detecta e invoca hooks usando o controle de processos de PHP (`proc_open`).

## Hooks executados por padrão

As operações de repositório executam automaticamente os hooks listados em [Hooks executados pelo Pitmaster](#hooks-pitmaster-runs). Para desativá-los em uma instância de repositório, passe as opções ao abri-la:

```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()` aceitam as mesmas opções. A execução de um hook requer `proc_open()`; em hosts que o desativam, abra os repositórios com `['hooks' => false]`. O CLI sempre abre o repositório com hooks ativados.

Pitmaster procura hooks somente em `<gitDir>/hooks`. Ignora `core.hooksPath` e, em uma árvore de trabalho vinculada, procura em `.git/worktrees/<name>/hooks`; por isso, os scripts compartilhados de `.git/hooks` não são executados ali ([#12](https://github.com/inline0/pitmaster/issues/12)).

`HookRunner`, descrito abaixo, ignora essas opções: chamá-lo diretamente sempre executa o hook.

## HookRunner

```php
use Pitmaster\Hooks\HookRunner;

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

## Verificar se um hook existe

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

Um hook existe se o arquivo estiver presente em `.git/hooks/<name>` e for executável.

## Executar um 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
```

### Com argumentos

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

### Com stdin

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

O hook `pre-push` recebe informações de referência via stdin.

### Verificar sucesso

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

`runAndCheck()` é um atalho que executa o hook e retorna `true` se o código de saída for 0. Se o hook não existir, retorna `true` (sem hook, não há objeção).

## Listar hooks instalados

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

Lista todos os arquivos executáveis em `.git/hooks/` (exclui arquivos `.sample`).

`run()`, quando o hook não existe ou não é executável, retorna código de saída 0 e saída vazia. Se não for possível iniciar o processo, retorna código de saída 1 e `Failed to start hook` em stderr.

## Instalar um hook

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

Grava o conteúdo do script em `.git/hooks/<name>` e define a permissão de execução (`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 executados por Pitmaster

`HookRunner` pode executar qualquer hook pelo nome. `Repository` executa automaticamente estes:

| Hook | Executado por | Argumentos | Pode abortar? |
|------|--------|-----------|-----------|
| `pre-commit` | `commit()` | nenhum | Sim (código de saída diferente de zero) |
| `prepare-commit-msg` | `commit()` | arquivo de mensagem, depois `message` (ou `merge` ao concluir uma mesclagem) | Sim |
| `commit-msg` | `commit()` | arquivo de mensagem (`.git/COMMIT_EDITMSG`); alterações no arquivo são usadas | Sim (código de saída diferente de zero) |
| `post-commit` | `commit()` | nenhum | Não (informativo) |
| `pre-rebase` | `rebase()` | revisão upstream | Sim |
| `post-checkout` | `checkout()` | HEAD antigo, HEAD novo, `1` | Não (informativo) |
| `post-merge` | `merge()`, `mergeOctopus()`, `mergeContinue()` | `0` | Não (informativo) |
| `pre-push` | `push()` e suas variantes | nome remoto, URL; linhas de referência via stdin | Sim (código de saída diferente de zero) |

Um hook com falha faz a operação lançar `RuntimeException('<name> hook failed')`; a saída do hook não é incluída. Os hooks de commit só são executados por `commit()`: commits de mesclagem criados por `merge()` não os executam. `post-checkout` não é executado para `addWorktree()`, `reset()` ou `restore()`.

Hooks do lado do servidor (`pre-receive`, `update`, `post-receive`, `post-update`), `pre-auto-gc` e `post-rewrite` nunca são executados automaticamente.

## Ambiente do hook

Quando Pitmaster executa um hook, ele fornece este ambiente:

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

Estas são as únicas variáveis disponíveis para o hook: `PATH`, `HOME` e todas as demais variáveis do processo PHP não são repassadas ([#13](https://github.com/inline0/pitmaster/issues/13)). Use caminhos absolutos nos scripts de hook ou defina `PATH` dentro do script; um shebang `#!/usr/bin/env php` só encontra interpretadores no caminho padrão do sistema.

O hook é executado em `dirname($gitDir)`, que é a árvore de trabalho de um diretório `.git` normal.

## Formato do script de hook

Os hooks podem ser escritos em qualquer linguagem. A primeira linha deve ser um shebang:

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

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

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

## Exemplo: pre-commit com Pitmaster

`commit()` já executa `pre-commit`, `prepare-commit-msg`, `commit-msg` e `post-commit`; portanto, o código da aplicação só precisa tratar a falha:

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

Chame `HookRunner` manualmente somente em um identificador aberto com `['hooks' => false]` ou para hooks que Pitmaster não executa.

## Hooks de exemplo

Git inicializa repositórios com arquivos de hook `.sample`; `Pitmaster::init()` cria um diretório `hooks/` vazio. O Pitmaster de `listHooks()` exclui arquivos `.sample`:

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