前言
前段时间,部门的前端我的项目迁徙到 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 💩 slip into your code base!
lint-staged 的应用
-
装置 lint-staged
pnpm install lint-staged --save-dev
-
配置 lint-staged
个别状况下,倡议lint-staged
搭配着Husky
一起应用,当然这不是必须的,只须要保障lint-staged
会在 pre-commit hook 中被运行就能够了。在搭配 Husky 应用的状况下,能够运行上面的命令,在 pre-commit hook 中运行lint-staged
npx husky add .husky/pre-commit "npx lint-staged"
对于 lint-staged 的配置,在模式上与常见的工具包的配置形式大同小异,能够通过在 package.json 中增加一个 lint-staged
项、也能够在根目录增加一个 .lintstagedrc.json
文件等,上面以在 package.json 中配置为例:
配置项中的 key 为 glob 模式匹配语句,值为要运行的命令(能够配置多个),例如想要为暂存区中 src 文件夹下所有的 .ts 和 .tsx 文件运行 eslint 查看以及 ts 类型查看,那么配置如下:
具体的配置文档看这这里
如果 git hooks 脚本运行失败(过程完结时返回的状态码不为 0),那么会终止后续操作。比方上例中 eslint 查看报错,那么会间接终止 commit,git commit 命令失败。
lint-staged 是如何晓得以后暂存区有哪些文件的
事实上,lint-staged 外部也没有什么黑魔法,它在外部运行了 git diff --staged --diff-filter=ACMR --name-only -z
命令,这个命令会返回暂存区的文件信息,相似如下所示的代码:
const {execSync} = require('child_process');
const lines = execSync('git diff --staged --diff-filter=ACMR --name-only -z')
.toString()
const stagedFiles = lines
.replace(/\u0000$/, '')
.split('\u0000')
commitizen
在应用 Git 过程中,不可避免的须要填写 commit message,这其实是一件相当令人头疼的事件。如果没有良好的 commit message 标准,那么在查看历史 commit 的时候只会一脸懵 *。
而 commitizen
能够帮助开发者填写 commit 信息
commitizen 的应用
-
装置 commitizen
pnpm install commitizen -D
-
初始化 commitizen
npx commitizen init cz-conventional-changelog --save-dev --save-exact --pnpm
commitizen init 做了什么
- 装置
cz-conventional-changelog
适配器 npm 模块 - 将其保留到 package.json 的 devDependencies 中
-
config.commitizen 配置增加到 package.json 中 如下所示:
"config": { "commitizen": {"path": "./node_modules/cz-conventional-changelog"} }
commitizen 自身只提供命令行交互框架以及一些 git 命令的执行,理论的规定则须要通过适配器来定义,commitizen 留有对应的适配器接口。而 cz-conventional-changelog
就是一个 commitizen 适配器。
此时运行 npx cz
命令 就会呈现以下命令行交互页面:
这个适配器生成的 commit message 模板如下
<type>(<scope>): <subject>
< 空行 >
<body>
< 空行 >
<footer>
这也是最常见的提交约定,当然也能够装置其余适配器,或者自定义适配器来定制本人想要的 commit message 模板。
当运行 npx cz
,commitizen 在通过适配器模板以及用户的输出拿到最终的 commit message 后,会在外部运行 git commit -m "XXX"
命令,到此为止,就实现了一次 git commit 操作
更多对于 commitizen 的具体等信息能够看 github 和 cz-git
自定义 commitizen 适配器
如果你想自定义适配器,那么能够抉择应用 cz-customizable 这个工具包。
在没有这个工具包的状况下,如果想要自定义一个 commitizen 适配器,那么你还须要把握 inquirer
的 API,commitizen 只会为适配器传递一个 inquirer 对象,适配器的规定须要通过这个 inquirer 对象来创立规定,这是在不太易用,而 cz-customizable
能够让我咱们只专一于规定而不必去思考 inquirer 的 API。
cz-customizable 的应用
-
commitizen 配置
"config": { "commitizen": {"path": "./node_modules/cz-customizable"} }
-
cz-customizable 配置,在根目录新增一个
.cz-config.js
文件,配置示例如下module.exports = { types: [{ value: 'feat', name: 'feat: A new feature'}, {value: 'fix', name: 'fix: A bug fix'}, ], scopes: [{name: 'accounts'}, {name: 'admin'}], allowTicketNumber: false, messages: { type: "Select the type of change that you're committing:", scope: '\nDenote the SCOPE of this change (optional):', customScope: 'Denote the SCOPE of this change:', }, subjectLimit: 100, };
这里是对于 cz-customizable
更具体的 示例 和 配置
应用 git cz 命令运行 commitizen
在全局 PATH 配置正确的状况下,也能够间接应用 git cz
命令去运行 commitizen。如果你在我的项目中装置了 commitizen, 那么在你的我的项目下的 node_modules/.bin
目录下将会看到两个脚本: cz
和 git-cz
, 如下图所示:
这两个脚本的内容是截然不同的,官网的文档中会举荐在 package.json 的 scripts 中增加如下内容:
commit: "cz"
这样就能够应用 npm run commit
来运行 commitizen 了。然而如果想要应用 git cz
命令运行 commitizen,那么则要求 git-cz
文件所在的目录在全局的 PATH 下,运行以下命令来查看 PATH
echo $PATH
PATH 以冒号分隔,检查一下所有的 PATH 中是否有一条能匹配到你的 cz 脚本,一般来说都是有的,如果没有,那么你能够在你的 ~/.zshrc
或者 ~/.bash_profile
中加上一条:
PATH=$PATH:./node_modules/.bin
而后从新加载一下配置文件,运行 source ~/.zshrc
或者 source ~/.bash_profile
,这样在你我的项目根目录下 就能够间接应用 git cz
命令了。
如果你是用 npm 全局装置的 commitizen,那么你大概率不须要放心 PATH 的问题,因为 npm 的依赖装置门路下的 bin 文件夹会被 node 或者 NVM 主动退出到 PATH 中。
回到刚刚所说的 node_modules/.bin 文件夹 下的 git-cz
脚本,实际上它是 git cz
命令能够运行的要害。不晓得你是否纳闷,为什么应用 Git 能够去运行一个 npm 库,实际上,这是 git 自定义命令。想要增加一个 git 自定义命令有如下几个要求:
- 是一个可执行文件
- 文件名必须是
git-XXX
- 这个文件所在门路必须在你的 PATH 下
所以前文中,提到想要运行 git cz
命令,须要全局 PATH 配置正确。
你也能够根据上述要求尝试增加别的自定义 git 命令。须要留神的是,要检查一下你增加的 shell 脚本是否具备可执行权限,若没有可执行权限会导致如下报错 _git: 'your command' is not a git command_
, 此时能够运行 _chmod a+x <path to your file>_
来批改文件的权限使其可运行即可。
commitlint
commitlint 这个工具库,能够通过配置一些规定来校验 commit message 是否标准。
那么咱们曾经有了 commitizen 为什么还须要 commitlint 呢?上文中说到,commitizen 的作用是帮助开发者填写 commit message,尽管能够通过抉择不同的适配器或者自定义适配器来制订对应的 commit 信息标准以及模板,然而短少了对 commit message 的校验性能,开发者依然可能在无心中应用原生的 git commit
命令来提交,而 commitlint 在 commit-msg
这个 git hook 中对 commit message 进行校验,正好解决了这个问题。
commitlint 的应用
- 装置
pnpm install --save-dev @commitlint/config-conventional @commitlint/cli
- 应用 husky 增加 commit-msg hook
npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1
- commitlint 配置
在我的项目根目录减少一个commitlint.config.js
文件, 文件内容如下:
module.exports = {extends: ['@commitlint/config-conventional'],
// 自定义局部规定
rules: {'scope-case': [0, 'always', 'camel-case'],
'scope-empty': [2, 'never'],
'scope-enum': [2, 'always', [...]],
},
};
commitlint 与 commitizen 一样,分为两局部,一部分是执行的主程序,另一部分是规定或者说是适配器。@commitlint/cli
是执行的主程序,@commitlint/config-conventional
则是规定。commitlint 和 commitizen 别离采纳了策略模式和适配器模式,因而都领有十分高的可用性和良好的扩展性。
在 commitlint 的配置文件中,能够先援用一个 commitlint 规定包,而后在定义局部本人想要的规定,就像 eslint 的配置一样。
须要留神的是,在将 commitlint 增加到 commit-msg hooks 中时,执行 commitlint 的 shell 命令中 --edit $1
参数是必须的,这个参数的意思是:存储 commit message 的临时文件门路是 $1
, 而$1
则是 Git 传给 commit-msg hook 的参数,它的值是 commit message 的长期存储文件的门路,默认状况下是 .git/COMMIT_EDITMSG
。如果不传这个参数,那么 commitlint 将无奈得悉以后的 commit message 是什么。
更多 commitlint 的相干详情看这里
commitlint 与 commitizen 的配置共用
前文中说到 commitlint 解决了 commitizen 没有对 commit message 做校验的问题,然而应用了 commitlint 后,新的问题呈现了,如果 commitlint 的规定集与 commitizen 的适配器中的规定不统一,那么可能会导致应用 commitizen 生成的 commit message 被 commitlint 校验时不通过从而 git commit 失败。
解决这个问题的方法有两种:
- 将 commitizen 的适配器规定翻译为 commitlint 规定集,已有的对应工具包为 commitlint-config-cz,这个包须要你所应用的 commitizen 适配器为
cz-customizable
,也就是自定义适配器。 - 将 commitlint 规定集转化为 commitizen 的适配器,已有对应的工具包为 @commitlint/cz-commitlint
这里以第二种选用 @commitlint/cz-commitlint
为例:
-
装置 @commitlint/cz-commitlint
pnpm install --save-dev @commitlint/cz-commitlint
-
批改 packages.json 中 commitizen 的配置
"config": { "commitizen": {"path": "./node_modules/@commitlint/cz-commitlint"} }
conventional-changelog 生态
关上 commitlint 的 github 仓库,就会发现它在 conventional-changelog 这个 Organization 下,而 commitizen/cz-cli 这个仓库的 README.md 文件中也提到了 conventional-changelog 生态:
For this example, we’ll be setting up our repo to use AngularJS’s commit message convention, also known as conventional-changelog.
这也难怪为什么 commitlint 还专门提供了一个 @commitlint/cz-commitlint 包来配合 commitizen。
那么 conventional-changelog 生态还蕴含什么呢?
反对 Conventional Changelog 的插件
- grunt
- gulp
- atom
- vscode
- emacs
Conventional Changelog 生态中的重要模块
- conventional-changelog-cli – the full-featured command line interface –_功能丰富的命令行接口_
- standard-changelog – command line interface for the angular commit format. –_angular 格调的命令行接口_
- conventional-github-releaser – Make a new GitHub release from git metadata –_通过 git 元数据生成一个新的 GitHub release_
- conventional-recommended-bump – Get a recommended version bump based on conventional –commits 依据 conventional 格调的提交生成举荐的版本变更
- conventional-commits-detector – Detect what commit message convention your repository is using —_对存储库应用的 commit 音讯约定进行查看_
- commitizen – Simple commit conventions for internet citizens.
- commitlint – Lint commit messages
因为本文次要是讲 git hooks,这里对于 conventional-changelog 生态就不开展讲了,有趣味的话能够自行去看一下他们的 github 仓库 和 这篇文章