
# Хуки

Перехватчики Git — это исполняемые скрипты в `.git/hooks/`, которые запускаются в определённые моменты операций git. Pitmaster обнаруживает и запускает перехватчики с помощью управления процессами в PHP (`proc_open`).

## Перехватчики запускаются автоматически

При операциях с репозиторием автоматически запускаются перехватчики, перечисленные в разделе [Hooks Pitmaster](#hooks-pitmaster-runs). Чтобы отключить их для дескриптора репозитория, передайте параметры при его открытии:

```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()` и `Pitmaster::clone()` принимают одинаковые параметры. Для запуска перехватчика требуется `proc_open()`; если хост отключает эту возможность, открывайте репозитории с помощью `['hooks' => false]`. CLI всегда открывает репозиторий с включёнными перехватчиками.

Pitmaster ищет перехватчики только в `<gitDir>/hooks`. Он игнорирует `core.hooksPath`, а в связанном рабочем дереве ищет в `.git/worktrees/<name>/hooks`, поэтому общие скрипты `.git/hooks` там не запускаются ([#12](https://github.com/inline0/pitmaster/issues/12)).

`HookRunner`, описанный ниже, игнорирует эти параметры: при прямом вызове перехватчик запускается всегда.

## HookRunner

```php
use Pitmaster\Hooks\HookRunner;

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

## Проверка наличия перехватчика

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

Перехватчик существует, если файл находится по адресу `.git/hooks/<name>` и является исполняемым.

## Запуск перехватчика

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

### С аргументами

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

### Через stdin

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

Перехватчик `pre-push` получает информацию о ссылке через stdin.

### Проверка успешного выполнения

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

`runAndCheck()` — это вспомогательная функция, которая запускает перехватчик и возвращает `true`, если код завершения равен 0. Если перехватчик отсутствует, возвращается `true` (отсутствие перехватчика означает отсутствие возражений).

## Список установленных перехватчиков

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

Перечисляет все исполняемые файлы в `.git/hooks/` (файлы `.sample` исключаются).

При отсутствии перехватчика или если он не является исполняемым `run()` возвращает код завершения 0 и пустой вывод. Если запустить процесс не удаётся, возвращается код завершения 1, а в stderr выводится `Failed to start hook`.

## Установка перехватчика

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

Записывает содержимое скрипта в `.git/hooks/<name>` и задаёт право на выполнение (`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);
```

## Перехватчики, запускаемые Pitmaster

`HookRunner` может запускать любой перехватчик по имени. Автоматически запускаются следующие перехватчики `Repository`:

| Перехватчик | Запускается командой | Аргументы | Может прервать операцию? |
|------|--------|-----------|-----------|
| `pre-commit` | `commit()` | нет | Да (ненулевой код завершения) |
| `prepare-commit-msg` | `commit()` | файл сообщения, затем `message` (или `merge` при завершении слияния) | Да |
| `commit-msg` | `commit()` | файл сообщения (`.git/COMMIT_EDITMSG`); изменения файла используются | Да (ненулевой код завершения) |
| `post-commit` | `commit()` | нет | Нет (только информирует) |
| `pre-rebase` | `rebase()` | ревизия upstream | Да |
| `post-checkout` | `checkout()` | старый HEAD, новый HEAD, `1` | Нет (только информирует) |
| `post-merge` | `merge()`, `mergeOctopus()`, `mergeContinue()` | `0` | Нет (только информирует) |
| `pre-push` | `push()` и его варианты | имя удалённого репозитория, URL; строки ссылок через stdin | Да (ненулевой код завершения) |

Если перехватчик завершился с ошибкой, операция вызывает исключение `RuntimeException('<name> hook failed')`; вывод перехватчика не включается. Перехватчики коммита запускаются только из `commit()`: коммиты слияния, созданные командой `merge()`, их не запускают. `post-checkout` не запускается для `addWorktree()`, `reset()` или `restore()`.

Серверные перехватчики (`pre-receive`, `update`, `post-receive`, `post-update`), `pre-auto-gc` и `post-rewrite` никогда не запускаются автоматически.

## Окружение перехватчика

При запуске перехватчика Pitmaster передаёт ему следующее окружение:

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

Это единственные переменные, доступные перехватчику: `PATH`, `HOME` и все остальные переменные процесса PHP не передаются ([#13](https://github.com/inline0/pitmaster/issues/13)). В скриптах перехватчиков используйте абсолютные пути или задайте `PATH` внутри скрипта; shebang `#!/usr/bin/env php` находит интерпретаторы только в системном пути по умолчанию.

Перехватчик запускается в `dirname($gitDir)` — рабочем дереве обычного каталога `.git`.

## Формат скрипта перехватчика

Перехватчики можно писать на любом языке. Первая строка должна содержать shebang:

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

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

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

## Пример: pre-commit с Pitmaster

`commit()` уже запускает `pre-commit`, `prepare-commit-msg`, `commit-msg` и `post-commit`, поэтому в коде приложения достаточно обработать ошибку:

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

Вызывайте `HookRunner` самостоятельно только для дескриптора, открытого с помощью `['hooks' => false]`, или для перехватчиков, которые Pitmaster не запускает.

## Примеры перехватчиков

Git создаёт репозитории с файлами перехватчиков `.sample`; `Pitmaster::init()` создаёт пустой каталог `hooks/`. В файле `listHooks()` для Pitmaster исключены файлы `.sample`:

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