规范 Git 工作流实践。在进行任何代码更改时使用。在提交、分支、解决冲突或需要跨多个并行流组织工作时使用。在发布版本、选择语义版本号、打标签或编写变更日志时使用。
Git 工作流与版本管理
概述
Git 是你的安全网。将提交视为保存点,分支视为沙箱,历史视为文档。随着 AI 代理高速生成代码,有纪律的版本控制是让变更可管理、可审查、可回滚的机制。
何时使用
始终使用。每次代码变更都通过 Git 进行。
核心原则
基于主干开发(推荐)
保持 main 始终可部署。在短期特性分支上工作,并在 1-3 天内合并回主干。长期存在的开发分支是隐藏成本——它们会偏离、产生合并冲突并延迟集成。DORA 研究一致表明,基于主干开发与高效工程团队相关。
main ──●──●──●──●──●──●──●──●──●── (始终可部署)
╲ ╱ ╲ ╱
●──●─╱ ●──╱ ← 短期特性分支 (1-3 天)
这是推荐的默认方式。使用 GitFlow 或长期分支的团队可以调整原则(原子提交、小变更、描述性消息)以适应其分支模型——提交纪律比具体分支策略更重要。
- 开发分支是成本。 分支每存在一天,就积累合并风险。
- 发布分支是可接受的。 当需要在 main 前进的同时稳定发布时使用。
- 特性开关 > 长分支。 优先将未完成的功能部署在特性开关后,而不是在分支上保留数周。
1. 尽早提交,频繁提交
每个成功的增量都应有一个独立的提交。不要积累大量未提交的变更。
工作模式:
实现切片 → 测试 → 验证 → 提交 → 下一个切片
不要这样:
实现所有功能 → 希望它能工作 → 巨大提交
提交是保存点。如果下一个更改破坏了某些东西,你可以立即回滚到最后一个已知的良好状态。
2. 原子提交
每个提交只做一件事:
# 好:每个提交自包含
git log --oneline
a1b2c3d 添加任务创建端点及验证
d4e5f6g 添加任务创建表单组件
h7i8j9k 连接表单到 API 并添加加载状态
m1n2o3p 添加任务创建测试(单元 + 集成)
# 坏:所有内容混在一起
git log --oneline
x1y2z3a 添加任务功能,修复侧边栏,更新依赖,重构工具函数
3. 描述性消息
提交消息解释为什么,而不仅仅是什么:
# 好:解释意图
feat: 为注册端点添加邮箱验证
防止无效邮箱格式到达数据库。
在路由处理程序级别使用 Zod 模式验证,
与 auth.ts 中现有的验证模式一致。
# 坏:描述从 diff 中显而易见的内容
更新 auth.ts
格式:
<类型>: <简短描述>
<可选正文,解释为什么,而不是什么>
类型:
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
使用工作树
对于并行 AI 代理工作,使用 Git 工作树同时运行多个分支:
# 为特性分支创建工作树
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 将带你回到最后一个成功状态。
变更摘要
在任何修改之后,提供结构化的摘要。这使审查更容易,记录范围纪律,并暴露意外更改:
所做的更改:
- src/routes/tasks.ts: 为 POST 端点添加验证中间件
- src/lib/validation.ts: 使用 Zod 添加 TaskCreateSchema
我故意未触及的内容:
- src/routes/auth.ts: 存在类似的验证缺口但不在范围内
- src/middleware/error.ts: 错误格式可以改进(单独任务)
潜在问题:
- Zod 模式严格——拒绝额外字段。确认这是期望的行为。
- 添加了 zod 作为依赖(gzip 后 72KB)——已在 package.json 中
这种模式能及早发现错误假设,并为审查者提供清晰的变更地图。“未触及”部分尤其重要——它表明你遵守了范围纪律,没有进行未经请求的改造。
提交前卫生
每次提交前:
# 1. 检查即将提交的内容
git diff --staged
# 2. 确保没有机密
git diff --staged | grep -i "password\|secret\|api_key\|token"
# 3. 运行测试
npm test
# 4. 运行 lint
npm run lint
# 5. 运行类型检查
npx tsc --noEmit
使用 Git 钩子自动化:
// package.json (使用 lint-staged + husky)
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}
处理生成的文件
- 提交生成的文件仅当项目期望它们时(例如
package-lock.json、Prisma 迁移) - 不要提交构建输出(
dist/、.next/)、环境文件(.env)或 IDE 配置(.vscode/settings.json,除非共享) - 拥有
.gitignore涵盖:node_modules/、dist/、.env、.env.local、*.pem
使用 Git 进行调试
# 查找哪个提交引入了错误
git bisect start
git bisect bad HEAD
git bisect good <已知良好提交>
# Git 检出中点;在每个点运行测试以缩小范围
# 查看最近更改的内容
git log --oneline -20
git diff HEAD~5..HEAD -- src/
# 查找谁最后更改了特定行
git blame src/services/task.ts
# 搜索提交消息中的关键字
git log --grep="validation" --oneline
发布与版本管理
提交是你自己跟踪变更的方式;版本是你的消费者跟踪变更的方式。当其他任何东西依赖你的代码时——另一个团队、一个发布的包、一个部署的客户端——"main 上的最新版本"就不再是回答“我在运行什么,升级安全吗?”的充分答案。版本号和变更日志是回答这个问题的契约。
语义化版本
对于任何有消费者的东西,使用 MAJOR.MINOR.PATCH 版本,让数字传达含义:
MAJOR 破坏性变更——消费者必须更改代码才能升级
MINOR 新功能,向后兼容——升级安全
PATCH 错误修复,向后兼容——升级安全
数字是一个承诺,所以代码必须与之匹配。一个改变消费者依赖的行为的“补丁”是一个伪装成小版本的重大变更(海勒姆定律——参见 api-and-interface-design 技能)。当不确定变更是否破坏性时,假设它是;一个意外的重大版本比一个损坏的消费者代价小得多。
标记发布,让标签成为真相来源
发布是历史中不可变的点,而不是移动的分支。标记它以便始终可以重现:
git tag -a v1.4.0 -m "发布 1.4.0"
git push origin v1.4.0
从标签派生版本,而不是在分散的文件中手动编辑,这样工件、标签和变更日志永远不会不一致。
为人类编写变更日志
变更日志不是 git log。它是经过策划的、面向消费者的回答“什么变了,我关心吗?”——按 Added / Changed / Fixed / Deprecated / Removed / Security 分组,最新在上,每个条目围绕用户影响而非内部机制表述。
## [1.4.0] - 2025-06-12
### Added
- 通过 CSV 批量导入任务
### Fixed
- 周期性任务截止日期的时区偏移
### Deprecated
- `GET /v1/tasks/all` — 使用分页的 `GET /v1/tasks`(2.0 中移除)
在进行变更的同时编写条目,趁影响还新鲜——而不是在发布时从提交考古中重建。破坏性变更需要迁移说明和弃用窗口(遵循 deprecation-and-migration 技能);实际发布是 shipping-and-launch 技能的工作——本节是提供输入的版本契约。
常见合理化
| 合理化 | 现实 |
|---|---|
| “功能完成时我再提交” | 一个巨大的提交无法审查、调试或回滚。每个切片单独提交。 |
| “消息不重要” | 消息是文档。未来的你(和未来的代理)需要理解什么变了以及为什么。 |
| “我稍后会全部压缩” | 压缩会破坏开发叙事。从一开始就保持干净的增量提交。 |
| “分支增加开销” | 短期分支是免费的,并防止冲突工作碰撞。长期分支才是问题——在 1-3 天内合并。 |
| “我稍后会拆分这个更改” | 大型更改更难审查,部署风险更大,更难回滚。在提交前拆分,而不是之后。 |
| “我不需要 .gitignore” | 直到包含生产密钥的 .env 被提交。立即设置它。 |
| “只是一个小修复,升补丁版本” | 检查消费者能观察到什么。他们依赖的行为变化是重大变更,无论 diff 大小如何。 |
| “变更日志就是提交日志” | 提交是为你自己;变更日志是为消费者,按影响策划。从原始提交生成会埋没重要内容。 |
| “我们会在发布时编写变更日志” | 到那时影响是从记忆中重建的,一半都丢失了。在变更时编写条目。 |
红旗
- 大量未提交的变更积累
- 提交消息如“fix”、“update”、“misc”
- 格式化更改与行为更改混合
- 项目中无
.gitignore - 提交
node_modules/、.env或构建产物 - 长期分支与 main 显著偏离
- 强制推送到共享分支
- 破坏性变更以次要或补丁版本发布
- 发布没有标签,或版本号手动编辑与标签不同步
- 面向用户的发布没有变更日志条目,或变更日志只是转储的提交消息
验证
对于每次提交:
- [ ] 提交只做一件事
- [ ] 消息解释为什么,遵循类型约定
- [ ] 提交前测试通过
- [ ] diff 中无机密
- [ ] 无仅格式化的更改与行为更改混合
- [ ]
.gitignore涵盖标准排除项
对于每次发布(任何有消费者的东西):
- [ ] 版本号提升与变更匹配:破坏性 → major,新增 → minor,修复 → patch
- [ ] 发布已打标签,版本从标签派生,而非手动编辑不同步
- [ ] 变更日志有按影响分组的、经过策划的、人类可读的条目






