Chapter 05

合并与变基

Merge vs Rebase——两种整合分支的策略,以及如何做出正确选择

回顾 Merge 的两种方式

Fast-forward(快进合并)

当被合并分支(feature)是目标分支(main)的直接后代时,Git 默认执行 fast-forward:只移动指针,不产生新的 commit。

Before:main ──▶ A ── B
└── C ── D ◄── feature

After git merge feature(fast-forward):
A ── B ── C ── D ◄── main ◄── feature
# 默认会尝试 fast-forward
git merge feature

# 禁止 fast-forward,强制创建 merge commit
git merge --no-ff feature -m "Merge feature/login into main"

Three-way Merge(三方合并)

当两条分支各自都有新提交时,Git 找到它们的公共祖先(merge base),对三方进行比较,生成一个新的 merge commit(有两个父提交)。

Before:A ── B ── C ── D ◄── main
└── E ── F ◄── feature

After git merge --no-ff feature:
A ── B ── C ── D ────────── M ◄── main
└── E ── F ──────┘
(M 是新的 merge commit,有 D 和 F 两个父 commit)

什么是 Rebase(变基)

Rebase 的字面意思是"重新设置基底"。git rebase main 在 feature 分支上执行,意思是:把 feature 分支上的提交,从与 main 的分叉点开始,全部"重新播放"到 main 的最新提交之后

Before:
A ── B ── C ── D ◄── main
└── E ── F ◄── feature

After git rebase main(在 feature 分支上执行):
A ── B ── C ── D ◄── main
└── E' ── F' ◄── feature

(E' 和 F' 是重新应用的新 commit,哈希不同于 E 和 F!)

rebase 完成后,feature 分支看起来就好像是在最新的 main 基础上开发的。此时再 merge 到 main,会自动 fast-forward,历史完全线性。

# 在 feature 分支上执行,以 main 为新基底
git switch feature
git rebase main

# rebase 完成后,切回 main 合并(此时会 fast-forward)
git switch main
git merge feature  # fast-forward,无 merge commit

Rebase vs Merge 视觉对比

维度MergeRebase
历史记录保留真实的分支历史(有 merge commit)线性历史,像从未分叉过
Merge Commit产生(three-way 时)不产生
提交哈希原始 commit 哈希不变重新应用,产生新哈希
适用场景保留团队协作历史、长期特性分支个人功能分支、保持干净主线
安全性总是安全不能对已推送的公共分支使用

Rebase 的黄金法则

!

不要对已推送到公共分支的提交做 Rebase!
原因:rebase 会改写提交历史(产生新的哈希)。如果你 rebase 了已经推送给他人的 commit,其他人本地仍然有"旧版本"的 commit,两个历史出现分叉,合并时会产生极度混乱的冲突。这条规则只有一个例外:在你独自维护的个人 feature 分支上可以 rebase,因为没有人依赖这些 commit 的哈希。

简单记忆:公共分支用 merge,私有分支用 rebase

交互式 Rebase

交互式 rebase(git rebase -i)是 Git 最强大的历史整理工具,允许你对一系列提交进行任意修改:合并、拆分、重排序、删除、修改提交信息。

# 对最近 5 个提交进行交互式整理
git rebase -i HEAD~5

# 从某个特定提交(不含)开始
git rebase -i abc1234

执行后会打开编辑器,显示类似:

pick e3f1a2b feat: add login form
pick 7c9d4e1 fix: typo in login
pick 2a8f3c5 fix: another typo
pick b4e7d9f refactor: extract validation
pick 1d5c8a3 test: add login tests

# Rebase 操作说明:
# p, pick   = 使用这个提交(不做任何变更)
# r, reword = 使用这个提交,但修改提交信息
# e, edit   = 使用这个提交,但暂停以便修改内容
# s, squash = 使用这个提交,合并到上一个提交(保留信息)
# f, fixup  = 类似 squash,但丢弃此提交的信息
# d, drop   = 删除这个提交

常用操作示例

合并多个提交(squash)

pick e3f1a2b feat: add login form
squash 7c9d4e1 fix: typo in login
squash 2a8f3c5 fix: another typo
pick b4e7d9f refactor: extract validation
pick 1d5c8a3 test: add login tests

保存后,前三个提交会被合并为一个,Git 会再次打开编辑器让你编辑合并后的提交信息。

