跳转到正文

钩子

Git 钩子是在 .git/hooks/ 中可执行的脚本,会在 git 操作的特定时点运行。Pitmaster 使用 PHP 的进程控制功能(proc_open)检测并调用钩子。

钩子默认运行

仓库操作会自动运行 Pitmaster 运行的钩子 中列出的钩子。若要为某个仓库句柄关闭这些钩子,请在打开仓库时传入选项:

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

下文介绍的 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)。在钩子脚本中使用绝对路径,或在脚本中设置 PATH;#!/usr/bin/env php shebang 只能在系统默认路径中查找解释器。

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

钩子脚本格式

钩子可以用任何语言编写。第一行必须是 shebang:

命令行
#!/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)