本文希望教你如何成为一个 git cherry-pick 的 “master”!通过使用 git cherry-pick 来轻松地维护多个分支版本,再也不会让 multi-version maintaining 成为你心头上的那把令你屡次痛心的剑了!
本文所有内容都会基于以下(精心构造的)示例,该例子涵盖了大部分工程上容易出现的 Git log pattern(如有其他 corner-case ,欢迎联系我,一起努力让该文变得对大家更有帮助)。
贯穿本文的示例场景,这是该示例基于时间序的 Git 提交历史。
| |
快速创建该示例。
| |
当前 Git 提交历史示意图如下。

Git cherry-pick 的命令的基本原理是根据用户所选择的提交,根据提交中的差异信息(diff)将这些提交移植至用户目标版本中。如将 hotfix 应用至其他 LTS 版本中是该功能的一个典型应用。
git cherry-pick 的大致用法为:
| |
此处的 <commit>... 即为用户希望移植的提交(集合),这是本文讨论的要点。
<commit> 可以为单一提交(commit),也可以为一个版本区间(revision range)。若为 revision range,则该命令会将该 revision range 中的所有 commit 都解析出来,最终成为一连串的单一 commit 1。cherry-pick 可以同时接受多个 <commit> ,此时表现类似于 git rev-list 中的 --no-walk 行为2。
那我们依次来讨论 <commit>... 为单一 commit 以及 revision range 的情况。
Single commit
Normal commit
回到上文的例子,如果仅需要将 G2 选取出来,我们可以这样操作。
此时会出现合并冲突(merge conflict),输出如下所示。
| |
这段内容告知我们这些信息:green 文件在当前(暂存)版本 HEAD 中并不存在,但在选取的 G2 提交中存在。如果需要该文件,则使用 git add 将其提交至暂存区,若希望保留当前暂存版本的状态,即删除该文件,则使用 git rm 将 green 文件舍弃。
我们希望在选取 G2 之后能够保留 green 文件,故采取如下操作。
此时 cherry-pick 操作已经完成,如果继续执行 git cherry-pick --continue ,则此时会显示 error: no cherry-pick or revert in progress ,即当前没有进行任何 cherry-pick 任务。
查看一下当前的提交记录,则会发现 G2 已经在我们当前的分支 cp-single-normal-commit 上了。
Merge commit
那如果我们想选取一个 merge commit 呢,比如将 R4 选取出来。
当执行完这条 cherry-pick 命令之后,你会得到以下输出。
默认情况下,cherry-pick 不处理 merge commit 并直接报错。因为在 merge commit 中,会有多个 parent 信息,但此时 Git 并不知道该使用哪个 parent 作为 mainline。在错误信息中,也同时提示了我们,如果要选取 merge commit ,则需要使用 -m (亦为 --mainline)选项来指定哪个 parent 是主线3。
通过 git show 命令可以获得 merge commit 的多个 parent,且从 1 开始编号。由于该例中我们需要选取的 mainline parent 是 R3(17e2629) ,因此在 cherry-pick 中选择的是 -m 1。
让我们再来试一次。
| |
cherry-pick 圆满完成!此时再看一下当前的提交记录,则发现在 cp-single-merge-commit 分支上产生了一个新的 R4 提交。
现在我们再来讲讲刚刚的 -m 1 发生了什么。如果现在去看 cp-single-merge-commit 这个测试分支上的文件,则会发现有 green ,而没有 red 。
这是因为我们在选取 merge commit 时,使用的是 mainline 1 ,即 red 分支。因此 cherry-pick 事实是以 red 为基础,寻找 mainline 2 green 分支与 red 的差异,选取的就是 green 分支上所做的修改了。
Revision range
Git 中可用多种方法来表示 revision (版本,或修订快照)4,这里我们主要讨论 revision range(版本区间)5。
对于 revision range,有以下六种表示法:
^<rev>:(脱字符-表示法)表示排除<rev>以及它所有可到达的父辈 commit。<r1>..<r2>(两点-范围表示法):等同于^r1 r2,即 包含<r2>以及其可到达的父辈 commit ,并排除<r1>以及其可到达的父辈 commit。如果需要包括
<r1>,可使用这种写法:<r1>^..<r2>。<r1>...<r2>(三点-对称差分表示法):包含所有<r1>或<r2>及其可到达的父辈 commit,并排除<r1>和<r2>两者可到达的共同父辈 commit。<rev>^@:包含<rev>的所有父辈,但排除<rev>本身。<rev>^!:包含<rev>本身,但排除<rev>所有父辈。即表示单个<rev>commit。注意:
<rev>(表示<rev>及其所有父辈)在 revision range 的语境中不同于<rev>^!。仅有指定--no-walk参数时,两者才可以认为是相同的(都仅表示<rev>本身)。<rev>^-[<n>]:包含<rev>及其所有父辈,但排除<rev>的第<n>个 parent 及其可到达的所有父辈。<n>的缺省值为 1。
看起来很复杂,我们来用文中的场景来举两个范围表示法的例子。
首先,考虑 <r1> 和 <r2> 都在同一分支上的情况,如 G1 (05719c8) 和 G3 (c950910)。
05719c8(G1)^..c950910(G3) 的含义应当是:
- 包含
G3及其所有父辈。 - 并排除
G1的所有父辈(不排除G1)。
因此结果应当是选出从 G1 到 G3 的所有提交,示意图如下,黄色为被包含的节点,灰色则代表被排除的节点。

