
# フック

Gitフックは、`.git/hooks/`内にある実行可能スクリプトで、git操作の特定のタイミングで実行されます。PitmasterはPHPのプロセス制御（`proc_open`）を使ってフックを検出し、実行します。

## フックはデフォルトで実行される

リポジトリ操作では、[Hooks Pitmaster runs](#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`は無視され、リンクされたworktreeでは`.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`フックは、ref情報をstdinから受け取ります。

### 成功を確認する

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

`runAndCheck()`はフックを実行する便利なメソッドで、終了コードが0の場合に`true`を返します。フックが存在しない場合は`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()` | なし | はい（終了コードが0以外） |
| `prepare-commit-msg` | `commit()` | メッセージファイル、その後`message`（マージ完了時は`merge`） | はい |
| `commit-msg` | `commit()` | メッセージファイル（`.git/COMMIT_EDITMSG`）。ファイルへの編集内容が使われます | はい（終了コードが0以外） |
| `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から受け取る | はい（終了コードが0以外） |

フックが失敗すると、操作は`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`を設定してください。`#!/usr/bin/env php`のshebangでは、システムのデフォルトパスにあるインタープリターしか見つかりません。

フックは`dirname($gitDir)`で実行されます。通常の`.git`ディレクトリでは、ここが作業ツリーになります。

## フックスクリプトの形式

フックは任意の言語で記述できます。1行目にはshebangが必要です。

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

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

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

## 例：Pitmasterを使ったpre-commit

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