← 返回博客

Git cherry-pick 详解:何时使用(何时不该用)

tutorial git

什么是 Git Cherry-Pick?

一个关键的 bug 修复刚刚进入你的开发 branch -- 但同样的 bug 也存在于生产环境。你不能把整个开发 branch merge 到发布 branch,因为它塞满了做到一半的功能。你真正需要的,是把那一个 commit 单独拿出来,应用到别的地方。这正是 git cherry-pick 的用途。

cherry-pick 会取出某个现有 commit 引入的更改,并在你当前的 branch 上以一个全新的 commit 重放这些更改。新的 commit 携带相同的 diff,默认还有相同的提交信息,但它的父节点不同,因此 SHA 也不同。这个细节比看起来更重要:Git 通过 SHA 识别 commit,所以 cherry-pick 之后,你的仓库里会有两个不同的 commit,恰好引入了同一处更改。请记住这一点 -- 它既解释了 cherry-pick 为什么如此有用,也解释了为什么滥用它会在日后带来麻烦。

什么时候 cherry-pick 是正确的工具

把 hotfix 回移到发布 branch

这是最经典的场景。你在 main 上修复了一个 bug,但用户正在使用的是 2.3 版本,他们现在就需要这个修复。把修复 cherry-pick 到发布 branch:

git switch release/2.3
git cherry-pick a1b2c3d

发布 branch 只会得到这一个修复,main 上的其他任何内容都不会顺道跟过来。

commit 提交到了错误的 branch

你本想在功能 branch 上提交 commit,结果却在 main 上。cherry-pick 提供了一条干净的恢复路径:切换到 commit 应该在的 branch,在那里 cherry-pick 它,然后回去把它从不该在的 branch 上删除。如果你对删除这一步没有把握,我们的指南如何撤销一个 Git commit 详细介绍了每种方案。

从废弃的 branch 中抢救一个修复

有时一次实验没有结果,但 branch 中间的某个 commit 修复了一个真正的 bug。与其 merge 一条没有未来的 branch,不如把那个有价值的 commit cherry-pick 到你的活跃 branch 上,然后让实验的其余部分安心地被删除。

基本用法

最简单的形式只需一个 commit:

git cherry-pick a1b2c3d

Git 会应用该 commit 的更改,并立即在你当前的 branch 上创建一个新 commit,复用原来的提交信息。你也可以一次传入多个 commit,Git 会按照你列出的顺序逐个应用:

git cherry-pick a1b2c3d f4e5d6c

应该掌握的实用选项

-x:记录 commit 的来源

回移修复时,你以后会想知道某个 commit 来自哪里。-x 选项会在新 commit 的提交信息末尾追加一行,例如 (cherry picked from commit a1b2c3d...):

git cherry-pick -x a1b2c3d

在长期存在的公共 branch 之间做 cherry-pick 时,请把这变成习惯。六个月后,这一行字能立刻告诉你某个修复是否进入了某个发布版本,以及它来自哪里。

挑选一段 commit 范围

使用两个点的语法可以 cherry-pick 整个范围:

git cherry-pick A..B

注意:这会应用 A 之后的 commit,一直到 B(含 B)。A 本身被排除在外。如果你想包含 A,请写:

git cherry-pick A^..B

^ 的意思是 "A 的父节点",这样 A 就重新回到了范围之内。混淆这两种语法是最常见的 cherry-pick 错误之一。

--no-commit:只应用更改而不提交

有时你想要这些更改,但不想要自动生成的 commit -- 比如想把挑选来的多个 commit 合并成一个,或者想在提交前调整结果:

git cherry-pick --no-commit a1b2c3d

更改会进入你的工作目录和 staging 区,等你准备好后再自己创建 commit。

处理 cherry-pick 过程中的冲突

cherry-pick 产生冲突的原因和 merge 一样:你挑选的那个 commit 是基于与当前 branch 不同的代码编写的。冲突发生时,Git 会在操作中途停下,并标记出冲突的文件:

error: could not apply a1b2c3d... fix login timeout
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue"

