git checkout A && git cherry-pick B..C vs git rebase --onto A B C

3 阅读3分钟

git checkout A && git cherry-pick B..C vs git rebase --onto A B C

By Opus4.7

两者搬运的提交集合是一样的(都是 B..C,重放到 A 之上),但**「谁动了、谁没动」完全不同**。这正是它们最关键的区别。

一、执行前的初始状态(统一假设)

        D---E---F---G   (C, HEAD)
       /
      B
     /
A---X---Y                (A 是某个分支或 commit)
  • 当前 checked out 在 C 上。
  • 假设 A、B、C 都是分支名(A、C 是分支,B 可以是分支也可以是 commit)。

二、方式一:git checkout A && git cherry-pick B..C

执行后

        D---E---F---G        (C 不变!仍指向旧的 G)
       /
      B
     /
A---X---Y---D'---E'---F'---G'   (A, HEAD)   ← A 前进了

各分支变化

对象变化
A被改写:A 指针前进到 G',多了 4 个新提交
B❌ 完全不变
C完全不变,仍指向原来的 G
HEAD在 A 上
旧的 D…G仍然挂在 C 上,不是悬空对象,可正常访问
新的 D'…G'新增提交,挂在 A 上

特点

  • C 是「源」,只读不动;A 是「目标」,被推进。
  • 相当于复制B..C 这段提交在仓库里同时存在两份(旧的在 C 链上,新的在 A 链上)。
  • 没有「丢历史」的问题,C 上原有的 reviews/tags/PR 全部完好。

三、方式二:git rebase --onto A B C

执行后

                        (旧的 D E F G 变成悬空对象,无引用)

      BB 上不再有任何 C 的痕迹
     /
A---X---Y---D'---E'---F'---G'   (C, HEAD)   ← C 被移到这里

各分支变化

对象变化
AA 指针不动(A 还是原来那个 commit)
B❌ 完全不变
C被改写:C 现在指向 G',原来的 D…G 被「断开」
HEAD在 C 上
旧的 D…G变成悬空对象,只能通过 reflog 找回
新的 D'…G'挂在 A 之上,是 C 新的提交链

特点

  • C 是「被搬运者」,指针被改写;A 是「目标基底」,指针不动
  • 相当于移动B..C 这段历史只剩一份,旧的那份被丢弃。
  • C 的所有协作者都会看到历史改写,需要强推。

四、一张对比表(核心差异)

维度checkout A + cherry-pick B..Crebase --onto A B C
A 分支✅ 前进,多出 4 个新提交❌ 不动
C 分支❌ 不动✅ 被改写到 G'
HEAD 停留AC
旧 D…G 提交仍由 C 引用,活着变成悬空对象(无引用)
语义复制 B..C 到 A移动 C 到 A 之上
是否需要强推A 的强推取决于 A 是否已发布C 几乎肯定要强推
影响的协作者用 A 的人用 C 的人
冲突处理git cherry-pick --continue/--abort/--skipgit rebase --continue/--abort/--skip
空提交默认报错(需 --allow-empty自动跳过
典型用途把别处的提交「拿过来」加到当前线上把当前分支「搬到」别的基底上

五、一句话区分

  • cherry-pick「我(A)从你(C)那儿抄一份过来」 —— A 变,C 不变。
  • rebase --onto「把你(C)连根挪到 A 后面」 —— C 变,A 不变。

六、什么时候用哪个

cherry-pick B..C 的场景

  • 想保留 C 的历史不动(比如 C 是已发布的 release 分支)。
  • 只想把某段改动额外应用到另一条线(如 hotfix 同步到 main 和 release 两条分支)。
  • A 是当前你想「累积」改动的工作分支。

rebase --onto A B C 的场景

  • 想把 C 整体改基,比如 C 原本基于 B,现在想让它基于 A。
  • 需要把 C 中间一段无关历史(B 之前的部分)剥离掉。
  • C 是个人 feature 分支,强推无压力。

七、一个容易忽略的等价关系

git rebase --onto A B C ≈ 下面这套组合拳:

git checkout A
git cherry-pick B..C       # A 前进到 G'
git branch -f C HEAD       # 强行把 C 指针挪到 G'
git checkout C
git branch -f A <A 原来的 commit>   # 把 A 还原回去

也就是:rebase --onto 的本质 = cherry-pick 复制一份 + 把 C 指针移过去 + 让 A 看起来没动过。理解这个等价式,两条命令的差异就一目了然了。