阅读 husky 源码,学习 git hooks的使用

背景

在实际项目中,我们会有这样的需求,在代码提交前,需要对代码做规范检查,对提交的信息做校验。

这就需要用到 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

我们在使用的时候,一般会用到 installadd 两个命令:

一个 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去实现代码提交检查,提交信息校验。

相关推荐
沛沛老爹9 分钟前
CI/CD是什么?
运维·git·ci/cd
向阳花花花花1 小时前
git clone 和 conda 换源
git·conda
sin220110 小时前
idea集合git使用
git
木心12 小时前
Git基本操作快速入门(30min)
git·github
LXL_2413 小时前
Git_撤销本地commit_查找仓库中大文件
git
yg_小小程序员14 小时前
鸿蒙开发(16)使用DevEco Studio上的Git工具进行多远程仓管理
git·华为·harmonyos
每天八杯水D18 小时前
Git完整使用经历
git
xianwu54321 小时前
反向代理模块。开发
linux·开发语言·网络·c++·git
前端_库日天1 天前
部署自己的git托管平台
git·ubuntu·docker