8

Mastering Git Cherry-pick

 3 years ago
source link: https://blog.triplez.cn/posts/mastering-git-cherry-pick/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Mastering Git Cherry-pick

April 8, 2022 · 7 分钟 · TripleZ

本文希望教你如何成为一个 git cherry-pick 的 “master”!通过使用 git cherry-pick 来轻松地维护多个分支版本,再也不会让 multi-version maintaining 成为你心头上的那把令你屡次痛心的剑了!

本文所有内容都会基于以下(精心构造的)示例,该例子涵盖了大部分工程上容易出现的 Git log pattern(如有其他 corner-case ,欢迎联系我,一起努力让该文变得对大家更有帮助)。

贯穿本文的示例场景,这是该示例基于时间序的 Git 提交历史。

$ git --no-pager log --oneline --graph --date-order
* f2c1619 (HEAD -> red) R6
*   e6899ea R5 merge branch 'blue' into 'red'
|\
* \   0979d45 R4 merge branch 'green' into 'red'
|\ \
| | * 186da41 (blue) B3
| * | c950910 (green) G3
* | | 17e2629 R3
| | * 69edfc9 B2
| * | 059425a G2
| * | 05719c8 G1
| | * ebb218d B1
| |/
* / 8c6595b R2
|/
* 6581ff8 R1
* 2787f8f (master) init commit

快速创建该示例。

当前 Git 提交历史示意图如下。

git-cherry-pick-overall

Git cherry-pick 的命令的基本原理是根据用户所选择的提交,根据提交中的差异信息(diff)将这些提交移植至用户目标版本中。如将 hotfix 应用至其他 LTS 版本中是该功能的一个典型应用。

git cherry-pick 的大致用法为:

git cherry-pick [options] <commit>...

此处的 <commit>... 即为用户希望移植的提交(集合),这是本文讨论的要点。

<commit> 可以为单一提交(commit),也可以为一个版本区间(revision range)。若为 revision range,则该命令会将该 revision range 中的所有 commit 都解析出来,最终成为一连串的单一 commit 1cherry-pick 可以同时接受多个 <commit> ,此时表现类似于 git rev-list 中的 --no-walk 行为2

那我们依次来讨论 <commit>... 为单一 commit 以及 revision range 的情况。

Single commit#

Normal commit#

回到上文的例子,如果仅需要将 G2 选取出来,我们可以这样操作。

# 回到 master 新建一条分支用于测试
$ git checkout master
$ git checkout -b cp-single-normal-commit
# G2 的提交 SHA 值为 059425a
$ git cherry-pick 059425a

此时会出现合并冲突(merge conflict),输出如下所示。

CONFLICT (modify/delete): green deleted in HEAD and modified in 059425a (G2). Version 059425a (G2) of green left in tree.
error: could not apply 059425a... G2
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'

这段内容告知我们这些信息:green 文件在当前(暂存)版本 HEAD 中并不存在,但在选取的 G2 提交中存在。如果需要该文件,则使用 git add 将其提交至暂存区,若希望保留当前暂存版本的状态,即删除该文件,则使用 git rmgreen 文件舍弃。

我们希望在选取 G2 之后能够保留 green 文件,故采取如下操作。

# 将 green 提交至暂存区
$ git add green
# 已修复所有合并冲突,继续进行 cherry-pick
$ git cherry-pick --continue

此时 cherry-pick 操作已经完成,如果继续执行 git cherry-pick --continue ,则此时会显示 error: no cherry-pick or revert in progress ,即当前没有进行任何 cherry-pick 任务。

查看一下当前的提交记录,则会发现 G2 已经在我们当前的分支 cp-single-normal-commit 上了。

$ git --no-pager log --oneline --graph --date-order
* 0457362 (HEAD -> cp-single-normal-commit) G2
* 2787f8f (master) init commit

Merge commit#

那如果我们想选取一个 merge commit 呢,比如将 R4 选取出来。

# 回到 master 新建一条分支用于测试
$ git checkout master
$ git checkout -b cp-single-merge-commit
# R4 的提交 SHA 值为 0979d45
$ git cherry-pick 0979d45

当执行完这条 cherry-pick 命令之后,你会得到以下输出。

error: commit 0979d45f1b46f72730188c5c01b3f2c7f41b18e6 is a merge but no -m option was given.
fatal: cherry-pick failed

默认情况下,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

$ git --no-pager show 0979d45
commit 0979d45f1b46f72730188c5c01b3f2c7f41b18e6
Merge: 17e2629 c950910
Author: Triple-Z <[email protected]>
Date:   Thu Mar 31 01:29:31 2022 +0800

    R4 merge branch 'green' into 'red'

让我们再来试一次。

$ git cherry-pick -m 1 0979d45

cherry-pick 圆满完成!此时再看一下当前的提交记录,则发现在 cp-single-merge-commit 分支上产生了一个新的 R4 提交。

$ git --no-pager log --oneline --graph --date-order
* 987aba7 (HEAD -> cp-single-merge-commit) R4 merge branch 'green' into 'red'
* 2787f8f (master) init commit

现在我们再来讲讲刚刚的 -m 1 发生了什么。如果现在去看 cp-single-merge-commit 这个测试分支上的文件,则会发现有 green ,而没有 red

$ ls -lh
total 16
-rw-r--r--  1 triplez  staff    18B  4  7 19:06 green
-rw-r--r--  1 triplez  staff     5B  3 31 01:29 init

