前言
前段时间,部门的前端项目迁移到 monorepo 架构,笔者在其中负责跟 git 工作流相关的事情,其中就包括 git hooks 相关的工程化的实践。用到了一些常用的相关工具如 husky、lint-staged、commitizen、commit-lint 等,以此文记录一下整个的实践过程和踩过的坑。
注意:下文中的例子以及命令都是基于 Mac OS,如果你是 windows 用户,也不用担心,文中也会阐述大致原理和运行逻辑,对应的 windows 命令可以推理得知。
Git Hooks
Git Hooks 是什么
大多数同学应该都对 git hooks
相当了解,但是笔者还是想在这里详细解释一下。
首先是 hook
,这其实是计算机领域中一个很常见的概念,hook
翻译过来的意思是钩子或者勾住,而在计算机领域中则要分为两种解释:
- 拦截消息,在消息到达目标前,提前对消息进行处理
- 对特定的事件进行监听,当某个事件或动作被触发时也会同时触发对应的
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 示例
从图中可以看到,默认的 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 的使用
- 安装 husky
pnpm install husky --save-dev
- husky 初始化
npx husky install
- 设置 package.json 的 prepare。来保证 husky 可以正常运行
npm set-script prepare "husky install"
- 添加 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