Chapter 09

Git Hooks 与自动化

在每次 commit、push 前自动运行检查——用 Husky + lint-staged + commitlint 守护代码质量

什么是 Git Hooks

Git Hooks 是 Git 在特定事件发生时(提交前、推送前、合并后等)自动执行的脚本。它们存放在仓库的 .git/hooks/ 目录中。

# 查看所有可用的 hooks 示例
ls .git/hooks/
# 输出:applypatch-msg.sample  commit-msg.sample  pre-commit.sample ...
# .sample 结尾的是示例,去掉 .sample 后缀并给予执行权限即可激活

Hooks 可以是任何可执行脚本:Shell、Python、Node.js 等。钩子脚本返回非零退出码时,相应的 Git 操作会被中止

常用客户端 Hooks

pre-commit — 提交前检查

在执行 git commit 时,创建提交对象之前运行。最常用于代码检查、格式化、单元测试。

#!/bin/sh
# .git/hooks/pre-commit

# 运行 ESLint 检查
npm run lint
if [ $? -ne 0 ]; then
  echo "❌ ESLint 检查失败,提交已中止"
  exit 1
fi
echo "✅ Lint 检查通过"

commit-msg — 验证提交信息格式

在提交信息被保存后运行,接收提交信息文件路径作为参数。用于验证 Conventional Commits 等格式规范。

#!/bin/sh
# .git/hooks/commit-msg
# $1 是包含提交信息的临时文件路径

commit_msg=$(cat "$1")
pattern='^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .+'

if ! echo "$commit_msg" | grep -qE "$pattern"; then
  echo "❌ 提交信息不符合 Conventional Commits 规范"
  echo "   示例: feat: add login page"
  exit 1
fi

pre-push — 推送前运行

git push 将数据发送到远程之前运行。常用于在推送前运行完整测试套件,防止推送失败的代码。

#!/bin/sh
# .git/hooks/pre-push

echo "🧪 运行测试套件..."
npm test
if [ $? -ne 0 ]; then
  echo "❌ 测试失败,推送已中止"
  exit 1
fi

prepare-commit-msg — 自动修改提交信息

在提交信息编辑器打开之前运行。常用于自动在提交信息中插入 Jira ticket 号(从分支名中提取)。

#!/bin/sh
# .git/hooks/prepare-commit-msg
# 从分支名中提取 Jira issue 号(如 feature/PROJ-123-login)

branch=$(git symbolic-ref --short HEAD)
jira_ticket=$(echo "$branch" | grep -oE '[A-Z]+-[0-9]+')

if [ -n "$jira_ticket" ]; then
  # 在提交信息末尾追加 Jira ticket 号
  echo -e "\n\nJira: $jira_ticket" >> "$1"
fi

服务端 Hooks(简介)

Hook触发时机常见用途
pre-receive接收 push 之前验证所有 commit 的权限、格式
update每个分支更新时保护特定分支(如禁止 force push)
post-receivepush 完成后触发部署、发送通知

服务端 hooks 存在于 Git 服务器仓库中,对所有推送者生效,无法被客户端绕过。GitHub/GitLab 提供了 Webhooks 和 Protected Branch 等更友好的替代方案。

创建一个实用的 pre-commit Hook

以下脚本检查是否有 console.log 语句被遗忘在 JS 文件中:

#!/bin/sh
# .git/hooks/pre-commit

# 获取暂存的 JS/TS 文件
staged_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|jsx|tsx)$')

if [ -z "$staged_files" ]; then
  exit 0
fi

# 检查是否包含 console.log
found=$(echo "$staged_files" | xargs grep -l 'console\.log')

if [ -n "$found" ]; then
  echo "❌ 发现遗留的 console.log:"
  echo "$found"
  echo "请移除后再提交。"
  exit 1
fi

exit 0
# 激活 hook(给予执行权限)
chmod +x .git/hooks/pre-commit

Husky — 团队共享 Git Hooks

.git/hooks/ 目录不会被 Git 追踪(不在版本控制中),所以每个开发者需要手动配置 hooks,无法团队共享。Husky 解决了这个问题:它将 hooks 存放在项目根目录的 .husky/ 目录中,可以提交到 Git,团队所有成员自动共享。

# 安装 Husky
npm install husky --save-dev

# 初始化 Husky(创建 .husky/ 目录和 prepare 脚本)
npx husky init

# 此时 package.json 中会有:
# "scripts": { "prepare": "husky" }
# 其他开发者 npm install 后自动安装 hooks
# .husky/ 目录结构
.husky/
├── pre-commit      # 可以提交到 Git!
└── commit-msg
# 添加 pre-commit hook
echo "npx lint-staged" > .husky/pre-commit
chmod +x .husky/pre-commit

# 添加 commit-msg hook(使用 commitlint)
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
chmod +x .husky/commit-msg

lint-staged — 只检查暂存的文件

如果 pre-commit 每次都对整个项目运行 lint,速度会很慢。lint-staged 只对已 git add 的文件(staged files)运行检查,大幅提升速度。

