背景
在实际项目中,我们会有这样的需求,在代码提交前,需要对代码做规范检查,对提交的信息做校验。
这就需要用到 git 的一些 hook。
了解 git hooks
如官网介绍的,Git 能在特定的重要动作发生时触发自定义脚本。它分为客户端的和服务器端的。客户端钩子由诸如提交和合并这样的操作所调用,而服务器端钩子作用于诸如接收被推送的提交这样的联网操作。
大概看2个常用的钩子:
pre-commit
发生在提交代码(commit)前,是第一个触发的hook。
一般用于跑本地测试、代码检查。
你可以用 --no-verify
参数来忽略这个hook的执行。
commit-msg
接收一个参数,存有当前提交信息的临时文件的路径,一般用于检验提交的信息符不符合规范。
其他一些钩子可以看官网。
那么如何使用这个钩子,让钩子生效呢?
官网中有说明,把一个正确命名(不带扩展名)且可执行的文件放入 .git
目录下的 hooks
子目录中,即可激活该钩子脚本。
现在我们试一下。
一般情况下,用 vscode 打开一个初始化的git项目,.git 目录是被隐藏的,为了让编辑变得更加方便,可以直接在vscode上编辑,而不使用vim来编辑,所以把 .git 给显示出来。具体配置在setting
-> 搜索 "exclude" 字符 -> 选择移除 **/.git
这一项。
这样 .git 目录就可以显示出来了。
修改其中一个 pre-commit,我们试试效果。
.git/hooks/pre-commit
js
#!/usr/bin/env node
console.log('pre-commit hook triggered');
// 获取提交的文件列表
const execSync = require('child_process').execSync;
const files = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf-8' }).trim().split('\n');
console.log('Files to be committed:', files);
commit之后的打印:
shell
$ git commit -m 'test commit'
pre-commit hook triggered
Files to be committed: [ 'package.json' ]
[main 0a3de63] test commit
1 file changed, 1 insertion(+), 1 deletion(-)
那么这么直接使用存在什么问题?
在项目中 .git 文件夹是存储在本地,不被提交到远程仓库的,别人是无法共享你的文件的。
在我们实际的项目上,一般是用 husky 来定义git hook的,我们看看它是怎么做的。
探索 husky 源码
从 package.json 中查看入口文件,为 lib/bin.js
。
lib
为打包目录,对应的源文件为 src/bin.ts
。
其实整个项目的文件,主要就2个。
意向不到的简单。
接下来看一下 bin
文件做了什么事。
ts
#!/usr/bin/env node
import p = require('path')
import h = require('./')
function help(code: number) {
console.log(`Usage:
husky install [dir] (default: .husky)
husky uninstall
husky set|add <file> [cmd]`)
process.exit(code)
}
const [, , cmd, ...args] = process.argv
const ln = args.length
const [x, y] = args
// 设置或添加 hook
const hook = (fn: (a1: string, a2: string) => void) => (): void =>
!ln || ln > 2 ? help(2) : fn(x, y)
// 定义了一些命令
const cmds: { [key: string]: () => void } = {
install: (): void => (ln > 1 ? help(2) : h.install(x)),
uninstall: h.uninstall,
set: hook(h.set),
add: hook(h.add),
['-v']: () =>
console.log(require(p.join(__dirname, '../package.json')).version),
}
try {
cmds[cmd] ? cmds[cmd]() : help(0)
} catch (e) {
console.error(e instanceof Error ? `husky - ${e.message}` : e)
process.exit(1)
}
所有的 src/bin.ts
源码内容就以上这些,主要就是定义了几个命令,然后运行其中对应的命令。
这里看一下罗列的几个命令:
- instanll
- uninstall
- set
- add
我们在使用的时候,一般会用到 install
和 add
两个命令:
一个 install
命令是在 scripts.prepare 上添加。
json
{
"scripts": {
"prepare": "husky install"
}
}
add
就是用于添加 hook。
命令的执行定义在 src/index.ts
文件中。
先看 install
函数。
ts
export function install(dir = '.husky'): void {
// HUSKY 参数为0,跳过安装
if (process.env.HUSKY === '0') {
l('HUSKY env variable is set to 0, skipping install')
return
}
// 检查是不是一个git项目
if (git(['rev-parse']).status !== 0) {
l(`git command not found, skipping install`)
return
}
const url = 'https://typicode.github.io/husky/guide.html#custom-directory'
// 确保安装的目录在项目内
if (!p.resolve(process.cwd(), dir).startsWith(process.cwd())) {
throw new Error(`.. not allowed (see ${url})`)
}
// 确保是在根目录下
if (!fs.existsSync('.git')) {
throw new Error(`.git can't be found (see ${url})`)
}
try {
fs.mkdirSync(p.join(dir, '_'), { recursive: true })
fs.writeFileSync(p.join(dir, '_/.gitignore'), '*')
fs.copyFileSync(p.join(__dirname, '../husky.sh'), p.join(dir, '_/husky.sh'))
// 最关键的一句,设置git的hooksPath
const { error } = git(['config', 'core.hooksPath', dir])
if (error) {
throw error
}
} catch (e) {
l('Git hooks failed to install')
throw e
}
l('Git hooks installed')
}
忽略一些边界的判断,我们看到最核心的其实就一句代码 git(['config', 'core.hooksPath', dir])
。
以下是 git
函数的定义:
ts
import cp = require('child_process')
const git = (args: string[]): cp.SpawnSyncReturns<Buffer> =>
cp.spawnSync('git', args, { stdio: 'inherit' })
就是用子进程执行一段命令 git config core.hooksPath [dir]
。
目的就是修改git hook的目录,默认的为 .git/hooks
下,现在可以自己定义执行目录 [dir]
。
这样就解决了我们上面说的问题 .git 文件夹是存储在本地,不被提交到远程仓库的,别人是无法共享你的文件
的问题。
其实到这里,我们基本已经可以满足我们的需求了。
我们可以再看下,add
做了什么事。
ts
export function set(file: string, cmd: string): void {
const dir = p.dirname(file)
if (!fs.existsSync(dir)) {
throw new Error(
`can't create hook, ${dir} directory doesn't exist (try running husky install)`,
)
}
fs.writeFileSync(file, `#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
${cmd}
`, { mode: 0o0755 })
if (os.type() === 'Windows_NT') {
git(['update-index', '--add', '--chmod=+x', file])
}
}
export function add(file: string, cmd: string): void {
if (fs.existsSync(file)) {
fs.appendFileSync(file, `${cmd}\n`)
} else {
set(file, cmd)
}
}
删掉部分 log 信息,剩下的就是上面这些,主要做的是判断存不存在这个hook文件,存在,就把命令添加到文件中,如果不存在,就调用 set
方法。
set
方法做的事情,就是写入以下代码,并将文件变成可执行文件。
shell
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
${cmd}
我不太熟悉 sh 脚本,也就是 _/husky.sh
这个文件的内容,查阅了一下,主要作用就是帮助开发人员在Git提交、推送、合并等操作前后执行自定义的脚本,捕获一些错误,可以忽略它。
总结
了解 husky
的源码之后,我们会发现其实核心就一句代码 git config core.hooksPath [dir]
。那么哪怕不用 husky
,我们也可以实现自己的git hooks去实现代码提交检查,提交信息校验。