14、Git钩子机制:客户端钩子与服务端钩子实战

Git钩子这东西,说白了就是一套“触发器”。你在Git仓库里做某些操作时,比如提交代码、推送分支,Git会自动去检查某个脚本,然后决定让不让你继续。我刚开始接触Git钩子时,觉得它挺神秘的,后来发现其实就是一堆放在特定目录下的脚本文件。

嗯,今天我们就来聊聊客户端钩子和服务端钩子。我会结合我自己的踩坑经历,把pre-commit、commit-msg、pre-receive、post-receive这几个最常用的钩子讲透。

什么是Git钩子?

每个Git仓库下都有一个.git/hooks目录。你进去看看,里面有一堆.sample结尾的示例文件。这些就是钩子的模板。去掉.sample后缀,改成可执行文件,钩子就生效了。

钩子分两类:

  • 客户端钩子:在你本地仓库触发,比如提交前、提交后、推送前。
  • 服务端钩子:在远程仓库触发,比如收到推送前、收到推送后。

我个人习惯把客户端钩子用来做“质量门禁”,服务端钩子用来做“流程管控”。两者配合,基本能杜绝大部分低级错误。

核心思路:客户端钩子帮你拦住“不该提交的代码”,服务端钩子帮你拦住“不该合并的分支”。

客户端(本地仓库) 服务端(远程仓库) git commit pre-commit 钩子 commit-msg 钩子 提交成功 / 失败 git push pre-receive 钩子 post-receive 钩子 推送成功 / 拒绝 git push

客户端钩子实战

pre-commit:提交前的最后一道防线

pre-commit 钩子在输入提交信息之前触发。说白了,它是你代码质量的“守门员”。

我在项目中遇到过最头疼的事:同事提交了带调试日志的代码,结果CI跑了一小时才发现。后来我强制团队在pre-commit里加了检查,问题就再没出现过。

下面是一个实用的pre-commit脚本示例:

#!/bin/bash
# pre-commit 钩子:检查代码风格、禁止调试代码、禁止大文件

echo "🔍 正在运行 pre-commit 检查..."

# 1. 检查是否包含调试代码
if git diff --cached --name-only | xargs grep -l "console\.log\|debugger\|TODO\|FIXME" 2>/dev/null; then
  echo "❌ 错误:提交中包含调试代码或待办事项!"
  echo "   请移除 console.log / debugger / TODO / FIXME 后再提交。"
  exit 1
fi

# 2. 检查是否包含大文件(超过5MB)
for file in $(git diff --cached --name-only); do
  if [ -f "$file" ]; then
    size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
    if [ "$size" -gt 5242880 ]; then
      echo "❌ 错误:文件 $file 超过 5MB!"
      echo "   请使用 Git LFS 管理大文件。"
      exit 1
    fi
  fi
done

# 3. 运行代码格式化(以Prettier为例)
if command -v prettier &> /dev/null; then
  echo "🎨 正在格式化代码..."
  git diff --cached --name-only --diff-filter=ACM | \
    grep -E '\.(js|ts|jsx|tsx|json|css|md)$' | \
    xargs prettier --write 2>/dev/null
  git add -u
fi

echo "✅ pre-commit 检查通过!"
exit 0

小技巧:pre-commit 脚本里如果修改了文件,记得用 git add -u 重新暂存。不然你格式化完代码,提交的还是旧版本。这个坑我踩过不止一次。

commit-msg:规范你的提交信息

commit-msg 钩子在用户输入提交信息之后、提交完成之前触发。它接收一个参数:存放提交信息的临时文件路径。

为什么要规范提交信息?你想想看,项目上线后出问题了,你翻git log看到一堆“fix bug”、“update”、“aaa”……你根本不知道哪个提交改了什么东西。我曾经在一个遗留项目里翻了两小时log,最后放弃了,直接去问老同事。

下面是一个强制使用 Conventional Commits 规范的 commit-msg 钩子:

#!/bin/bash
# commit-msg 钩子:校验提交信息格式

commit_msg_file="$1"
commit_msg=$(cat "$commit_msg_file")

# 定义正则:type(scope): description
pattern="^(feat|fix|docs|style|refactor|perf|test|chore|ci)(\([a-zA-Z0-9_-]+\))?: .{1,72}$"

if ! echo "$commit_msg" | grep -qE "$pattern"; then
  echo "❌ 提交信息格式不正确!"
  echo ""
  echo "正确格式示例:"
  echo "  feat(login): 添加登录验证码功能"
  echo "  fix(api): 修复用户列表分页错误"
  echo "  docs(readme): 更新安装说明"
  echo ""
  echo "允许的类型:feat, fix, docs, style, refactor, perf, test, chore, ci"
  exit 1
fi

