你的本地管道 – Git hooks

在我们成功建立了一个简单但已经非常有用的 CI/CD 管道之后,我们现在想看看如何在本地开发环境中运行一些已经完成的步骤,甚至是在将它们提交到版本库之前。这听起来像是重复劳动—​我们为什么要运行相同的工具两次呢?

还记得本章开头的图 11.1—​根据发现错误的时间估算的修复错误的相对成本:我们越早发现一个错误,成本就越低,或者说花费的精力就越少。当然,如果我们在 CI/CD 过程中发现了错误,那也比在生产环境中发现的早得多。

不过,管道并不是免费的。我们的示例应用程序构建速度很快,只用了大约一分钟。然而,试想一下,一个成熟的 Docker 设置,创建所有必要的容器已经花费了大量时间。而现在,仅仅因为一个小 bug 就导致构建失败,而如果你没有忘记在提交代码前执行单元测试,你本可以在 2 分钟内解决这个问题。你可能刚刚喝了一杯茶或咖啡,当你回来时却发现构建失败了。这种情况很烦人,既浪费金钱,又浪费计算能力。

单元测试、代码嗅探器或静态代码分析等快速运行检查正是我们希望在开始全面构建变更之前执行的。我们不能依靠自己自动执行这些检查,因为我们是人类。我们会忘记一些事情,但机器不会。

如果你使用 Git 进行开发(目前大多数开发人员都使用 Git),我们可以利用 Git 钩子的内置功能来自动执行这些检查。Git 钩子是在特定事件(如每次提交前后)发生时自动执行的 shell 脚本。

对于我们的需求,提交前钩子尤其有用。每次运行 git commit 命令时,它都会被执行,如果执行的脚本返回错误,它就会中止提交。在这种情况下,代码不会被添加到版本库中。

设置 Git 挂钩

手动设置 Git 钩子需要一定的 shell 脚本知识,因此我们希望使用一个名为 CaptainHook 的软件包来帮助我们。使用这个工具,我们可以安装任何我们喜欢的钩子,甚至可以使用一些高级功能,而无需掌握 Linux。

你可以使用 Phive 轻松下载 Phar(更多信息请参见第 9 章,PHP 质量工具的组织),或者使用 Composer 安装,就像我们现在要做的:

$ composer require --dev captainhook/captainhook
bash

接下来,我们需要创建一个 captainhook.json 文件。该文件包含项目的钩子配置。由于该文件将被添加到版本库中,因此我们要确保团队中的其它开发人员也能使用该文件。要创建该文件,我们可以运行以下命令:

$ vendor/bin/captainhook configure
bash

CaptainHook 会问你几个问题,并根据你的回答生成一个配置文件。不过,你也可以跳过这一步,直接创建文件,就像我们现在要做的那样。打开你喜欢的编辑器,编写以下代码:

{
    "config": {
      "fail-on-first-error": true
    },
    "pre-commit": {
        "enabled": true,
        "actions": [
            {
              "action": "vendor/bin/php-cs-fixer fix --dry-run"
            },
            {
              "action": "vendor/bin/phpstan"
            }
        ]
    }
}
json

每个钩子都有自己的部分。在 pre-commit 钩子部分,enabled 可以为 truefalse,后者会禁用钩子,但会将配置保留在文件中,方便调试。正如你所看到的,这些命令就像你在 第 7 章 "代码质量工具" 中已经知道的命令一样。

每个要执行的操作都需要写在单独的操作部分。在前面的例子中,我们将 PHP-CS-Fixer 和 PHPStan 配置为在 pre-commit 时执行。

由于这两个工具都有额外的配置文件,因此除了告诉 PHP-CS-Fixer 只进行干运行—​即只在发现代码样式违规时通知我们—​之外,我们不需要指定任何其它选项。

config 部分,你可以指定更多的配置参数。我们希望在错误发生后立即停止钩子的执行,因此将 fail-on-first-error 设为 true。否则,CaptainHook 会先运行所有检查,然后再告诉你结果。当然,这只是个人喜好问题。

CaptainHook 文档

我们无法在本书中列出 CaptainHook 的所有功能。不过,我们鼓励你查看 https://captainhookphp.github.io/captainhook 上的官方文档,进一步了解这个工具。

配置完成后,请将此 JavaScript Object Notation(JSON) 文件以 captainhook.json 为名保存在项目根文件夹中。这就是我们要做的所有配置工作。

现在我们只需安装钩子,即在 .git/hooks 中生成钩子文件。具体步骤如下:

$ vendor/bin/captainhook install -f
bash

