husky@9.1.7源码阅读

299 阅读3分钟

husky@9.1.7 发布日期 2024年11月18日

husky 是一个流行的 Git 钩子工具,用于在 Git 操作(如 commitpush 等)的特定阶段自动执行脚本(如代码 lint、格式化、测试等),从而在提交代码前拦截不规范的内容,保障代码质量和团队开发规范的一致性。

package.json

husky-9.1.7/package.json

{
  "name": "husky",
  "version": "9.1.7",
  "type": "module",
  "description": "Modern native Git hooks",
  "keywords": [
    "git",
    "hooks",
    "pre-commit"
  ],
  "repository": {
    "type": "git",
    "url": "git+https://github.com/typicode/husky.git"
  },
  "funding": "https://github.com/sponsors/typicode",
  "license": "MIT",
  "author": "typicode",
  "bin": {
    "husky": "bin.js"
  },
  "exports": "./index.js",
  "engines": {
    "node": ">=18"
  }
}

bin.js文件

husky-9.1.7/bin.js

#!/usr/bin/env node
import f, { writeFileSync as w } from 'fs'
import i from './index.js'

let p, a, n, s, o, d

p = process // 用于存储 process 对象(Node.js 进程信息)。
a = p.argv[2] // 存储命令行参数(用户输入的指令)。

// 1、处理 init 命令(初始化 husky)
if (a == 'init') {
  n = 'package.json' // 存储文件名(如 package.json)。
  s = f.readFileSync(n) // 存储package.json文件读取的字符串内容。
  o = JSON.parse(s) // 存储解析后的 JSON 对象(如 package.json 的内容)。

    // 确保 scripts 存在,并设置 prepare 脚本为 'husky'
    ;(o.scripts ||= {}).prepare = 'husky'

  // 写回 package.json,保持原有的缩进格式(制表符或 2 空格)
  w(n, JSON.stringify(o, 0, /\t/.test(s) ? '\t' : 2) + '\n')

  // 输出 index.js 导出的信息(可能是初始化成功提示)
  p.stdout.write(i())

  // 创建 .husky 目录(忽略已存在的错误)
  try { f.mkdirSync('.husky') } catch {}
  // 在 .husky/pre-commit 中写入默认钩子脚本(用 npm/yarn/pnpm 执行 test)
  w('.husky/pre-commit', (p.env.npm_config_user_agent?.split('/')[0] ?? 'npm') + ' test\n')
  p.exit()  // 退出进程
}

// 声明一个函数,用于输出 deprecated(废弃)命令的提示。
d = c => console.error(`husky - ${c} command is DEPRECATED`)

// 2、处理废弃命令(add/set/uninstall)
if (['add', 'set', 'uninstall'].includes(a)) { d(a); p.exit(1) }

// 3、处理 install 命令(兼容提示)
if (a == 'install') d(a)

// 4、默认处理
p.stdout.write(i(a == 'install' ? undefined : a))

执行 husky init 后做了什么?

  1. package.json 文件中,在scripts属性添加 prepare:husky
  2. 执行下面的 index.js 文件。
  3. 创建 .husky 目录,一般已存在,因为在 index.js文件中已创建过。
  4. .husky/pre-commit 文件写入内容。

执行 husky 其他命令做了什么?

  • 如果是执行了 husky addhusky sethusky uninstall 回提示命令已废弃,然后退出。
  • 如果执行了 husky install ,也会提示该命令已废弃,然后执行 index.js文件到逻辑。
  • huskyhusky instll 执行了逻辑一样,除了 install 会提示命令已废弃。

image.png

image.png

执行 npx huksy init,会多创建一个 .husky/pre-commit文件,以及在package.json文件的scripts添加 prepare:husky

image.png

index.js文件

husky-9.1.7/index.js

import c from 'child_process'
import f, { readdir, writeFileSync as w } from 'fs'
import p from 'path'

