2417 字
约 8 分钟
17
S01-A19 · rebase 还是 merge:历史形状与协作成本

一个团队统一用"rebase 后再合并",历史是一条直线,看起来很干净。直到一次多人协作的分支被 rebase,产生了 11 个"解决冲突"的重复劳动——因为每个人都要把自己那一段重新解一遍,而冲突的解法各不相同。

merge 与 rebase 的区别不只是"历史好不好看",而是**"重写历史"这件事把成本转移给了谁**。rebase 让历史线性,代价是"每个被 rebase 的提交都是新的",于是所有基于旧提交的工作都要重来。


1. 两种操作的本质

操作 做了什么
merge 新建一个合并提交,保留两条父链
rebase 把提交"复制"到新基底上,产生新哈希
squash 把多个提交压成一个
cherry-pick 复制单个提交到别处

第二项是关键:rebase 不是"移动提交",而是"复制提交并丢弃原件"。所以"同一个改动"在 rebase 前后是两个不同的提交(哈希不同),这个"复制"还带来一个后果:rebase 后的分支与原来不是"同一个分支",只是名字一样。于是"我明明推过一次"与"为什么又要解冲突"的矛盾感,全部来自这里。
而所有引用旧哈希的东西(别人的分支、CI 记录、评论)都对不上了。

// 一次 rebase 的事实
before:  A - B - C (feature)       base: main@X
after:   X - B' - C' (feature)     ← B、C 被"复制",哈希变了
// 影响:别人基于 B 的工作、指向 C 的评论、CI 的构建记录,全部失去关联

2. 什么时候该 rebase

场景 建议
本地未推送的提交整理 适合
个人分支追主干(未共享) 适合
共享分支 不适合
已推送且有人基于它工作 不适合
需要保留"何时合入"信息 不适合

第一项与第二项的共同前提是"没有别人依赖这些提交"。第一项还有个"时机"细节:在 push 之前整理是免费的,push 之后再整理就要"强推",而强推会改写远端——这是所有混乱的起点。
一旦推送且有人基于它,rebase 就变成了"给别人制造冲突"。

第四项(合入信息)在"事故回溯"里很有用:"这个改动是哪天进主干的"能直接回答"是不是这次发布引入的",而 rebase 会把提交时间"改成"原始创作时间。
第五项在"审计/回溯"要求高的团队里重要:merge 提交保留了"什么时候合入主干"这个事实,而 rebase 把它抹平了。

3. 什么时候该 merge

场景 建议
共享分支合入主干 适合
需要保留分支拓扑 适合
多人协作的长期分支 适合
需要可追溯的合入点 适合

第二项的价值在"看懂历史":一条分支从哪来、中间合过什么,merge 保留了这个结构第二项也让 git log --graph 有意义:merge 的形状能看出"并行开发"的规模,而一条直线看不出任何协作信息。
;而 rebase 把它压成直线,于是"这段改动属于哪个特性"要靠 commit message 猜。

