Git 事件钩子 提交前、推送前以及在大门处阻止不良代码
预提交、提交信息和预推送钩子是shell脚本,会在git写入提交或发送推送之前运行。以下是将它们配置为捕获代码检查失败、不良的提交信息和泄露的密钥的详细方法——附有您可以立即使用的实际示例。
Git 内置了钩子——在工作流程中的特定阶段触发的 Shell 脚本。大多数仓库都拥有这些脚本,但它们处于休眠状态。 .git/hooks/ 作为 .sample 文件。大多数开发者都忽视它们,直到出现错误提交或泄露的 API 密钥,才后悔当初没有关注。
本文涵盖每个项目中值得配置的三个钩子: pre-commit, commit-msg,并且 pre-push。每个钩子都能拦截一类错误。每个钩子都是一个你可以立即使用的 Shell 脚本。
钩子的位置
每个 Git 仓库都有一个 .git/hooks/ 目录。运行 ls .git/hooks/ 后,你会看到示例文件。Git 忽略所有以 .sample 结尾的文件。要激活一个钩子,请移除该后缀并使其可执行:
cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
约定很简单:退出码为 0,Git 继续执行;退出非零码,Git 中止,并打印你写入标准错误的内容。
pre-commit:你最值得使用的钩子
pre-commit 在你输入 git commit 之后、提交对象被写入之前运行。它无法看到提交信息——因为提交信息尚未被写入。它所做的就是检查已暂存的文件,如果发现异常则中止。 能 关键细节:使用
来获取你实际暂存的文件。每次提交都对整个项目进行检查会很慢,并且会暴露你未引入的问题。 git diff --cached --name-only lint 示例(ESLint)
标志会跳过已删除的文件——没有意义去检查你刚刚删除的内容。
#!/bin/sh
# Lint only staged JS files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.js$')
if [ -z "$STAGED_FILES" ]; then
exit 0
fi
echo "Running ESLint on staged files..."
echo "$STAGED_FILES" | xargs ./node_modules/.bin/eslint
if [ $? -ne 0 ]; then
echo "ESLint failed. Fix errors before committing."
exit 1
fi
exit 0
这 --diff-filter=ACM 秘密扫描示例
一个直接的模式扫描,用于发现明显的错误——如硬编码的 API 密钥、配置文件中意外提交的密码:
对于超出玩具项目的场景,应搭配专用扫描工具使用。
#!/bin/sh
# Block commits with obvious secrets
PATTERNS="(AWS_SECRET|api_key\s*=|password\s*=|PRIVATE KEY)"
if git diff --cached | grep -qiP "$PATTERNS"; then
echo "Potential secret detected in staged changes. Aborting commit."
exit 1
fi
exit 0
detect-secrets 来自 Yelp 的工具非常适合作为 pre-commit 钩子——它维护一个基准文件,以避免现有标记字符串阻止每次提交。 trufflehog 更适合用于事后扫描历史或在 CI 中运行。 commit-msg:强制消息格式
此钩子接收一个参数:一个包含你草稿提交信息的临时文件路径。读取该文件,验证其内容,若无效则退出 1 以拒绝。该文件是可写的——你可以选择规范化消息而不是拒绝,尽管这在第一次使用时会让人感到意外。
强制执行
常规提交格式 是最常见的用途。其回报是自动生成的变更日志、可读的输出,以及能够解析提交类型以决定发布内容的 CI 流程: 这无法检测到格式正确但毫无意义的提交信息,例如 git log 。这是一个文化问题,而非工具问题。
#!/bin/sh
COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# type(scope): description — scope is optional
PATTERN="^(feat|fix|chore|docs|style|refactor|test|perf|ci|build|revert)(\(.+\))?: .{1,72}"
if ! echo "$COMMIT_MSG" | grep -qP "$PATTERN"; then
echo ""
echo "Invalid commit message. Use Conventional Commits format:"
echo " type(scope): short description"
echo ""
echo "Valid types: feat, fix, chore, docs, style, refactor, test, perf, ci, build, revert"
echo "Example: feat(auth): add OAuth2 login flow"
echo ""
echo "Your message: $COMMIT_MSG"
exit 1
fi
exit 0
pre-push:在推送至远程仓库前的最后一道防线 fix: fix thingspre-push 在调用
之后、任何数据发送到远程之前运行。它通过标准输入接收远程名称和 URL。这里应运行实际测试——而不是一个检查器(在 pre-commit 中运行),而是验证行为的真正测试。
一个残酷的事实:如果你的测试套件耗时超过 60 到 90 秒,人们就会使用 git push 。这里只运行快速的单元测试。集成测试和端到端测试应放在 CI 中,因为慢是可接受的。一个慢到被绕过的钩子,比没有钩子更糟糕。
#!/bin/sh
echo "Running tests before push..."
npm test
if [ $? -ne 0 ]; then
echo "Tests failed. Push aborted."
exit 1
fi
exit 0
绕过钩子——以及何时可以这样做 --no-verify这两个标志存在是有原因的。一个故障的钩子会阻止热修复,这种事会令整个团队对 Git 钩子这一概念产生抵触。当你有正当理由时使用
:例如钩子在无关文件上误触发,或紧急情况下修复比门禁更重要。
git commit --no-verify
git push --no-verify
如果你频繁使用它,说明钩子需要调整。最常见的原因包括:运行过慢、在未更改的文件上失败,或模式匹配的误报你从未清理过。 --no-verify 与团队共享钩子
是每个克隆本地的——它被有意地不提交到仓库中。让钩子在团队中持续生效的两种方法:
方法 1:core.hooksPath
.git/hooks/ 将钩子存储在一个已提交的目录中(例如
),然后指向 Git 的该路径:
为了在新克隆时自动配置,添加一个 .githooks/脚本到
git config core.hooksPath .githooks
——npm 会在 prepare 时自动运行它。 package.json 确保脚本可执行,将其提交到仓库,任何克隆并运行 npm install:
{
"scripts": {
"prepare": "git config core.hooksPath .githooks"
}
}
的人都会自动获得配置好的钩子。 npm install 方法 2:Husky
Husky
是 JavaScript/Node 项目中的标准选择。它为你处理钩子的配置,并为每个钩子提供一个独立的文件在 中。 core.hooksPath 然后以纯 Shell 脚本的形式添加钩子: .husky/:
npx husky init
Husky 9(2024 年初发布)完全弃用了 JSON 配置,转而使用纯 Shell 脚本。对于新设置而言,比 v4/v8 更简单,但从 Husky 4 升级是一个破坏性变更,官方迁移指南低估了这一点——旧的
echo "npm test" > .husky/pre-push
chmod +x .husky/pre-push
格式已完全不再有效。如果你正在升级现有仓库,需要预留时间。 .huskyrc 非 JavaScript 项目更适合使用
方法。没有必要为了钩子管理而引入 Node 依赖。 core.hooksPath 在钩子运行前审查你的差异
在终端中显示暂存更改。当你在比较两个版本的配置文件或检查重构实际改变了哪些内容时,视觉差异更易于快速扫描。
git diff --cached IO Tools’ 文本差异工具 让你可以粘贴两个版本并查看具体变化——在提交前审计即将进入的更改时非常有用。 Git 钩子是少数完全嵌入现有工作流程的自动化工具之一——无需 CI 账户、无需额外服务、无需成本。一个在 pre-commit 钩子中拒绝 lint 失败的钩子,其可靠性与你编写的 Shell 脚本完全一致。这是一个优点:你可以立即阅读、调试和修改它。配置上述三个钩子,你就能填补“本地能运行”与“主分支能运行”之间的最大差距。
Git 钩子:pre-commit、pre-push 和在门口阻止坏代码 2
