这份指南将带你全面、深入地了解Git钩子。从基础概念到高级用法,从手动编写到团队管理,希望能成为你工作流中一份实用的参考。
🪝 一、Git 钩子是什么?
Git 钩子是在 Git 仓库中特定事件(如 commit、push、merge)发生前后自动执行的脚本。它是 Git 内置的自动化工具,能帮助你:
- 强制执行团队规范:例如检查代码格式和提交信息风格。
- 保障代码质量:在提交前运行单元测试或代码检查工具。
- 优化开发流程:推送后自动触发部署,或 merge 后自动安装依赖。
🗂️ 二、钩子如何工作(基础篇)
在开始编写钩子前,有必要先了解它们的基本工作原理。
文件存放
仓库的 .git/hooks 目录用于存放钩子脚本。git init 时,Git 会在此生成一些示例脚本(文件名以 .sample 结尾)。只有移除了 .sample 后缀且具备可执行权限的脚本才会在相应事件发生时被调用。
工作目录
Git 调用钩子时,工作目录通常会切换到仓库的根目录。但 pre-receive、post-receive、post-update、update 这类服务器端钩子例外,它们会在 $GIT_DIR 目录下执行。
跨平台可执行权限
在 macOS 和 Linux 系统上,必须确保钩子文件具有可执行权限(chmod +x <hook-name>),否则 Git 将不会执行。Windows 系统通常不需要额外设置。
影响执行结果
钩子的执行结果通过**退出码(Exit Code)**来控制 Git 的行为:
- 退出码 0:表示钩子执行成功,Git 将继续进行原操作。
- 非零退出码:表示钩子执行失败,Git 将中止当前操作。
⚙️ 三、钩子的“维护”命令(git hook)
从 Git 2.36.0 版本开始,官方引入了 git hook 命令,用于统一管理和调用钩子。
| 命令 | 功能说明 | 示例 |
|---|---|---|
git hook run <hook-name> | 手动触发一个指定的钩子,主要用于调试或测试。 | git hook run pre-commit |
git hook run --ignore-missing <hook-name> | 触发钩子,如果钩子文件不存在,不报错,静默退出。 | git hook run --ignore-missing pre-push |
git hook run --to-stdin=<file> <hook-name> | 触发钩子,并将指定文件的内容作为钩子的标准输入。 | git hook run --to-stdin=commit-msg.txt commit-msg |
这个命令统一了钩子的调用方式,方便其他工具与 Git 的钩子系统集成。
🧩 四、客户端钩子(常用)
客户端钩子在开发者的本地机器上运行,是实施编码规范和质量检查的第一道防线。
1. pre-commit
- 触发时机: 在用户输入提交信息前,即将创建提交对象时启动。
- 典型应用: 代码格式化(如 Prettier)、代码质量检查(如 ESLint)、静态分析(如 Linter)、检查敏感信息(如密码、API密钥)。
- 跳过方式:
git commit --no-verify - 示例
场景1:强制代码风格(ESLint)
#!/bin/sh
# 获取所有已暂存(Staged)的 JS/TS 文件
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|jsx|tsx)$')
if [ -n "$FILES" ]; then
echo "Running ESLint on staged files..."
# 可选:运行 lint-staged 仅检查暂存文件
npx eslint $FILES
if [ $? -ne 0 ]; then
echo "ESLint检查失败,请修复错误后再提交。"
exit 1
fi
fi
exit 0
场景2:防止提交敏感信息
#!/bin/sh
# 检查暂存区是否有疑似密码/密钥的模式
PATTERNS="(password|secret|api_key|token)(\s*=\s*['\"]?\S+)"
# 使用更安全的 git grep 配合 --cached 检查暂存区
if git grep --cached -q -E "$PATTERNS"; then
echo "❌ 错误:检测到提交中可能包含敏感信息!"
exit 1
fi
exit 0
这个脚本会检查暂存区中的所有文件,如果发现疑似密码或密钥的模式(如 password、secret、api_key、token),就会中止提交。
2. prepare-commit-msg
- 触发时机: 在
pre-commit之后,默认提交信息生成之后,但在提交信息编辑器启动之前运行。 - 典型应用: 自动生成或填充提交信息模板,例如插入 Jira Ticket ID、自动添加 Signed-off-by 签名或 Co-authored-by 跟踪信息。
- 示例
场景:注入 Jira Ticket ID(从分支名提取)
#!/bin/sh
COMMIT_MSG_FILE=$1
CURRENT_BRANCH=$(git symbolic-ref --short HEAD)
# 从分支名中查找 Jira Ticket ID 模式 (大写字母和数字)
TICKET_ID=$(echo "$CURRENT_BRANCH" | grep -o '[A-Z]\+-[0-9]\+' | head -1)
if [ -n "$TICKET_ID" ] && ! grep -q "$TICKET_ID" "$COMMIT_MSG_FILE"; then
# 将 Ticket ID 以 "[Ticket-ID] " 的格式插入到提交信息顶部
echo "[$TICKET_ID] $(cat $COMMIT_MSG_FILE)" > $COMMIT_MSG_FILE
echo "✅ Added Jira Ticket ID: $TICKET_ID to commit message"
fi
exit 0
3. commit-msg
- 触发时机: 用户编辑完提交信息后,在创建提交对象前运行。
- 典型应用: 强制提交信息格式,如要求遵循
type: subject或 Conventional Commits 规范。 - 示例
场景:强制执行 Conventional Commits 格式
#!/bin/sh
# 提交信息文件路径作为第一个参数传入
COMMIT_MSG=$(cat "$1")
# 定义 Conventional Commits 的正则表达式
PATTERN='^(feat|fix|docs|style|refactor|perf|test|chore)(\(.+\))?: .{1,50}'
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "❌ 提交信息不符合规范!"
echo "请使用以下格式: <type>(<scope>): <subject>"
echo "示例: feat(parser): add ability to parse arrays"
exit 1
fi
exit 0
4. post-commit
- 触发时机: 在
commit操作完全完成后。 - 典型应用: 发送本地通知、记录提交统计信息、触发不影响构建流程的轻量级任务。
- 示例
场景:发送桌面通知
#!/bin/sh
# 获取最近一次提交的哈希和标题
COMMIT_HASH=$(git rev-parse HEAD)
SHORT_HASH=$(git rev-parse --short HEAD)
MSG=$(git log -1 --pretty=%B $COMMIT_HASH | head -1)
# 发送 macOS 系统的桌面通知
osascript -e "display notification \"$MSG\" with title \"Commit Success: $SHORT_HASH\""
5. pre-rebase
- 触发时机: 在
git rebase操作修改提交历史之前运行。 - 典型应用: 保护特定分支不被变基,避免团队协作中因
rebase公有分支而产生问题。 - 示例
场景:禁止对 main 分支进行 rebase
#!/bin/sh
# 获取将要被 rebase 的分支
UPSTREAM=$(git rev-parse --symbolic-full-name "$1" 2>/dev/null)
BRANCH=$(git rev-parse --symbolic-full-name "$2" 2>/dev/null)
if [ "$BRANCH" = "refs/heads/main" ]; then
echo "❌ 错误:不能直接对 main 分支执行 rebase 操作!"
exit 1
fi
exit 0
这个脚本会检查 git rebase 的目标分支,如果发现是 main 分支,就会拒绝执行并给出提示。
6. post-checkout
- 触发时机: 在
git checkout或git switch成功切换分支后运行。 - 典型应用: 根据不同的分支自动管理依赖或环境配置。例如,切换到后端分支时安装 Python 依赖,切换到前端分支时安装 Node.js 依赖。
- 示例
场景:切换分支后自动安装依赖
#!/bin/sh
# 获取切换前的分支(新分支)
PREV_HEAD=$1
NEW_HEAD=$2
# 检查是否是分支切换(而非 checkout file),1 代表切换分支
if [ $3 -eq 1 ]; then
# 获取新分支名
NEW_BRANCH=$(git symbolic-ref --short HEAD)
if echo "$NEW_BRANCH" | grep -q "^backend/"; then
echo "🔄 切换到后端分支,正在安装依赖..."
pip install -r requirements.txt
elif echo "$NEW_BRANCH" | grep -q "^frontend/"; then
echo "🔄 切换到前端分支,正在安装依赖..."
npm install
fi
fi
exit 0
7. post-merge
- 触发时机:
git pull或git merge成功合并后运行。 - 典型应用: 更新项目依赖、清理构建缓存、根据合并后的主分支情况触发通知。
- 示例
场景:合并后检查并安装新依赖
#!/bin/sh
# 检查 package.json 或 requirements.txt 文件是否在本次合并中被修改
if git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep --quiet "package.json"; then
echo "📦 package.json 已更新,正在安装新的 npm 依赖..."
npm install
fi
exit 0
🌐 五、服务器端钩子(守护远程仓库)
服务器端钩子在服务器端的裸仓库中执行,需要管理员权限进行配置。它们为服务端的中央仓库提供了强大的管理能力。
1. pre-receive
- 触发时机: 客户端执行
git push后(推送操作开始时),在服务器端接受任何引用(refs)更新之前立即运行。 - 典型应用: 执行全局合规性检查,可以读取传入提交的具体内容,来决定是否接纳这次推送。
- 示例
场景:强制执行分支命名规范
#!/bin/sh
# 在 pre-receive 钩子中,通过标准输入(stdin)获取推送信息
# 输入格式: <old-value> SP <new-value> SP <ref-name> LF
zero_commit="0000000000000000000000000000000000000000"
# 定义允许的分支名前缀
allowed_pattern='^(refs/heads/(main|develop|feature/|bugfix/)|refs/tags/v)'
while read old new ref; do
# 检查所有新增或更新的引用
if ! echo "$ref" | grep -qE "$allowed_pattern"; then
echo "❌ 策略拒绝:分支名 $ref 不符合命名规范!"
echo "✅ 允许的分支前缀: main, develop, feature/, bugfix/, tags/v"
exit 1
fi
done
exit 0
2. update
- 触发时机: 比
pre-receive更细粒度,会为客户端推送中的每一个待更新的引用(分支或标签)分别调用一次。 - 典型应用: 执行分支级别的个性化检查,例如禁止创建新分支、禁止删除特定分支、控制谁能向特定分支推送等。
- 示例
场景:保护 main 主分支不被强制推送(--force)
#!/bin/sh
# update 钩子的命令行参数: refname, old-object, new-object
refname="$1"
old_rev="$2"
new_rev="$3"
# 仅检查 main 分支
if [ "$refname" = "refs/heads/main" ]; then
# 检查是否是强制推送(非快进合并)
if [ "$(git merge-base "$old_rev" "$new_rev")" != "$old_rev" ]; then
echo "❌ 错误:不能对 main 分支进行强制推送!"
exit 1
fi
fi
exit 0
3. post-receive
- 触发时机:
pre-receive和update钩子成功执行且所有引用更新完成后运行。 - 典型应用: 负责后续的自动化操作,如触发 CI/CD 流水线、更新部署镜像、向聊天室发送通知等。
- 示例
场景:推送后触发自动化部署
#!/bin/bash
# 在 post-receive 中,只需处理更新后的状态,无需检查是否应该接受本次推送
while read old new ref; do
# 如果收到 main 分支的更新,则触发部署脚本
if [ "$ref" = "refs/heads/main" ]; then
echo "📦 Main 分支已更新,正在触发自动化部署..."
# 调用部署脚本,或将命令放入后台执行以防超时
nohup /path/to/deploy.sh > /var/log/deploy.log 2>&1 &
fi
done
exit 0
这个钩子会监控 main 分支的更新,一旦收到更新,就会触发部署脚本,实现自动化部署。
4. post-update
- 触发时机: 所有引用更新完毕,通常是一个更轻量级的
post-receive,一般不通过标准输入传递信息。 - 典型应用: 用于更新一些非 Git 仓库的辅助信息,如更新网页服务、刷新缓存、更新一个纯文本的文件列表等。
- 示例
场景:推送后自动创建一个日期标签
#!/bin/sh
# 检查 master 分支是否发生了变化
changed_master=0
for ref in "$@"; do
if echo "$ref" | grep -q "refs/heads/master"; then
changed_master=1
break
fi
done
# 检查 master 是否有更新
if [ $changed_master -eq 1 ] || [ -z "$@" ]; then
# 获取当前日期,例如 2026-04-25
DATE=$(date +%F)
echo "🏷️ 正在为今天的 master 分支创建 release 标签:$DATE..."
# 检查该标签是否已存在,若不存在则创建
if ! git rev-parse "release-$DATE" >/dev/null 2>&1; then
git tag -a "release-$DATE" -m "Release for $DATE"
echo "✅ 标签 release-$DATE 已创建"
else
echo "⚠️ 标签 release-$DATE 已存在"
fi
fi
每次向 master 分支成功推送新内容时,这个脚本都会创建一个以当前日期为名的 release 标签。
⚙️ 六、高级配置与技巧
1. 在 Windows 上使用钩子
- Git Bash环境:直接在
.git/hooks目录中创建同名文件(无扩展名),并以#!/bin/sh开头。 - 直接调用.exe文件:钩子文件可以是任何可执行文件,包括
.exe、.cmd或.ps1(需系统执行策略允许)。 - 使用WSL:在WSL中开发的仓库,其钩子行为与原生Linux完全一致。
2. 管理钩子的现代工具(推荐)
手动管理 .git/hooks 存在无法被版本控制和团队共享困难的痛点。现代钩子管理工具旨在解决这些问题:
| 工具 | 核心特点 | 团队共享方式 |
|---|---|---|
| Husky | 基于 Node.js,在 package.json 或 .husky 目录配置钩子。 | npm install 时自动安装钩子 |
| Lefthook | 基于 Go 语言,使用 YAML 配置(lefthook.yml),支持并行任务执行与结果输出过滤,性能优秀。 | lefthook install 命令配置 |
| pre-commit | 基于 Python 的框架,采用 .pre-commit-config.yaml 声明式配置,支持来自社区的几百个现成钩子。 | pre-commit install 命令配置 |
其中,Husky 是 Node.js 生态中最主流的钩子管理工具。其核心配置思路如下:
- 安装与初始化:当然,在项目根目录执行:
npm install --save-dev husky npx husky inithusky init命令会做两件事:在package.json中添加"prepare": "husky"脚本,并在.husky目录创建pre-commit示例钩子。 - 配置钩子:所有钩子都将作为可执行脚本存放在
.husky/目录下。例如,创建.husky/pre-commit文件:#!/bin/sh . "$(dirname "$0")/_/husky.sh" npm run lint注意:
.husky/_/husky.sh是 Husky 的核心运行脚本,第一行#!/bin/sh是保证脚本可执行的,必须保留(不要直接复制粘贴示例而忽略了解)。 - 团队中共享:当你或你的同事执行
git clone后,运行npm install,prepare脚本会自动执行husky命令,它会读取.husky目录下的所有钩子文件并安装到.git/hooks中,确保每个团队成员都使用完全相同的一组钩子。
3. 调试钩子脚本
- 日志追踪:在脚本中使用
echo "Debug message" >&2将调试信息输出到标准错误流,以便在终端看到。 - 临时跳过:对于
pre-commit和commit-msg钩子,可以使用git commit --no-verify跳过。 - 专门调试:使用
git hook run <hook-name>命令指定运行。
4. 启用所有钩子示例
如果你刚开始接触钩子,不想从零开始编写,可以一次性启用所有 Git 自带的示例脚本:
# 进入 hooks 目录并一次性启用所有示例
cd .git/hooks
for f in *.sample; do mv "$f" "${f%.sample}"; done
需要注意的是,这些示例脚本不可直接用于生产环境,但可以作为你编写自己钩子时的绝佳模板。
5. 钩子模板与 core.hooksPath
init.templateDir:你可以通过全局 Git 配置git config --global init.templateDir ~/.git-templates,然后在~/.git-templates/hooks/目录中放入自定义钩子。这样,任何通过git init或git clone创建的新仓库都会自动拥有这些钩子。core.hooksPath:这是一个项目级的配置项,允许你修改 Git 查找钩子的默认路径。例如,你可以将钩子存放在版本控制下的.githooks文件夹中,并执行git config core.hooksPath .githooks,这样钩子就能跟随项目共享了。
6. 钩子的软链接技巧
如果你在本地有很多仓库,且希望它们都用同一套钩子脚本,可以在每个仓库的 .git/hooks 目录下创建软链接(Symlink),指向一个中心化的钩子目录。例如:
ln -s ~/my-global-hooks/pre-commit .git/hooks/pre-commit
但需要注意,Git 在调用钩子时可能会改变工作目录,软链接的脚本需要能正确处理路径问题。
📊 Git 钩子完全列表 & 速查表
下表汇总了所有常见的钩子及其关键信息,方便快速查阅。
| 类别 | 钩子名称 | 触发时机 | 主要用途 | 参数 |
|---|---|---|---|---|
| 客户端 | applypatch-msg | git am 应用补丁前 | 检查或编辑补丁的提交信息 | 提交信息文件路径 |
| 客户端 | pre-applypatch | git am 应用补丁后,提交前 | 在提交前检查工作目录 | 无 |
| 客户端 | post-applypatch | git am 完成提交后 | 用于通知,不影响结果 | 无 |
| 客户端 | pre-commit | git commit 前 | 运行 linter、格式检查、单元测试 | 无 |
| 客户端 | prepare-commit-msg | 默认提交信息生成后,编辑器启动前 | 填充提交信息模板 | 提交信息文件路径、提交来源、SHA-1 |
| 客户端 | commit-msg | 用户输入提交信息后,创建提交前 | 强制检查提交信息格式 | 提交信息文件路径 |
| 客户端 | post-commit | git commit 完成后 | 触发本地通知、记录日志 | 无 |
| 客户端 | pre-rebase | git rebase 修改历史前 | 防止在特定分支上变基 | 上游分支、要变基的分支 |
| 客户端 | post-checkout | git checkout 或 git switch 成功后 | 自动管理分支依赖、环境配置 | 前 HEAD 的 SHA-1、新 HEAD 的 SHA-1、flag |
| 客户端 | post-merge | git merge 成功后 | 合并后更新依赖、清理缓存 | |
| 客户端 | pre-push | git push 前 | 推送前运行集成测试 | 远程仓库名、远程仓库 URL |
| 客户端 | pre-auto-gc | git gc --auto 前 | 在自动垃圾回收前做检查 | 无 |
| 客户端 | post-rewrite | 被 git commit --amend 或 git rebase 重写的提交后 | 在历史重写后采取相应措施 | 触发命令、标准输入(stdin) |
| 服务器端 | pre-receive | git push 开始时 | 执行全局合规性、准入检查 | 无(通过标准stdin接收信息) |
| 服务器端 | update | pre-receive 之后,为每个更新的引用运行一次 | 实施分支级别的访问控制与强制策略 | 引用名、旧对象 SHA-1、新对象 SHA-1 |
| 服务器端 | post-receive | update 成功后 | 触发 CI/CD、发通知、自动部署 | 无(通过标准stdin接收信息) |
| 服务器端 | post-update | 所有引用更新完毕后 | 更新非仓库相关资源、创建标签 | 通过命令行接收更新后的引用名 |
掌握了这些钩子,你就相当于拥有了定制 Git 工作流的强大能力。不过,针对钩子的管理和团队协作往往是大家实际落地时最关心的问题。
你目前是在个人项目里尝试自动化某些操作,还是想为团队统一推行一些规范呢?这两者在钩子的部署和复杂度上差别还挺大的。告诉我你的具体场景,我再为你挑选最合适的实现方案。