"强推"是这类问题的集中爆发点,所以团队应当有明确规则:哪些分支禁止强推(main、release/*、共享的 feature/*),并靠"远端保护规则"落地,而不是靠人记得。

// 三条可落地的规则
1 main/release/*:禁止强推(服务端保护)
2 共享 feature 分支:禁止强推(约定 + CI 检查)
3 个人分支:允许强推,但先在本地整理好再推
// 判据:这条分支"会不会有第二个人 push"

4. 折中:squash merge

做法 结果
squash merge 一个提交进主干,分支历史丢弃
merge --no-ff 保留分支结构 + 合并提交
rebase merge 线性但哈希全变

第一项还有个"回滚"上的优势:一个 PR 一个提交,回滚就是 revert 一个;而"分支内十个提交"要 revert 十个(或用 -m 处理合并提交)。
第一项在"每个 PR 一个提交"的团队里最流行:主干干净、每个改动一个提交,代价是"分支内的迭代过程"消失在历史里(但在 PR 页面里还能看到)。

它的隐含要求是"PR 描述写清楚"——因为提交信息只剩一条,所有上下文都在 PR 里。

5. 冲突的重复劳动

场景 冲突次数
长期分支 + 频繁 rebase 每次都重解
长期分支 + merge 主干 一次解(合并提交)
短分支 + squash 一次解
多人在同一分支 rebase 每人重解一遍

第四项就是开头那个 11 次的情况:rebase 让"解过的冲突"在新哈希下重新出现第四项的机制是"每个提交都是新提交",于是冲突的上下文(哪两个版本冲突)变了——所以 git 无法自动套用上次的解法,除非冲突块完全一样。
,而 git 不会记住"你上次怎么解的"(除非开 rerere)。

rerere 的"自动套用"是有风险的:它按"冲突块内容"匹配,而语义可能已经变了。所以它能省时间,但不能替代"看一眼"。
rerere(reuse recorded resolution)能缓解:它记录"这个冲突怎么解的",下次遇到同样冲突自动套用。但它只在本地生效,且需要开启。

// 开启 rerere(减少重复解冲突)
git config --global rerere.enabled true
// 效果:同一冲突第二次出现时,git 自动套用上次的解法
// 边界:只在"完全相同的冲突块"上生效,跨文件/跨上下文不适用

"找回丢失的提交"应当有一个标准动作序列,而不是临场摸索:

// 五步找回(rebase 搞丢提交时)
git reflog                      # 找到 rebase 前的 HEAD(形如 HEAD@{5})
git branch backup HEAD@{5}      # 建一个备份分支指向它
git log --oneline backup        # 确认提交在
git reset --hard backup         # 回到该状态(或 cherry-pick 需要的提交)
// 关键:先建备份分支,再动手,避免二次丢失

6. 排错:五种"rebase/merge 出问题"

现象 成因 处理方向
强推后同事全乱 重写了共享历史 禁用共享分支 rebase
冲突反复出现 每次 rebase 重解 开 rerere 或改 merge
找不到"何时合入" rebase 抹平了合入点 关键分支用 merge
历史太乱看不懂 全用 merge 主干用 squash
提交丢失 rebase 中误操作 reflog 找回

第五项是"救命"知识:git reflog 记录了 HEAD 的每次移动第五项的边界是"时间":reflog 条目会在 git gc 时被清理(默认 90 天,不可达对象 30 天)。所以"搞丢了"要尽快处理,而不是下周再说。
,于是"rebase 搞丢了提交"通常能找回(在 git gc 之前)。

最后一个"度量"视角:看"平均每个 PR 的冲突解决次数"。若这个数在上升,说明"分支太长"或"rebase 太频繁"——而它通常是流程问题,不是 git 用法问题。

// 两个可用于判断的数(按周统计)
PR 存活时长(首次提交到合并)  > 3 天 → 分支偏长,冲突概率上升
每 PR 冲突解决次数             > 1.5 → 检查是否在为 rebase 重复解冲突
// 用法:改流程(拆小 PR、缩短分支),而不是改 git 命令

7. 边界:什么情况无所谓用哪个

场景 为什么 做法
单人仓库 无协作成本 随意
一次性分支(不合入) 历史不留 随意
已冻结的分支(只读) 不再变动 不操作
极短分支(1 个提交) 两种等价 随意

最后一条与"团队"有关的边界:"用 rebase 还是 merge"其实是协作规范的产物,不是技术优劣。所以它应当写进团队约定(谁强推、哪些分支禁推),而不是让每个人按喜好选。
判断口径一句话:这些提交有没有"别人依赖"。有(已推送、有人基于它),就只能 merge;没有(本地、未共享),rebase 是安全的整理手段。


8. 自测

  1. rebase 的本质是什么?为什么说它"不是移动提交"?
  2. rebase 适合哪两种场景?它们的共同前提是什么?
  3. merge 保留了哪个"事实"而 rebase 抹平了它?它对审计有什么价值?
  4. squash merge 的隐含要求是什么?为什么"PR 描述"变得关键?
  5. 多人 rebase 同一分支时,为什么"解过的冲突"会重复出现?rerere 解决什么、不解决什么?
  6. 判断"能否 rebase"的那个问题是什么?
S01-A19 · rebase 还是 merge:历史形状与协作成本
https://www.clxhxhhr.top/posts/6555/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。