提交前自动检查最直接的方式是在 .git/hooks 下放一个可执行的 pre-commit 脚本。它适合在本地 commit 动作发生前拦截代码格式、语法错误或调试残留。不过它并不是一条放之四海皆准的规则,具体怎么落地,取决于你的项目规模、团队协作方式以及现有 CI 流程。
先判断项目形态和 hook 的生效范围
配置 Git hooks 之前,首先要确认你的项目是独立仓库还是 monorepo 的一部分,因为 hooks 存放在每个仓库各自的 .git/hooks 目录下,并不会随分支或远端自动同步。如果团队里有新人克隆仓库,他本地并不会自动拥有你写好的 hook 脚本。一个常用的判断条件是检查 .git/hooks 是否存在且可写,以及当前用户是否有执行脚本的权限。另外,要确定你希望拦截的是所有提交还是只针对特定文件路径,因为 pre-commit 阶段可以读取暂存区内容,但默认情况下它不会自动过滤未暂存的改动。
这段话里最关键的两个信息是:hook 不会随仓库自动同步,以及 pre-commit 的输入是暂存区。如果你在一个独立仓库里做个人项目,直接改 .git/hooks/pre-commit 就够用;如果是多人协作的 monorepo,就需要把脚本纳入版本库,并提供一个安装脚本,把 hook 复制到每个成员的本地目录。另外,建议先确认你的 Git 版本,有些旧版本对 hooks 路径和权限处理可能有细微差别。
在 .git/hooks 下写一个最小脚本
推荐使用 pre-commit 框架或自定义脚本,但自定义脚本时建议先用一个简单的 shell 示例验证流程。在 .git/hooks/pre-commit 文件中,先获取暂存区文件列表:git diff --cached --name-only --diff-filter=ACM。然后针对这些文件依次执行检查,比如运行 eslint 或 gofmt。如果检查失败,输出明确的错误信息,并用 exit 1 终止提交。注意不要直接对工作区所有文件做检查,因为那会漏掉部分未暂存改动,也会产生误报。脚本务必赋予可执行权限,否则会被 Git 静默忽略。
下面是一个可以拿来改的示例,它先筛选出暂存区待检查的文件,再交给 eslint:
#!/bin/sh
set -euo pipefail
files=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$files" ]; then
exit 0
fi
# 只检查 JS/TS 文件,按需调整扩展名
echo "$files" | grep -E '\.(js|jsx|ts|tsx)$' | xargs npx eslint
写完记得执行 chmod +x .git/hooks/pre-commit。如果你用的是 gofmt,可以把最后一行换成 echo "$files" | xargs gofmt -l,然后用 test -z "$(gofmt -l $files)" 这类写法判断是否有需要格式化的文件。这里的检查命令要和你的项目工具链匹配,比如 Python 项目可能用 ruff 或 black,需要结合环境确认。
用一个故意失败的提交验证 hook
写好的 hook 可以通过一个最简单的测试来验证:故意修改一个文件并暂存,然后执行 git commit。正常情况下,如果设置了错误检查,提交会被拒绝,同时终端会打印你自定义的错误提示。要确认 hook 是否真的生效,可以在 hook 里临时加一行 echo 调试信息,再执行提交。若没有输出,先检查文件是否以 shebang 开头(如 #!/bin/sh),以及文件是否具有执行权限。另外,注意 Windows 环境下换行符可能是 CRLF,这会导致脚本解析异常,建议统一为 LF 并在 .gitattributes 中显式指定。
这个验证思路的核心是“故意制造一个失败”。比如你可以在一个 JS 文件里写一行分号后面再加空格,然后 git add 之后提交,观察 hook 是否拦截。如果脚本直接运行没有输出,也可以先单独执行 bash .git/hooks/pre-commit 看有没有语法错误,再检查 .git/hooks 目录的权限和 Git 的 core.hooksPath 是否被改动过。在 Windows 环境下,如果脚本一直不执行,优先确认换行符是否被转成 CRLF。
容易忽略的边界和坑
pre-commit 并不是一个强制机制,它有明显的边界。第一个坑是脚本里没有设置 set -e,导致某个检查命令失败了,脚本仍然继续执行,最终以 exit 0 结束,提交被放行。建议在脚本开头加上 set -euo pipefail,并在每条检查命令后面显式检查退出码。第二个坑是检查范围没有限制在暂存区,而是对整个项目跑格式化或 lint,这样会误伤未暂存的改动,甚至可能自动修改工作区文件。所以前面提到的 --diff-filter=ACM 不能去掉。第三个坑是 Git LFS 或一些工具可能已经占用了同一个 hook 文件,直接覆盖会破坏原有逻辑,需要先备份,再在自己的脚本末尾调用原有 hook。
团队协作时还需要注意:core.hooksPath 会把 hooks 目录整体指向另一个路径,原有的 sample 文件不会生效。如果你用这个配置来分发脚本,要确保那个路径下有完整的 hook 集合。另外,pre-commit 只拦本地 commit,拦不住 push,如果要在 push 前做检查,需要额外写 pre-push hook 或在 CI 流水线里加一步。某些 GUI 客户端在提交时可能不会执行本地 hooks,这一点要在团队文档里写清楚,避免有人绕过检查却不知情。
只要把“判断范围、写脚本、验证失败、检查边界”这四步走完,一个提交前自动检查的 hook 就能在你的项目里稳定工作。后续要扩展更多检查项,只需要维护脚本和安装流程。