
# 钩子

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 接收 ref 信息。

### 检查是否成功

```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()` | 无 | 是（退出代码非零） |
| `prepare-commit-msg` | `commit()` | 消息文件，然后是 `message`（完成合并时则为 `merge`） | 是 |
| `commit-msg` | `commit()` | 消息文件（`.git/COMMIT_EDITMSG`）；对文件的修改会被采用 | 是（退出代码非零） |
| `post-commit` | `commit()` | 无 | 否（仅供参考） |
| `pre-rebase` | `rebase()` | 上游修订版本 | 是 |
| `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`；`#!/usr/bin/env php` shebang 只能在系统默认路径中查找解释器。

钩子会在 `dirname($gitDir)` 中运行；对于普通的 `.git` 目录，该位置就是工作树。

## 钩子脚本格式

钩子可以用任何语言编写。第一行必须是 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"
}
```

只有在使用 `['hooks' => false]` 打开的句柄上，或针对 Pitmaster 不会运行的钩子，才需要自行调用 `HookRunner`。

## 示例钩子

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