Git 是现代软件开发中不可或缺的工具。在将特性分支(Feature Branch)的工作合并回主分支(如 main 或 master)时,我们通常面临两种主要的集成策略:git merge 和 git rebase。虽然两者都能达到目的,但它们对项目历史记录的影响却截然不同。
本文将深入对比这两种方法,并重点介绍如何利用 Git Rebase,特别是交互式变基(Interactive Rebase),来确保项目的提交历史保持清晰、线性的“完美”状态。
1. Git Merge:保持原貌,但历史记录可能冗余
git merge 的核心思想是保留历史事件的真实记录。当你合并一个分支时,Git 会创建一个特殊的“合并提交”(Merge Commit)。这个提交有两个父节点,清晰地记录了两个分支是如何在何时何地汇合的。
优点: 简单安全,不会改写历史,完全保留了分支结构和时间顺序。
缺点: 频繁的合并会导致大量的合并提交,使 git log 看起来错综复杂,失去了线性的“故事感”。
Merge 示例(非线性历史)
假设 main 分支和 feature 分支都各自进行了提交:
git init my_repo && cd my_repo
echo "A" > file.txt && git add . && git commit -m "A: Initial commit"
# 创建并切换到 feature 分支
git checkout -b feature
echo "B" >> file.txt && git commit -am "B: Feature work 1"
# main 分支有了新的提交
git checkout main
echo "C" >> file.txt && git commit -am "C: Hotfix on main"
# 合并 feature 分支
git merge feature --no-ff
git log --oneline --graph --all
# * (Merge Commit) 整合 feature 分支
# |\
# | * B: Feature work 1
# * | C: Hotfix on main
# |/
# * A: Initial commit
可以看到,main 历史中出现了一个岔路口和合并提交,历史记录是非线性的。
2. Git Rebase:重写历史,创造完美的线性故事
git rebase(变基)的核心思想是重写历史,让你的分支提交看起来像是从目标分支的最新点开始的。它通过将当前分支的提交一个个“剪切”下来,然后粘贴到目标分支的最新提交之上。
优点: 历史记录干净、线性,易于阅读和理解。在将特性分支集成回主分支时,如果主分支采用 Fast-forward 合并,最终只留下一个单线的提交历史。
缺点: Rebase 会创建新的提交对象(SHA-1 ID会改变)。绝对不要对已经推送至公共仓库的提交进行 Rebase!(除非你明确知道你在做什么,并且所有协作者都同意使用强制推送)。
Rebase 示例(线性历史)
我们使用与上述 Merge 示例相同的基础状态,但这次我们使用 Rebase:
# 假设当前在 feature 分支
git checkout feature
# 将 feature 分支变基到 main 分支的最新状态 C 上
git rebase main
# Rebase 成功后,B' 现在位于 C 之后
# 切换回 main,进行快速合并
git checkout main
git merge feature # 默认是 Fast-forward
git log --oneline --graph --all
# * B': Feature work 1 (新提交 ID)
# * C: Hotfix on main
# * A: Initial commit
此时的历史记录是完全线性的,B 提交被重写为 B’,并紧随 C 之后,仿佛 B 一开始就是在 C 之后开发的。
3. 实践:利用交互式变基(Interactive Rebase)清理提交
仅仅是 git rebase main 就可以保持线性,但为了维护“完美”的历史,我们通常需要先清理特性分支上那些临时的、WIP(Work In Progress)的提交。
交互式变基 git rebase -i 允许我们在变基过程中对提交进行编辑,例如:
* squash:将多个提交合并成一个。
* fixup:合并提交,但不保留原始提交信息。
* edit:停止变基过程,允许修改提交内容。
* drop:删除某个提交。
步骤:如何将多个 WIP 提交合并成一个清晰的提交
假设你在 feature 分支上做了 3 个临时的提交,现在你想把它们合并成一个最终提交:
git checkout feature
# 假设最近的 3 个提交是临时的
git log -3 --oneline
# 对最近的 3 个提交进行交互式变基
git rebase -i HEAD~3
执行上述命令后,Git 会打开一个文本编辑器,显示类似以下内容:
pick 1a2b3c4 Commit: Initialize feature
pick 5d6e7f8 Commit: Minor typo fix
pick 9g0h1i2 Commit: Added documentation
# Commands: p, pick = use commit
# r, reword = use commit, but edit 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 = discard the commit
... (其他帮助信息)
操作目标: 保留第一个提交 (1a2b3c4),并将后面的两个提交 (5d6e7f8 和 9g0h1i2) 合并到第一个提交中。
修改后的编辑器内容:
pick 1a2b3c4 Commit: Initialize feature
squash 5d6e7f8 Commit: Minor typo fix
squash 9g0h1i2 Commit: Added documentation
保存并关闭编辑器后,Git 会提示你编辑新的合并提交信息。完成后,这三个提交就会变成一个干净、有意义的提交。
总结与建议
| 特性 | Git Merge | Git Rebase |
|---|---|---|
| 历史结构 | 非线性(分支结构保留) | 线性(提交历史如单行故事) |
| 提交 ID | 不改变原提交 ID | 变基的提交 ID 会改变 |
| 安全性 | 极高(不改写历史) | 低(不应用于公共分支) |
| 日志清晰度 | 低(有大量合并提交) | 极高 |
使用建议:
- 在本地特性分支上, 积极使用 git rebase -i 来清理、压缩提交,确保提交历史清晰。
- 在集成到主分支之前, 使用 git rebase main 来更新你的特性分支,解决冲突,并确保你的代码基于最新的主干代码。
- 对于共享的、公共的分支(如 **main),** 始终使用 git merge (或 Pull Request 流程中设置的 Merge 策略,如 Squash and Merge,它在 PR 层面实现了 Rebase 的效果) 以避免重写历史导致的混乱。但在大型项目的主分支上,如果团队统一要求纯净的线性历史,往往会禁止普通的 Merge Commit,强制使用 Rebase 或 Squash Merge 策略。
汤不热吧