让我们再看看当前的提交记录。Bingo! G1 ,G2 ,G3 这三个提交已经被选取出来了。
那 <r1> 和 <r2> 在不同分支上,是什么情况呢?
我们以 G1 (05719c8) 和 B2 (69edfc9) 作为用例。
05719c8(G1)^..69edfc9(B2) 的含义应当是:
- 包含
B2及其所有父辈。 - 并排除
G1的所有父辈(不排除G1)。
由于 B2 及其所有父辈中,并不包括 G1。因此我们可以将 G1^..B2 理解为包含 B2 及其所有父辈,且排除 B2 和 G1 的共同父辈后的结果。自然就只剩下 B1 和 B2 两个 commit 了。示意图如下,黄色为被包含的节点,灰色则代表被排除的节点。

让我们再看看当前的提交记录,确实是只选择了 B1 和 B2 两个提交。
Rerere
Rerere 是“重用已记录的冲突解决方案(reuse recorded resolution)”,它是一种简化冲突解决的方法6 7。
如果你经常进行大量的 merge, rebase 或 cherry-pick,或在维护一个长期不同于主干的分支8,那么非常建议开启 rerere 功能。
开启 rerere 非常简单,仅需要进行一次全局配置即可。
| |
在本地仓库中直接创建
.git/rr-cache文件夹,也可以为该仓库开启rerere。
What’s next
在笔者撰写该文的过程中,也看到了 Microsoft 的 Raymond Chen 写的 Stop cherry-picking, start merging 系列文章,他在其中提及了许多工程实践中 cherry-pick 可能导致的 pitfall。接下来的时间里,笔者将会逐一阅读该系列文章,并根据文中案例去分析 cherry-pick 是否能够在常用软件开发工作流给我们带来足够的收益,以及,是否应该 stop cherry-picking, start merging。
只有在深入了解工具后,我们才能更好地运用工具,真正实现效率提升。
git-cherry-pick <commit>…
https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt-ltcommitgt82308203 ↩︎git-rev-list –no-walk
https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt---no-walksortedunsorted ↩︎git-cherry-pick -m, –mainline
https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt--mltparent-numbergt ↩︎gitrevisions: Specifying Revisions
https://git-scm.com/docs/gitrevisions/#_specifying_revisions ↩︎gitrevisions: Specifying Ranges
https://git-scm.com/docs/gitrevisions/#_specifying_ranges ↩︎Pro Git (zh): Git 工具 - Rerere
https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-Rerere#ef_rerere ↩︎Pro Git (en): Git Tools - Rerere
https://git-scm.com/book/en/v2/Git-Tools-Rerere ↩︎Pro Git (zh): 分布式 Git - 维护项目 - Rerere
https://git-scm.com/book/zh/v2/%E5%88%86%E5%B8%83%E5%BC%8F-Git-%E7%BB%B4%E6%8A%A4%E9%A1%B9%E7%9B%AE#_rerere ↩︎

本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。