Appearance
自定义钩子
概述
自定义 Git Hooks 允许你根据项目需求编写特定的自动化脚本。本章将介绍如何编写实用的钩子脚本,以及推荐一些常用的钩子工具。
编写钩子脚本
基本原则
bash
#!/bin/bash
# 钩子脚本的基本结构
# 1. 设置错误时退出
set -e
# 2. 输出提示信息
echo "正在执行钩子检查..."
# 3. 执行检查逻辑
# ...
# 4. 返回结果
exit 0 # 成功
# exit 1 # 失败获取 Git 信息
bash
#!/bin/bash
# 获取当前分支
BRANCH=$(git branch --show-current)
echo "当前分支: $BRANCH"
# 获取暂存的文件
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
echo "暂存的文件:"
echo "$STAGED_FILES"
# 获取暂存文件的内容差异
DIFF=$(git diff --cached)
echo "差异内容:"
echo "$DIFF"
# 获取最近一次提交信息
LAST_MSG=$(git log -1 --pretty=%B)
echo "最近提交: $LAST_MSG"
# 获取提交哈希
COMMIT_HASH=$(git rev-parse HEAD)
echo "提交哈希: $COMMIT_HASH"颜色输出
bash
#!/bin/bash
# 定义颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 使用颜色
echo -e "${GREEN}成功: 检查通过${NC}"
echo -e "${YELLOW}警告: 请注意${NC}"
echo -e "${RED}错误: 检查失败${NC}"pre-commit 示例
检查代码风格
bash
#!/bin/bash
# .git/hooks/pre-commit
set -e
echo "检查代码风格..."
# 获取暂存的 JS/TS 文件
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|jsx|tsx)$' || true)
if [ -z "$FILES" ]; then
echo "没有需要检查的文件"
exit 0
fi
# 运行 ESLint
echo "运行 ESLint..."
npx eslint $FILES --max-warnings=0
echo "代码风格检查通过!"
exit 0检查调试代码
bash
#!/bin/bash
# .git/hooks/pre-commit
echo "检查调试代码..."
# 获取暂存的文件
FILES=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$FILES" ]; then
exit 0
fi
# 检查调试代码
PATTERNS=(
"console\.log"
"console\.debug"
"debugger"
"print\("
"var_dump"
"dd\("
)
FOUND=0
for pattern in "${PATTERNS[@]}"; do
MATCHES=$(git diff --cached -S "$pattern" --name-only)
if [ -n "$MATCHES" ]; then
echo "发现调试代码模式: $pattern"
echo "文件: $MATCHES"
FOUND=1
fi
done
if [ $FOUND -eq 1 ]; then
echo ""
echo "错误: 请移除调试代码后再提交"
exit 1
fi
exit 0检查文件命名
bash
#!/bin/bash
# .git/hooks/pre-commit
echo "检查文件命名规范..."
# 获取暂存的文件
FILES=$(git diff --cached --name-only --diff-filter=ACM)
ERRORS=0
for FILE in $FILES; do
# 检查文件名是否包含空格
if [[ "$FILE" =~ [[:space:]] ]]; then
echo "错误: 文件名包含空格 - $FILE"
ERRORS=$((ERRORS + 1))
fi
# 检查文件名是否包含中文
if [[ "$FILE" =~ [\u4e00-\u9fa5] ]]; then
echo "错误: 文件名包含中文 - $FILE"
ERRORS=$((ERRORS + 1))
fi
# 检查文件名是否以小写字母开头
BASENAME=$(basename "$FILE")
if [[ ! "$BASENAME" =~ ^[a-z] ]]; then
echo "警告: 文件名应以小写字母开头 - $FILE"
fi
done
if [ $ERRORS -gt 0 ]; then
exit 1
fi
exit 0检查敏感信息
bash
#!/in/bash
# .git/hooks/pre-commit
echo "检查敏感信息..."
# 敏感信息模式
PATTERNS=(
"password\s*=\s*['\"][^'\"]+['\"]"
"api[_-]?key\s*=\s*['\"][^'\"]+['\"]"
"secret[_-]?key\s*=\s*['\"][^'\"]+['\"]"
"token\s*=\s*['\"][^'\"]+['\"]"
"-----BEGIN.*PRIVATE KEY-----"
"aws_access_key_id"
"aws_secret_access_key"
)
DIFF=$(git diff --cached)
FOUND=0
for pattern in "${PATTERNS[@]}"; do
if echo "$DIFF" | grep -qiE "$pattern"; then
echo "警告: 可能包含敏感信息 - $pattern"
FOUND=1
fi
done
if [ $FOUND -eq 1 ]; then
echo ""
echo "错误: 检测到可能的敏感信息,请检查后再提交"
echo "如果确认不是敏感信息,可以使用 --no-verify 跳过此检查"
exit 1
fi
exit 0运行测试
bash
#!/bin/bash
# .git/hooks/pre-commit
echo "运行测试..."
# 只在关键文件变化时运行测试
CHANGED_FILES=$(git diff --cached --name-only)
if echo "$CHANGED_FILES" | grep -qE '\.(js|ts|jsx|tsx)$'; then
echo "检测到代码变化,运行测试..."
npm test -- --passWithNoTests --silent
fi
exit 0pre-push 示例
防止推送到受保护分支
bash
#!/bin/bash
# .git/hooks/pre-push
PROTECTED_BRANCHES=("main" "master" "develop" "staging")
while read local_ref local_oid remote_ref remote_oid; do
BRANCH=$(echo "$remote_ref" | sed 's|refs/heads/||')
for protected in "${PROTECTED_BRANCHES[@]}"; do
if [[ "$BRANCH" == "$protected" ]]; then
echo "错误: 不允许直接推送到 $BRANCH 分支"
echo "请通过 Pull Request 进行合并"
exit 1
fi
done
done
exit 0推送前运行完整测试
bash
#!/bin/bash
# .git/hooks/pre-push
echo "推送前运行完整测试..."
# 获取推送的分支
while read local_ref local_oid remote_ref remote_oid; do
BRANCH=$(echo "$remote_ref" | sed 's|refs/heads/||')
# 只在推送到 main 时运行完整测试
if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then
echo "推送到 $BRANCH,运行完整测试..."
npm run test:full
if [ $? -ne 0 ]; then
echo "错误: 测试失败,不允许推送"
exit 1
fi
fi
done
exit 0检查远程分支状态
bash
#!/bin/bash
# .git/hooks/pre-push
while read local_ref local_oid remote_ref remote_oid; do
BRANCH=$(echo "$remote_ref" | sed 's|refs/heads/||')
# 获取远程分支的最新状态
git fetch origin $BRANCH 2>/dev/null
REMOTE_OID=$(git rev-parse origin/$BRANCH 2>/dev/null || echo "")
if [ -n "$REMOTE_OID" ] && [ "$REMOTE_OID" != "$local_oid" ]; then
# 检查本地是否包含远程的所有提交
if ! git merge-base --is-ancestor $REMOTE_OID $local_oid; then
echo "警告: 远程分支 $BRANCH 有新的提交"
echo "请先拉取远程更改: git pull origin $BRANCH"
echo ""
read -p "是否继续推送? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
fi
done
exit 0其他实用钩子
post-merge 示例
bash
#!/bin/bash
# .git/hooks/post-merge
echo "合并完成,检查依赖更新..."
# 检查 package.json 是否变化
if git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep -q "package.json"; then
echo "package.json 已更新,安装依赖..."
npm install
fi
# 检查 package-lock.json 是否变化
if git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep -q "package-lock.json"; then
echo "package-lock.json 已更新,同步依赖..."
npm ci
fi
# 检查 composer.json 是否变化
if git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep -q "composer.json"; then
echo "composer.json 已更新,安装依赖..."
composer install
fi
# 检查数据库迁移
if git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep -q "migrations/"; then
echo "检测到数据库迁移文件,请手动执行迁移"
fi
exit 0post-checkout 示例
bash
#!/bin/bash
# .git/hooks/post-checkout
# $1: 之前的 HEAD
# $2: 新的 HEAD
# $3: 是否是分支切换 (1) 或文件检出 (0)
PREV_HEAD=$1
NEW_HEAD=$2
BRANCH_SWITCH=$3
if [ "$BRANCH_SWITCH" = "1" ]; then
echo "切换分支完成"
# 更新依赖
if [ -f "package.json" ]; then
echo "检查依赖更新..."
npm install --prefer-offline --silent
fi
# 清理构建缓存
if [ -d "node_modules/.cache" ]; then
echo "清理构建缓存..."
rm -rf node_modules/.cache
fi
# 更新环境配置
if [ -f ".env.example" ] && [ ! -f ".env" ]; then
echo "复制环境配置模板..."
cp .env.example .env
fi
fi
exit 0prepare-commit-msg 示例
bash
#!/bin/bash
# .git/hooks/prepare-commit-msg
# $1: 包含提交信息的文件路径
# $2: 提交来源 (message, template, merge, squash, commit)
# $3: 相关提交的 SHA (amend 时)
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
# 如果已经有提交信息,不修改
if [ "$COMMIT_SOURCE" = "message" ] || [ "$COMMIT_SOURCE" = "merge" ]; then
exit 0
fi
# 获取当前分支
BRANCH=$(git branch --show-current)
# 从分支名提取问题编号
# 例如: feature/ABC-123-new-feature -> ABC-123
ISSUE=$(echo "$BRANCH" | grep -oE '[A-Z]+-[0-9]+' | head -1)
# 如果找到问题编号,添加到提交信息
if [ -n "$ISSUE" ]; then
echo "" >> "$COMMIT_MSG_FILE"
echo "" >> "$COMMIT_MSG_FILE"
echo "Refs: $ISSUE" >> "$COMMIT_MSG_FILE"
fi
exit 0钩子工具推荐
Husky
Node.js 项目最流行的 Git Hooks 管理工具。
bash
# 安装
npm install husky --save-dev
# 初始化
npx husky install
# 添加钩子
npx husky add .husky/pre-commit "npm test"
# package.json 配置
{
"scripts": {
"prepare": "husky install"
}
}配置示例:
bash
# .husky/pre-commit
#!/bin/bash
. "$(dirname "$0")/_/husky.sh"
npm run lint
npm testbash
# .husky/commit-msg
#!/bin/bash
. "$(dirname "$0")/_/husky.sh"
npx --no -- commitlint --edit "$1"lint-staged
只检查暂存的文件,提高效率。
bash
# 安装
npm install lint-staged --save-dev
# package.json 配置
{
"lint-staged": {
"*.{js,ts,jsx,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}
# 配合 Husky
# .husky/pre-commit
npx lint-stagedcommitlint
验证提交信息格式。
bash
# 安装
npm install @commitlint/cli @commitlint/config-conventional --save-dev
# 配置
# commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert']
],
'subject-max-length': [2, 'always', 50],
'body-max-line-length': [2, 'always', 72]
}
}
# 配合 Husky
# .husky/commit-msg
npx --no -- commitlint --edit "$1"pre-commit
Python 项目的通用钩子框架。
bash
# 安装
pip install pre-commit
# 配置
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-merge-conflict
- repo: https://github.com/psf/black
rev: 23.1.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
# 安装钩子
pre-commit install
# 手动运行
pre-commit run --all-filessimple-git-hooks
轻量级的 Git Hooks 管理工具。
bash
# 安装
npm install simple-git-hooks --save-dev
# package.json 配置
{
"simple-git-hooks": {
"pre-commit": "npm run lint",
"commit-msg": "node scripts/validate-commit-msg.js $1"
},
"scripts": {
"postinstall": "simple-git-hooks"
}
}钩子脚本模板
通用 pre-commit 模板
bash
#!/bin/bash
# .githooks/pre-commit
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${GREEN}运行 pre-commit 检查...${NC}"
# 获取暂存文件
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$STAGED_FILES" ]; then
exit 0
fi
# 1. 检查调试代码
echo "检查调试代码..."
DEBUG_PATTERNS="console\.(log|debug)|debugger|print\(|var_dump|dd\("
if git diff --cached | grep -qE "$DEBUG_PATTERNS"; then
echo -e "${RED}错误: 发现调试代码${NC}"
exit 1
fi
# 2. 检查代码风格
echo "检查代码风格..."
JS_FILES=$(echo "$STAGED_FILES" | grep -E '\.(js|ts|jsx|tsx)$' || true)
if [ -n "$JS_FILES" ]; then
npx eslint $JS_FILES --max-warnings=0 --quiet
fi
# 3. 检查敏感信息
echo "检查敏感信息..."
SENSITIVE_PATTERNS="password|secret|api_key|token"
if git diff --cached | grep -qiE "$SENSITIVE_PATTERNS"; then
echo -e "${YELLOW}警告: 可能包含敏感信息${NC}"
fi
echo -e "${GREEN}所有检查通过!${NC}"
exit 0总结
| 钩子 | 用途 | 推荐工具 |
|---|---|---|
| pre-commit | 代码检查、格式化 | Husky + lint-staged |
| commit-msg | 提交信息验证 | Husky + commitlint |
| pre-push | 运行测试、保护分支 | Husky |
| post-merge | 更新依赖 | 原生脚本 |
| post-checkout | 分支切换后处理 | 原生脚本 |
最佳实践:
- 使用工具管理钩子,避免手动配置
- 只检查暂存的文件,提高效率
- 提供清晰的错误信息和修复建议
- 允许通过
--no-verify跳过(紧急情况)
