当前位置: 首页 > news >正文

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

写在前面:这一章要解决什么

如果你已经会了分支和合并,心里可能还有一个更具体的困惑:每次 merge 都会多出一个合并提交,分支线缠成一团。有没有办法把代码改动「理成一条直线」?

学完后,你应该能:

  1. 说清楚 rebase 和 merge 的区别(用「搬家」的比喻)
  2. git rebase把分支线整理成直线
  3. git rebase -i压缩、改写、重排提交记录
  4. 遇到 rebase 冲突时冷静解决
  5. 牢记「已推送的提交绝对不 rebase」这条铁律

读者设定:大一同学,已经学过分支和合并,知道 commit、branch、merge 是什么,但看到分叉的历史线会头疼。


1. 定位:为什么要讲 rebase

1.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 的话说:

  1. 找到两个分支的分叉点(共同祖先)
  2. 把你的分支上每个提交的改动「记忆」下来
  3. 先把你的分支指针移到目标分支的最新位置(新基线)
  4. 把记忆下来的改动一个一个重新应用上去
  5. 每重新应用一次,就产生一个新的提交(哈希值不同)

2.2 看图说话

图:merge 保留分叉,rebase 变成直线。

merge 之后的历史:

* 5a3b1c2 Merge branch 'feature' |\ | * 45b9122 feat B | * f3b398d feat A * | 15c7f3e main fix |/ * 812a70d base

rebase 之后的历史:

* 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别人拉代码时冲突地狱铁律:只对自己的本地分支 rebase
rebase 过程中慌了不知道怎么退卡在中间状态git rebase --abort一键回到 rebase 之前
rebase 冲突时没搞清要保留什么改错了更麻烦冲突标记和 merge 一样,先看懂再改
以为 rebase 能代替一切不该 rebase 的地方乱用公共分支用 merge,私人的功能分支才用 rebase

3. 建议学习顺序

  1. 先看第 2 节搞懂「搬家」比喻
  2. 跟着第 5 节做一遍基础 rebase
  3. 再学交互式 rebase
  4. 最后学冲突处理和安全规矩

4. 动手准备(建可丢弃目录)

请找一个可以随便删的练习目录,不要用正在交的作业仓库练手。

mkdirlab-rebasecdlab-rebasegitinit-bmaingitconfig user.name"Ada Example"gitconfig user.email"ada@example.com"

验证版本:

git--version
git version 2.43.0

5. 跟着做:一次完整的 rebase 实验

5.1 先准备两条分叉的分支

# 在 main 上建基线echo"base content">file.txtgitaddfile.txt&&gitcommit-m"base: 初始文件"# main 继续前进:修一个 bugecho"bug fix from main">>file.txtgitaddfile.txt&&gitcommit-m"main: 修复一个 bug"# 回到分叉点,创建功能分支gitcheckout-bfeature 812a70d

白话翻译:-b feature创建并切换到 feature 分支,812a70d是分叉点。

# 在 feature 分支上做两个功能提交echo"feature A">feature-a.txtgitaddfeature-a.txt&&gitcommit-m"feature: 功能 A"echo"feature B">feature-b.txtgitaddfeature-b.txt&&gitcommit-m"feature: 功能 B"

现在看看历史线:

gitlog--graph--all--oneline
* 45b9122 feature: 功能 B * f3b398d feature: 功能 A | * 15c7f3e main: 修复一个 bug |/ * 812a70d base: 初始文件

白话翻译:|/就是分叉点,两条线从812a70d分开。

5.2 用 merge 合并(先看看老办法)

gitcheckout main&&gitmerge feature
gitlog--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 feature&&gitrebase main
Successfully 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 main&&gitmerge feature
Updating 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-10

9. 真实场景速览

9.1 课程大作业:功能分支要同步主分支

你和组员协作写大作业,你的feature-login分支写了两天,Meanwhile 组员已经把注册功能合并到 main 了。

gitcheckout feature-login&&gitrebase 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. 进阶补充(选读)

  1. rebase 的底层原理:rebase 其实是一连串git cherry-pick。它找到分叉点后,依次把你分支上的每个提交「摘」下来,在新基线上重新「种」上去。
  2. onto 参数:git rebase --onto new-base upstream branch可以把 branch 上从 upstream 之后的提交搬到 new-base 上,适合从一个大功能分支里「切」出一部分提交。
  3. Autosquash:如果你提交时用了git commit --fixup=哈希--squash=哈希,rebase 时加--autosquash参数,Git 会自动把 fixup 提交放到对应的目标提交旁边。
  4. rebase 和 merge 可以共存:团队里常用的工作流是「本地用 rebase 保持直线,合进 main 时用 merge 保留分叉记录」。不用非此即彼。

11. 小实验(动手练习 + 通过标准)

实验甲:基础 rebase——把分叉变成直线

  1. 建一个可丢弃目录,在 main 上提交一个基线文件
  2. main 上再提交一次(比如修个 bug)
  3. 从基线处创建 feature 分支,做两个功能提交
  4. git log --graph --all --oneline确认看到分叉
  5. 在 feature 分支上执行git rebase main
  6. 再看git log --graph --all --oneline,确认变成一条直线

