
# Хуки

Хуки Git — це виконувані сценарії в `.git/hooks/`, які запускаються в певні моменти операцій git. Pitmaster виявляє та запускає хуки за допомогою керування процесами в PHP (`proc_open`).

## Хуки запускаються за замовчуванням

Під час операцій із репозиторієм автоматично запускаються хуки зі списку [Хуки 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; рядки ref через 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/`. Pitmaster у `listHooks()` виключає файли `.sample`:

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