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
2
3
4
5
6
# 单个
git cherry-pick <commit>

# 范围
git cherry-pick A..B # 不含 A,含 B
git cherry-pick A^..B # 含 A,含 B

⚠️ 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
2
git cherry-pick -n abc1234 def5678
# 此时变更已经 stage,但没建 commit,自己 git commit 一次

-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
2
3
# 列出 feature 上有但 main 上没有的 commit
git cherry main feature
# 输出:+ 表示 feature 独有,- 表示等价 commit 已存在 main

git cherry 比对的不是 hash,而是 patch 内容(commit 的 diff),所以即使 cherry-pick 产生了新 hash,它也能识别出来。

更可视化的版本:

1
2
git log --cherry-mark --left-right main...feature
# 输出里 = 表示等价 commit 两边都有(已经挑过),< 或 > 表示只在某一边

回灌前扫一眼,能避开重复 cherry-pick。基本用法到此结束,但 cherry-pick 真正的麻烦不在命令本身,在两个不报错的陷阱。先看第一个。

3. ⚠️ 陷阱一:公共祖先陷阱

cherry-pick 最危险的陷阱:merge 完全没有冲突,但你的修复静默丢失了。

3.1 场景

线上系统有个 feature flag enableNewLogin,初始 false。开发流程:

  • main,线上分支
  • feature/v2,长期开发分支,已有大量自己的提交

事情按下面顺序发生:

第 1 步,feature/v2 上完成了 new login,开启了 flag。提交 F2enableNewLogin: false → true

第 2 步,业务紧急要求开启,cherry-pick F2 到 main 上线。新提交 M2enableNewLogin: false → true

第 3 步,QA 在 feature 上测试 new login,发现某些场景下崩溃。决定先关掉重新设计。提交 F3enableNewLogin: true → false

第 4 步,feature/v2 完成,merge 到 main。

  • 你预期:main 上 enableNewLogin = false(跟着 feature 的最新决定回退)
  • 实际:merge 没有冲突,main 上 enableNewLogin = true(线上 bug 继续暴露)

3.2 推演

最终的 commit graph:

1
2
A (false) ── M2 (true) ── M3 (??)  ●  main
└── F2 (true) ── F3 (false) ────┘

merge 的合并基础(merge-base)是 A。git 做三路合并,对每一行看三方状态:

端点enableNewLogin
祖先 Afalse
当前分支 main 的 M2true(从祖先变了)
要合进来的 feature 的 F3false(跟祖先一样)

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
2
3
4
git checkout main
# 修复 bug,提交
git checkout feature/v2
git merge main

确实需要 cherry-pick 时,回灌前先用 git cherrygit 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
2
3
release ── M (merge commit)
├─ parent 1: release 上一个提交
└─ parent 2: feature/auth-fix 的最后一个提交

你想把 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
2
git show <hash> | head -3
# 输出里有 "Merge: aaaaaaa bbbbbbb" 这一行,就是 merge commit

确认主干是哪一侧。绝大多数情况下,-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
2
3
4
5
6
git cherry-pick <c>            # 单个
git cherry-pick A^..B # 范围(含 A 和 B)
git cherry-pick -x <c> # 回灌场景,记录来源
git cherry-pick -n <c> # 先挑不提交
git cherry-pick -m 1 <c> # 挑 merge commit
git cherry main feature # 检查哪些已挑过

两个静默陷阱:cherry-pick 后续 merge 同一分支会让修改消失(陷阱一);cherry-pick merge commit 选错 -m 会挑成反向 diff(陷阱二)。

优先方案:长生命分支之间走「反向流」,cherry-pick 留给真正只挑几个的场景。想看更细的语义,下面是官方资料。

7. 参考资料