首页 > 技术文章 > 前端 Git-Hooks 工程化实践

dtux 2022-06-28 14:16 原文

前言

前段时间,部门的前端项目迁移到 monorepo 架构,笔者在其中负责跟 git 工作流相关的事情,其中就包括 git hooks 相关的工程化的实践。用到了一些常用的相关工具如 husky、lint-staged、commitizen、commit-lint 等,以此文记录一下整个的实践过程和踩过的坑。

注意:下文中的例子以及命令都是基于 Mac OS,如果你是 windows 用户,也不用担心,文中也会阐述大致原理和运行逻辑,对应的 windows 命令可以推理得知。

Git Hooks

Git Hooks 是什么

大多数同学应该都对 git hooks 相当了解,但是笔者还是想在这里详细解释一下。
首先是 hook,这其实是计算机领域中一个很常见的概念,hook 翻译过来的意思是钩子或者勾住,而在计算机领域中则要分为两种解释:

  1. 拦截消息,在消息到达目标前,提前对消息进行处理
  2. 对特定的事件进行监听,当某个事件或动作被触发时也会同时触发对应的 hook
    也就是说 hook 本身也是一段程序,只是它会在特定的时机被触发。

理解了 hook 这一概念,那么 git hooks 也就不难理解了。git hooks 就是在运行某些 git 命令时,被触发的对应的程序。

在前端领域,钩子的概念也并不少见,比如 Vue 声明周期钩子、React Hooks、webpack 钩子等,说到底它们都是在特定的时机触发的方法或者函数

常见的 Git Hooks 有哪些

git hooks 分为两类

客户端 hook

  • pre-commit hook, 在运行 git commit 命令时且在 commit 完成前被触发
  • commit-msg hook, 在编辑完 commit-msg 时被触发,并且接受一个参数,这个参数是存放当前 commit-msg 的临时文件的路径
  • pre-push hook, 在运行 git push 命令时且在 push 命令完成前被触发

服务端 hook

  • pre-receive 在服务端接受到推送时且在推送过程完成前被触发
  • post-receive 在服务端接收到推送且推送完成后被触发

这里只列举了一部分,更多的 git hooks 详细信息见官方文档

在本地 git 仓库中的 .git/hooks 文件夹中也可以看到常用的 git hooks 示例

file

从图中可以看到,默认的 git hooks 都是 shell 脚本,只需要将 git hooks 的示例文件的 .sample 扩展名去掉,那么示例文件即可生效。
一般来说,在前端工程中应用 git hooks 都是运行 javaScript 脚本,就像这样

#!/bin/sh
node your/path/to/script/xxx.js

或者是这样

#!/usr/bin/env node
// javascript code ...

原生的 Git Hooks 的缺陷

原生的 git hooks 有一个比较大的问题是 .git 文件夹下的内容不会被 Git 追踪。这就表示,无法保证让一个仓库中所有的成员都使用同样的 git hooks,除非仓库的所有成员都手动同步同一份 git hooks,但这显然不是个好办法。

Husky

Husky 的使用

  1. 安装 husky
pnpm install husky --save-dev
  1. husky 初始化
npx husky install
  1. 设置 package.json 的 prepare。来保证 husky 可以正常运行
npm set-script prepare "husky install"
  1. 添加 git hooks
npx husky add .husky/${hook_name} ${command}

husky install 命令做了什么

事实上,husky install 命令是解决 git hooks 问题的关键

  • 第一步: husky install 会在项目根目录下创建 .husky 以及 .husky/_ 文件夹(文件夹也可以自定义),然后在 .husky/_ 文件夹下创建 husky.sh 脚本文件。 这个文件的作用就是保证通过 husky 创建的脚本能够正常运行,它的实际应用的地方后面会讲到。更多关于这个脚本的讨论可以看这里 github issue
  • 第二步: husky install 会运行 git config core.hooksPath ${path/to/hooks_dir},这个命令用来指定 git hooks 的路径,此时观察项目下 .git/config 文件, [core] 下面会多出一条配置: hooksPath = xxx。当 git hooks 被某些命令触发时,Git 会运行 core.hooksPath 指定的文件夹下的 git hook。

更多关于 husky 的配置、命令相关文档,看这这里

值得注意的是 core.hooksPath 是 Git v2.9 推出的新特性,而 Husky 也是在 v6 版本开始使用 core.hooksPath 这个特性。在这之前的版本,Husky 会直接覆盖 .git/hooks 文件夹下所有的 hook,来使通过 Husky 配置的 hooks 生效。另外,在配置了 core.hooksPath 后 Git 会忽略 .git/hooks 文件夹下的 git hooks

husky add 命令做了什么

当运行如下命令

npx husky add .husky/pre-commit npx eslint

.husky 目录下会新增一个 pre-commit 文件,文件内容为

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx eslint

此时已经成功添加了一个 pre-commit git hook,这个脚本会在运行 git commit 命令时执行。
在脚本的第二行,引用了上面所说的 .husky.sh 文件,也就是说通过 husky 创建的 git hook 在被触发时,都会执行这个脚本。

梳理一下,husky 是如何解决原生的 git hooks 的问题的,首先前面已经提到了原生 git hooks 主要的问题是 git 无法跟踪 .git/hooks 下的文件,但是这个问题已经被 git core.hooksPath 解决了,那么新的问题就是,开发者仍然需要手动设置 git core.hooksPath。 husky 在 install 命令中帮助我们设置了 git core.hooksPath,然后在 package.json 的 scripts 中添加 "prepare": "husky install",这样每次安装依赖的时候就会执行 husky install,因此就可以保证设置的 git hooks 可以被触发了。

常用的 git 相关工具库

lint-staged

在 pre-commit hook 中,一般来说都是对当前要 commit 的文件进行校验、格式化等,因此在脚本中我们需要知道当前在 Git 暂存区的文件有哪些,而 Git 本身也没有向 pre-commit 脚本传递相关参数,lint-staged 这个包为我们解决了这个问题,lint-staged 的文档中第一句这样说道:

Run linters against staged git files and don't let

推荐阅读