此时你有三条出路:

  • git cherry-pick --continue -- 在编辑完冲突文件并用 git add 加入 staging 之后,这条命令会完成 cherry-pick 并创建 commit。
  • git cherry-pick --abort -- 取消整个操作,把你的 branch 恢复到开始前的精确状态。什么都不会丢失,什么都不会被应用。
  • git cherry-pick --skip -- 在挑选多个 commit 时,跳过当前这个,继续处理下一个。当范围中的某个 commit 已经被应用过或者无关紧要时,这很有用。

解决冲突本身的方式与解决 merge 冲突完全相同:打开每个冲突文件,在相互竞争的 hunk 之间做出选择(或将它们合并),删除冲突标记,然后把文件加入 staging。如果冲突标记仍然让你头疼,我们关于可视化解决 Git merge 冲突的教程同样适用于 cherry-pick。

不要用 cherry-pick 做的事

不要 cherry-pick 整条 branch

如果你发现自己正在把五个、十个、二十个 commit 从一条 branch cherry-pick 到另一条,停下来。那是 merge 和 rebase 的工作。两者都能在 branch 之间转移工作,同时让 Git 理解它们之间的关系,使后续操作保持简单且无冲突。如果你不确定哪一个适合你的工作流,我们的 rebase vs merge 对比文章详细分析了各自的取舍。

重复的历史日后会反咬一口

请记住:cherry-pick 创建的是一个拥有新 SHA 的新 commit,而 Git 不会以任何正式方式记录这两个 commit 代表"同一处更改"。如果你把功能 branch 上的 commit cherry-pick 到 main,之后又还是 merge 了那条功能 branch,Git 就必须调和同一批更改的两份副本。多数情况下它会悄无声息地成功。但只要其中任何一份副本事后被修改过,你就会遇到令人困惑的冲突,两边看起来几乎一模一样 -- 而且你的历史中同一处更改出现了两次,有两个日期和两个 SHA,这让 git loggit bisect 之类的工具更难推理。

一条好的经验法则:cherry-pick 是用来转移例外情况的 -- 一个 hotfix、一个放错位置的 commit、一个被抢救出来的修复。一旦它成为你在 branch 之间转移工作的常规手段,你的历史就是在告诉你:分支策略需要重新考虑了。

在 GitSquid 中使用 cherry-pick

在 GitSquid 中,cherry-pick 就在你期望的位置:在 commit 图上右键点击任意 commit,从上下文菜单中选择 cherry-pick 选项即可。无需在日志和终端之间来回复制 SHA。

更大的优势在于提前知道你将面对什么。GitSquid 的 Conflict Predictor -- 对所有人免费 -- 会在你执行 merge、rebase 或 cherry-pick 之前,精确显示哪些文件会发生冲突,并附带 hunk 预览和语法高亮。你不必在操作进行到一半时才发现冲突,而是可以提前看到它们,然后决定是继续、换一个 commit,还是从容地准备解决方案。

而当 cherry-pick 真的发生冲突时,"Resolve in scratch worktree" 会以新标签页的形式打开一个临时 worktree,让你在那里解决所有问题,而你的活跃 checkout 保持原封不动。

快速参考

命令 作用
git cherry-pick <sha> 把一个 commit 应用到当前 branch
git cherry-pick <sha1> <sha2> 按顺序应用多个 commit
git cherry-pick -x <sha> 在提交信息末尾追加来源 SHA
git cherry-pick A..B 挑选 A 之后到 B 的 commit(不含 A)
git cherry-pick A^..B 挑选包含 A 在内的范围
git cherry-pick --no-commit <sha> 只应用更改,不创建 commit
git cherry-pick --continue 解决冲突后完成挑选
git cherry-pick --abort 取消并把 branch 恢复原状
git cherry-pick --skip 跳过当前 commit,继续处理其余的

cherry-pick 是一把手术刀,不是一把铁锹。用于偶尔的精确提取 -- 回移一个 hotfix、纠正一个放错位置的 commit、抢救一个修复 -- 它是 Git 中最令人愉悦的命令之一。把它当作 merge 的替代品来用,它就会悄悄地让你的历史塞满重复内容,日后回头纠缠你。分清这两者,回移时加上 -x,并把 --abort 记在心里作为逃生通道。

想在动手之前就看到哪些文件会冲突吗?免费下载 GitSquid,让 Conflict Predictor 为你的下一次 cherry-pick 消除猜测。