export default (d = '.husky') => {

  // 1. 环境与前置检查
  // 若环境变量 HUSKY 被设置为 0,表示用户手动禁用 husky,直接返回跳过安装的提示。
  if (process.env.HUSKY === '0') return 'HUSKY=0 skip install'

  // 检查传入的路径 d 中是否包含 ..(上级目录),若有则返回错误(禁止跨目录操作,避免安全风险)。
  if (d.includes('..')) return '.. not allowed'

  // 检查当前目录是否存在 .git 文件夹(即是否在 Git 仓库中),若不存在则返回错误
  // husky 依赖 Git 钩子机制,必须在 Git 仓库中使用
  if (!f.existsSync('.git')) return `.git can't be found`

  // 定义一个便捷函数 _,用于生成 husky 内部钩子目录(.husky/_)下的文件路径。例如:
  // _() 返回 .husky/_
  // _('husky.sh') 返回 .husky/_/husky.sh
  let _ = (x = '') => p.join(d, '_', x)

  // 执行 Git 命令 git config core.hooksPath .husky/_
  // spawnSync 同步执行命令
  // status 是命令退出码(0 表示成功,非 0 表示失败),stderr 是错误输出。
  let { 
    status: s, 
    stderr: e 
  } = c.spawnSync('git', ['config', 'core.hooksPath', `${d}/_`])
  
  // 若 status 为 null,表示 git 命令未找到(可能未安装 Git),返回错误提示。
  if (s == null) return 'git command not found'
  // 若 status 非 0(命令执行失败),返回 Git 命令的错误输出(如权限问题导致配置失败)。
  if (s) return '' + e

  // 强制删除旧的 husky.sh 文件(若存在),避免残留文件影响新配置。
  f.rmSync(_('husky.sh'), { force: true })
  
  // 创建 .husky/_ 目录
  // recursive: true 确保父目录不存在时也能创建,如 .husky 未创建时自动生成
  f.mkdirSync(_(), { recursive: true })

  // 在 .husky/_ 目录下创建 .gitignore 文件,内容为 *(忽略该目录下所有文件,避免提交钩子脚本到 Git 仓库)。
  w(_('.gitignore'), '*')

  // 复制 husky 核心脚本
  f.copyFileSync(new URL('husky', import.meta.url), _('h'))

  // 为每个钩子生成对应的脚本文件
  l.forEach(h => w(_(h), `#!/usr/bin/env sh\n. "\$(dirname "\$0")/h"`, { mode: 0o755 }))

  // 向 .husky/_/husky.sh 写入消息 msg
  w(_('husky.sh'), msg)
  return ''
}

这段代码做了什么?

  1. 若环境变量 process.env.HUSKY 设置0 ,退出。
  2. 若参数 路径d 中是否包含 .. ,退出。
  3. 执行 git 命令 git config core.hooksPath .husky/_
  4. 删除旧文件 husky.sh
  5. 创建 .husky/_ 目录。
  6. .husky/_ 目录下创建 .gitignore 文件,内容为 *
  7. 复制 husky文件 到 _/h 文件。
  8. 为每个钩子生成对应脚本文件。
  9. .husky/_/husky.sh文件写入内容。

.husky/_/husky.sh

echo "husky - DEPRECATED

Please remove the following two lines from $0:

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

They WILL FAIL in v10.0.0
"

git config core.hooksPath

git config core.hooksPath 是用来配置 Git 钩子脚本的存放目录的命令 ——可以通过它修改 Git 钩子的默认路径,也可以查询当前钩子路径的配置值。

  • Git 钩子脚本默认存放在仓库的 .git/hooks 目录下。
  • .git 目录是 Git 的私有目录,不会被提交到版本库,所以默认的 .git/hooks 里的钩子脚本无法在团队间同步。
# 将当前仓库的 Git 钩子路径设置为项目根目录的 .husky/_ 目录
git config core.hooksPath .husky/_