Article
Git 协作规范
前置说明
本文默认读者已经理解 Git 的基本原理,包括 commit、branch、HEAD、merge、rebase、远程分支和冲突解决
因此,本文不再解释 Git 是什么,也不重复讲对象模型和分支原理。重点放在团队协作中如何写 commit、整理历史,以及选择合适的合并方式
为什么需要 Git 规范
Git 提交历史不只是代码变更记录,更是团队协作的知识库。它真正的读者通常不是当前作者,而是 reviewer、排障者和未来维护者
但好的提交历史不会自然产生。它需要两个层面的规范:
- Commit 规范 - 如何写好单个提交:规范的 message 格式、清晰的拆分粒度、合理的组织方式
- Merge 规范 - 如何将分支合入主干:选择合适的合并策略,保持历史清晰可维护
Git 规范不是为了追求格式整齐,而是为了降低协作成本。好的提交历史应该像一本技术日志,让任何人都能:
- 在 5 分钟内理解一个 PR 的改动范围和意图
- 通过
git bisect快速定位引入问题的提交 - 在半年后仍能理解当时的决策背景和权衡
- 安全地回滚某个功能而不影响其他改动
规范化的 commit 还可以服务自动化流程。例如生成 changelog、判断语义化版本升级类型、触发 CI/CD、关联 issue 或 PR
Commit Message 规范
格式规范
Commit Message 建议采用 Conventional Commits 1.0.0:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]最常见的提交如下:
feat(auth): add password reset flow其中 type 表示变更类型,scope 表示影响范围,description 简短说明这个提交做了什么
常用类型包括:
feat: 新增功能
fix: 修复问题
docs: 文档变更
test: 测试变更
refactor: 重构,不改变外部行为
style: 格式调整,不影响逻辑
chore: 构建、依赖、脚手架等杂项
ci: CI 配置变更
build: 构建系统或外部依赖变更feat 通常对应 SemVer 的 MINOR 版本升级,fix 通常对应 PATCH 版本升级
破坏性变更需要显式标记。可以在 type 或 scope 后加 !:
feat(api)!: remove legacy user endpoint也可以在 footer 中写:
BREAKING CHANGE: remove legacy user endpoint写作规则
Conventional Commits 解决的是结构问题,Chris Beams 的 七条规则 解决的是可读性问题
团队可以采用以下规则:
- subject 和 body 之间空一行
- subject 尽量简短,英文建议 50 字符附近
- subject 不以句号结尾
- subject 使用动作表达,描述“应用这个提交会发生什么”
- body 按合适宽度换行,英文建议 72 字符
- body 解释 what 和 why,不重复解释 how
- issue、PR、BREAKING CHANGE 等元信息放在 footer
一个需要 body 的提交可以这样写:
fix(order): preserve payment state after retry
The previous retry flow recreated the order payment state, which could
drop the provider transaction id after a transient network failure.
This commit keeps the existing payment state and only updates retry
metadata.
Refs: #123如果改动非常简单,一行 subject 就够了:
docs: fix typo in deployment guidebody 的价值不在于复述 diff。diff 已经说明代码怎么变了,body 应该解释为什么要变、之前的问题是什么、这个方案有什么取舍
对比两个常见写法:
fix bug
update auth
change configfix(auth): handle expired refresh token
chore(config): align staging API base URL坏 subject 的问题不是短,而是缺少边界和意图。好的 subject 应该让 reviewer 在展开 diff 前,就能大致判断改动影响哪里、解决什么问题
Commit 组织策略
本地 Commit 与 PR Commit
本地 commit 和 PR commit 的目标不同。本地 commit 服务开发过程,PR commit 服务代码审查、问题排查和长期维护
本地 commit 服务开发者
PR commit 服务 reviewer、维护者、bisect 和未来历史本地开发时,可以使用小步提交保存上下文:
wip: try new auth callback
debug: log token refresh response
fix: handle missing refresh token
style: adjust login form spacing这些提交对开发者有价值,因为它们能帮助回滚、对比实验、保存中间状态
但它们通常不适合作为 PR 历史。PR commit 应该整理成清晰的逻辑序列:
feat(auth): add refresh token flow
fix(auth): handle missing refresh token
test(auth): cover token refresh failure如何拆分 Commit
一个好的 commit 应该是一个完整、独立、可理解的逻辑变更
如果一个 subject 无法准确概括整个 diff,通常说明这个 commit 太大,应该拆分
建议拆分的情况:
- 重构和行为变更分开
- 测试和无关实现分开
- UI、API、数据模型、迁移、文档按可 review 单元分开
- 格式化调整不要混在业务变更里
例如,不建议一个 commit 同时做这些事:
refactor user service
add password reset
format all files
update docs更好的方式是拆成:
refactor(user): isolate password service
feat(auth): add password reset flow
docs(auth): document password reset behavior拆分与合并 Commit 的方法
使用 Interactive Rebase
git rebase -i 是整理提交历史最强大的工具,可以重新排序、合并、拆分、修改或删除提交
# 整理最近 3 个提交
git rebase -i HEAD~3
# 整理从某个 commit 之后的所有提交
git rebase -i <commit-hash>
# 整理当前分支相对于 main 的所有提交
git rebase -i main执行后会打开编辑器,显示待整理的提交列表:
pick a1b2c3d feat(auth): add login form
pick d4e5f6g fix: button state
pick h7i8j9k docs: update auth guide
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash" but discard this commit's log message
# d, drop = remove commit常用操作:
- 合并提交:将
pick改为squash或fixup,会合并到上一个 commit - 拆分提交:将
pick改为edit,rebase 会在该 commit 处暂停 - 删除提交:将
pick改为drop或直接删除该行 - 重新排序:直接调整行的顺序
- 修改 message:将
pick改为reword
拆分单个 Commit
如果一个 commit 包含多个逻辑变更,可以用 edit 拆分:
# 1. 启动 interactive rebase
git rebase -i HEAD~3
# 2. 将要拆分的 commit 标记为 edit
# 保存退出后,rebase 会在该 commit 处暂停
# 3. 撤销该 commit,但保留改动
git reset HEAD^
# 4. 分批暂存并提交
git add src/auth/login.ts
git commit -m "feat(auth): add login form"
git add src/auth/validation.ts
git commit -m "feat(auth): add input validation"
# 5. 继续 rebase
git rebase --continue使用 Reset 重新组织
git reset 可以撤销提交但保留改动,适合快速重新组织最近的提交:
# 撤销最近 2 个 commit,改动回到暂存区
git reset --soft HEAD~2
# 撤销最近 2 个 commit,改动回到工作区
git reset HEAD~2
# 或
git reset --mixed HEAD~2
# 撤销最近 2 个 commit,丢弃所有改动(危险)
git reset --hard HEAD~2--soft 适合重新组织 commit message 或合并提交,--mixed 适合重新选择暂存内容
部分暂存
使用 git add -p 可以交互式地选择文件中的部分改动暂存,实现更细粒度的 commit 拆分:
git add -p src/auth/service.ts
# Git 会逐个展示改动块(hunk),询问是否暂存
# y = 暂存这个块
# n = 不暂存
# s = 拆分成更小的块
# e = 手动编辑这个块
# q = 退出修改最近一次 Commit
如果只需要修改最近一次提交,可以用 --amend:
# 修改 commit message
git commit --amend -m "feat(auth): add login form with validation"
# 追加改动到最近一次 commit
git add forgotten-file.ts
git commit --amend --no-edit注意:--amend 会改写历史,如果 commit 已推送到远程,需要 git push --force-with-lease
整理历史的最佳实践
- 在本地整理:只整理未推送的提交,避免改写已共享的历史
- 先备份:整理前创建备份分支
git branch backup-feature - 小步验证:每次 rebase 后运行测试,确保功能正常
- 保持原子性:拆分后的每个 commit 应该能独立通过测试
- 使用 —force-with-lease:如果必须改写远程历史,用
--force-with-lease而非--force
Git 合并策略

