Git Hooks 和 Husky 怎么选

很多项目都会在提交前跑一些检查:

text 复制代码
git commit
  -> format
  -> lint
  -> typecheck
  -> commit message check

这类动作通常会想到两个方案:直接写 Git hooks,或者用 Husky。

它们解决的是同一个入口问题:在 Git 的某些操作发生前后,自动执行一段脚本。但它们的关注点不一样。Git hooks 是 Git 自带的机制;Husky 是让团队更容易把 hooks 放进项目并随依赖安装的工具。

这篇文章整理一下两者的边界,以及什么情况下该怎么选。

先区分几个名字

严格来说,Git 默认的 hooks 目录是:

text 复制代码
.git/hooks/

里面可以放 pre-commitcommit-msgpre-push 这类脚本。Git 官方文档把这些脚本称为 githooks

有些项目会创建一个仓库内可提交的目录,比如:

text 复制代码
.githooks/

然后通过 core.hooksPath 告诉 Git 去这个目录找 hooks:

bash 复制代码
git config core.hooksPath .githooks

所以这里有三个概念:

名称 含义
.git/hooks/ Git 默认 hooks 目录,通常不会进入版本控制
.githooks/ 项目自定义目录名,本身不是 Git 的特殊目录
core.hooksPath Git 配置项,用来修改 hooks 目录位置

Husky 本质上也是基于 core.hooksPath 工作。它把 hooks 放在项目里的 .husky/ 目录,并在安装阶段帮你把 Git 的 hooks 路径指过去。

Git hooks 适合什么

原生 Git hooks 最大的优点是简单、直接、没有额外依赖。

比如只想在自己本地提交前检查一下当前仓库状态,可以直接写:

sh 复制代码
#!/bin/sh

pnpm typecheck

保存到:

text 复制代码
.git/hooks/pre-commit

再加执行权限:

bash 复制代码
chmod +x .git/hooks/pre-commit

之后每次 git commit 前都会执行这段脚本。脚本返回非 0,提交就会中断。

这种方式很适合个人工作流:

  • 给自己的机器加一个临时检查
  • 在某个仓库里做一次性保护
  • 写和项目无关的本地自动化
  • 不希望引入 npm 依赖
  • 项目不是 Node 生态

它也适合一些偏底层的场景。比如你在维护一个 Go、Rust 或 Shell 项目,不想为了 hooks 引入 Node 依赖,那么 .githooks/ + core.hooksPath 就够了。

可以把 hooks 目录提交进仓库:

text 复制代码
.githooks/
└── pre-commit

然后让团队成员手动执行一次:

bash 复制代码
git config core.hooksPath .githooks

这样脚本内容可以版本化,行为也比较透明。

原生 Git hooks 的问题

原生 hooks 有一个很明显的问题:默认的 .git/hooks/ 不会被提交。

也就是说,你在本地写了一个 .git/hooks/pre-commit,它只存在于你自己的仓库副本里。别人 clone 项目后不会自动得到这段脚本。

.githooks/core.hooksPath 可以把脚本内容提交进去,但还有一个安装问题:每个人都需要执行一次配置命令。只要有人忘了配置,这些 hooks 对他就是不存在的。

另一个问题是跨平台。hooks 脚本通常是 shell。团队里如果有人使用 Windows,或者项目脚本依赖特定 shell 工具,行为可能不一致。

所以原生 hooks 很适合小范围和低依赖的场景,但不太适合承担「团队所有人都必须自动启用」这种职责。

Husky 适合什么

Husky 做的事情不是发明一种新的 hooks 机制,而是把 Git hooks 变成项目依赖的一部分。

现在的典型安装方式是:

bash 复制代码
pnpm add --save-dev husky
pnpm exec husky init

初始化后通常会得到:

text 复制代码
.husky/
└── pre-commit

package.json 里也会有类似的脚本:

json 复制代码
{
  "scripts": {
    "prepare": "husky"
  }
}

这样团队成员安装依赖时,Husky 会把当前仓库的 hooks 路径设置到 .husky/。这就是它比手写 .githooks/ 更方便的地方:脚本内容在仓库里,启用过程挂在包管理器生命周期上。

Husky 尤其适合 Node 项目:

  • 前端项目
  • npm package
  • monorepo
  • 需要搭配 lint-staged
  • 需要统一提交信息规范
  • 希望新人 pnpm install 后自动获得 hooks

比如只检查暂存区里的文件,可以这样搭:

bash 复制代码
pnpm add -D husky lint-staged
pnpm exec husky init

package.json

json 复制代码
{
  "lint-staged": {
    "*.{js,ts,vue,astro,md}": [
      "prettier --write",
      "eslint --fix"
    ]
  }
}

.husky/pre-commit

sh 复制代码
pnpm lint-staged

这样每次提交前只处理 staged files,不用对整个项目跑格式化和 lint,速度会友好很多。

Husky 不适合什么

Husky 也不是所有项目都该加。

如果项目不是 Node 生态,只是为了启用 hooks 而引入 package.json、包管理器和 dev dependency,收益通常不高。这个时候直接用 .githooks/ 会更朴素。

