云计算百科
云计算领域专业知识百科平台

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

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

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

学完后,你应该能:

  • 说清楚 rebase 和 merge 的区别(用「搬家」的比喻)
  • 用 git rebase 把分支线整理成直线
  • 用 git rebase -i 压缩、改写、重排提交记录
  • 遇到 rebase 冲突时冷静解决
  • 牢记「已推送的提交绝对不 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 的话说:

  • 找到两个分支的分叉点(共同祖先)
  • 把你的分支上每个提交的改动「记忆」下来
  • 先把你的分支指针移到目标分支的最新位置(新基线)
  • 把记忆下来的改动一个一个重新应用上去
  • 每重新应用一次,就产生一个新的提交(哈希值不同)
  • 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 变成了 214be71,f3b398d 变成了 a937ce2——同样的代码改动,但哈希值不同了

    2.3 关键认知:rebase 会产生新提交

    rebase 不是「移动」提交,而是「拆掉重建」。

    每个新提交的哈希值都和原来不同,因为:

    • 父提交变了(从旧基线变成新基线)→ 整个提交的哈希就变了
    • 哈希变了 → 这就是一个全新的提交

    这也是为什么已推送的提交不能 rebase 的根本原因:别人已经基于旧哈希在干活了,你偷偷换成新哈希,别人的世界就崩了。

    2.4 新手最常踩的坑

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

    3. 建议学习顺序

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

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

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

    mkdir lab-rebase
    cd lab-rebase
    git init -b main
    git config user.name "Ada Example"
    git config user.email "ada@example.com"

    验证版本:

    git –version

    git version 2.43.0


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

    5.1 先准备两条分叉的分支

    # 在 main 上建基线
    echo "base content" > file.txt
    git add file.txt && git commit -m "base: 初始文件"

    # main 继续前进:修一个 bug
    echo "bug fix from main" >> file.txt
    git add file.txt && git commit -m "main: 修复一个 bug"

    # 回到分叉点,创建功能分支
    git checkout -b feature 812a70d

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

    # 在 feature 分支上做两个功能提交
    echo "feature A" > feature-a.txt
    git add feature-a.txt && git commit -m "feature: 功能 A"

    echo "feature B" > feature-b.txt
    git add feature-b.txt && git commit -m "feature: 功能 B"

    现在看看历史线:

    git log –graph –all –oneline

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

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

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

    git checkout main && git merge feature

    git log –graph –all –oneline

    * 5a3b1c2 Merge branch 'feature'
    |\\
    | * 45b9122 feature: 功能 B
    | * f3b398d feature: 功能 A
    * | 15c7f3e main: 修复一个 bug
    |/
    * 812a70d base: 初始文件

    白话翻译: merge 产生了一个合并提交,历史线变成了菱形。

    5.3 撤销 merge,改用 rebase 试试

    # 退回去
    git reset –hard 15c7f3e

    # 切到功能分支并 rebase 到 main
    git checkout feature && git rebase main

    Successfully rebased and updated refs/heads/feature.

    看看现在的历史线:

    git log –graph –all –oneline

    * 7d4e8a3 feature: 功能 B
    * a937ce2 feature: 功能 A
    * 15c7f3e main: 修复一个 bug
    * 812a70d base: 初始文件

    白话翻译: 一条直线!功能 A 和 B 的哈希都变了,是「新楼层上重建的新提交」。

    5.4 最后把 feature 合并进 main(快进合并)

    git checkout main && git merge 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 里可以用的动作:

    动作缩写干什么
    pick p 保留这个提交,原样应用
    reword r 保留代码改动,但修改提交信息
    squash s 把这个提交和前一个合并成一条
    fixup f 和 squash 一样,但丢弃这个提交的信息
    drop d 直接扔掉这个提交
    edit e 在这个提交处暂停,你可以改代码后再继续

    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 不看冲突内容就全盘接受,可能覆盖掉别人的代码
    同时开两个终端对同一个仓库做 rebase Git 会打架,状态乱成一锅粥

    铁律:永远不要 rebase 已经推送到共享分支上的提交。 违反这条规则的后果是:团队成员拉代码时会发现历史对不上,要么合不出来,要么重复提交满天飞。如果一定要改已推送的历史,必须和所有相关的人协调好,而且要用 –force-with-lease 而不是 –force。

    8.3 推荐的最小流程(背下来)

    # 1. 确认在功能分支上
    git branch
    # 2. 确认工作区干净
    git status
    # 3. 看一眼当前历史
    git log –oneline -5
    # 4. 执行 rebase
    git rebase main
    # 5. 如果有冲突:
    # – 打开冲突文件,看 <<<<<<< 和 >>>>>>> 之间的内容
    # – 手动改成你想要的结果
    # – git add 冲突文件
    # – git rebase –continue
    # 6. 如果搞砸了:
    # – git rebase –abort(回到第 3 步的状态)
    # 7. 确认结果
    git log –graph –oneline -10


    9. 真实场景速览

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

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

    git checkout feature-login && git rebase main

    你的登录功能就「搬」到包含注册功能的最新代码上面了。如果冲突,按第 8 节的流程解决。

    9.2 提交太多太碎,提交前想整理

    你写代码时习惯每改一点就 commit 一次,现在 git log 一看有 8 个提交,信息全是「改了点东西」「再改改」「应该好了」。

    git rebase -i HEAD~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。

    git rebase -i main
    # 整理好之后
    git push –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

    问 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
    放弃 rebase git 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 翻车了,也还有办法救人。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 16 — 改写历史 rebase:搬家到新楼层,不是合并
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!