回顾 Merge 的两种方式
Fast-forward(快进合并)
当被合并分支(feature)是目标分支(main)的直接后代时,Git 默认执行 fast-forward:只移动指针,不产生新的 commit。
└── 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(有两个父提交)。
└── 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 的最新提交之后。
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 视觉对比
| 维度 | Merge | Rebase |
|---|---|---|
| 历史记录 | 保留真实的分支历史(有 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
--squash 与 squash rebase 的区别:--squash 不会保留 feature 分支上的任何 commit 引用,合并后 feature 分支和 main 之间没有"共同祖先"关系,Git 无法自动知道这个 feature 已经合并。
何时用 Merge,何时用 Rebase
- 用 Merge 合并 feature 到 main/develop 时(保留完整协作历史);合并 hotfix 到多个分支;公共分支之间的整合;你想保留"哪些提交来自哪个分支"的信息时。
- 用 Rebase 在提交 PR 前,将个人 feature 分支同步到最新 main(让审阅者看到干净的差异);整理个人提交历史(合并零散的 fix commit);保持长期维护的私有分支跟踪主线变化。
- 用 --squash 合并短期的功能分支,不希望保留其内部的零散 commit,只关心最终结果。GitHub 的 "Squash and merge" 按钮就是这个操作。