用 fixup 快速清理

pick e3f1a2b feat: add login form
fixup 7c9d4e1 fix: typo in login
fixup 2a8f3c5 fix: another typo

fixup 类似 squash,但直接丢弃被合并的提交信息,不再询问。适合修复小错误的提交。

修改提交信息(reword)

reword b4e7d9f refactor: extract validation

删除某个提交(drop)

drop 1d5c8a3 test: add login tests

解决 Rebase 冲突

rebase 过程中也可能产生冲突(在重新应用每个 commit 时)。与 merge 冲突不同,rebase 冲突需要逐个提交地解决。

# rebase 中途遇到冲突时,解决冲突文件后:
git add conflicted-file.js
git rebase --continue   # 继续处理下一个提交

# 如果某个提交解决后实际没有变更(与目标分支相同内容),跳过
git rebase --skip

# 放弃整个 rebase 操作,回到 rebase 之前的状态
git rebase --abort

git merge --squash

--squash 是一种特殊的合并方式:将 feature 分支的所有提交压缩为一组变更放入暂存区,你再手动提交一个新的单一 commit。

git switch main
git merge --squash feature/login
git commit -m "feat: complete user login feature"
# feature 分支的所有 commit 被压成一个,不产生 merge commit

--squashsquash rebase 的区别:--squash 不会保留 feature 分支上的任何 commit 引用,合并后 feature 分支和 main 之间没有"共同祖先"关系,Git 无法自动知道这个 feature 已经合并。

何时用 Merge,何时用 Rebase

git rerere — 自动重用冲突解决方案

rerere(Reuse Recorded Resolution)是 Git 的一个强大但鲜为人知的功能:记住你如何解决某个冲突,并在下次遇到相同冲突时自动应用。对于长期维护的分支(如需要频繁与 main 同步的 feature 分支)极为有用。

# 启用 rerere(推荐全局启用)
git config --global rerere.enabled true

# 也可以只对当前仓库启用
git config rerere.enabled true

# 查看 rerere 的缓存状态
git rerere status

# 查看 rerere 识别出的冲突详情
git rerere diff

# 手动触发 rerere(一般不需要,git merge/rebase 时自动触发)
git rerere

rerere 实战场景

# 场景:feature 分支需要频繁同步 main 的变化

# 第一次 rebase 时遇到冲突
git switch feature/big-refactor
git rebase main
# 遇到冲突,手动解决 src/auth.js 中的冲突
git add src/auth.js
# rerere 自动记录了这个解决方案!
git rebase --continue

# 一周后,main 又有了新提交,再次 rebase
git rebase main
# 遇到相同冲突时,rerere 自动应用上次的解决方案
# 输出:Resolved 'src/auth.js' using previous resolution.
git add src/auth.js   # 确认 rerere 的解决方案
git rebase --continue

rerere 缓存的冲突解决方案不会随仓库推送——它存储在 .git/rr-cache/,这个目录不被 Git 追踪。每个开发者的 rerere 缓存是独立的,无法共享。如果你换了机器或重新克隆,需要重新建立缓存。

Rebase 时同一 commit 解决冲突多次的问题 — 当交互式 rebase 的范围包含多个提交,且同一文件有冲突时,每次 --continue 都是针对下一个 commit 的冲突,需要逐个解决。rerere 在这种情况下特别有价值,可以避免重复劳动。

merge 与 rebase 的常见误区

在 main 上执行 rebase 是错误的git rebase feature 在 main 上执行,会将 main 的提交"移动"到 feature 之后,这会改写 main 的历史。正确姿势:始终在 feature 分支上执行 git rebase main,将 feature 接到最新的 main 上。

squash merge 后原分支显示"未合并"git merge --squash 合并后,Git 并不知道这个分支已经"合并"了(因为没有共同的 merge commit),执行 git branch --merged 时该分支不会出现在列表中。需要手动 git branch -D 强制删除。

本章小结 — merge 保留真实历史(three-way merge 产生 merge commit),rebase 重写历史创造线性提交链(rebase 后 commit 哈希改变)。黄金法则:公共分支用 merge,私有分支用 rebase。交互式 rebase(-i)是最强大的历史整理工具。rerere 通过记录并复用冲突解决方案,减少反复 rebase 的重复劳动,强烈建议全局启用。