这是 Git 手册系列的最后一篇。前三篇解决了"会用 → 用得对 → 懂原理",本篇解决最后一件事:让 Git 在团队和工程体系里成为可靠的基础设施,并在灾难发生时救得回来。
目录
- Monorepo vs Polyrepo:仓库拓扑决策
- 大型仓库的性能优化
- 提交规范:Conventional Commits
- 签名提交与提交者验证
- 密钥防护:从预防到泄漏处置
- CI/CD 中的 Git 技巧
- 灾难恢复 SOP
- 一套可直接落地的团队 Git 规范
- 终极速查表
Monorepo vs Polyrepo:仓库拓扑决策
| Monorepo(一个仓库存所有项目) | Polyrepo(每项目一个仓库) | |
|---|---|---|
| 跨项目改动 | 一次提交原子完成 | 要发版、升依赖、多 PR 联动 |
| 代码共享 | 直接 import | 发包 / submodule |
| CI | 需要增量构建,配置复杂 | 简单直接 |
| 权限边界 | 靠 CODEOWNERS,粒度粗 | 天然隔离 |
| 克隆体积 | 大(可用 sparse checkout 缓解) | 小 |
| 代表 | Google、Meta、Vue、React 生态 | 多数中小团队 |
决策建议:
- 项目间强耦合、频繁联动发布(如前端组件库 + 应用)→ monorepo
- 团队/项目独立、发布节奏不同、需要强权限边界 → polyrepo
- 不确定就先 polyrepo;拆容易,合回去很难
monorepo 配套工具:Nx、Turborepo、Bazel(构建缓存与增量)、changesets(多包版本管理)。
大型仓库的性能优化
仓库几万提交、几百 MB 之后,这些手段能救体验:
# 浅克隆:只拿最近的历史(CI 必备)
git clone --depth 1 <url>
# 部分克隆:不下载全部 blob,按需懒加载(本地开发大仓库神器)
git clone --filter=blob:none <url>
# 稀疏检出:只 checkout 需要的目录
git sparse-checkout init --cone
git sparse-checkout set apps/web packages/ui
# 让 status / diff 在大仓库里更快
git config core.fsmonitor true
git config core.untrackedCache true
git maintenance start # 后台自动做 gc、打包、提交图等维护
提交图(commit-graph)单独提一句:它让 git log --graph、合并基计算在大仓库里快一个数量级,git maintenance start 会自动开启。
提交规范:Conventional Commits
格式:
<type>(<scope>): <subject>
<body>
<footer>
常用 type:
| type | 含义 | 影响版本 |
|---|---|---|
feat | 新功能 | 次版本 +1 |
fix | 修 bug | 修订号 +1 |
perf | 性能优化 | 修订号 +1 |
refactor | 重构(不改行为) | 不升 |
docs / style / test / chore / ci / build | 文档 / 格式 / 测试 / 杂务 | 不升 |
带 ! 或 footer 写 BREAKING CHANGE: | 破坏性变更 | 主版本 +1 |
示例:
feat(auth): 支持手机号验证码登录
- 新增 /auth/sms/send 与 /auth/sms/verify 接口
- 验证码 5 分钟有效,限频 1 条/分钟
Closes #128
为什么值得推行:版本号和 CHANGELOG 可以全自动生成。配套工具链:
# commitizen:交互式生成规范提交信息
pnpm add -D commitizen cz-conventional-changelog
# commitlint + husky:不合规的提交直接拒绝
pnpm add -D @commitlint/cli @commitlint/config-conventional
echo "npx commitlint --edit \$1" > .husky/commit-msg
# semantic-release / release-please:按提交记录自动升版本、发 CHANGELOG
签名提交与提交者验证
Git 的 user.name / user.email 可以随便填——git commit --author="Linus <[email protected]>" 就能伪装任何人。要证明"这个提交真的是我提交的",用签名:
SSH 签名(推荐,零门槛复用现有密钥)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true # 之后每次提交自动签名
GitHub 设置里把同一个公钥添加为 Signing Key,你的提交就会显示 Verified 徽章。
验证别人的提交
git log --show-signature # 查看签名验证结果
git merge --verify-signatures main
为什么这很重要
- 供应链安全:防止有人伪装核心维护者提交恶意代码
- 审计合规:金融、政企场景要求提交可追溯真人
- 配合分支保护规则"要求签名提交",从平台侧强制
密钥防护:从预防到泄漏处置
密钥进仓库是最高频的安全事故。三层防御:
第一层:本地预防
# gitleaks:提交/推送前扫描
brew install gitleaks
gitleaks git --pre-commit --staged --verbose # 接入 husky pre-commit
.gitignore 里永远有:.env、*.pem、*.key、credentials*。
第二层:服务端拦截
- GitHub:开启 Push Protection(推送时扫描,疑似密钥直接拒绝)
- GitLab:开启 Secret Detection
- 自建:pre-receive hook 跑 gitleaks
第三层:泄漏后的处置 SOP
密钥一旦进过任何一次提交,就视为已泄漏,即使后来的提交删了它:
- 立即吊销 / 轮换密钥(这是唯一有效的补救,改历史只是清理痕迹)
- 用
git filter-repo从全历史删除该文件(见高级篇) - 全员重新克隆
- 排查密钥使用日志,评估影响面
- 复盘:为什么绕过了第一层和第二层
CI/CD 中的 Git 技巧
浅克隆 + 按需加深
# GitHub Actions
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 需要 tag / 全历史(如算版本号)时才用 0,否则保持默认 1
只跑受影响范围的测试
# 与合并基比较,得到改动文件列表
git diff --name-only origin/main...HEAD | grep -q '^apps/web/' && run_web_tests
版本号来自 Git
git describe --tags --always --dirty
# v1.2.0-5-ga1b2c3d → 距 v1.2.0 之后第 5 个提交,哈希 a1b2c3d
这是给构建产物打版本标识的零维护方案,前端塞进 __VERSION__、后端塞进 /healthz 返回值。
自动打 tag / 生成 CHANGELOG
release-please 或 semantic-release 会读取 Conventional Commits,自动算出版本号、生成 CHANGELOG、创建 release PR 和 tag。从此"发版"不是一个手工动作。
保护分支 + 必需检查
平台侧配置(GitHub Branch Protection / GitLab Protected Branches):
- 禁止直接 push main,必须走 PR
- 必须 ≥1 人 approve
- 必须通过 CI 检查
- 禁止 force push 和删除
灾难恢复 SOP
场景 1:误删了远程分支
# 任何拉过该分支的人本地都有完整历史
git switch -c feature/xxx <已知的最新提交id 或 reflog 里找>
git push -u origin feature/xxx
场景 2:对 main 执行了 force push,覆盖掉别人的提交
# 找到被覆盖前的 main 位置(任何同事本地、CI 缓存、或平台 Events API)
git reflog show main # 在还没 pull 的机器上
git push --force origin <旧提交id>:main
教训:在平台设置里禁掉 main 的 force push,从制度上消灭这个场景。
场景 3:仓库本地损坏(对象丢失、索引损坏)
git fsck --full # 诊断
git clone <remote-url> rescue # 最省事:远程还在就直接重克隆
# 远程也没了但同事机器上有:任何人的 clone 都是完整备份
场景 4:rebase / amend 错了,想回到操作前
git reflog # 找到 rebase 之前的 HEAD 位置
git reset --hard HEAD@{5}
场景 5:整个 .git 目录被删了
未推送的提交彻底丢失——这就是为什么"每天至少 push 一次"写进了初级篇。能做的只有从远程重新克隆,工作区文件还在的话手动对比恢复。
通用原则:Git 里 99% 的"丢失"都能用 git reflog + git fsck 找回;剩下 1% 是"从未 commit / 从未 push",只能靠备份习惯预防。
一套可直接落地的团队 Git 规范
以下是本篇的浓缩,可以直接贴进团队 Wiki:
分支
main永远可发布,受保护,禁止直推、禁止 force push- 功能分支命名:
<type>/<简述>,如feat/sms-login、fix/pay-timeout - 功能分支活不过 3 天,合并后删除
提交
- 遵循 Conventional Commits,commitlint 强制
- 一次提交做一件事;
wip、tmp类提交合并前必须 squash - 开启 SSH 提交签名
同步
- 每天开工先
git rebase origin/main(功能分支上) - 每天至少 push 一次
- 公共分支只用 merge,功能分支同步用 rebase,永不 rebase 公共历史
评审
- 一切代码经 PR 合入 main,CI 通过 + 至少 1 人 approve
- PR 保持小:≤400 行变更为佳,大了拆
发布
- tag 遵循 semver,由 release-please 自动生成
- 线上 bug 走
hotfix/*分支,修完同时回合 main
安全
- pre-commit 跑 gitleaks,服务端开 Push Protection
- 密钥泄漏 = 立即轮换,其次才是清历史
终极速查表
| 场景 | 方案 |
|---|---|
| 大仓库克隆 | --filter=blob:none + sparse-checkout |
| 提交规范 | Conventional Commits + commitlint |
| 自动发版 | release-please / semantic-release |
| 构建版本号 | git describe --tags --always --dirty |
| 提交防伪 | SSH 签名 + 平台 Verified |
| 密钥扫描 | gitleaks(本地)+ Push Protection(服务端) |
| 密钥泄漏 | 轮换密钥 → filter-repo 清历史 → 全员重克隆 |
| 误删/覆盖找回 | git reflog → reset --hard / 强推恢复 |
| 仓库损坏 | git fsck 诊断,重新克隆 |
| 协作红线 | 保护 main、禁 force push、一切走 PR |
系列回顾
- 初级篇:四个区域、三板斧、远程同步——能用
- 中级篇:分支模型、merge/rebase、reflog——用得对
- 高级篇:对象模型、bisect、worktree、submodule、LFS——懂原理
- 终极篇:工程化、安全、SOP、团队规范——成体系
Git 的学习曲线不在命令数量,而在心智模型。四篇读完,剩下的就是每天在真实项目里踩坑、翻 reflog、读 graph——祝你的每一次 git push 都心安理得。