引子:门铃与快递员的故事
你有没有遇到过这种情况——快递员按门铃,你开门接过快递,关上门才发现包装破了,东西摔坏了。如果能在快递员离开前就发现这个问题就好了。
git hooks 就是那个“按门铃“的机制。它在 git 执行某些操作的关键节点插入一个检查点,让你有机会在事情发生前做点什么——比如检查代码风格、跑单元测试、确保提交信息符合规范。
但 git hooks 本身只是一个框架,你需要手动创建hook文件、编写脚本、管理多台机器上的配置——这很快就会变成一团乱麻。所以社区出现了各种工具来简化这个过程,其中最流行的两个就是 husky 和 lefthook。
今天这篇文章,我们从头开始,一步步配置两个工具,最后给出一个实操性的对比结论。无论你是刚接触 git 的新手,还是想了解这两个工具区别的老手,看完都会有收获。
第一章:git hooks 基础
什么是 git hooks
git hooks 是 git 内置的脚本触发机制。当你在终端执行 git commit、git push 等命令时,git 会在特定时机自动运行预设的脚本。这些脚本让你有机会在 git 操作完成前(或完成后)插入自定义逻辑。
一个经典的例子是 pre-commit hook:每次你执行 git commit,git 会先运行这个 hook 里的脚本。如果脚本返回非零值(表示检查失败),commit就会被取消。这样就能确保有问题的代码不会进入版本历史。
常见 hooks 种类
git 支持的 hooks 很多,下面是最常用的几种:
提交相关:
pre-commit:在git commit执行前运行,最常用的场景是做代码风格检查和单元测试prepare-commit-msg:在提交信息编辑器打开前运行,可以用来自动生成提交信息模板commit-msg:在提交信息填写完成后运行,常用于验证提交信息格式(如是否符合Conventional Commits规范)post-commit:在 commit 完成后运行,通常用于通知、记录等
推送相关:
pre-push:在git push执行前运行,可以用来检查远程分支的最新状态post-push:在 push 完成后运行,常用于触发CI/CD pipeline
其他:
pre-rebase:在 rebase 执行前运行post-merge:在 merge 完成后运行,常用于恢复文件权限、生成文档等
为什么需要管理工具
直接写 git hooks 脚本并不难,但当团队变大、项目变复杂时,问题就来了:
配置分散:每个开发者的 .git/hooks 目录是本地目录,无法提交到 git 仓库。新同事加入时需要手动复制脚本,一致性无法保证。
跨平台问题:shell 脚本在 Linux/Mac 和 Windows 上的行为可能不一致。Windows 的 git bash 支持 shell 语法,但原生的 Windows CMD 和 PowerShell 就另说了。
调试困难:当 hook 脚本执行失败时,错误信息可能不够清晰,难以定位问题。
脚本复用性差:如果你有多个项目,每个项目都需要类似的 hooks,复制粘贴代码会导致维护噩梦。
husky 和 lefthook 就是为了解决这些问题而生的。它们让你把 hooks 配置写在代码仓库里,所有人共享同一份配置,同时提供跨平台支持和更友好的使用体验。
第二章:husky 入门
什么是 husky
husky 是目前最流行的 git hooks 管理工具之一,由 typicode 开发维护。它的设计理念是“让 git hooks 像代码一样管理“——你把 hooks 配置写在项目里,通过 husky 自动安装到 .git/hooks 目录。
husky 的特点是简单直接:它本质上就是一个帮你创建和管理 .git/hooks 目录里那些原始文件的工具。安装后,你可以在项目根目录看到一个 .husky 目录,里面就是你定义的 hooks。
安装 husky
husky 通过 npm(或 yarn/pnpm)安装。先确认你的项目已经初始化了 git 仓库:
git init # 如果还没有初始化的话然后安装 husky 作为开发依赖:
npm install husky --save-dev安装完成后,用 husky 官方推荐的初始化命令初始化:
npx husky install这个命令会做两件事:
- 在项目根目录创建
.husky目录 - 修改 git 配置,让 git 知道去
.husky目录找 hooks
初始化完成后,你每次 git commit,git 就会自动执行 .husky 目录下的 hooks。
配置第一个 pre-commit hook
我们用一个实际场景来演示:让 husky 在每次 commit 前运行 ESLint 检查代码风格。
第一步:确保项目有 ESLint 配置
假设你已经有了 ESLint 配置(如果没有,可以先用 npm init @eslint/config 快速生成一个)。确保 package.json 里有检查脚本:
{ "scripts": { "lint": "eslint . --ext .js,.ts,.tsx,.jsx" }}第二步:创建 pre-commit hook 文件
使用 husky 的 add 命令创建 hook:
npx husky add .husky/pre-commit "npm run lint"这会在 .husky/pre-commit 文件里写入:
#!/usr/bin/env sh. "$(dirname -- "$0")/_/husky.sh"
npm run lint解释一下这个文件的内容:
#!/usr/bin/env sh:shebang,声明这是一个 shell 脚本. "$(dirname -- "$0")/_/husky.sh":加载 husky 的公共函数库,确保环境变量正确设置npm run lint:你要执行的命令
第三步:提交 .husky 目录
建议把 .husky 目录也提交到 git 仓库,这样团队成员克隆代码后只需要运行 npx husky install 就能自动配置好 hooks:
git add .huskygit commit -m "chore: add husky git hooks"第四步:测试
现在,随便改一个文件,然后尝试 commit:
echo "console.log('test')" > test.jsgit add test.jsgit commit -m "test: testing husky"如果代码通过 ESLint 检查,commit 会成功;如果 ESLint 报错了,commit 会被取消,你就需要先修复问题再重试。
husky 的工作原理
husky 的原理其实很简单:
npx husky install修改 git 的core.hooksPath配置,把 hooks 的查找路径从.git/hooks改成.husky- 你通过
npx husky add命令在.husky目录创建 hook 文件 - 当 git 执行到对应的时机时,会去
.husky目录查找并执行对应的 hook 脚本
这个设计的优点是配置可视化:你可以在 .husky 目录里看到所有定义的 hooks,每个 hook 就是一个单独的文件,非常直观。
husky 的常用命令
# 初始化 huskynpx husky install
# 创建或追加 hooknpx husky add .husky/pre-commit "npm run lint"
# 删除某个 hookrm .husky/pre-commit
# 查看所有 hooksls .husky/husky 的局限性
husky 简单直观,但它有几个局限:
串行执行:husky 的 hook 脚本是顺序执行的。如果你要跑 ESLint + Prettier + TypeScript 检查,每个任务都要等上一个完成才能开始。
Windows 兼容性问题:husky 依赖 shell 环境,在某些 Windows 环境(尤其是非 Git Bash 环境)可能出现兼容性问题。
单项目适用:husky 适合单个仓库的场景。如果你是 monorepo 结构,需要在多个子项目里配置相同的 hooks,维护成本会变高。
这些问题促成了 lefthook 的诞生。
第三章:lefthook 入门
什么是 lefthook
lefthook 是另一个流行的 git hooks 管理工具,由 Evil Martians 团队开发和维护。它的设计理念是“功能强大的 hooks 管理器“,支持并行执行、灵活的配置文件、远程 hooks 共享等功能。
和 husky 相比,lefthook 的定位更像是一个通用的任务运行器——你可以用它来管理不仅仅是 git hooks,而是各种自动化任务。它的配置文件(lefthook.yml)使用 YAML 格式,比 husky 的文件方式更结构化。
安装 lefthook
lefthook 支持多种安装方式,最常用的是通过 npm 安装:
npm install lefthook --save-dev或者用其他包管理器:
# Yarnyarn add lefthook --dev
# pnpmpnpm add lefthook --save-dev
# 全局安装npm install -g lefthook安装完成后,需要在项目里初始化:
npx lefthook install这个命令会在 .git/hooks 目录里安装 lefthook 的入口脚本。和 husky 不同的是,lefthook 安装后会在 .git/hooks 目录创建对应的 hook 文件(如 pre-commit),这些文件会调用 lefthook 来执行你在配置文件里定义的命令。
配置 lefthook
lefthook 的配置写在项目根目录的 lefthook.yml(或 lefthook.yaml)文件中。
基础配置示例:
pre-commit: parallel: true commands: lint: run: npm run lint type-check: run: npm run type-check这个配置定义了 pre-commit hook,要做的事情:
parallel: true:并行执行下面的命令commands:要执行的命令列表(这里的每个子项如lint、type-check是命令名称,可以自定义)run:要执行的命令(支持 npm/yarn/pnpm 脚本)
第一个完整的 lefthook 配置
让我们用一个完整场景来演示:配置 pre-commit 运行 ESLint 和 Prettier,pre-push 运行单元测试。
第一步:确保 package.json 有对应脚本
{ "scripts": { "lint": "eslint . --ext .js,.ts,.tsx,.jsx", "format": "prettier --write .", "test": "jest" }}第二步:创建 lefthook.yml
在项目根目录创建 lefthook.yml:
pre-commit: parallel: true commands: lint: run: npm run lint env: - CI=true format: run: npx prettier --check {staged_files} env: - CI=true
pre-push: parallel: true commands: test: run: npm run test env: - CI=true这个配置演示了两个常用场景:
pre-commit:并行运行 ESLint 代码检查和 Prettier 格式检查,只检查暂存区的文件({staged_files}变量会传入被暂存的文件路径)pre-push:在 push 前运行单元测试,确保代码在推送前通过所有测试
第三步:测试
现在进行 commit,看看 lefthook 是否正常执行:
echo "console.log('test')" > test.jsgit add test.jsgit commit -m "feat: add test file"lefthook 会并行运行 lint 和 format 检查,如果所有检查通过,commit 就会成功。
lefthook 的并行执行
lefthook 的一大亮点是支持并行执行。把 parallel: true 加在 hook 层级,该 hook 下的所有命令会同时运行:
pre-commit: parallel: true commands: lint: run: npm run lint type-check: run: npm run type-check format: run: npx prettier --check {staged_files}如果你有多个耗时的检查任务,并行执行可以大幅缩短总耗时。
lefthook 的变量和过滤器
lefthook 提供了有用的变量来引用 git 上下文:
{staged_files}:本次提交暂存的文件列表{all_files}:所有被跟踪的文件{push_files}:即将被推送的文件列表{files}:通过files过滤器匹配到的文件{cmd}:当前执行的命令{0},{1},{2}…:git hook 的位置参数(如{1}是 commit-msg hook 的第一个参数,即提交信息文件路径){lefthook_job_name}:当前 job 的名称
你还可以在命令级别使用 files 过滤器来限定命令只作用于特定文件:
pre-commit: commands: lint: run: npm run lint files: src/**/*.{js,ts}这只会在有 JS/TS 文件被修改时运行 lint。
lefthook 的全局安装跳过
有时候你可能想跳过 hooks(比如在 CI 环境或者临时调试时)。 lefthook 支持通过环境变量跳过:
# 跳过所有 hooksLFHOOK_SKIP=true git commit -m "temp commit"
# 跳过特定 hookLFHOOK_SKIP=pre-commit git commit -m "temp commit"你也可以在 lefthook.yml 里通过 skip 注释让 lefthook 在安装时跳过某个 hook:
# skip: pre-commitpre-commit: commands: # ...lefthook 的远程 hooks 共享
lefthook 支持从远程仓库拉取 hooks 配置,这在 monorepo 场景或者团队共享配置时非常有用:
remotes: - git_url: https://github.com/evilmartians/lefthook-git-hooks ref: v1.0.0 configs: - lefthook.yml你可以指定一个远程仓库的 URL 和配置文件路径,lefthook 会自动下载并合并远程的 hooks 配置。
lefthook vs husky:命令对照
| 操作 | husky | lefthook |
|---|---|---|
| 初始化 | npx husky install |
npx lefthook install |
| 添加 hook | npx husky add .husky/pre-commit "cmd" |
编辑 lefthook.yml |
| 删除 hook | rm .husky/pre-commit |
编辑 lefthook.yml |
| 跳过 hooks | git commit --no-verify -m "msg" |
LFHOOK_SKIP=true git commit |
| 查看状态 | ls .husky/ |
cat lefthook.yml |
第四章:横向对比
配置复杂度对比
husky:基于文件的配置
husky 的配置是目录式的——每个 hook 就是一个文件。要添加一个新的 pre-commit 检查,你需要:
- 运行
npx husky add .husky/pre-commit "command" - 或者手动创建文件并写入内容
优点是配置直观,每个 hook 对应一个文件;缺点是如果 hook 脚本很长,文件会变得臃肿,而且没有结构化的元数据管理。
lefthook:基于 YAML 的配置
lefthook 把所有 hooks 配置集中在一个 lefthook.yml 文件里。你可以在一个文件里管理所有 hooks 的配置,包括并行设置、环境变量、文件过滤器等。
优点是配置集中、结构化、可编程;缺点是初学者可能需要先了解 YAML 语法。
结论:如果你的项目 hook 脚本简单、hook 数量少,husky 的文件式配置更直观;如果项目复杂、hook 数量多,lefthook 的 YAML 配置更易维护。
功能特性对比
| 特性 | husky | lefthook |
|---|---|---|
| 并行执行 | ❌ | ✅ |
| 配置文件结构化程度 | 低(文件式) | 高(YAML) |
| Windows 兼容性 | ⚠️ 一般 | ✅ 更好 |
| monorepo 支持 | ❌ | ✅ |
| 远程 hooks 共享 | ❌ | ✅ |
| 交互式终端输出 | ✅ | ✅ |
| 文件过滤器 | ❌ | ✅ |
| 社区生态 | 大 | 中等 |
| 文档完善程度 | 完善 | 完善 |
性能对比
lefthook 因为支持并行执行,在有多任务场景下会有明显的性能优势。假设你有两个检查任务各需要 5 秒:
husky(串行):5秒 + 5秒 = 10秒
lefthook(并行):max(5秒, 5秒) = 5秒
对于小项目来说这个差异可能不明显,但在大型项目里,多几个检查任务,时间差距就会累积。
生态与社区
husky 是目前最流行的 git hooks 管理工具,拥有更大的社区和更多的使用者。如果你在开发过程中遇到问题,更容易找到解决方案。
lefthook 虽然社区较小,但增长势头不错。它由 Go 语言编写,性能优秀,支持并行执行,也吸引了一批追求效率的开发者。
CI/CD 环境处理
husky:
# 在 CI 中跳过 huskygit commit -m "ci: skip husky" --no-verify或者在 CI 配置中设置环境变量:
# GitHub Actions- name: Commit env: HUSKY: 0 run: | git commit -m "ci: skip husky"lefthook:
# 跳过所有 hooksLFHOOK_SKIP=true git commit -m "ci: skip hooks"两个工具都支持在 CI 环境中跳过 hooks,方式都比较优雅。
适用场景总结
| 场景 | 推荐工具 | 原因 |
|---|---|---|
| 小型个人项目 | husky | 配置简单,够用就好 |
| 需要并行执行 | lefthook | 内置并行支持 |
| monorepo 项目 | lefthook | 支持远程 hooks 共享和 workspace 配置 |
| 团队协作项目 | 两者皆可 | 看团队偏好 |
| Windows 为主的项目 | lefthook | 跨平台兼容更好 |
| 追求简单直观 | husky | 文件式配置一目了然 |
第五章:实战选型建议
小型个人项目:husky
如果你是个人开发者,项目规模不大,只需要一两个检查任务(比如 ESLint),husky 是更合适的选择。
原因:
- 配置简单,学习成本低
- 文件式配置直观,不需要理解 YAML
- 社区大,遇到问题容易搜索到解决方案
典型配置:
npm install husky --save-devnpx husky installnpx husky add .husky/pre-commit "npm run lint"团队协作项目:lefthook
如果你是团队开发者,项目有多个检查任务,需要确保团队成员统一配置,lefthook 是更好的选择。
原因:
- YAML 配置可以提交到仓库,确保团队配置一致
- 并行执行减少等待时间
- 文件过滤器可以让不同任务只检查相关文件
典型配置:
pre-commit: parallel: true commands: lint: run: npm run lint files: src/**/* format: run: npx prettier --check {staged_files} files: src/**/*.{js,ts,jsx,tsx}monorepo 项目:lefthook
如果是 monorepo 结构,有多个子包需要统一管理 hooks,lefthook 是必选。
lefthook 支持在根目录配置一个 lefthook.yml 来管理所有子项目的 hooks,也可以通过 remotes 配置从远程仓库拉取共享的 hooks 配置。
典型配置:
pre-commit: parallel: true commands: lint: run: npm run lint files: packages/*/src/**/* format: run: npx prettier --check {staged_files} files: packages/*/src/**/*迁移建议
从 husky 迁移到 lefthook:
- 先安装 lefthook:
npm install lefthook --save-dev - 初始化:
npx lefthook install - 根据
.husky目录里的文件,创建对应的lefthook.yml配置 - 测试确认工作正常
- 移除 husky:
npm uninstall husky && rm -rf .husky
从 lefthook 迁移到 husky:
- 先安装 husky:
npm install husky --save-dev - 初始化:
npx husky install - 根据
lefthook.yml里的配置,创建对应的.huskyhook 文件 - 测试确认工作正常
- 移除 lefthook:
npm uninstall lefthook && rm lefthook.yml
迁移过程中建议在新旧工具并行运行一段时间,确认没有问题后再移除旧工具。
CI/CD 集成
GitHub Actions:
name: CIon: [push, pull_request]
jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" - run: npm ci # lefthook 跳过 - run: LFHOOK_SKIP=true npm run lintGitLab CI:
lint: script: - npm ci - LFHOOK_SKIP=true npm run lint第六章:常见问题
Q: husky 的 .husky 目录需要提交到 git 吗?
建议提交。.husky 目录包含了所有 hooks 的定义,提交到 git 可以确保团队成员获得一致的配置。新成员克隆代码后只需运行 npx husky install 即可自动配置好 hooks。
Q: lefthook 能和 husky 共存吗?
技术上可以,但强烈不建议。两个工具都会管理 git hooks 安装,可能会产生冲突。如果你同时启用两者,git 可能会执行重复的检查,或者在卸载其中一个时影响另一个。
建议选择一个工具,并坚持使用。
Q: 在 CI 环境中如何跳过 hooks?
husky:
git commit -m "message" --no-verify或者设置环境变量 HUSKY=0。
lefthook:
LFHOOK_SKIP=true git commit -m "message"建议使用环境变量方式,更加显式和可控。
Q: pre-commit/prek 和 husky/lefthook 有什么区别?
这是两类定位不同的工具:
-
husky 和 lefthook:是 git hooks 管理器。它们帮你创建和管理
.git/hooks目录下的原始 hook 文件。它们本身不提供任何检查逻辑,只负责运行你指定的命令。 -
pre-commit 和 prek:是 pre-commit 框架。它们定义了一套 hook 配置格式(
.pre-commit-config.yaml),你可以从这个生态里拉取现成的 hook 脚本(如trailing-whitespace、check-yaml等)。prek 是 pre-commit 的 Rust 重新实现,性能更好,不需要预装 Python。
简单来说:
- 如果你只想运行自己写的脚本(ESLint、Jest 等),用 husky 或 lefthook
- 如果你想利用 pre-commit 生态的现成检查,用 pre-commit 或 prek
- 两者不是互斥的,在某些场景下可以结合使用
Q: 如何调试 hooks 不工作的问题?
husky:
- 检查
.husky目录是否存在 - 检查 hook 文件是否有执行权限:
ls -la .husky/pre-commit - 手动运行 hook 脚本看看是否有错误:
sh .husky/pre-commit - 检查 git 的
core.hooksPath配置:git config core.hooksPath
lefthook:
- 检查
lefthook.yml语法是否正确 - 用
lefthook run pre-commit命令手动运行看是否有错误 - 添加
lefthook.yml配置debug: true来看详细日志 - 检查
{staged_files}等变量是否正确解析
Q: 如何只在提交某些文件时触发检查?
lefthook:
使用 files 过滤器:
pre-commit: commands: lint: run: npm run lint files: src/**/*.{js,ts}husky:
需要自己在 hook 脚本里加逻辑:
#!/usr/bin/env sh. "$(dirname -- "$0")/_/husky.sh"
# 只检查暂存区中的 src 文件git diff --cached --name-only --diff-filter=ACM | grep -qE '^src/.*\.(js|ts)$' && npm run lintQ: 如何让多个命令在 commit 前并行执行?
lefthook: 直接配置 parallel: true
husky: 需要自己在脚本里加后台任务处理,不推荐。遇到这种需求建议换 lefthook。
第七章:结语
好了,我们从 git hooks 的基础概念开始,一路走到 husky 和 lefthook 的配置实战,再到横向对比和选型建议。该到总结的时候了。
核心要点回顾
- git hooks 是 git 内置的触发机制,让你在 commit、push 等操作前后插入自定义逻辑
- husky 简单直接,文件式配置,适合小型项目和初学者
- lefthook 功能更强,支持并行执行、YAML 配置、远程 hooks,适合复杂项目和团队协作
- 选择工具看场景:个人项目用 husky,团队项目、monorepo 用 lefthook
- CI 环境中记得跳过 hooks,用环境变量比
--no-verify更优雅
动手实践
纸上得来终觉浅。如果你是第一次接触这两个工具,我强烈建议你新建一个测试项目,把 husky 和 lefthook 都配一遍,亲自感受一下两者的差异。只有实际操作过,才能真正理解哪个更适合你。
资源链接
祝你的代码质量越来越好,提交信息越来越规范,团队协作越来越顺畅!
如果有任何问题或建议,欢迎在评论区留言讨论。