理解了 commit 的组织方式后,接下来看如何将分支合入主干。合并策略决定的是主干历史的形状:线性、保留分支边界,还是压缩成单个结果
| 策略 | 历史形态 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| Fast-forward | 线性 | 分支短、无分叉 | 历史简洁易读 | 丢失分支边界 |
| —ff-only | 线性(强制) | 严格线性历史要求 | 保护线性历史 | 需要频繁 rebase |
| Three-way merge | 非线性 | 两分支都有新提交 | 保留真实拓扑 | 历史复杂 |
| —no-ff | 非线性(强制) | 保留功能边界 | 可整体回滚 | 增加 merge commit |
| Squash merge | 线性 | 过程杂乱、结果单一 | 主干简洁 | 丢失演进过程 |
选择时先看三个问题:主干是否必须线性、分支里的 commit 是否有长期价值、是否需要保留功能边界
| 场景 | 推荐策略 | 命令示例 |
|---|---|---|
| 分支短、历史干净 | —ff-only | git merge --ff-only feature/foo |
| 团队要求线性历史 | rebase + —ff-only | git rebase origin/main && git merge --ff-only |
| 功能分支有多个有意义 commit | 保留 commit + —no-ff | git merge --no-ff feature/foo |
| 过程杂乱但最终改动单一 | squash merge | 平台 Squash Merge 或 git merge --squash |
| 需要 bisect 和保留演进过程 | 避免 squash | git merge --no-ff feature/foo |
Fast-forward
目标分支直接移动指针到源分支最新 commit,不产生 merge commit
适用场景:从 main 拉出分支后,main 没有新增提交,或当前分支已 rebase 到最新 main
git checkout main
git merge feature/login # 自动 fast-forward代价:历史非常简洁,但看不出哪些 commit 曾经属于同一个功能分支
—ff-only
git merge --ff-only 只允许 fast-forward,无法快进时直接失败,强制保护线性历史
适用场景:团队希望主干始终保持线性,并且要求开发分支在合并前主动 rebase 到最新主干
git checkout main
git merge --ff-only feature/login
# 如果失败,说明 main 已分叉,需要先 rebase代价:开发者需要更频繁地同步主干。分支存在冲突时,必须先解决 rebase 或重新整理历史
Three-way Merge
当两个分支都有新提交时,Git 基于共同祖先、当前分支 tip、待合并分支 tip 三个点计算合并结果,生成 merge commit
适用场景:团队接受非线性历史,并希望保留分支真实演进过程
代价:历史拓扑更接近真实协作过程,但阅读主干时会出现更多分叉和 merge commit
—no-ff
即使可以 fast-forward,也强制生成 merge commit,保留「分支作为整体被合入」的语义
适用场景:功能分支包含多个有意义 commit,希望保留功能边界,方便整体回滚
git checkout main
git merge --no-ff feature/payment代价是历史更复杂。如果每个小改动都强制 merge commit,主干会变得臃肿
Squash Merge
把源分支所有改动压成一个新 commit 合入目标分支,不保留原分支 commit 拓扑
适用场景:PR 过程里有大量 wip、debug、fix lint 等过程提交,但最终改动可以被一个 subject 清楚概括
什么时候 Squash
Squash 的目标不是减少 commit 数量,而是删除没有长期价值的过程噪音
适合 squash 的提交:
wip: add login form
fix: button state
debug: print response
fix typo in previous commit
fix lint
remove temp log合入主干时 squash 成:
feat(auth): add login form不应该 Squash 的情况
如果多个 commit 分别代表清晰的设计步骤、可回滚单元或 bisect 边界,它们应该保留。不要为了「看起来干净」而 squash 掉有意义的历史
如何选择合并策略
没有一种策略适合所有团队。策略选择应该服务历史价值,而不是机械追求线性或 commit 数量少
工作流示例
偏线性历史的团队
git fetch origin
git rebase origin/main
git checkout main
git merge --ff-only feature/foo保留功能边界的团队
git checkout main
git merge --no-ff feature/foo以 PR 为主、希望主干简洁的团队
使用平台提供的 Squash Merge 功能
推荐团队工作流
可以把团队协作拆成四个阶段,每个阶段只关注一个目标
开发中
本地小步提交。不要为了追求一开始就完美,而牺牲开发过程中的安全 checkpoint
PR 前
整理历史。删除 debug、wip、临时日志和无意义 fixup,把提交整理成 reviewer 能理解的逻辑序列
如果分支历史杂乱,但最终改动单一,可以 squash。如果分支历史清晰,并且每个 commit 都有独立价值,应该保留
合并前
合并前,同步最新主干:
git fetch origin
git rebase origin/main如果分支已经推送到远程,rebase 后更新远程分支时优先使用:
git push --force-with-lease--force-with-lease 比 --force 更安全,因为它会避免覆盖别人已经推送到远程的更新
合并时
根据团队规则选择合并策略。短分支优先保持线性,过程杂乱但结果单一时使用 squash,有清晰演进价值的功能分支保留 commit 和分支边界
PR 前检查清单
提交 PR 前,建议按以下分组检查:
Commit 质量
- subject 清晰且符合 Conventional Commits
- 去掉 debug、wip、临时日志
- 每个 commit 是一个逻辑变更
- 复杂变更写了 body,解释 why 而非 how
代码组织
- 没有混入无关格式化
- 测试、文档、实现放在合适的 commit 中
- 重构和行为变更分开
合并准备
- 分支基于最新 main
- 选择了合适的合并策略
- 如需 rebase,已使用
--force-with-lease更新远程
历史价值检验
好的 Git 历史应该能回答三个问题:
- 这个变更做了什么?
- 为什么要这样做?
- 它是如何进入主干的?
最终目标不是让历史「看起来少」,而是让历史有用