ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

16 — 改写历史 rebase:搬家到新楼层,不是合并

2026/8/12 19:36:39 拓冰建站 浏览量
16 — 改写历史 rebase:搬家到新楼层,不是合并 写在前面这一章要解决什么如果你已经会了分支和合并心里可能还有一个更具体的困惑每次 merge 都会多出一个合并提交分支线缠成一团。有没有办法把代码改动「理成一条直线」学完后你应该能说清楚 rebase 和 merge 的区别用「搬家」的比喻用git rebase把分支线整理成直线用git rebase -i压缩、改写、重排提交记录遇到 rebase 冲突时冷静解决牢记「已推送的提交绝对不 rebase」这条铁律读者设定大一同学已经学过分支和合并知道 commit、branch、merge 是什么但看到分叉的历史线会头疼。1. 定位为什么要讲 rebase1.1 一句话先记住rebase 把你的改动搬家到别人最新代码的上面让历史变成一条直线。不是把两份代码「和稀泥」混在一起那是 merge而是把你的提交一个一个「搬到」新基线的楼上。1.2 没有 rebase 会怎样痛点场景表场景没有 rebase 的痛有了 rebase 之后功能分支开发三天主分支已更新merge 产生合并提交历史线分叉又汇合rebase 让你的提交变成一条直线提交了 5 次小修改全是「修 typo」历史全是「修 typo」「又修 typo」丢人交互式 rebase 把 5 个 squash 成 1 个想调整提交顺序只能删了重来git rebase -i拖一拖顺序就行写错提交信息改不了git rebase -i选 reword只改文字代码审查时历史太乱审查的人看得眼花rebase 之后一条线审查更轻松1.3 和你已经会的事对比你已经会了git merge把两个分支的代码合到一起会产生一个合并提交历史会分叉再汇合。对比项merge你已经会的rebase这一章要学的历史线形状分叉 汇合有合并提交一条直线没有合并提交本质两个分支「和稀泥」你的提交「搬家」到新基线上面结果代码一样一样安全性对已推送分支安全对已推送分支危险适用场景合并公共分支整理自己的功能分支小提示最终代码内容是一样的只是历史的书写方式不同。merge 保留了「分叉过」这个事实rebase 改写了历史让你假装「一切都是在最新代码上顺滑开发的」。2. 本质rebase 到底是什么白话 比喻 图解2.1 搬家到新楼层主分支 main是一楼最近有人翻修了地面换成了新地板。你的功能分支是二楼你在旧地板上装修了自己的房间。现在你想让你的房间建在新地板上面。怎么办merge在一楼和二楼之间搭一个空中走廊两个楼层都保留走廊就是合并提交。rebase把你的整个房间搬到新地板上面。你的房间不变但下面的地基变成了最新的。用 Git 的话说找到两个分支的分叉点共同祖先把你的分支上每个提交的改动「记忆」下来先把你的分支指针移到目标分支的最新位置新基线把记忆下来的改动一个一个重新应用上去每重新应用一次就产生一个新的提交哈希值不同2.2 看图说话图merge 保留分叉rebase 变成直线。merge 之后的历史* 5a3b1c2 Merge branch feature |\ | * 45b9122 feat B | * f3b398d feat A * | 15c7f3e main fix |/ * 812a70d baserebase 之后的历史* 214be71 feat B新哈希 * a937ce2 feat A新哈希 * 15c7f3e main fix * 812a70d base白话翻译merge 后有个菱形分叉5a3b1c2是合并提交rebase 后一条直线但45b9122变成了214be71f3b398d变成了a937ce2——同样的代码改动但哈希值不同了2.3 关键认知rebase 会产生新提交rebase 不是「移动」提交而是「拆掉重建」。每个新提交的哈希值都和原来不同因为父提交变了从旧基线变成新基线→ 整个提交的哈希就变了哈希变了 → 这就是一个全新的提交这也是为什么已推送的提交不能 rebase的根本原因别人已经基于旧哈希在干活了你偷偷换成新哈希别人的世界就崩了。2.4 新手最常踩的坑坑后果怎么避对已推送的分支执行 rebase别人拉代码时冲突地狱铁律只对自己的本地分支 rebaserebase 过程中慌了不知道怎么退卡在中间状态git rebase --abort一键回到 rebase 之前rebase 冲突时没搞清要保留什么改错了更麻烦冲突标记和 merge 一样先看懂再改以为 rebase 能代替一切不该 rebase 的地方乱用公共分支用 merge私人的功能分支才用 rebase3. 建议学习顺序先看第 2 节搞懂「搬家」比喻跟着第 5 节做一遍基础 rebase再学交互式 rebase最后学冲突处理和安全规矩4. 动手准备建可丢弃目录请找一个可以随便删的练习目录不要用正在交的作业仓库练手。mkdirlab-rebasecdlab-rebasegitinit-bmaingitconfig user.nameAda Examplegitconfig user.emailadaexample.com验证版本git--versiongit version 2.43.05. 跟着做一次完整的 rebase 实验5.1 先准备两条分叉的分支# 在 main 上建基线echobase contentfile.txtgitaddfile.txtgitcommit-mbase: 初始文件# main 继续前进修一个 bugechobug fix from mainfile.txtgitaddfile.txtgitcommit-mmain: 修复一个 bug# 回到分叉点创建功能分支gitcheckout-bfeature 812a70d白话翻译-b feature创建并切换到 feature 分支812a70d是分叉点。# 在 feature 分支上做两个功能提交echofeature Afeature-a.txtgitaddfeature-a.txtgitcommit-mfeature: 功能 Aechofeature Bfeature-b.txtgitaddfeature-b.txtgitcommit-mfeature: 功能 B现在看看历史线gitlog--graph--all--oneline* 45b9122 feature: 功能 B * f3b398d feature: 功能 A | * 15c7f3e main: 修复一个 bug |/ * 812a70d base: 初始文件白话翻译|/就是分叉点两条线从812a70d分开。5.2 用 merge 合并先看看老办法gitcheckout maingitmerge featuregitlog--graph--all--oneline* 5a3b1c2 Merge branch feature |\ | * 45b9122 feature: 功能 B | * f3b398d feature: 功能 A * | 15c7f3e main: 修复一个 bug |/ * 812a70d base: 初始文件白话翻译merge 产生了一个合并提交历史线变成了菱形。5.3 撤销 merge改用 rebase 试试# 退回去gitreset--hard15c7f3e# 切到功能分支并 rebase 到 maingitcheckout featuregitrebase mainSuccessfully rebased and updated refs/heads/feature.看看现在的历史线gitlog--graph--all--oneline* 7d4e8a3 feature: 功能 B * a937ce2 feature: 功能 A * 15c7f3e main: 修复一个 bug * 812a70d base: 初始文件白话翻译一条直线功能 A 和 B 的哈希都变了是「新楼层上重建的新提交」。5.4 最后把 feature 合并进 main快进合并gitcheckout maingitmerge featureUpdating 15c7f3e..7d4e8a3 Fast-forward白话翻译Fast-forward表示快进合并没有合并提交历史线完全是一条直线。6. 命令分组按场景分组6.1 基本 rebase日常最常用命令干什么git rebase main把当前分支的提交搬到 main 的最新代码上面git rebase main feature不管当前在哪个分支把 feature 搬到 main 上面git rebase --abort放弃整个 rebase回到 rebase 之前git rebase --continue解决冲突后继续执行剩余的 rebase 步骤6.2 交互式 rebase改写提交历史命令干什么git rebase -i HEAD~3交互式重做最近 3 个提交git rebase -i main交互式把当前分支搬到 main 上面同时可以改写每个提交交互式 rebase 里可以用的动作动作缩写干什么pickp保留这个提交原样应用rewordr保留代码改动但修改提交信息squashs把这个提交和前一个合并成一条fixupf和 squash 一样但丢弃这个提交的信息dropd直接扔掉这个提交edite在这个提交处暂停你可以改代码后再继续6.3 取消和挽救命令干什么git rebase --abort放弃本次 rebase回到 rebase 之前git rebase --skip跳过当前正在处理的提交慎用git reflog查看 HEAD 的移动历史找回 rebase 之前的旧提交git reset --hard 旧哈希用 reflog 找到旧哈希后回退到 rebase 之前7. 对照表前后对比 / 选项对比7.1 rebase vs merge什么时候用哪个场景用 rebase用 merge自己的功能分支要同步主分支的最新代码推荐——历史更干净也行但会有合并提交把功能分支合进 main先 rebase 再快进合并直接 merge 也完全可以多人同时在用的公共分支绝对不要用 merge想保留「同时开发了两个功能」这一事实rebase 会丢掉这个信息merge 保留了分叉开源项目提交 PR很多项目要求先 rebase看项目规定7.2 交互式 rebase 的动作对照你想干什么选什么动作举个例子这个提交没问题原样保留pick默认就是 pick提交信息写错了想改reword「fix bug」→「修复登录页面验证码不显示的 bug」3 个小提交想合成 1 个squash「修 typo」「又修」「再修」→「修复文档中的拼写错误」和前一个合并但不要这个提交的信息fixup中间步骤的提交信息没价值这个提交不要了drop试试看的代码后来没用上想修改这个提交的代码edit提交里少改了一个文件补上7.3 冲突标记对照图冲突标记长这样。标记含义白话解释 HEAD当前分支rebase 时是你正在重放的提交「你」的版本分隔线上面是你的下面是别人的 feature被合并进来的分支「别人」的版本8. 安全习惯硬规矩 踩坑提醒8.1 建议这样做习惯原因只对自己的、还没推送的分支做 rebase没人基于你的旧提交在干活改写历史不会影响别人rebase 之前先git status确认工作区干净脏工作区会导致 rebase 出错或中途卡住rebase 之前先看一下分支线git log --graph确认你知道要 rebase 的范围冲突时冷静看标记想清楚要保留什么别急着删先理解rebase 失败了先--abort回到原点重新来比硬着头皮走下去安全8.2 绝对不要这样做禁止操作后果对已推送到远程的公共分支执行 rebase别人的本地历史和你不一样了拉代码时冲突地狱在 rebase 中途用--skip随意跳过提交你跳过的那个提交的代码改动就丢了rebase 时遇到冲突直接git add .git rebase --continue不看冲突内容就全盘接受可能覆盖掉别人的代码同时开两个终端对同一个仓库做 rebaseGit 会打架状态乱成一锅粥铁律永远不要 rebase 已经推送到共享分支上的提交。违反这条规则的后果是团队成员拉代码时会发现历史对不上要么合不出来要么重复提交满天飞。如果一定要改已推送的历史必须和所有相关的人协调好而且要用--force-with-lease而不是--force。8.3 推荐的最小流程背下来# 1. 确认在功能分支上gitbranch# 2. 确认工作区干净gitstatus# 3. 看一眼当前历史gitlog--oneline-5# 4. 执行 rebasegitrebase main# 5. 如果有冲突# - 打开冲突文件看 和 之间的内容# - 手动改成你想要的结果# - git add 冲突文件# - git rebase --continue# 6. 如果搞砸了# - git rebase --abort回到第 3 步的状态# 7. 确认结果gitlog--graph--oneline-109. 真实场景速览9.1 课程大作业功能分支要同步主分支你和组员协作写大作业你的feature-login分支写了两天Meanwhile 组员已经把注册功能合并到 main 了。gitcheckout feature-logingitrebase main你的登录功能就「搬」到包含注册功能的最新代码上面了。如果冲突按第 8 节的流程解决。9.2 提交太多太碎提交前想整理你写代码时习惯每改一点就 commit 一次现在git log一看有 8 个提交信息全是「改了点东西」「再改改」「应该好了」。gitrebase-iHEAD~8在编辑器里把 8 个提交的pick改成pick a1b2c3d 实现登录验证 squash e4f5g6h 改了点东西 squash i7j8k9l 再改改 squash m0n1o2p 应该好了 pick q3r4s5t 实现注册页面 squash t5u6v7w 修了注册的 typo squash w8x9y0z 又修 drop a1b2c3e 这次改没用保存退出后Git 会依次处理前 4 个合成 1 个第 5-7 个合成 1 个第 8 个直接删掉。最终 8 个提交变成 2 个干干净净。9.3 代码审查前整理历史提 PR 之前用git rebase -i把零碎的提交压缩成几个有意义的提交。改完之后 force push 到你自己的 fork。gitrebase-imain# 整理好之后gitpush --force-with-lease origin feature-branch--force-with-lease比--force安全——如果别人在你之后也推了代码它会拒绝推送不会覆盖别人的工作。10. 进阶补充选读rebase 的底层原理rebase 其实是一连串git cherry-pick。它找到分叉点后依次把你分支上的每个提交「摘」下来在新基线上重新「种」上去。onto 参数git rebase --onto new-base upstream branch可以把 branch 上从 upstream 之后的提交搬到 new-base 上适合从一个大功能分支里「切」出一部分提交。Autosquash如果你提交时用了git commit --fixup哈希或--squash哈希rebase 时加--autosquash参数Git 会自动把 fixup 提交放到对应的目标提交旁边。rebase 和 merge 可以共存团队里常用的工作流是「本地用 rebase 保持直线合进 main 时用 merge 保留分叉记录」。不用非此即彼。11. 小实验动手练习 通过标准实验甲基础 rebase——把分叉变成直线建一个可丢弃目录在 main 上提交一个基线文件main 上再提交一次比如修个 bug从基线处创建 feature 分支做两个功能提交用git log --graph --all --oneline确认看到分叉在 feature 分支上执行git rebase main再看git log --graph --all --oneline确认变成一条直线通过标准rebase 后历史线是一条直线没有合并提交功能提交的哈希值变了。实验乙交互式 rebase——压缩提交在一个分支上连续提交 3 次信息分别是「步骤一」「步骤二」「步骤三」执行git rebase -i HEAD~3把第二个和第三个的pick改成squash在弹出的编辑器里编辑合并后的提交信息确认git log --oneline只剩 1 个提交通过标准3 个提交压缩成 1 个代码内容不变。实验丙rebase 冲突解决在 main 和 feature 分支上修改同一个文件的同一行写不同内容在 feature 分支上执行git rebase main看到冲突提示后打开文件查看标记手动解决冲突保留你想要的内容删掉标记git add冲突文件然后git rebase --continue确认 rebase 完成历史线是直线通过标准能独立从冲突状态走到 rebase 完成最终代码是你期望的内容。实验丁rebase abort——随时能跑制造一个 rebase 冲突同实验丙前两步看到冲突后不解决直接执行git rebase --abort确认git log --graph --oneline和 rebase 之前一模一样通过标准abort 后仓库完整回到 rebase 之前的状态没有任何残留。12. 常见问题 FAQ问 1rebase 和 merge 到底哪个更好没有绝对的好坏。merge 保留真实历史rebase 让历史更整洁。自己的功能分支用 rebase 整理合进公共分支用 merge 记录。两条路最终的代码是一样的。问 2为什么 rebase 后提交的哈希值变了因为提交的哈希是根据「内容 父提交 作者信息 时间」算出来的。rebase 改变了父提交所以哈希必然不同。代码改动不变但提交对象是全新的。问 3rebase 到一半不想做了怎么办git rebase --abort一秒回到 rebase 之前。这是 rebase 的安全网大胆用。问 4我在 rebase 时冲突了怎么知道该保留谁的 HEAD和之间是你当前正在重放的提交的代码和之间是新基线上的代码。根据实际情况决定保留哪部分。问 5交互式 rebase 弹出的编辑器我不会用怎么办默认编辑器可能是 vim。进去后按i进入编辑模式改完按Esc输入:wq保存退出。不习惯的话可以设置编辑器git config core.editor code --wait用 VS Code或git config core.editor nano。问 6squash 和 fixup 有什么区别squash 会把两个提交的提交信息都保留让你编辑合并后的信息。fixup 直接丢弃被合并提交的信息只保留第一个提交的信息。13. 总结13.1 一页速记点记住什么rebase 是什么把你的提交「搬家」到新基线上面历史变直线和 merge 的区别merge 保留分叉rebase 改写成直线最终代码一样哈希会变搬家 拆掉重建提交是新的哈希不同交互式 rebase-i参数可以 squash / reword / drop / reorder冲突处理和 merge 一样看标记解决后add--continue放弃 rebasegit rebase --abort随时退回铁律永远不要 rebase 已推送到共享分支的提交什么时候用自己的本地功能分支同步主分支 / 整理提交 / 代码审查前找回旧提交git reflog查看git reset --hard回退13.2 思维升华rebase 改写的是「历史怎么讲」不是「代码是什么」。同样的代码merge 讲了一个「两条路汇合」的故事rebase 讲了一个「一路向上」的故事。故事不同结局一样。但记住公共的历史不能你一个人偷偷改写——改故事之前先确认只有你一个人在读这本书。13.3 延伸阅读Pro Git 中文版 — 变基Git 官方文档 — git-rebaseGit 术语表命令输出样例验证环境Git 2.43.0演示作者信息为虚构Ada Example adaexample.com。13.4 检查清单能用「搬家到新楼层」的比喻解释 rebase 给同学听能说出 rebase 和 merge 的 3 个区别能独立完成一次基础 rebase不分叉变成直线能用交互式 rebase 压缩多个提交遇到 rebase 冲突不慌知道如何解决知道--abort可以随时放弃 rebase牢记铁律不 rebase 已推送到共享分支的提交完成 4 个小实验rebase 给了你改写历史的超能力但「能力越大责任越大」——只改自己的历史不要动别人的。下一章我们讲 reflog就算 rebase 翻车了也还有办法救人。