git的cherry-pick的使用
1. Cherry-pick 是什么,何时该用
一句话:把别处的某几个 commit 复制到当前分支,每次产生新的 commit hash。
跟 merge / rebase 的本质区别在粒度:
- merge / rebase 处理整条分支的所有提交
- cherry-pick 处理你点名的几个提交
什么时候考虑它:
- 线上 bug 修复在 develop 上做了,需要同步到 release / main(hotfix 回灌)
- 跨仓库 / 跨分支只想要某个同事的某次提交
- release 分支上挑选验证通过的功能上线
什么时候别用它(先想其他方案):
- 想把整条 feature 分支搬过去,用 merge / rebase
- 想撤销某个已 push 的提交,用 revert
- 长生命分支日常同步,用 merge,cherry-pick 会埋陷阱(见第 3 节)
记一句:cherry-pick 是「挑几个」,不是「搬一片」。下面进入具体用法。
2. 基础用法
2.1 单个与范围
1 | # 单个 |
⚠️ A..B 是半开区间,A 不会被挑过去。要带上 A,写 A^..B。这一条踩过的人不少。
冲突处理是 git cherry-pick --continue / --abort / --skip 三连,跟 rebase / merge 是同一套,不展开。
2.2 几个值得记的 Flag
-x,回灌场景必加。在新 commit 的 message 里追加一行 (cherry picked from commit abc1234),记录来源。否则三个月后你想知道 main 上某个 fix 来自哪里,只能靠考古。
1 | git cherry-pick -x abc1234 |
-n / --no-commit,把变更挑过来但不立即建 commit。常见用法是把几个 commit 合并成一个,或者挑过来再调整内容 / message 再提交。
1 | git cherry-pick -n abc1234 def5678 |
-e / --edit,挑的同时编辑 commit message。
2.3 挑错了怎么撤
挑过来发现不对,分两种情况:
- 还没 push:
git reset --hard HEAD~,把刚才那个 commit 扔掉 - 已经 push:
git revert <hash>反向应用,再 push 一次
正在 cherry-pick 中途遇到冲突想退出,用 git cherry-pick --abort,回到执行前的状态。
2.4 检查哪些 Commit 已经挑过去了
回灌场景最容易出的问题是重复挑:同一个 fix 被挑过两次,或者忘了上次挑过没。git 自带工具可以查:
1 | # 列出 feature 上有但 main 上没有的 commit |
git cherry 比对的不是 hash,而是 patch 内容(commit 的 diff),所以即使 cherry-pick 产生了新 hash,它也能识别出来。
更可视化的版本:
1 | git log --cherry-mark --left-right main...feature |
回灌前扫一眼,能避开重复 cherry-pick。基本用法到此结束,但 cherry-pick 真正的麻烦不在命令本身,在两个不报错的陷阱。先看第一个。
3. ⚠️ 陷阱一:公共祖先陷阱
cherry-pick 最危险的陷阱:merge 完全没有冲突,但你的修复静默丢失了。
3.1 场景
线上系统有个 feature flag enableNewLogin,初始 false。开发流程:
main,线上分支feature/v2,长期开发分支,已有大量自己的提交
事情按下面顺序发生:
第 1 步,feature/v2 上完成了 new login,开启了 flag。提交 F2:enableNewLogin: false → true。
第 2 步,业务紧急要求开启,cherry-pick F2 到 main 上线。新提交 M2:enableNewLogin: false → true。
第 3 步,QA 在 feature 上测试 new login,发现某些场景下崩溃。决定先关掉重新设计。提交 F3:enableNewLogin: true → false。
第 4 步,feature/v2 完成,merge 到 main。
- 你预期:main 上
enableNewLogin = false(跟着 feature 的最新决定回退) - 实际:merge 没有冲突,main 上
enableNewLogin = true(线上 bug 继续暴露)
3.2 推演
最终的 commit graph:
1 | A (false) ── M2 (true) ── M3 (??) ● main |
merge 的合并基础(merge-base)是 A。git 做三路合并,对每一行看三方状态:
| 端点 | enableNewLogin |
|---|---|
| 祖先 A | false |
| 当前分支 main 的 M2 | true(从祖先变了) |
| 要合进来的 feature 的 F3 | false(跟祖先一样) |
git 的判断逻辑是:main 改了这一行,feature 没改,取 main 的版本 = true。
但你想要的是 feature 的最新意图(false)。git 不知道 feature 曾经把它从 false 改成 true 又改回 false,三路合并只看三个端点的状态,不看中间过程。
3.3 一句话原理
cherry-pick 不是「把这个 commit 过户」,而是「重新做一遍这个变更」,产生全新的 commit hash。从 git 的视角,main 上的 M2 和 feature 上的 F2 是两个互不相识的提交,尽管它们改了同样的代码。merge 时 git 找它们最近的公共祖先,结果就是 A(cherry-pick 之前的状态)。于是 git 得出「feature 在这一行上没改过」的错误结论。
3.4 怎么避免
优先走「反向流」:hotfix 在 main 上修,再 merge 回 develop / feature。这样不会出现 cherry-pick 制造的孤儿提交,merge-base 也会跟着前移。
1 | git checkout main |
确实需要 cherry-pick 时,回灌前先用 git cherry 或 git log --cherry-mark(见 2.4)扫一遍,避开重复挑同一个 commit 的姊妹问题。陷阱一是普通 commit 之间的事;挑 merge commit 时,还有第二个陷阱。
4. ⚠️ 陷阱二:cherry-pick 一个 Merge Commit
这个陷阱比上一个少见,但同样静默:命令成功,commit 也建好了,但语义可能完全反过来。
4.1 为什么需要 -m
普通 commit 只有一个父,diff 一目了然。merge commit 有两个父,它表示「分支 A 和分支 B 在这里汇合」。git 不知道你要哪一边的 diff。
直接 cherry-pick 一个 merge commit 会报错:
1 | fatal: commit <hash> is a merge but no -m option was given. |
-m N 告诉 git:以第 N 个父为基准计算 diff,把另一边带进来的修改作为 patch 应用过来。
4.2 选反的后果
假设有这样的合并:
1 | release ── M (merge commit) |
你想把 feature/auth-fix 带进来的修改 cherry-pick 到另一条分支:
git cherry-pick -m 1 M,相对 parent 1 的 diff = 「parent 2 带来了什么」 = feature/auth-fix 的修改(你要的)git cherry-pick -m 2 M,相对 parent 2 的 diff = 「parent 1 带来了什么」 = release 在 merge 前比 feature 多出来的东西。这通常是反向结果,不是你要的。
选反的结果:命令成功、commit message 也对,但代码里挑过去的是别的东西。等 QA 发现 fix 没生效,已经一周后了。
4.3 怎么避免
先确认这是不是 merge commit:
1 | git show <hash> | head -3 |
确认主干是哪一侧。绝大多数情况下,-m 1 是对的。约定俗成:你站在主干分支上 git merge feature,merge commit 的 parent 1 就是主干、parent 2 是 feature。
保险写法,cherry-pick 之前先 diff 看一眼:
1 | git show -m --first-parent <merge-commit> |
这条命令展示的就是 -m 1 视角的 diff,肉眼对一下是不是你想要的内容,再敲 cherry-pick。两个陷阱讲完,回到一个更基本的问题:什么场景该用 cherry-pick,什么场景该换工具?
5. 决策框架:cherry-pick / Merge / Rebase / Revert 怎么选
旧文末尾写「停止使用 cherry-pick」,其实不必这么极端。问题不在 cherry-pick 本身,在用错场景。
flowchart TD
Start[要把变更带过去] --> Q{粒度?}
Q -->|整条分支的所有提交| Branch{保留分叉历史?}
Branch -->|要| Merge["git merge<br/>历史有分叉,真实记录"]
Branch -->|不要,要直线历史| Rebase["git rebase<br/>历史是直的,但 commit 都换了 hash"]
Q -->|指定的某几个提交| Cherry["git cherry-pick<br/>注意第 3、4 节的陷阱"]
Q -->|撤销某个已 push 的提交| Revert["git revert<br/>反向应用,产生新 commit"]口诀:整条用 merge / rebase,几个用 cherry-pick,反向用 revert。
cherry-pick 和 revert 长得像、方向相反:想让代码「多一份某变更」用 cherry-pick,「少一份某变更」用 revert。决策框架明确后,把要点压成一页备忘,方便回查。
6. 一页备忘
1 | git cherry-pick <c> # 单个 |
两个静默陷阱:cherry-pick 后续 merge 同一分支会让修改消失(陷阱一);cherry-pick merge commit 选错 -m 会挑成反向 diff(陷阱二)。
优先方案:长生命分支之间走「反向流」,cherry-pick 留给真正只挑几个的场景。想看更细的语义,下面是官方资料。
7. 参考资料
- git 官方文档:git-cherry-pick(1)
- Pro Git book 第 5.3 章:Maintaining a Project