如果 hooks 里要跑很慢的任务,也不应该一股脑塞进 Husky。比如完整构建、全量测试、端到端测试。这些任务更适合放在 CI 里。提交前的 hooks 应该尽量快,否则开发者最后会习惯性使用:

bash 复制代码
git commit --no-verify

这不是开发者的问题,而是 hooks 设计得太重。

我的经验是:

  • pre-commit 适合跑格式化、轻量 lint、只针对 staged files 的检查
  • commit-msg 适合检查提交信息格式
  • pre-push 可以跑稍重一点的 typecheck 或单元测试
  • 完整 build、全量测试、安全扫描应该放 CI

本地 hooks 是提前反馈,不是最终防线。

怎么选择

可以按这个顺序判断。

只给自己用

选原生 Git hooks。

比如你想在个人仓库里提交前自动生成某个文件,或者提醒自己不要把某些本地配置提交上去,直接写 .git/hooks/pre-commit 就够了。

这种脚本不需要影响别人,也不需要进入仓库。

团队项目,但不是 Node 项目

优先考虑 .githooks/ + core.hooksPath

把脚本提交进仓库:

text 复制代码
.githooks/pre-commit
.githooks/commit-msg

再在 README 或初始化脚本里写清楚:

bash 复制代码
git config core.hooksPath .githooks

如果项目本来就有 make setupjust bootstrap 之类的初始化命令,也可以把这行放进去。

团队 Node 项目

选 Husky。

因为 Node 项目本来就有包管理器和依赖安装步骤,Husky 可以顺着这条链路完成启用,不需要额外解释太多。

常见组合是:

text 复制代码
Husky + lint-staged + prettier/eslint
Husky + commitlint
Husky + pnpm typecheck

其中 lint-staged 放在 pre-commitcommitlint 放在 commit-msgtypecheck 可以视项目速度放在 pre-commitpre-push

需要强制保证质量

选 CI。

不管用了原生 hooks 还是 Husky,都不能把它当成唯一质量门禁。Git hooks 可以被 --no-verify 跳过,也可能因为本地环境不同而没有运行。

真正不能绕过的检查应该放到 CI:

text 复制代码
pull request
  -> install
  -> lint
  -> typecheck
  -> test
  -> build

本地 hooks 负责更早发现问题,CI 负责最终拦截问题。

一个比较稳的实践

如果是一个前端或全栈 Node 项目,我一般会这样安排:

text 复制代码
.husky/
├── pre-commit
└── commit-msg

.husky/pre-commit

sh 复制代码
pnpm lint-staged

.husky/commit-msg

sh 复制代码
pnpm commitlint --edit "$1"

package.json

json 复制代码
{
  "scripts": {
    "prepare": "husky",
    "typecheck": "tsc --noEmit"
  },
  "lint-staged": {
    "*.{js,ts,tsx,vue,astro,md,json}": [
      "prettier --write",
      "eslint --fix"
    ]
  }
}

CI 里再跑完整检查:

bash 复制代码
pnpm install --frozen-lockfile
pnpm typecheck
pnpm lint
pnpm test
pnpm build

这套分工比较清楚:

  • 提交前只做快检查
  • 提交信息在本地先校验
  • 完整质量门禁交给 CI
  • 本地 hooks 失败时,开发者能快速修复

结论

如果只看一句话:

个人本地自动化用原生 Git hooks;团队 Node 项目用 Husky;真正必须通过的检查放 CI。

再细一点:

场景 选择
只给自己用 .git/hooks/
想把 hooks 脚本提交进仓库 .githooks/ + core.hooksPath
Node 团队项目 Husky
只检查 staged files Husky + lint-staged
检查 commit message commit-msg hook,可搭配 commitlint
不允许绕过的质量门禁 CI

Husky 的价值不在于它比 Git hooks 更「强」,而在于它把 hooks 的分发和启用做得更适合团队协作。

反过来,原生 Git hooks 的价值也不在于功能少,而在于足够直接、足够透明。项目越小、依赖越少,它越舒服。

真正重要的是不要把所有检查都塞进一个 hook。hook 应该是开发体验的一部分,而不是每次提交时都要跨过去的一堵墙。

相关推荐
李明卫杭州1 小时前
Vue 3 响应式三剑客:ref、shallowRef、reactive,你真的用对了吗?
前端
触底反弹1 小时前
🔥 JavaScript this 指向全解析:面试必考的 5 大绑定规则
前端·javascript·面试
Csvn2 小时前
TypeScript 类型守卫的 4 个实战级别:从 `filter(Boolean)` 到自定义类型谓词
前端
骑士雄师2 小时前
langchain:推荐切片策略
服务器·前端·langchain
云浪2 小时前
拆开 WAV,看见声音——JavaScript 二进制数组实战全解析
前端·javascript·ecmascript 6
_瑞3 小时前
深入理解灵动岛
前端·ios·app
想你依然心痛3 小时前
嵌入式日志系统:分级日志、环形缓冲区与远程输出——运行时调试、非侵入
android·前端·javascript
这是个栗子3 小时前
【Vue代码分析】 import.meta.env.DEV:现代前端工程中的环境变量
前端·javascript·vue.js·环境变量
像牛奶却不是牛奶3 小时前
记录几个前端实用小函数
前端·javascript