Cherry 选择从开发分支到部署分支的父合并提交

Posted

技术标签:

【中文标题】Cherry 选择从开发分支到部署分支的父合并提交【英文标题】:Cherry pick a parent merged commit from develop branch to deployment branch 【发布时间】:2021-12-02 17:29:45 【问题描述】:

假设我有一个分支develop,它有一个合并的 PR,其中包含许多提交,例如 ABC,所以我想获得所有提交,或者如果可能的话,我得到提交 A 假设它是其他提交 (B and C) 中所有文件的父级,我将它们复制到新分支 tester-branch

这样做之后,我提出了一个分支 tester-branch 的 PR,指向另一个分支 Deployment

我尝试了类似的方法,但没有奏效,

git checkout tester-branch
git cherry-pick -m 1 commit-A
git add .
git commit -M "message"
git push branch-name

我最接近的是,它是从 develop 分支中提取除 ABC 之外的其他提交,这是我不想要的,我只想要 A 的确切提交因为它是父级,或者像ABC 这样将它们全部组合在一起,我将它们组合在一起。

所以总而言之,问题是,如何从一个合并的 PR 中获取特定的提交到一个新的分支?

【问题讨论】:

【参考方案1】:

TL;DR

简单地挑选感兴趣的提交,而不是合并提交。

合并提交提交。因此,就像任何提交一样,它有一个快照。 合并提交唯一不同的是它有多个父提交。 没有提交存储更改;所有 提交存储快照

cherry-pick 命令以一种基本的“我们希望发生的事情”的方式工作,将提交的快照与其父提交的快照进行比较。也就是说,假设我们有一个简单的线性提交链,右侧有较新的提交,我们可以这样绘制:

... <-F <-G <-H ...

每个提交实际上都是由一些看起来很随机的大哈希 ID 来处理的。 Like trying to get around in Tokyo, nothing is numbered sequentially; you need a map. 在 Git 的例子中,地图显示如果你想从 H 继续前进,下一个提交在向后方向是提交 G。提交H 字面意思是存储提交G 的随机哈希ID。1我们说H 指向 G。反过来,G 存储了较早提交的哈希 ID,我们将其称为 F 以方便绘制,因此 G 向后指向 FF,作为提交,将再次向后指向,依此类推。

每个提交有一个每个文件的完整快照。因此,如果我们使用提交 H 并想查看“发生了什么变化”,我们让 Git 从 H 中找到早期提交 G 的哈希 ID。然后我们让 Git 提取两个快照并比较它们。两次提交中的某些文件完全相同。2 其他文件则不然,如果我们想要一个实际的 git diff 来显示发生了什么变化,那么 Git 现在必须根据需要弄清楚这一点,给我们看。

(这就是为什么您可以使用不同的选项运行git diffgit log -pgit show,以不同的方式显示差异。每次运行git diff 或类似的,Git 重新计算 变化。)

因此,cherry-pick 需要运行一个差异,从提交到其父级(单数)。这告诉我们改变了什么:哪些文件不同,并且——通过git diff的输出——这给了我们一个秘诀:在这里添加一行,在那里删除一行,很快就会有什么旧(父)提交现在是新(子)提交中的内容。


