第十章. Git Rebase 教程:变基与合并(Merge)的本质区别
第十章. Git Rebase 教程:变基与合并(Merge)的本质区别
Prorise第十章. Git Rebase 教程:变基与合并(Merge)的本质区别
摘要:Git 的强大不仅在于记录历史,更在于它允许我们要么“像记账员一样如实记录”,要么“像小说家一样编排故事”。本章将深入 Git 的高级核心——变基(Rebase)。我们将揭示 Rebase 的算法本质,学习如何使用交互式模式(Interactive)对外科手术级的提交历史进行重构,并掌握跨分支移植代码的高级技巧,最终打造出清晰、线性的项目演进图谱。
本章学习路径
- 算法解构:通过搭建模拟环境,理解 Rebase 的“寻找公共祖先”与“补丁重放”机制。
- 交互重塑:掌握
git rebase -i,像编辑文本一样合并、拆分、修改和删除历史提交。 - 高级移植:学习
--onto和cherry-pick,在不同分支间精确地移动代码片段。
10.1. Rebase 的核心算法与操作
在上一章中,我们处理了远程仓库的同步问题,了解了 git pull 的默认行为是合并(Merge)。但在实际开发中,频繁的合并会产生大量无意义的“合并气泡”(Merge Bubble),导致历史线错综复杂。本节我们将引入 Rebase(变基)作为替代方案,它能够将分叉的历史重新拉直,但同时也伴随着改写历史的高风险。
10.1.1. 变基的物理本质:寻找最近公共祖先 (LCA) 与补丁重放
Rebase 字面意思是“重新设立基底”。它的核心逻辑并非简单的“移动”,而是“复制并再生”。理解这一点,是掌握 Rebase 的关键。
算法流程解析:当我们在 feature 分支上执行 git rebase master 时,Git 实际上执行了以下三步:
- 寻找 LCA:找到两个分支的最近公共祖先(Lowest Common Ancestor)提交。
- 生成补丁:将
feature分支上从 LCA 之后的所有差异保存为临时补丁(Patch)。 - 基底切换与重放:将当前分支切断,接到
master的最新提交后面,然后依次应用补丁。
【实战演练】搭建 Rebase 模拟实验室
为了安全地练习这些高危操作,我们不应该直接在生产项目中实验。我们需要构建一个完全本地化的“中央-分布式”模拟环境。
环境架构图:
1 | central-repo.git (模拟 GitHub 远程库) |
步骤 1:创建模拟环境
我们将创建一个裸仓库作为服务端,并克隆出两个客户端。
1 | # 1. 创建实验根目录 |
步骤 2:制造分叉历史
现在我们让 Alice 和 Bob 同时开发,制造出典型的分叉场景。
1 | # === Bob 的操作 == = |
此时,历史已经分叉。Bob 的提交超前于 Alice 的基底。
步骤 3:执行变基操作
Alice 想要同步 Bob 的修改,但不想产生合并提交。
1 | # Alice 切换回 feature 分支(如果不在的话) |
执行结果分析:
1 | * 8bff584 (HEAD -> feature/login) feat: login module |
- Before:Alice 的基底是
init,旁边分叉出了 Bob 的fix。 - After:Alice 的
feat: login module被 “剪切” 并 “粘贴” 到了 Bob 的fix: critical bug后面。此时历史变成了一条直线。
10.1.2. 黄金法则与禁区:为何在公共共享分支执行 Rebase 是团队灾难?
Rebase 虽然能让历史变得整洁,但它有一个致命的副作用:修改历史。
每一次 Rebase,Git 都会重新计算提交的 SHA-1 哈希值。这意味着,虽然代码内容没变,但提交的 “身份证号” 变了。对于 Git 来说,它们就是全新的提交。
必须死守的铁律:
绝对不要在公共共享分支(如 master, develop)上执行 Rebase,除非你是该分支的唯一维护者。
灾难演示:
1 | 初始状态(大家都同步): |
如果 Alice 在本地对 master 分支执行了 Rebase 并强制推送到远程:
- 远程的
master历史被重写,旧的提交 A 被新的提交 A’ 取代。 - Bob 的本地
master分支还是基于旧的提交 A。 - 当 Bob 试图拉取代码时,Git 会发现历史不连续,导致大量的冲突甚至代码丢失。
适用场景建议:
| 场景 | 推荐操作 | 原因 |
|---|---|---|
| 本地个人分支 | Rebase | 整理思路,保持提交线清晰,便于 Code Review。 |
| 拉取公共更新 | Rebase | git pull --rebase,避免本地产生无意义的合并节点。 |
| 合并回主分支 | Merge | git merge --no-ff,保留 “特性分支存在过” 的真实历史证据。 |
10.1.3. 冲突中断处理:--continue, --skip, --abort 的状态机流转
Rebase 的冲突处理比 Merge 更繁琐,因为它是 逐个应用补丁。如果有 10 个提交需要 Rebase,理论上你可能需要解决 10 次冲突。
冲突演示:
让我们在模拟环境中制造一个冲突。
1 | # 1. Alice 修改 README(与 Bob 的修改冲突) |
此时终端会输出报错信息:
1 | Auto-merging README.md |
报错翻译:
“冲突(内容):README.md 发生合并冲突。错误:无法应用提交 ‘feat: update readme’。提示:手动解决冲突后标记为已解决,然后运行 continue;或者跳过此提交;或者终止变基。”
此时,你进入了一个“Rebase 中间态”。你必须通过以下状态机命令流转出来:
操作流程:
- 解决冲突:使用
git mergetool,手动保留需要的代码,删除<<<<<<<等标记。 - 标记解决:
git add README.md(注意:不要 执行git commit)。 - 继续流程:
1 | git rebase --continue |
其他两个选项的含义:
git rebase --abort:后悔药。完全撤销变基操作,回到变基前的状态。当你发现冲突太复杂搞不定时,用这个。git rebase --skip:丢弃。丢弃当前正在应用的这个补丁(提交)。通常用于发现“这个提交的修改已经被包含在上游里了”,或者这个提交本身就是错误的。
10.1.4. 自动暂存魔法:配置 rebase.autoStash 避免频繁手动 Stash
Git 不允许在工作区有未提交修改(Dirty Working Directory)时执行 Rebase。
痛点:你正在写代码,突然需要同步远程代码。你不得不:
git stash(暂存修改)git pull --rebasegit stash pop(恢复修改)
解决方案:开启自动暂存配置,让 Git 替你完成这三步。
1 | # 全局开启自动暂存 |
配置后,当工作区有修改时执行 Rebase,Git 会自动创建一个临时 Stash,变基完成后自动 Pop 出来,大大提升开发流畅度。
10.2. 交互式变基 (Interactive):提交历史的显微外科手术
在上一节中,我们学习了如何通过 Rebase 保持历史线性。但在实际工作中,我们的本地开发过程往往是混乱的:为了保存进度而随意提交的 “wip”、拼写错误的 “fix typo”、甚至是不小心提交的敏感文件。
如果说标准的 Rebase 是将这一团混乱“整体搬迁”,那么交互式变基(Interactive Rebase)就是一场精密的显微外科手术。它允许我们在搬迁之前,对提交记录进行 合并、修改、拆分、甚至删除。
10.2.1. 搭建手术台:环境准备与各种脏提交
为了掌握这项技术,我们必须先制造一个“烂摊子”。请在一个空的练习目录下执行以下脚本,它会模拟一个典型的混乱开发场景。
步骤 1:配置 VS Code 为默认编辑器(关键!)
交互式变基需要频繁弹出编辑器来修改指令。为了避免掉入 Vim 的操作陷阱,必须确保 Git 使用我们熟悉的 VS Code,且必须配置 --wait 参数,告诉终端“等待编辑器关闭后再继续”。
1 | # 配置 VS Code 为默认编辑器 |
步骤 2:生成混乱历史
1 | # 初始化练习仓库 |
步骤 3:查看诊断报告
1 | git log --oneline |
你现在的历史应该是这样的(Hash 值会不同,但顺序一致):
1 | e5d4c3b (HEAD -> master) config: database connection <-- 危险文件,需删除 |
10.2.2. 启动手术:指令重排与合并 (Squash/Fixup)
我们的目标是清理最近的 5 个提交(从 config 到 feat: add login)。
操作步骤:
执行命令:
1
2# 对最近的 5 个提交进行交互式变基
git rebase -i HEAD~5VS Code 界面交互:
Git 会自动打开一个名为git-rebase-todo的文件。内容如下:1
2
3
4
5pick 2a3b4c5 feat: add login function
pick 8h9i0j1 wip: layout work
pick 4f5e6d7 fix: typo in css
pick a1b2c3d feat: add logouuuuut function
pick e5d4c3b config: database connection注意:这里的顺序是 从旧到新 排列的(与
git log相反)。最上面的是最早的提交。编写手术脚本:我们需要修改每行开头的
pick关键字。请直接在 VS Code 中修改为以下内容:1
2
3
4
5pick 2a3b4c5 feat: add login function <-- 保持不变
squash 8h9i0j1 wip: layout work <-- 合并到上一个提交
fixup 4f5e6d7 fix: typo in css <-- 合并并丢弃日志
reword a1b2c3d feat: add logouuuuut function <-- 稍后修改提交信息
drop e5d4c3b config: database connection <-- 直接删除该提交术语解析:
- squash (s):将当前提交合并到 上一个 提交中,并保留两条提交的日志,稍后会弹出合并窗口让你整理。
- fixup (f):将当前提交合并到 上一个 提交中,但 直接丢弃 当前提交的日志,仅保留上一个提交的信息。适合处理 “fix typo” 这类无意义记录。
- reword ®:保留提交内容,但暂停 Rebase 让我们修改提交信息。
- drop (d):直接从历史中抹除该提交和对应的文件修改。
执行第一阶段:保存文件(Ctrl+S)并关闭编辑器窗口(Ctrl+W 或点击关闭按钮)。终端会检测到编辑器关闭,开始执行脚本。
10.2.3. 过程干预:修改日志与解决合并
Rebase 脚本开始执行后,会根据我们的指令多次暂停。
干预 1:处理 Squash (合并日志)
Git 遇到 squash 指令后,会再次打开 VS Code,展示合并后的日志预览:
1 | # This is a combination of 2 commits. |
我们需要将其整理为一个干净的提交信息。修改为:
1 | feat: add login function and layout styles |
保存并关闭编辑器。
干预 2:处理 Reword (修正拼写)
紧接着,Git 遇到 reword 指令,再次打开 VS Code。这次是为了那个拼写错误的提交。
将:
1 | feat: add logouuuuut function |
修改为:
1 | feat: add logout function |
保存并关闭编辑器。
干预 3:处理 Drop (可能的冲突)
Git 遇到 drop 指令时,会尝试删除 config: database connection。如果该提交引入的文件(secret.config)在后续没有被其他提交修改,Git 会静默删除成功。
最终验证:
1 | git log --oneline |
输出将变得非常清爽:
1 | [新Hash] feat: add logout function <-- 拼写已修正 |
那个危险的 secret.config 提交也彻底消失了。
10.3. 高级移植技巧:Onto
有时我们需要在不同分支之间精准地“剪切”或“复制”代码片段,而不是合并整个分支。
10.3.1. 手术刀式移植:git rebase --onto
这是一个极易被忽视但极其强大的命令,专门解决“依赖剥离”问题。
场景描述:你基于 main 开发了 feature-A,然后为了省事,直接基于 feature-A 开发了 feature-B。现在的需求是:feature-A 代码质量太差被废弃了,但 feature-B 是好的,需要单独合入 main。
文件位置演示:
1 | src/ |
我们希望把 B.txt 带到 main,同时丢掉 A.txt。
步骤 1:构建依赖环境
1 | # 回到主分支 |
此时的拓扑结构:master -> feature-A -> feature-B
步骤 2:执行精准移植
我们需要告诉 Git:“请把 feature-B 上面,不包含 feature-A 的那部分提交,剪切下来,移植到 master 上。”
1 | # 语法:git rebase --onto <目标基底> <旧父分支> <要移动的分支> |
执行结果:
Git 自动完成了移植。
1 | git log --oneline |
你会发现当前分支只有 feat: B good code,且目录下只有 B.txt(以及 master 原有的文件),A.txt 及其提交彻底消失了。
10.3.2. 跨分支的点对点复制:git cherry-pick
如果说 rebase --onto 是大范围的“剪切并搬迁”,那么 cherry-pick 就是精细化的“复制并粘贴”。
场景描述:你正在 dev 分支开发下一代的大版本功能,期间发现了一个严重的 Bug。你在 dev 分支上顺手提交了一个修复(Commit X)。此时,生产环境的 master 分支报警了,需要立即上线这个修复。
困境:你不能合并 dev 到 master,因为 dev 里还有一大堆没写完的实验性代码。
解法:就像在水果篮里只挑那一颗樱桃一样,我们只把 Commit X “摘”出来,应用到 master 上。
步骤 1:准备模拟场景
1 | # 回到主分支,模拟生产环境 |
步骤 2:定位目标
我们需要找到那个修复 Bug 的提交哈希值。
1 | git log --oneline |
假设输出如下(请记录下 fix: ... 这一行的 Hash,例如 a1b2c3d):
1 | 9f8e7d6 (HEAD -> dev) feat: another feature |
步骤 3:执行“摘樱桃”
1 | # 1. 切换到接收端分支(master) |
执行结果分析:
此时 master 分支上会多出一个新的提交。
- 内容:完全包含了
fix: null pointer exception的修改。 - 哈希:这是一个 全新 的提交,有新的 Hash 值,与
dev分支上的那个互不影响。 - 状态:
dev分支依然保持原样(复制而非剪切)。
【进阶】批量摘取与冲突处理
摘取多个不连续提交:
1
git cherry-pick <Hash A> <Hash B>
摘取连续的一段提交:
1
2
3
4
5# 复制从 A 到 B 的所有提交(不包含 A,包含 B)
git cherry-pick A..B
# 如果要包含 A
git cherry-pick A^..B遇到冲突:与 Rebase 类似,Cherry-pick 可能会遇到冲突。处理逻辑完全一致:
- 手动解决代码冲突。
git add <file>git cherry-pick --continue(注意不是 commit)
10.4 本节小结
核心要点
- 交互式 Rebase 的本质:它是一个脚本执行器。通过修改
git-rebase-todo文件,我们可以重新编排 Git 的执行逻辑。 - VS Code 集成关键:必须配置
core.editor "code --wait",否则 Rebase 会在编辑器打开的瞬间直接结束并报错。 - Onto 的价值:
--onto是解决分支依赖污染的唯一解法,能实现“隔山打牛”式的代码移植。
速查代码
1 | # 1. 交互式变基(最近 N 个提交) |