通过标准:rebase 后历史线是一条直线,没有合并提交,功能提交的哈希值变了。

实验乙:交互式 rebase——压缩提交

  1. 在一个分支上连续提交 3 次,信息分别是「步骤一」「步骤二」「步骤三」
  2. 执行git rebase -i HEAD~3
  3. 把第二个和第三个的pick改成squash
  4. 在弹出的编辑器里编辑合并后的提交信息
  5. 确认git log --oneline只剩 1 个提交

通过标准:3 个提交压缩成 1 个,代码内容不变。

实验丙:rebase 冲突解决

  1. 在 main 和 feature 分支上修改同一个文件的同一行,写不同内容
  2. 在 feature 分支上执行git rebase main
  3. 看到冲突提示后,打开文件查看<<<<<<<标记
  4. 手动解决冲突(保留你想要的内容,删掉标记)
  5. git add冲突文件,然后git rebase --continue
  6. 确认 rebase 完成,历史线是直线

通过标准:能独立从冲突状态走到 rebase 完成,最终代码是你期望的内容。

实验丁:rebase abort——随时能跑

  1. 制造一个 rebase 冲突(同实验丙前两步)
  2. 看到冲突后,不解决,直接执行git rebase --abort
  3. 确认git log --graph --oneline和 rebase 之前一模一样

通过标准:abort 后仓库完整回到 rebase 之前的状态,没有任何残留。


12. 常见问题 FAQ

问 1:rebase 和 merge 到底哪个更好?
没有绝对的好坏。merge 保留真实历史,rebase 让历史更整洁。自己的功能分支用 rebase 整理,合进公共分支用 merge 记录。两条路最终的代码是一样的。

问 2:为什么 rebase 后提交的哈希值变了?
因为提交的哈希是根据「内容 + 父提交 + 作者信息 + 时间」算出来的。rebase 改变了父提交,所以哈希必然不同。代码改动不变,但提交对象是全新的。

问 3:rebase 到一半不想做了怎么办?
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"

问 6:squash 和 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-rebase
  • Git 术语表

命令输出样例验证环境:Git 2.43.0;演示作者信息为虚构:Ada Example <ada@example.com>

13.4 检查清单

  • 能用「搬家到新楼层」的比喻解释 rebase 给同学听
  • 能说出 rebase 和 merge 的 3 个区别
  • 能独立完成一次基础 rebase(不分叉变成直线)
  • 能用交互式 rebase 压缩多个提交
  • 遇到 rebase 冲突不慌,知道如何解决
  • 知道--abort可以随时放弃 rebase
  • 牢记铁律:不 rebase 已推送到共享分支的提交
  • 完成 4 个小实验

rebase 给了你改写历史的超能力,但「能力越大,责任越大」——只改自己的历史,不要动别人的。下一章我们讲 reflog:就算 rebase 翻车了,也还有办法救人。

http://www.jsqmd.com/news/1380440/

相关文章:

  • JavaScript基础语法深度解析:从变量声明到异步编程的完整指南
  • AI多Agent协作系统实战(二十七):一个CSS属性引发的连环崩坏——loading-shimmer透明文字到script标签全面错乱
  • 机器视觉硬件选型实战:从相机、镜头到光源的完整指南
  • 微信小程序 page-container 与 share-element 组件实战:提升交互质感与转场动画
  • AI驱动测试:从TDD到智能测试的演进与实践
  • 2026鹤岗外墙漏水避坑指南 - 管道一点通
  • 2026芜湖外墙漏水避坑指南 - 房屋修缮
  • 数据结构与算法核心要点及工程实践解析
  • VMOS Pro + Xposed + 小黄鸟:绕过SSL证书绑定实现安卓应用抓包
  • Unity游戏资源逆向提取实战:AssetRipper原理、应用与问题修复全解析
  • 暑假日训【二叉树/链表】
  • 线路板曝光机如何决定PCB制造精度上限
  • PostgreSQL 官方 Windows 安装
  • 在现实环境中,你能够稳定完成什么事情。
  • springboot 智能用药提醒与药物相互作用预警平台
  • Arduino IDE驱动ATTINY13A:从硬件连接到低功耗编程全攻略
  • 2026镇江外墙漏水避坑指南 - 房屋修缮
  • day26
  • 【项目编号:project85223】毕业设计选题推荐|Django电商用户行为数据分析及可视化平台
  • 四大音乐平台统一API:如何用一套代码解决多平台音乐资源获取难题
  • SerialPlot终极指南:从串口数据到实时可视化的完整解决方案
  • 2026乌兰察布外墙漏水避坑指南 - 管道一点通
  • VMware虚拟机共享目录配置:Linux挂载与权限问题解决指南
  • PKC 第 070 个开关:摇一摇隐藏昵称的位置、验证方法与风险边界
  • PKC 第 097 个开关:自动更新个性签名的位置、验证方法与风险边界
  • 脱离现实的能力,很容易产生错觉。
  • ArcGIS水文分析自动化:从ModelBuilder到Python脚本工具的完整构建指南
  • UML建模在在线购物系统开发中的应用与实践
  • PKC 第 071 个开关:自动领利是的位置、验证方法与风险边界
  • 深入解析MFC文档/视图架构:从核心原理到BCG界面集成实践