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 变成悬空对象,无引用)
B ← B 上不再有任何 C 的痕迹
/
A---X---Y---D'---E'---F'---G' (C, HEAD) ← C 被移到这里
各分支变化
| 对象 | 变化 |
|---|---|
| A | ❌ A 指针不动(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..C | rebase --onto A B C |
|---|---|---|
| A 分支 | ✅ 前进,多出 4 个新提交 | ❌ 不动 |
| C 分支 | ❌ 不动 | ✅ 被改写到 G' |
| HEAD 停留 | A | C |
| 旧 D…G 提交 | 仍由 C 引用,活着 | 变成悬空对象(无引用) |
| 语义 | 复制 B..C 到 A | 移动 C 到 A 之上 |
| 是否需要强推 | A 的强推取决于 A 是否已发布 | C 几乎肯定要强推 |
| 影响的协作者 | 用 A 的人 | 用 C 的人 |
| 冲突处理 | git cherry-pick --continue/--abort/--skip | git 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 看起来没动过。理解这个等价式,两条命令的差异就一目了然了。