pre-push钩子须置于.git/hooks/pre-push,无后缀且有执行权限;go程序需读stdin获取ref信息,每行四字段,remote sha为全0表示删除。

pre-push hook 文件该放哪、怎么命名才生效
Git 的 pre-push 钩子必须放在仓库根目录下的 .git/hooks/pre-push,且文件需具备可执行权限(chmod +x .git/hooks/pre-push)。它不是 Go 源文件,而是一个 shell 脚本——哪怕你用 Go 写逻辑,也得靠它来触发。
常见错误是把 pre-push.go 直接丢进 .git/hooks/,Git 完全不会执行;或者写成 pre-push.sh 但没加执行权限,push 时静默失败,还以为检查没跑。
- 路径必须是
.git/hooks/pre-push(不能带后缀) - 内容第一行推荐写
#!/bin/sh,确保跨平台基础兼容 - 如果用 Go 编译成二进制,比如叫
gopushcheck,脚本里就调./gopushcheck "$@",别漏传参数
Go 程序怎么拿到即将推送的 refs 和远程信息
pre-push 脚本运行时,Git 会通过 stdin 把要推送的 ref 列表传进来,每行格式是:<local ref><local sha1><remote ref><remote sha1></remote></remote></local></local>。Go 程序不能靠 os.Args 拿到这些——必须读 stdin。
容易忽略的是:stdin 可能为空(比如 git push --delete),或含多行(多个分支推送),直接 bufio.NewReader(os.Stdin).ReadString('\n') 只读一行就漏检。
- 用
scanner := bufio.NewScanner(os.Stdin)循环扫描所有行 - 每行用
strings.Fields(line)拆,至少得有 4 个字段才合法 - 注意
remote sha1是0000000...表示删除操作,不是新提交 - 示例片段:
for scanner.Scan() { fields := strings.Fields(scanner.Text()) if len(fields)
为什么 go test / go vet 在 pre-push 里常“不报错却跳过”
根本原因是工作目录(pwd)在 pre-push 脚本中默认是 Git 根,但 Go 命令依赖 go.mod 位置。如果项目用了子模块、或你在子目录下执行 git push,go test 可能找不到模块,静默退出码 0,看起来像通过了。
另一个坑是超时:CI 里能跑 30 秒的测试,在本地 pre-push 里卡住会让用户以为 push 卡死,实际是 Go 测试夯在某个 goroutine 里没设 timeout。
- 在 Go 检查程序开头加
if _, err := os.Stat("go.mod"); os.IsNotExist(err) { os.Exit(1) } - 给
go test加-timeout=15s,避免阻塞 push 流程 - 不要用
go run main.go启动检查——每次编译慢,改一次 hook 就要等;编译好再调用二进制更稳 - stderr 输出会被 Git 捕获并显示在终端,但 stdout 默认不显示,调试时记得用
log.Printf打日志到 stderr
Go 检查失败时怎么让 push 中断并显示清晰错误
Git 要求 pre-push 脚本退出码非 0 才中止推送,但很多 Go 程序只用 log.Fatal 或 panic,结果输出混乱、没换行、甚至被截断。用户看到一串红字但不知道哪条是关键错误。
更隐蔽的问题是:Go 程序 exit 1 后,shell 脚本如果没设 set -e,后续命令仍会执行,导致“检查失败但还是推上去了”。
- Go 中明确用
os.Exit(1),别依赖 panic - 错误信息开头加
[pre-push-check]前缀,方便一眼识别来源 - shell 脚本里写
set -e,并在调用 Go 程序后加|| exit 1双保险 - 示例结尾:
./gopushcheck "$@" || { echo "❌ pre-push-check failed — aborting push"; exit 1; }
GOPATH / GOROOT 环境变量是否一致——这些细节不显眼,但一出问题就难定位。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











