| name | git-workflow-and-versioning |
| description | 结构化 Git 工作流实践。在进行任何代码变更时使用。在提交、创建分支、解决冲突,或需要跨多个并行流组织工作时使用。在进行版本发布、选择语义化版本号升级、打 Tag 或编写 Changelog 时使用。 |
Git 工作流与版本管理
概述
Git 是你的安全网。将提交视为保存点,将分支视为沙箱,将历史记录视为文档。随着 AI 智能体以高速生成代码,纪律严明的版本控制是保持变更可管理、可审查和可逆的机制。
何时使用
始终使用。每个代码变更都流经 Git。
核心原则
基于主干的开发(推荐)
保持 main 始终可部署。在 1-3 天内合并回主干的短期功能分支中工作。长期开发分支是隐性成本——它们会分叉、产生合并冲突并延迟集成。DORA 研究一致表明,基于主干的开发与高绩效工程团队相关。
main ──●──●──●──●──●──●──●──●──●── (始终可部署)
╲ ╱ ╲ ╱
●──●─╱ ●──╱ ← 短期功能分支(1-3 天)
这是推荐的默认策略。使用 gitflow 或长期分支的团队可以将这些原则(原子提交、小变更、描述性消息)适应到他们的分支模型中——提交纪律比具体的分支策略更重要。
- Dev 分支是成本。 分支存在的每一天都在累积合并风险。
- 发布分支是可接受的。 当你需要在 main 继续前进时稳定一个发布版本。
- 功能标志 > 长期分支。 优先将未完成的工作部署在标志后面,而不是在分支上保留数周。
1. 尽早提交、经常提交
每个成功的增量都应得到自己的提交。不要累积大量未提交的变更。
工作模式:
实现切片 → 测试 → 验证 → 提交 → 下一个切片
不要这样:
全部实现 → 希望它能工作 → 巨型提交
提交就是保存点。如果下一个变更破坏了什么,你可以立即恢复到上一个已知良好的状态。
2. 原子提交
每个提交做一件逻辑上自包含的事情:
# 好:每个提交是自包含的
git log --oneline
a1b2c3d 添加带验证的 RPC 创建端点
d4e5f6g 添加副本追赶调度器
h7i8j9k 为副本同步添加集成测试
m1n2o3p 添加请求校验(单元 + 集成)
# 坏:所有内容混在一起
git log --oneline
x1y2z3a 添加任务功能,修复侧边栏,更新依赖,重构工具函数
3. 描述性消息
提交消息解释为什么,而不仅仅是是什么:
# 好:解释意图
feat: 为注册端点添加邮箱验证
防止无效的邮箱格式进入数据库。
在协议处理层使用 protobuf 校验逻辑,
与 auth/handler.go 中现有的验证模式保持一致。
# 坏:描述从 diff 中显而易见的内容
更新 auth/handler.go
格式:
<类型>: <简短描述>
<可选正文,解释为什么,而非是什么>
类型:
feat — 新功能
fix — 缺陷修复
refactor — 既不修复缺陷也不添加功能的代码变更
test — 添加或更新测试
docs — 仅文档
chore — 工具、依赖项、配置
4. 保持关注点分离
不要将格式变更与行为变更混合。不要将重构与功能混合。每种类型的变更应该是单独的提交——理想情况下是单独的 PR:
# 好:分离关注点
git commit -m "refactor: 将验证逻辑提取为共享工具"
git commit -m "feat: 为注册添加手机号验证"
# 坏:混合关注点
git commit -m "重构验证并添加手机号字段"
将重构与功能工作分开。 重构变更和功能变更是两个不同的变更——分开提交。这使得每个变更更容易审查、回滚和在历史记录中理解。小型清理(重命名一个变量)可以根据审查者的判断包含在功能提交中。
5. 控制变更规模
目标每个提交/PR 约 100 行。超过约 1000 行的变更应该拆分。参见 code-review-and-quality 中的拆分策略,了解如何分解大变更。
~100 行 → 易于审查,易于回滚
~300 行 → 单个逻辑变更可接受
~1000 行 → 拆分为更小的变更
分支策略
功能分支
main(始终可部署)
│
├── feature/task-creation ← 每个功能一个分支
├── feature/user-settings ← 并行工作
└── fix/duplicate-tasks ← 缺陷修复
- 从
main(或团队的默认分支)创建分支
- 保持分支短期存在(1-3 天内合并)——长期分支是隐性成本
- 合并后删除分支
- 对于未完成的功能,优先使用功能标志而非长期分支
分支命名
feature/<简短描述> → feature/task-creation
fix/<简短描述> → fix/duplicate-tasks
chore/<简短描述> → chore/update-deps
refactor/<简短描述> → refactor/auth-module
使用 Worktree
对于并行的 AI 智能体工作,使用 git worktree 同时运行多个分支:
git worktree add ../project-feature-a feature/task-creation
git worktree add ../project-feature-b feature/user-settings
ls ../
project/ ← main 分支
project-feature-a/ ← task-creation 分支
project-feature-b/ ← user-settings 分支
git worktree remove ../project-feature-a
优势:
- 多个智能体可以同时在各自不同的功能上工作
- 不需要切换分支(每个目录有自己的分支)
- 如果一个实验失败,删除工作树——不会丢失任何东西
- 变更在被显式合并之前保持隔离
保存点模式
智能体开始工作
│
├── 进行一个变更
│ ├── 测试通过?→ 提交 → 继续
│ └── 测试失败?→ 回滚到上一个提交 → 调查
│
├── 进行另一个变更
│ ├── 测试通过?→ 提交 → 继续
│ └── 测试失败?→ 回滚到上一个提交 → 调查
│
└── 功能完成 → 所有提交形成一个干净的历史记录
这种模式意味着你永远不会丢失超过一个增量的工作。如果智能体偏离了轨道,git reset --hard HEAD 将你带回上一个成功的状态。
变更摘要
在任何修改之后,提供一个结构化的摘要。这使审查更容易,记录范围纪律,并暴露意外的变更:
已进行的变更:
- internal/handler/task.go:为 Create RPC 添加了请求校验
- internal/validation/task.go:使用 protobuf 定义添加了 TaskCreateValidator
我有意不触碰的内容:
- internal/handler/auth.go:有类似的验证缺口,但在范围之外
- internal/middleware/error.go:错误格式可以改进(单独的任务)
潜在担忧:
- 校验逻辑是严格的——拒绝额外字段。确认这是预期的。
- 添加了 validator 作为依赖项——已存在于 go.mod 中
此模式能早期捕获错误假设,并为审查者提供变更的清晰地图。"不触碰的内容"部分特别重要——它表明你遵守了范围纪律,没有进行未经请求的翻新。
提交前检查
每次提交之前:
git diff --staged
git diff --staged | grep -i "password\|secret\|api_key\|token"
go test ./... -race
golangci-lint run
go vet ./...
使用 Git Hook 自动化此流程:
.PHONY: pre-commit
pre-commit: test lint vet
test:
go test ./... -race
lint:
golangci-lint run
vet:
go vet ./...
处理生成的文件
- 提交生成的文件仅当项目期望它们时(例如
go.sum、protobuf 生成的桩代码)
- 不要提交构建输出(
target/、dist/)、环境文件(.env)或 IDE 配置(.vscode/settings.json 除非共享)
- 有一个
.gitignore,涵盖:target/、build/、.env、*.pem、*.o、*.so
使用 Git 进行调试
git bisect start
git bisect bad HEAD
git bisect good <已知良好的提交>
git log --oneline -20
git diff HEAD~5..HEAD -- src/
git blame internal/service/task.go
git log --grep="validation" --oneline
发布与版本管理
提交是你追踪变更的方式;版本是消费者追踪变更的方式。一旦有任何东西依赖你的代码——另一个团队、一个发布的包、一个部署的客户端——"main 上的最新版"就不再是"我在运行什么,升级安全吗?"的充分回答。版本号和 Changelog 是回答这个问题的契约。
语义化版本管理
对于任何有消费者的东西,版本 MAJOR.MINOR.PATCH 并让数字传达意义:
MAJOR 破坏性变更——消费者必须修改其代码才能升级
MINOR 新功能,向后兼容——升级安全
PATCH 缺陷修复,向后兼容——升级安全
数字是一份承诺,所以让代码与之匹配。一个改变消费者依赖行为的"补丁"版本是伪装的主版本(Hyrum 定律——参见 api-and-interface-design 技能)。当不确定一个变更是否是破坏性的时,假设它是;一个意外的主版本远比一个损坏的消费者便宜。
为发布打 Tag,让 Tag 成为真相的来源
发布是历史中不可变的点,而非移动的分支。打 Tag 以便它始终可以被复现:
git tag -a v1.4.0 -m "Release 1.4.0"
git push origin v1.4.0
从 Tag 派生版本号,而非在分散的文件中手动编辑它,这样构建产物、Tag 和 Changelog 永远不会不一致。
编写面向人类的 Changelog
Changelog 不是 git log。它是经过策划的、面向消费者的对"什么变了以及我在乎吗?"的回答——按 新增 / 变更 / 修复 / 弃用 / 移除 / 安全 分组,最新的在最上面,每个条目围绕用户影响来表述,而非内部机制。
## [1.4.0] - 2025-06-12
### 新增
- 通过 CSV 批量导入任务
### 修复
- 周期性任务截止日期的时区偏移
### 弃用
- `GET /v1/tasks/all` ——使用分页的 `GET /v1/tasks`(2.0 中移除)
在产生变更的同一个变更中编写条目,趁影响还很新鲜时——而非在发布时从提交考古学中重建。破坏性变更需要迁移说明和弃用窗口期(遵循 deprecation-and-migration 技能);实际发布是 shipping-and-launch 技能的工作——本节是输入该技能的版本化契约。
常见合理化借口
| 合理化借口 | 现实 |
|---|
| "功能完成时我再提交" | 一个巨型提交无法审查、调试或回滚。每个切片都提交。 |
| "消息不重要" | 消息就是文档。未来的你(和未来的智能体)需要理解什么变了以及为什么。 |
| "我以后压缩一下就行" | 压缩破坏了开发叙事。从一开始就优先使用干净的增量提交。 |
| "分支增加了开销" | 短期分支是免费的,可以防止冲突的工作碰撞。长期分支才是问题——在 1-3 天内合并。 |
| "我稍后再拆分这个变更" | 大变更更难审查、部署风险更高、更难回滚。在提交前拆分,而非之后。 |
| "我不需要 .gitignore" | 直到带有生产机密的 .env 被提交了。立即设置它。 |
| "这只是个小修复,升级补丁版本就行" | 检查消费者能观察到什么。他们依赖的行为变化是主版本,无论 diff 大小如何。 |
| "Changelog 就是提交日志" | 提交是给你的;Changelog 是给消费者的,按影响策划。从原始提交生成会掩盖重要的东西。 |
| "我们在发布时再写 Changelog" | 到那时影响是从记忆中重建的,一半已经遗漏。在变更时编写条目。 |
红旗警告
- 大量未提交的变更积累
- 提交消息如"fix"、"update"、"misc"
- 格式变更与行为变更混合
- 项目中没有
.gitignore
- 提交了
target/、vendor/、.env 或构建产物
- 与 main 显著偏离的长期分支
- 对共享分支进行 force-push
- 在次版本或补丁版本号升级中发布破坏性变更
- 没有 Tag 的发布,或手动编辑的版本号与 Tag 不同步
- 没有 Changelog 条目的面向用户发布,或 Changelog 只是倾倒的提交消息
验证
对于每个提交:
对于每个发布(任何有消费者的东西):