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