# 安装 lint-staged
npm install lint-staged --save-dev

package.json 中配置:

{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss}": [
      "stylelint --fix",
      "prettier --write"
    ],
    "*.{json,md}": [
      "prettier --write"
    ]
  }
}

commitlint — 验证提交信息

commitlint 检查提交信息是否符合 Conventional Commits 规范,配合 Husky 的 commit-msg hook 使用。

# 安装 commitlint 及 Conventional Commits 规范配置
npm install --save-dev @commitlint/cli @commitlint/config-conventional

创建配置文件 commitlint.config.js

// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    // 可选:自定义规则
    'subject-max-length': [2, 'always', 72],
    'type-enum': [2, 'always', [
      'feat', 'fix', 'docs', 'style', 'refactor',
      'test', 'chore', 'perf', 'ci', 'revert'
    ]]
  }
};

完整配置示例

将以上所有工具整合在一起的完整 package.json

{
  "name": "my-project",
  "scripts": {
    "prepare": "husky",
    "lint": "eslint src --ext .js,.ts",
    "test": "jest --passWithNoTests"
  },
  "devDependencies": {
    "@commitlint/cli": "^18.0.0",
    "@commitlint/config-conventional": "^18.0.0",
    "eslint": "^8.0.0",
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0",
    "prettier": "^3.0.0"
  },
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
    "*.{css,json,md}": ["prettier --write"]
  }
}

.husky/pre-commit 文件内容:

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

.husky/commit-msg 文件内容:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit "${1}"

绕过 Hooks(紧急情况):在紧急情况下,可以用 git commit --no-verify 跳过 hooks。但这应该是极少数例外,不能成为常态。团队应在 README 中明确说明 hooks 的用途和绕过策略。

服务端 Hooks 详解

服务端 hooks 运行在 Git 服务器上,对所有 push 操作生效,无法被客户端绕过。适合强制执行团队规范。

pre-receive — 最强大的服务端 Hook

#!/bin/bash
# .git/hooks/pre-receive(运行在 Git 服务器上)
# 读取 stdin:旧哈希 新哈希 分支名

while read oldrev newrev refname; do
  branch=$(echo "$refname" | sed 's,refs/heads/,,')

  # 禁止 force push 到 main 分支
  if [ "$branch" = "main" ]; then
    # 检查是否是 force push(newrev 不是 oldrev 的后代)
    if ! git merge-base --is-ancestor "$oldrev" "$newrev"; then
      echo "❌ Force push to main is not allowed!"
      exit 1
    fi
  fi

  # 验证所有新提交的 commit 信息格式
  git log "$oldrev..$newrev" --format="%s" | while read subject; do
    if ! echo "$subject" | grep -qE '^(feat|fix|docs|style|refactor|test|chore)'; then
      echo "❌ Invalid commit message: $subject"
      exit 1
    fi
  done
done

Hooks 与 CI/CD 的分工

检查类型放在 Git Hooks放在 CI/CD
代码格式化pre-commit(快速,毫秒级)可选,作为兜底
Lint 检查pre-commit(只检查暂存文件)完整扫描
提交信息格式commit-msg(必须本地做)可重复验证
单元测试可选(会拖慢提交速度)必须(完整测试套件)
集成测试不适合(太慢)必须
安全扫描可选(基础检查)必须(深度扫描)
构建验证不适合(太慢)必须

Hooks 中避免运行耗时任务 — pre-commit hook 如果运行时间超过 5 秒,开发者就会开始用 --no-verify 绕过。保持 hooks 快速:只检查暂存的文件(lint-staged),不运行完整测试套件,不运行编译构建。慢任务放到 CI/CD 中。

Hooks 不能替代服务端保护 — 客户端 hooks 存储在 .git/hooks/,不被 Git 追踪,任何人都可以修改或删除。即使有了 Husky,只要开发者删除了 .husky/ 目录或用 --no-verify,就可以完全绕过。真正无法绕过的保护需要在服务端(服务端 hooks 或 GitHub Protected Branch)实现。

调试 Hooks

# 查看 hooks 是否有执行权限
ls -la .git/hooks/
ls -la .husky/

# 手动测试 pre-commit hook
sh .git/hooks/pre-commit

# 或 husky hook
sh .husky/pre-commit

# 检查 husky 是否正确安装(查看 .git/config)
git config core.hooksPath
# 如果输出 .husky 则表示 Husky 已接管 hooks 目录

# 临时禁用所有 hooks(调试用)
git config core.hooksPath /dev/null

本章小结 — Git Hooks 是在 Git 操作触发时自动执行的脚本,分客户端(pre-commit、commit-msg、pre-push)和服务端(pre-receive、post-receive)两类。Husky 将 hooks 纳入版本控制实现团队共享;lint-staged 只检查暂存文件提升速度;commitlint 强制 Conventional Commits 规范。Hooks 适合快速本地检查,慢任务交给 CI/CD;服务端保护才是无法绕过的最后防线。