我们在这里使用 -f 选项,它代表强制。如果没有这个选项,CaptainHook 会要求我们分别安装每个钩子。请注意,CaptainHook 会为它支持的每个 Git 钩子安装一个文件,即使是那些你没有配置的钩子。不过这些钩子不会做任何事情。

要测试 pre-commit 钩子,可以使用以下命令手动执行,而无需提交任何内容:

$ vendor/bin/captainhook hook:pre-commit
bash

类似的命令也适用于 CaptainHook 支持的所有其它钩子。如果你修改了 captainhook.json 文件,别忘了再次使用 install -f 命令安装。

要确保钩子安装在本地开发环境中,可以在 composer.json 文件的脚本部分添加以下代码:

"post-autoload-dump": [
    "if [ -e vendor/bin/captainhook ]; then
        vendor/bin/captainhook install -f -s; fi"
]
json

我们使用 Composer 的 post-autoload-dump 事件来运行 install -f 命令。每次刷新 Composer 自动加载器(composer install 或 composer update)时都会执行该命令。这样,我们就能确保任何参与该项目的开发人员都能在开发环境中定期安装或更新钩子。通过使用 if [ -e vendor/bin/captainhook ],我们可以检查 CaptainHook 二进制文件是否存在,避免在未安装时破坏 CI 构建。

Git hook 实践

我们完成了 pre-commit 钩子的配置,并测试和安装了它。现在,我们可以看看它的实际效果了:如果你对程序代码做了任何改动,例如在 ProductController.php 文件中添加了一行空行,然后尝试提交改动,预提交钩子就会执行。如果更改违反了 PSR-12 标准,PHP-CS-Fixer 步骤就会失败,如下图所示:

image 2023 11 12 18 37 21 660
Figure 1. Figure 11.6: The pre-commit hook fails
自动修复代码风格问题

当然,你也可以在执行 PHP-CS-Fixer 时去掉 --dry-run 选项,让它自动修复问题。事实上,这是一种常见的做法,我们鼓励你也尝试一下。不过,这需要做更多的工作,因为你必须让用户知道他们更改的文件已被修复,并需要重新提交。为了使本示例简单明了,我们决定省略这一点。

现在我们知道 ProductController.php 文件需要修复。我们可以让 PHP-CS-Fixer 来完成这项工作,如下所示:

image 2023 11 12 18 42 49 772
Figure 2. Figure 11.7: Using PHP-CS-Fixer to automatically fix code style issues

现在,ProductController.php 又有了新的改动,但这些额外的改动还没有被暂存,也就是说,它们还没有被添加到提交中。但之前的更改仍在缓存中。下面的截图显示了此时运行 git status 的结果:

image 2023 11 12 18 43 35 964
Figure 3. Figure 11.8: Unstaged changes

现在只需再次添加 ProductController.php 文件,并再次运行 git commit 即可,如下面的截图所示:

image 2023 11 12 18 44 12 613
Figure 4. Figure 11.9: pre-commit hook passes

现在预提交钩子的两个步骤都通过了。现在只需 git push 已提交的更改。

高级用法

前面的例子只是一个非常基本的例子。当然,你可以在本地开发环境中做更多事情。例如,你可以添加更多工具,如 phpcpd 复制和粘贴检测器或 phpmd 混乱检测器,这两个工具我们都在 第 7 章 "代码质量工具" 中介绍过。

如果你的测试速度不是太慢(这取决于你和你的队友的耐心),你也应该考虑在本地运行测试。即使有运行速度较慢的测试,也可以将它们分成几个测试套件,只在 pre-commit 时执行运行速度较快的测试。

你还应该考虑只对修改过的文件而不是整个项目进行代码质量检查,就像我们在示例中做的那样。CaptainHook 提供了有用的 {$STAGED_FILES} 占位符,其中包含所有暂存文件。正如我们在这里看到的,使用起来非常方便:

{
    "pre-commit": {
        "enabled": true,
        "actions": [
            {
              "action": "vendor/bin/php-cs-fixer fix {$STAGED_FILES|of-type:php} --dry-run"
            },
            {
              "action": "vendor/bin/phpstan analyse {$STAGED_FILES|of-type:php}"
            }
        ]
    }
}
json

上例只对修改过的 PHP 文件进行检查。这样做有两个主要好处:首先,速度更快,因为你不必检查你没有接触过的代码。当然,速度的提升取决于代码库的大小。

其次,特别是如果你正在处理一个现有项目,并且刚刚开始引入这些检查,那么在整个代码库中运行这些检查是不可行的,因为你需要一次性修复太多文件。我们将在下一节详细讨论这个问题。