1请注意,没有办法向前,因为一旦你提交G,它就一成不变。没有任何东西——甚至 Git 本身——可以改变它。我们不知道未来提交 H 的哈希 ID 是什么,因为提交的哈希 ID 取决于提交中的每一位数据:源快照(我们不'还不知道),您的姓名和电子邮件地址(您可能会更改),您将进行下一次提交的确切日期和时间何时,等等。

我们唯一知道的关于未来提交的事情是它会得到一个大的、丑陋的、随机的但唯一的哈希ID。它将通过写出该哈希 ID 的行为来获得该哈希 ID,并在那时将其设置为一成不变。因此,我们将能够在未来的提交中存储现有提交的现有哈希 ID,但我们不知道 future 提交的 future 哈希 ID 是什么会的,所以我们不能存储这些。然后,一旦我们做出了未来的提交,它就会被卡住:我们不能添加散列 ID 到它!所以所有的提交,在 Git 中,总是指向后面。

2Git 会自动对这些文件进行重复数据删除,这使得 Git 很容易判断这两个文件是否相同。这反过来又避免了必须进行任何提取工作,因此将提交与另一个提交进行比较实际上只是比较已经知道不同文件的问题。已知相同的文件几乎立即被删除。


这就是为什么选择合并需要-m 标志

正如我们已经指出的,合并提交是一个具有多个父级的提交。当我们运行 git merge 时,我们会得到一个合并提交。3 也就是说,我们开始时说:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

我们在分支 branch1 上(这是附加的 HEAD 名称),我们运行:

git merge branch2

Git 现在找到提交 JL——我们当前的提交和我们想要合并的提交——并使用反向链接找到最佳起点共享提交,在这种情况下显然是4 commit H

Git 通过比较从 HJ 找出 我们 发生了什么变化,以及通过从 H 到 @ 进行比较 他们 发生了什么变化987654359@。这与从父提交到子提交的差异相同:我们只是一次跳过几个提交。然后git merge合并这两组更改,并将这些合并更改应用到H 中的快照。

这会在J 中的快照中添加他们为到达L 所做的更改。或者,从他们的角度来看,它具有在L 中的快照中添加我们 为到达J 所做的更改的效果。发生这种情况是因为这种变化的叠加保留了我们的更改添加了他们的,或者,从他们的角度来看,保留了他们的更改但添加我们的。5

最终的合并结果是一个新的提交M,而不是一个单一的父J,有两个父:

          I--J
         /    \₁
...--G--H      M   <-- branch1 (HEAD)
         \    /²
          K--L   <-- branch2

第一个父级,或者对于樱桃采摘,-m 1,是J第二个父级——-m 2——是L


3更准确地说,我们有时会从git merge 获得合并提交。一些git merge 操作根本不会产生合并提交,但在这种情况下,cherry-pick 没有问题,对吧? ?

4如果不是很明显,说明您已经很久没有使用版本控制系统了。不明显的合并基础在更多更复杂的图中。请参阅 Pretty Git branch graphs 中的一些示例,了解简单案例与困难案例。

5这里的代数非常简单除了,当涉及到合并冲突,或者特殊的——但实际上并不是那么不寻常的——在合并的情况下,我们和他们都以相同的方式修复了相同的问题,Git 只获取更改的一个副本。


樱桃采摘合并意味着“接受所有更改”

现在假设我们有这个:

           I--J
          /    \₁
         H      M   <-- branch1
        / \    /²
       /   K--L   <-- branch2
      /
...--G
      \
       N--O--P   <-- branch3 (HEAD)

我们目前坐在这里,通过branch3 提交P,它有一个快照和元数据,就像任何提交一样。所以我们签出的工作树匹配提交P,假设我们没有对其进行任何更改。

如果我们现在运行:

git cherry-pick -m 1 branch1

我们告诉 Git:去寻找提交 M。这是一个合并,所以转到它的第一个父级,J。区分 JM。弄清楚发生了什么变化。

那么:从JM 发生了什么变化?好吧,我们运行了git merge,并且git merge 结合了我们的 work-since-H他们的 work-since-H

JM 的差异是他们在KL 上对branch2 所做的更改的总和。 更准确地说,它是这些更改的总和,减去我们在提交 J 中已有的任何内容,加上或减去我们必须解决合并冲突时所做的任何其他事情。

这是 JM 的区别,所以我们告诉 Git 添加到提交 P,以便进行新的提交。如果我们想要他们在提交K 中修复了什么,但他们在提交L 中做了什么,那么要求的就是错误的。我们应该运行:

git cherry-pick <hash-of-K>

这将比较 HK,看看发生了什么变化,并尝试将 那些 更改添加到我们的提交 P

Cherry-pick 是一种合并,有点像

这里还有一件事需要注意,那就是git cherry-pick 操作实际上是 Git 的合并引擎实现的。

让我们看一个像这样的简单的cherry-pick操作:

          o--o--o--P--C--o--...    <-- their-branch
         /
...--o--o
         \
          o--...--o--H   <-- our-branch (HEAD)

从技术上讲,我们的分支是否相关都无关紧要:

...--P--C--...    <-- their-branch

...----H   <-- our-branch (HEAD)

但一般来说,只有在分支具有 某种 种关系的情况下,cherry-pick 才有意义。我们现在运行:

git cherry-pick <hash-of-C>

例如,我们通过运行 git log 并找到一个表明它修复了我们关心的错误的提交来获取提交 C 的哈希 ID。

Git 现在必须区分 PC 以查看它们的变化。这非常简单。也许他们改变了两三个文件,或者只是一个文件,或者数百个文件:无论他们改变了什么,这就是我们的兴趣改变。

但是:这只是从PC 的差异。它说要在文件 path/to/file.py 的第 1234 行添加一些行或其他任何内容。在这里,在我们的提交 H 中,如果 path/to/file.py 在第 4234 行或第 916 行有那一行,因为我们有一些大的变化而他们没有,反之亦然?

Git 可以四处搜索上下文,过去某些系统中的某些类似樱桃的命令的某些版本已经这样做了。但这很容易出错:上下文查找很容易出错。此外,如果我们已经重命名文件,或者如果他们有,那么在 我们的 源代码中它位于new/name.py 下,我们希望 Git 能够计算 也出来了。

为了让 Git 弄清楚 我们 对该文件做了什么,Git 需要将 P 中的快照与 H 中的快照进行比较。 Git 现在将找到 path/to/file.pynew/name.py,无论它具有哪个名称,并通过执行 this diff,发现原来的第 1234 行现在是第 4234 行、第 916 行或其他。

所以,Git 现在知道如何将他们的更改应用到我们的文件,即使更改已经移动,即使更改是针对不同的文件名。但是......我们之前看到过这个确切的故事,当我们运行git merge

Git 从某个共享起点开始,并运行两个 git diffs。 Git 然后在添加他们的 更改的同时保留我们的 更改。

Git 可以在这里简单地做同样的事情,使用P 作为“共享起点”,我们的HEAD 提交H 作为--ours 提交,他们的子提交C 作为@987654430 @ 犯罪。这就是 Git 所做的。

Git 中的底层合并代码分为两部分:

合并的动作,或者我喜欢称之为作为动词的合并,是组合变化的动作。这会将一些起始或 基本 提交(例如 P)与两个终点提交(我们的 H 和他们的 C)进行比较,找出发生了什么变化,结合 em> 更改,并将 combined 更改应用于基础。这保留了我们的更改,但添加了他们的更改。或者,它将基础H 与我们的J 和他们的L 进行比较,找出发生了什么变化,组合这些变化,并将这些变化应用到基础上。

存储合并的提交,或者我喜欢称之为作为名词的合并,只是像M这样的合并提交,有两个父级。6有时这里的“合并”是一个形容词(“合并提交”);英语变体很多。 As Calvin once said, "Verbing weirds language."

git merge 命令(可选)使 合并为名词或形容词提交。为此,它合并(作为动词)更改。 git cherry-pick 命令合并更改,然后进行普通(非合并)提交。


6Git 允许两个以上的父级,在 Git 中称为 章鱼合并。但是,它们并没有做任何普通的双亲合并无法做到的事情。

【讨论】:

以上是关于Cherry 选择从开发分支到部署分支的父合并提交的主要内容,如果未能解决你的问题,请参考以下文章

git cherry 选择一系列顺序提交,包括合并

Git命令cherry-pick,选择把一部分代码提交到另一个分支

在 Git 中将选定提交从一个分支合并到另一个分支的更好方法

樱桃选择合并

git cherry-pick:复制多个提交commit到当前分支

git 合并某个提交 git cherry-pick