本文最后更新于 2026-08-27,文章好久没更新了。

git rebase --onto 精确变基

git rebase --onto 是 rebase 的「精确定点」用法:不再是笼统地"把整个分支搬到最新 main 之上",而是手动指定三件事——把哪一段提交,接到哪个新基底上。它常用来从分支中摘掉/转移一段提交,是普通 rebase 和 cherry-pick 都不方便覆盖的场景。

前置知识:Git 重写提交历史(rebase)、cherry-pick挑拣提交。

命令形式

git rebase --onto <newbase> <upstream> [<branch>]

三个参数的含义(这是理解本命令的全部关键):

参数 含义 遗漏时的默认值
<newbase> 提交要接到的新基底 无
<upstream> 范围起点(不含):从此提交之后开始搬提交 必填
<branch> 范围终点:该分支上直到 HEAD 的所有提交被搬运;操作完成后检出/指向该分支 缺省为当前分支

即:把 (upstream, branch] 这一段左开右闭区间内的提交,逐条重放到 newbase 上。

一句话记忆:

在 <branch> 分支上、<upstream> 之后的那串提交,掐头去尾,接苗到 <newbase> 上。

注意 <upstream> 本身不被包含在内,所以实际书写时通常写「你不想要的第一笔提交」或「想保留段落的父提交」。

与普通 rebase 的对比

下面这张图最能说明差别——同样的三个参数习惯在两种命令里语义不同:

%%{init: {'gitGraph': {'mainBranchName': 'main'}} }%%
gitGraph
   commit id: "A"
   commit id: "B"
   commit id: "C" tag: "旧起点"
   branch feature
   checkout feature
   commit id: "D"
   commit id: "E"
   commit id: "F"

当前分支结构(字母即 hash 代号,C 为拉出分支时的旧起点):

A---B---C  main(旧起点)
         \
          D---E---F  feature
命令 搬运范围 结果
git rebase main feature 上所有不属于 main 的提交(D·E·F) D’·E’·F’ 接在 main 最新处
git rebase --onto B C feature C 之后到 feature 为止(D·E·F) 同上——但基底是 B,不取 main 新提交
git rebase --onto main E feature 只剩 F 一段 F’ 接到 main(甩掉 D·E)
git rebase --onto C D feature 仅 E·F E’·F’ 接回 C(摘掉 D)

普通 rebase 自动计算 <upstream>(两分支的合并基底 merge-base),因此往往"多搬";--onto 把 upstream 交给你自己指定,才能做到上面第 4 行那种"只搬中间一段"的精细活。

典型场景

场景一:分支拉错了起点

建 feature 时错误地基于了 dev(它还带着未合入的实验提交),你想基于干净的基线 devbase 重新开发。

             A---B---G  dev(G 为未合入的实验提交)
              \
               C---D  feature   ← 本想基于 devbase(A---B)开发

直接 git rebase --onto devbase dev 会把实验提交 G 也搬过去;普通 git rebase devbase 同样可能带上不想要的支线。正确做法是明确告诉 Git:只搬 feature 上、dev 之后的部分:

git rebase --onto devbase dev feature
# 等价写法:把「不想带上的最后一笔 G」之后的内容搬走
git rebase --onto devbase G feature

结果:

devbase:    A---B
                 \
                  C'---D'  feature

场景二:从一个大分支里切出小 PR

你一口气写完了 A→F 六笔提交,但只想先把其中两笔 PR 单独送审:

main:    A---B
                 \
                  C---D---E---F   big-feature

做法:为这两个 PR 各建新分支,用 --onto 搬运目标段:

# PR1:把 C、D 两笔单独拎出来基于 main
git branch pr1 big-feature          # 指向 D
git rebase --onto main B pr1       # 把 (B, D] 即 C~D 接到 main 上

# PR2:把 E、F 也独立出来,同样基于 main
git branch pr2 big-feature          # 指向 F
git rebase --onto main D pr2

原 big-feature 先不动,等两个 PR 都合入后再整体整理。

场景三:摘掉中间几笔坏提交

分支上是 C-D-E-F,其中 D、E 闯了祸不想留,但希望 C、F(及历史)结构尽量保持:

# (D, feature] 即 E、F 接到 C 上,D 被排除
git rebase --onto C D feature

若要砍的不止两笔、还需要改内容或调顺序,用交互式更直观:

git rebase -i <坏提交的父提交>   # 在列表里把对应行改为 drop

若想连 F 也一起甩掉(只要保留到 C),见场景五的 HEAD~K 简写。

场景四:vendor 分支更新后重放本地补丁

本地在 vendor-3.8 之上打了若干补丁,供应商已发布 vendor-3.9,需要把整条补丁队列搬到新版本之上:

# 把 (vendor-3.8, my-patches] 区间的本地补丁,重放到 vendor-3.9 顶部
git rebase --onto vendor-3.9 vendor-3.8 my-patches

一行命令即完成「补丁搬家」,比先 diff 出补丁再逐个 cherry-pick 高效得多。这也是 Yocto、Buildroot 等维护第三方源码补丁集(patch queue)时的常用手法。

场景五:把最近 K 笔提交搬到新基底

当前分支最近的 3 笔提交想整体接到 main 最新处(省略 <branch>,即操作当前分支):

git rebase --onto main HEAD~3

HEAD~3 作为 <upstream> 表示「3 笔之前的那个提交」,它本身被排除在搬运范围之外,因此只有最近 3 笔会被重放到 main 顶部。

常见问题

Q: rebase 中途冲突了怎么办?

正常的 rebase 冲突处理同样适用 --onto 形式:

# 解决冲突后
git add <files>
git rebase --continue     # 继续重放下一条提交

# 实在不想继续,放弃本次搬运,回到出发时的状态
git rebase --abort

Q: 找错 upstream 导致「提交凭空消失」

若把本应保留的提交写进了「排除区间」,这些提交只是不再位于新分支历史里,commit 对象依然存在,可从 reflog 或原分支名找回:

git reflog
git branch rescue abc1234      # 用原 hash 重建指针即可恢复

这也是为什么动手前强烈建议打个备份分支(见 rebase_history.md 的安全事项)。

Q: 与 cherry-pick 怎么选?

如果…… 更适合
搬整段连续的提交序列 rebase --onto
从中挑选零散的个别提交 cherry-pick A C F
只是本地整理自己的 feature 分支基线 git rebase main 就够

--onto 属于 rebase 家族,语义上重点在于"范围的迁移";当你要的就是"几个孤立提交"时,别硬凑参数,用 cherry-pick 更不容易出错。

Q: 为什么 hash 变了?可以强推吗?

任何 rebase 都会重放生成新提交,hash 必然改变。已推送的分支按下述方式推送(比 --force 安全,参考 rebase_history.md):

git push --force-with-lease origin <branch>

命令速查

# 完整形式:把 (upstream, branch] 接到 newbase
git rebase --onto <newbase> <upstream> <branch>

# 当前分支简写(省略 branch)
git rebase --onto <newbase> <upstream>

# 最近 K 笔全部搬到新基底
git rebase --onto main HEAD~K

# 补丁队列升级:把 old-V 之后的本地补丁重放到 new-V 上
git rebase --onto vendor-3.9 vendor-3.8 my-patches

# 卡住后的常规三件套
git rebase --continue
git rebase --abort
git reflog           # 救援入口

小结

  • git rebase --onto X Y Z = 把 (Y, Z] 区间的提交搬到 X 顶部;Y 不含是最易错点。
  • 主战场:分支换爹、切出小 PR、扔掉中间坏提交、补丁队列升级重放。
  • 动手前后照例:工作区干净、打备份分支、出问题靠 reflog + --abort 兜底。
  • 连续区间用 --onto,零散单发用 cherry-pick,整线搬到新 main 用普通 rebase。