# 检查提交信息长度
if [ ${#commit_msg} -gt 100 ]; then
  echo "❌ 提交信息过长(最大100字符),请精简描述。"
  exit 1
fi

echo "✅ 提交信息格式正确!"
exit 0

注意:commit-msg 钩子只检查格式,不检查内容是否合理。我曾经见过有人写 fix: 修复了一个bug,格式完全正确,但等于没写。所以团队规范里还得加上“描述要具体”这条软性要求。

服务端钩子实战

pre-receive:推送前的“安检门”

pre-receive 钩子在服务端收到推送数据后、更新引用之前触发。它从标准输入读取一行行的数据,格式是:旧值 新值 引用名

这个钩子特别适合做“分支保护”和“提交规范校验”。比如禁止直接推送到master分支,或者禁止包含敏感信息的提交。

下面是一个实用的 pre-receive 脚本:

#!/bin/bash
# pre-receive 钩子:保护主分支、禁止强制推送、检查提交信息

# 读取推送的引用信息
while read oldrev newrev refname; do
  # 获取分支名
  branch=${refname#refs/heads/}
  
  # 1. 保护主分支:只允许合并请求
  if [ "$branch" = "master" ] || [ "$branch" = "main" ]; then
    echo "❌ 错误:禁止直接推送到 $branch 分支!"
    echo "   请通过合并请求(Merge Request)提交代码。"
    exit 1
  fi
  
  # 2. 禁止强制推送(非快进推送)
  if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
    # 检查是否是快进推送
    merge_base=$(git merge-base "$oldrev" "$newrev")
    if [ "$merge_base" != "$oldrev" ]; then
      echo "❌ 错误:禁止强制推送(非快进推送)!"
      echo "   请使用 git pull --rebase 更新本地分支后再推送。"
      exit 1
    fi
  fi
  
  # 3. 检查新提交的提交信息格式
  for commit in $(git rev-list "$oldrev..$newrev" 2>/dev/null); do
    commit_msg=$(git log --format=%s -1 "$commit")
    if ! echo "$commit_msg" | grep -qE "^(feat|fix|docs|style|refactor|perf|test|chore|ci)(\(.*\))?: .+"; then
      echo "❌ 错误:提交 $commit 的提交信息格式不规范!"
      echo "   提交信息:$commit_msg"
      echo "   请使用 Conventional Commits 规范。"
      exit 1
    fi
  done
done

echo "✅ pre-receive 检查通过!"
exit 0

关键点:pre-receive 钩子返回非零退出码时,整个推送会被拒绝。这意味着你可以在服务端做“一票否决”。

post-receive:推送后的“自动化引擎”

post-receive 钩子在服务端更新引用之后触发。它同样从标准输入读取数据,但此时推送已经成功了。所以它适合做“通知”和“自动化部署”之类的事情。

我在项目中用 post-receive 做过这些事情:

  • 自动部署到测试环境
  • 发送企业微信/钉钉通知
  • 更新Jira任务状态
  • 触发CI/CD流水线

下面是一个简单的 post-receive 脚本:

#!/bin/bash
# post-receive 钩子:部署测试环境 + 发送通知

while read oldrev newrev refname; do
  branch=${refname#refs/heads/}
  
  # 只处理 master 分支的推送
  if [ "$branch" = "master" ] || [ "$branch" = "main" ]; then
    echo "🚀 检测到 $branch 分支更新,开始部署..."
    
    # 切换到项目目录并拉取最新代码
    cd /var/www/project
    git pull origin "$branch"
    
    # 执行部署脚本
    if [ -f "deploy.sh" ]; then
      bash deploy.sh
      echo "✅ 部署完成!"
    else
      echo "⚠️ 未找到 deploy.sh 脚本,跳过部署。"
    fi
    
    # 发送通知(示例:写入日志)
    echo "$(date): $branch 分支已更新,新提交: $newrev" >> /var/log/git-hooks.log
  fi
done

exit 0

建议:post-receive 里不要做太重的操作。如果部署需要很长时间,可以考虑用消息队列异步处理。不然推送的人要等很久才能看到结果。

钩子的管理与分发

钩子脚本默认在 .git/hooks 目录下,但这个目录不会被Git跟踪。所以团队里怎么共享钩子?

我推荐两种方式:

方式 说明 优点 缺点
项目内存储 在仓库根目录建 hooks/ 文件夹,存放脚本 版本控制、团队共享 需要手动复制到 .git/hooks
使用工具 如 husky(Node.js)、pre-commit(Python) 自动安装、跨平台 需要额外依赖

我个人更推荐用工具。比如前端项目用 husky,配合 lint-staged,体验非常好。你只需要在 package.json 里配置一下,团队其他人 npm install 后钩子就自动装好了。

避坑指南:我曾经在Windows和Mac混用的团队里吃过亏。Shell脚本在Windows上跑不起来。后来统一用Node.js写钩子,或者用跨平台的工具,才解决了这个问题。

总结

Git钩子机制,说白了就是“在关键节点插入自定义逻辑”。客户端钩子帮你管好自己,服务端钩子帮你管好团队。

几个要点再强调一下:

  • pre-commit 适合做代码质量检查,越早发现问题越好
  • commit-msg 适合规范提交信息,让git log变得可读
  • pre-receive 适合做分支保护和强制规范,是“最后一道防线”
  • post-receive 适合做自动化和通知,别放太重逻辑
  • 钩子脚本要团队共享,用工具管理比手动复制靠谱

嗯,Git钩子就聊到这里。记住一点:钩子不是银弹,它只是帮你减少人为失误的工具。真正重要的是团队达成共识,然后让钩子去执行这些共识。

公众号:蓝海资料掘金营,微信deep3321