这是因为我们在选取 merge commit 时,使用的是 mainline 1 ,即 red 分支。因此 cherry-pick 事实是以 red 为基础,寻找 mainline 2 green 分支与 red 的差异,选取的就是 green 分支上所做的修改了。

Revision range#

Git 中可用多种方法来表示 revision (版本,或修订快照)4,这里我们主要讨论 revision range(版本区间)5

对于 revision range,有以下六种表示法:

  1. ^<rev> :(脱字符-表示法)表示排除 <rev> 以及它所有可到达的父辈 commit。

  2. <r1>..<r2>(两点-范围表示法):等同于 ^r1 r2 ,即 包含 <r2> 以及其可到达的父辈 commit ,并排除 <r1> 以及其可到达的父辈 commit。

    如果需要包括 <r1>,可使用这种写法:<r1>^..<r2>

  3. <r1>...<r2> (三点-对称差分表示法):包含所有 <r1> <r2> 及其可到达的父辈 commit,并排除 <r1> <r2> 两者可到达的共同父辈 commit。

  4. <rev>^@包含 <rev> 的所有父辈,但排除 <rev> 本身。

  5. <rev>^!包含 <rev> 本身,但排除 <rev> 所有父辈。即表示单个 <rev> commit。

    注意: <rev> (表示 <rev> 及其所有父辈)在 revision range 的语境中不同于 <rev>^! 。仅有指定 --no-walk 参数时,两者才可以认为是相同的(都仅表示 <rev> 本身)。

  6. <rev>^-[<n>]包含 <rev> 及其所有父辈,但排除 <rev> 的第 <n> 个 parent 及其可到达的所有父辈。 <n> 的缺省值为 1。

看起来很复杂,我们来用文中的场景来举两个范围表示法的例子。

首先,考虑 <r1><r2> 都在同一分支上的情况,如 G1 (05719c8)G3 (c950910)

# 回到 master 新建一条分支用于测试
$ git checkout master
$ git checkout -b cp-range-same-branch
# G1 的提交 SHA 值为 05719c8,G3 的 SHA 值为 c950910
$ git cherry-pick 05719c8^..c950910

05719c8(G1)^..c950910(G3) 的含义应当是:

  • 包含 G3 及其所有父辈。
  • 排除 G1 的所有父辈(不排除 G1)。

因此结果应当是选出从 G1G3 的所有提交,示意图如下,黄色为被包含的节点,灰色则代表被排除的节点。

git-cherry-pick-g1-g3

让我们再看看当前的提交记录。Bingo! G1G2G3 这三个提交已经被选取出来了。

$ git --no-pager log --oneline --graph --date-order
* 32eac39 (HEAD -> cp-range-same-branch) G3
* d3b1130 G2
* c82c4c7 G1
* 2787f8f (master) init commit

<r1><r2> 在不同分支上,是什么情况呢?

我们以 G1 (05719c8)B2 (69edfc9) 作为用例。

# 回到 master 新建一条分支用于测试
$ git checkout master
$ git checkout -b cp-range-diff-branch
# G1 的提交 SHA 值为 05719c8,B2 的 SHA 值为 69edfc9
$ git cherry-pick 05719c8^..69edfc9

05719c8(G1)^..69edfc9(B2) 的含义应当是:

  • 包含 B2 及其所有父辈。
  • 排除 G1 的所有父辈(不排除 G1)。

由于 B2 及其所有父辈中,并不包括 G1。因此我们可以将 G1^..B2 理解为包含 B2 及其所有父辈,且排除 B2G1 的共同父辈后的结果。自然就只剩下 B1B2 两个 commit 了。示意图如下,黄色为被包含的节点,灰色则代表被排除的节点。

git-cherry-pick-g1-b2

让我们再看看当前的提交记录,确实是只选择了 B1B2 两个提交。

$ git --no-pager log --oneline --graph --date-order
* e63f214 (HEAD -> cp-range-diff-branch) B2
* aed6717 B1
* 2787f8f (master) init commit

Rerere#

Rerere 是“重用已记录的冲突解决方案(reuse recorded resolution)”,它是一种简化冲突解决的方法6 7

如果你经常进行大量的 merge, rebase 或 cherry-pick,或在维护一个长期不同于主干的分支8,那么非常建议开启 rerere 功能。

开启 rerere 非常简单,仅需要进行一次全局配置即可。

$ git config --global rerere.enabled true

在本地仓库中直接创建 .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。

只有在深入了解工具后,我们才能更好地运用工具,真正实现效率提升。


  1. git-cherry-pick <commit>…
    https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt-ltcommitgt82308203 ↩︎

  2. git-rev-list –no-walk
    https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt---no-walksortedunsorted ↩︎

  3. git-cherry-pick -m, –mainline
    https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt--mltparent-numbergt ↩︎

  4. gitrevisions: Specifying Revisions
    https://git-scm.com/docs/gitrevisions/#_specifying_revisions ↩︎

  5. gitrevisions: Specifying Ranges
    https://git-scm.com/docs/gitrevisions/#_specifying_ranges ↩︎

  6. Pro Git (zh): Git 工具 - Rerere
    https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-Rerere#ef_rerere ↩︎

  7. Pro Git (en): Git Tools - Rerere
    https://git-scm.com/book/en/v2/Git-Tools-Rerere ↩︎

  8. 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 国际许可协议进行许可。

Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK