git rebase --onto 精确变基
本文最后更新于 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。
- 感谢你赐予我前进的力量

