本文希望教你如何成为一个 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 国际许可协议进行许可。