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 的用途和绕过策略。