钩子
Git 钩子是在 .git/hooks/ 中可执行的脚本,会在 git 操作的特定时点运行。Pitmaster 使用 PHP 的进程控制功能(proc_open)检测并调用钩子。
钩子默认运行
仓库操作会自动运行 Pitmaster 运行的钩子 中列出的钩子。若要为某个仓库句柄关闭这些钩子,请在打开仓库时传入选项:
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)。
下文介绍的 HookRunner 会忽略这些选项:直接调用它时始终会运行钩子。
HookRunner
use Pitmaster\Hooks\HookRunner;
$hooks = new HookRunner($repo->gitDir());
检查钩子是否存在
if ($hooks->exists('pre-commit')) {
echo "pre-commit hook is installed\n";
}
如果 .git/hooks/<name> 处存在文件且该文件可执行,则钩子存在。
运行钩子
$result = $hooks->run('pre-commit');
echo $result['exitCode']; // 0 = success
echo $result['stdout']; // Hook's standard output
echo $result['stderr']; // Hook's standard error
使用参数
$result = $hooks->run('commit-msg', ['/path/to/COMMIT_EDITMSG']);
使用 stdin
$result = $hooks->run('pre-push', ['origin', 'https://github.com/user/repo.git'], $stdinData);
pre-push 钩子会从 stdin 接收 ref 信息。
检查是否成功
if ($hooks->runAndCheck('pre-commit')) {
// Hook passed (exit code 0), proceed with commit
} else {
// Hook rejected the operation
}
runAndCheck() 是一个便捷方法,会运行钩子,并在退出代码为 0 时返回 true。如果钩子不存在,则返回 true(没有钩子就表示没有异议)。
列出已安装的钩子
$installed = $hooks->listHooks();
// ['pre-commit', 'commit-msg', 'post-checkout']
列出 .git/hooks/ 中所有可执行文件(不包括 .sample 文件)。
如果钩子不存在或不可执行,run() 会以退出代码 0 返回空输出。如果无法启动进程,则会以退出代码 1 返回,并在 stderr 中输出 Failed to start hook。
安装钩子
$hooks->install('pre-commit', "#!/bin/bash\nphp vendor/bin/phpcs --standard=PSR12 .\n");
将脚本内容写入 .git/hooks/<name>,并设置可执行权限(chmod 0755)。
// 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 运行钩子时,会传递以下环境:
$env = [
'GIT_DIR' => $this->gitDir,
'GIT_WORK_TREE' => dirname($this->gitDir),
];
钩子只能获取这些变量:PATH、HOME 以及 PHP 进程中的其他变量都不会传递(#13)。在钩子脚本中使用绝对路径,或在脚本中设置 PATH;#!/usr/bin/env php shebang 只能在系统默认路径中查找解释器。
钩子会在 dirname($gitDir) 中运行;对于普通的 .git 目录,该位置就是工作树。
钩子脚本格式
钩子可以用任何语言编写。第一行必须是 shebang:
#!/bin/bash
# Bash hook
#!/usr/bin/env php
<?php
// PHP hook
#!/usr/bin/env python3
# Python hook
示例:使用 Pitmaster 执行 pre-commit
commit() 已经会运行 pre-commit、prepare-commit-msg、commit-msg 和 post-commit,因此应用代码只需处理失败情况:
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)