在任何地方我都看到了这一点:“... cherry-pick应用了提交引入的更改...”
我这样做了:在master中创建了这个文件:
** File 1 **
Content
** Footer **
然后扩展到branch2并提交更改:
** File 1 **
Content
Edit 1
** Footer **
然后是另一个:
** File 1 **
Content
Edit 2
Edit 1
** Footer **
现在我回到主人并尝试从branch2挑选最新的提交。我预计只会导入'Edit2',因为与前一个提交相比,该提交引入的更改不是这个吗?
我得到的是以下合并冲突:
** File 1 **
Content
<<<<<<< HEAD
=======
Edit 2
Edit 1
>>>>>>> b634e53...
** Footer **
现在我明显的问题是,我误解了樱桃选择是如何工作的,具体地说,为什么这里存在合并冲突,这将是git merge的快进?
重要通知:这不是一个关于合并冲突的问题,我感兴趣的是樱桃选择实际上在这里做什么。而且我不是出于好奇/无论如何问,而是因为我在工作中使用git遇到了麻烦。
正如几位人士在评论中指出的那样(并与其他问题建立了联系),git cherry-pick
实际上进行了三方合并。 How do cherry-pick and revert work?描述了这一点,但在内容方面比机制更多。
我描述了Why do I get this merge conflict with git rebase interactive?中一组特定合并冲突的来源,以及樱桃挑选和还原的一般概述,但我认为退一步并询问你所做的机制问题是个好主意。不过,我会重新构建它,因为这三个问题:
git show
或git log -p
如何将其显示为更改?git cherry-pick
或git revert
如何工作?回答最后一个问题需要先回答一个问题:
git merge
?那么,让我们按照正确的顺序采取这四个问题。这将是相当长的,如果你愿意,你可以直接跳到最后一节 - 但请注意它建立在第三部分的基础上,第三部分建立在第二部分的基础上,它建立在第一部分之上。
是的 - 但是,从技术上讲,提交是指快照,而不是一个快照。这非常简单明了。要使用Git,我们通常首先运行git clone
,它会为我们提供一个新的存储库。有时,我们首先创建一个空目录并使用git init
创建一个空存储库。无论哪种方式,我们现在有三个实体:
对象数据库包含四种类型的对象,Git调用提交,树,blob和带注释的标记。树和blob主要是实现细节,我们可以在这里忽略带注释的标签:为了我们的目的,这个大数据库的主要功能是保存我们所有的提交。然后,这些提交引用保存文件的树和blob。最后,它实际上是树 - 加 - blob的组合,它是快照。尽管如此,每个提交只有一棵树,而这棵树是让我们完成快照的其余部分,所以除了大量的恶魔实现细节之外,提交本身也可能是一个快照。
我们还不会深入研究杂草,但我们会说索引的工作原理是保存每个文件的压缩的,Git-ified,大部分冻结的副本。从技术上讲,它包含对实际冻结副本的引用,存储为blob。也就是说,如果你从做git clone url
开始,Git已经运行git checkout branch
作为克隆的最后一步。这个checkout
填充了分支顶端提交的索引,因此索引具有该提交中每个文件的副本。
实际上,大多数git checkout
操作都会从提交中填充索引和工作树。这使您可以查看和使用工作树中的所有文件,但工作树副本不是实际处于提交中的副本。提交中的内容是(是?)冻结,压缩,Git-ified,所有这些文件的永不改变的blob快照。这将永久保留这些文件的版本 - 或者只要提交本身存在 - 并且非常适合存档,但对于执行任何实际工作都没用。这就是Git de-Git将文件放入工作树的原因。
只要提交和工作树,Git可以在这里停下来。 Mercurial - 它在很多方面都像Git一样 - 停在这里:你的工作树就是你提出的下一个提交。您只需更改工作树中的内容然后运行hg commit
,它就会从您的工作树中进行新的提交。这有一个明显的优势,就是没有讨厌的索引制造麻烦。但它也有一些缺点,包括本身比Git的方法慢。在任何情况下,Git所做的就是从保存在索引中的先前提交信息开始,准备再次提交。
然后,每次运行git add
时,Git压缩和Git-ifies你添加的文件,并立即更新索引。如果只改变几个文件,然后git add
只改变这几个文件,Git只需要更新一些索引条目。因此,这意味着索引在其中始终具有下一个快照,采用特殊的Git压缩和准备冻结形式。
这反过来意味着git commit
只需要冻结索引内容。从技术上讲,它将索引转换为新树,为新提交做好准备。在少数情况下,例如在一些恢复之后,或者对于git commit --allow-empty
,新树实际上将与先前的提交相同,但是您不需要知道或关心它。
此时,Git会收集您的日志消息以及进入每个提交的其他元数据。它将当前时间添加为时间戳 - 这有助于确保每个提交都是完全唯一的,并且通常是有用的。它使用当前提交作为新提交的父哈希ID,使用通过保存索引生成的树哈希ID,并写出新的提交对象,该对象获取新的唯一提交哈希ID。因此,新提交包含您之前检出的任何提交的实际哈希ID。
最后,Git将新提交的哈希ID写入当前分支名称,以便分支名称现在引用新提交,而不是像以前那样引用新提交的父代。也就是说,无论提交是分支的尖端,现在提交都是分支尖端的一步。新提示是您刚刚提交的提交。
1您可以使用git checkout commit -- path
从一个特定提交中提取一个特定文件。这仍然首先将文件复制到索引中,因此这不是一个例外。但是,您也可以使用git checkout
将文件从索引复制到工作树,例如,您可以使用git checkout -p
选择性地,交互式地修补文件。这些变体中的每一个都有自己特殊的规则集,关于它对索引和/或工作树的作用。
由于Git从索引构建新的提交,因此经常重新检查文档可能是明智的 - 尽管很痛苦。幸运的是,git status
通过比较当前的提交与索引,然后比较索引与工作树,以及每个这样的比较,告诉你什么是不同的,告诉你很多关于索引现在的内容。所以很多时候,你不必随意携带每个Git命令对索引和/或工作树的影响的所有细节:你可以运行命令,然后使用git status
。
git show
or git log -p
show a commit as a change?每个提交都包含其父提交的原始哈希ID,这反过来意味着我们总是可以从最后一次提交的某些提交开始,然后向后工作以查找所有以前的提交:
... <-F <-G <-H <--master
我们只需要找到最后一次提交的方法。这样的方式是:分支名称,例如master
,标识最后一次提交。如果最后一个提交的哈希ID是H
,Git会在对象数据库中找到提交H
。 H
存储G
的哈希ID,Git从中找到G
,它存储F
的哈希ID,Git从中找到F
,依此类推。
这也是将提交显示为补丁的指导原则。我们让Git查看提交本身,找到它的父级,然后提取该提交的快照。然后我们让Git也提取提交的快照。现在我们有两个快照,现在我们可以比较它们 - 从之后的一个中减去前一个快照。无论有什么不同,都必须在快照中发生变化。
请注意,这仅适用于非合并提交。当我们让Git构建一个合并提交时,我们让Git存储不是一个而是两个父哈希ID。例如,在git merge feature
上运行master
之后,我们可能会:
G--H--I
/ \
...--F M <-- master (HEAD)
\ /
J--K--L <-- feature
提交M
有两个父母:它的第一个父母是I
,这是刚才master
的提示。它的第二个父亲是L
,这仍然是feature
的提示。提交M
作为I
或L
的简单更改是很难的,不可能的,真的,并且默认情况下,git log
根本不打算在这里显示任何更改!
(你可以告诉git log
和git show
,实际上,拆分合并:显示从I
到M
的差异,然后使用L
或M
显示从git log -m -p
到git show -m
的第二个独立差异.git show
命令产生,默认情况下,Git称之为组合差异,这有点奇怪和特殊:它实际上是由-m
运行两个差异,然后忽略他们所说的大部分内容,并且只显示来自的一些变化这两个提交。这与合并的工作方式有很大关系:想法是显示可能存在合并冲突的部分。)
这引出了我们的嵌入式问题,在我们进行挑选和回复之前,我们需要讨论这个问题。我们需要讨论git merge
的机制,即我们如何获得提交M
的快照。
git merge
?让我们首先注意到大多数合并的合并点,无论如何都要结合工作。当我们做git checkout master
然后git merge feature
时,我们的意思是:我做了一些关于master
的工作。其他人在feature
做了一些工作。我想将他们所做的工作与我所做的工作结合起来。有一个进行组合的过程,然后是一个更简单的过程来保存结果。
因此,真正的合并有两个部分导致像上面的M
这样的提交。第一部分是我喜欢称之为动词部分,合并。这部分实际上结合了我们不同的变化第二部分是合并或合并提交:这里我们使用“合并”这个词作为名词或形容词。
这里也值得一提的是,git merge
并不总是合并。命令本身很复杂,并且有很多有趣的标志参数来以各种方式控制它。在这里,我们只考虑它确实进行实际合并的情况,因为我们正在寻找合并以理解樱桃挑选和还原。
真正合并的第二部分是更容易的部分。一旦我们完成了合并过程,merge-as-a-verb,我们让Git以通常的方式使用索引中的任何内容进行新的提交。这意味着索引需要以其中的合并内容结束。 Git会像往常一样构建树并像往常一样收集日志消息 - 如果我们感觉特别勤奋,我们可以使用不那么好的默认值merge branch B
,或构建一个好的默认值。 Git会像往常一样添加我们的名字,电子邮件地址和时间戳。然后Git将写出一个提交 - 但不是存储,在这个新提交中,只是一个父,Git将存储一个额外的第二个父,这是我们运行git merge
时选择的提交的哈希ID。
例如,对于我们在git merge feature
上的master
,第一个父级将提交I
- 我们通过运行git checkout master
检查的提交。第二个父母将提交L
,feature
指向的那个。这就是合并的全部内容:合并提交只是一个至少有两个父项的提交,标准合并的标准两个父项是第一个与任何提交相同,第二个是我们的通过运行git merge something
挑选。
合并为动词是更难的部分。我们在上面提到过,Git将从索引中的任何内容进行新的提交。因此,我们需要将索引放入索引,或者将Git放入其中,结合工作的结果。
我们在上面声明我们对master
做了一些改变,他们 - 无论他们是谁 - 在feature
上做了一些改变。但是我们已经看到Git没有存储更改。 Git存储快照。我们如何从快照转变为变化?
我们已经知道了这个问题的答案!当我们看着git show
时,我们看到了它。 Git比较了两个快照。所以对于git merge
,我们只需要选择正确的快照。但哪些是正确的快照?
这个问题的答案在于提交图。在我们运行git merge
之前,图表看起来像这样:
G--H--I <-- master (HEAD)
/
...--F
\
J--K--L <-- feature
我们坐在I
的尖端master
上。他们的承诺是承诺L
,feature
的尖端。从I
,我们可以向后工作到H
然后G
然后F
然后推测E
等等。同时,从L
,我们可以倒退到K
然后J
然后F
和大概E
等等。
当我们真的做这个倒退技巧时,我们会聚在提交F
。显然,那么,无论我们做了什么改变,我们都从F
中的快照开始......无论他们做了什么改变,他们也从F
的快照开始!因此,我们要做的就是将两组变化结合起来:
F
和I
:这就是我们改变的F
和L
:这就是他们改变的从本质上讲,我们将让Git运行两个git diff
s。人们会弄清楚我们改变了什么,并且会弄清楚他们改变了什么。提交F
是我们的共同起点,或者在版本控制方面,是合并基础。
现在,为了实际完成合并,Git扩展了索引。 Git现在让索引保存每个文件的三个副本,而不是保存每个文件的一个副本。一份副本将来自合并基地F
。第二份副本将来自我们的提交I
。最后一个,第三个副本来自他们的提交L
。
同时,Git还会逐个文件地查看两个差异的结果。只要提交F
,I
和L
都拥有所有相同的文件,2只有这五种可能性:
案例5是唯一艰难的案例。对于所有其他人,Git知道 - 或者至少假设它知道 - 正确的结果是什么,因此对于所有其他情况,Git将有问题的文件的索引槽缩回到只有一个槽(编号为零),其中包含正确的结果。
但是,对于案例5,Git将三个输入文件的所有三个副本填充到索引中的三个编号槽中。如果文件名为file.txt
,:1:file.txt
保存F
的合并基本副本,:2:file.txt
保存我们的副本来自提交I
,而:3:file.txt
保存他们的副本来自L
。然后Git运行一个低级合并驱动程序 - 我们可以在.gitattributes
中设置一个,或者使用默认的一个。
默认的低级合并采用两个差异,从base到我们以及从base到他们,并尝试通过采用两组更改来组合它们。每当我们触摸文件中的不同行时,Git就会接受我们或他们的更改。当我们触摸相同的行时,Git声明合并冲突.3 Git将生成的文件作为file.txt
写入工作树,如果存在冲突,则使用冲突标记。如果你将merge.conflictStyle
设置为diff3
,冲突标记包括插槽1中的基本文件,以及插槽2和3中文件的行。我喜欢这种冲突样式比默认情况好得多,这省略了slot-1上下文并显示插槽2与插槽3的冲突。
当然,如果有冲突,Git声明合并冲突。在这种情况下,它(最终,在处理完所有其他文件之后)在合并过程中停止,在工作树中留下冲突标记混乱,并在索引中留下file.txt
的所有三个副本,位于插槽1,2中,但是如果Git能够自己解决两个不同的变更集,它会继续并删除第1-3个槽,将成功合并的文件写入工作树,4将工作树文件复制到正常插槽零处的索引,并像往常一样继续处理其余文件。
如果合并确实停止了,那么解决这个烂摊子就是你的工作。许多人通过编辑冲突的工作树文件,找出正确的结果,写出工作树文件,并运行git add
将该文件复制到索引中来做到这一点.5 copy-into-index步骤删除了阶段1-3条目并写入正常的阶段零条目,以便解决冲突并且我们准备提交。然后你告诉合并继续,或直接运行git commit
,因为git merge --continue
无论如何只运行git commit
。
这个合并过程虽然有点复杂,但最终非常简单:
这将合并或合并为动词过程,然后合并为名词步骤,进行合并提交,并完成合并。
2如果三个输入提交没有所有相同的文件,事情会变得棘手。我们可以添加/添加冲突,修改/重命名冲突,修改/删除冲突等等,所有这些都是我称之为高级别的冲突。这些也会在中间停止合并,并在适当时填充索引的第1-3个插槽。 -X
旗帜,-X ours
和-X theirs
,不会影响高级别的冲突。
3你可以使用-X ours
或-X theirs
让Git选择“我们的改变”或“他们的改变”,而不是停止冲突。请注意,您将此参数指定为git merge
的参数,因此它适用于所有存在冲突的文件。在冲突发生之后,可以使用git merge-file
以更智能和更具选择性的方式一次执行一个文件,但Git并不能让它变得如此简单。
至少,Git认为该文件已成功合并。 Git正是基于合并的两个方面触及同一文件的不同行,并且必须是好的,当它实际上不一定完全正常时。不过,它在实践中运作良好。
5有些人更喜欢合并工具,它们通常会显示所有三个输入文件,并允许您以某种方式构建正确的合并结果,以及如何依赖于工具。合并工具可以简单地从索引中提取这三个输入,因为它们就在三个插槽中。
git cherry-pick
and git revert
work?这些也是三方合并操作。他们使用提交图,方式类似于git show
使用它的方式。它们并不像git merge
那样华丽,即使它们使用合并作为合并代码的动词部分。
相反,我们从你可能拥有的任何提交图开始,例如:
...---o--P--C---o--...
. .
. .
. .
...--o---o---H <-- branch (HEAD)
H
和P
之间以及H
和C
之间的实际关系(如果有的话)并不重要。这里唯一重要的是当前(HEAD)提交是H
,并且有一些提交C
(孩子)与(一个,单个)父提交P
。也就是说,P
和C
直接是我们想要选择或还原的提交的父级和提交。
由于我们正在提交H
,这就是我们的索引和工作树中的内容。我们的HEAD附加到名为branch
的分支,branch
指向提交H
.6。现在,Git为git cherry-pick hash-of-C
做的很简单:
P
作为合并基础。H
作为我们的,并将C
作为他们的提交。这个合并为动词的过程发生在索引中,就像git merge
一样。当它全部成功完成 - 或者你清理了一团糟,如果它没有成功,你运行git cherry-pick --continue
-Git继续进行普通的非合并提交。
如果你回顾一下merge-as-a-verb过程,你会发现这意味着:
P
vs C
:这就是他们所改变的P
vs H
:这就是我们所改变的P
中的内容所以git cherry-pick
是一个三方合并。只是他们所改变的东西与git show
所展示的一样!与此同时,我们改变的是将P
变成H
所需的一切 - 我们确实需要这样做,因为我们希望将H
作为我们的起点,并且只将它们的变化添加到那里。
但这也是为什么樱桃挑选有时看到一些奇怪的 - 我们认为 - 冲突。它必须将整套P
-vs-H
变化与P
-vs-C
变化结合起来。如果P
和H
相距很远,那么这些变化可能是巨大的。
git revert
命令就像git cherry-pick
一样简单,事实上,它是由Git中的相同源文件实现的。它所做的只是使用提交C
作为合并基础并将P
作为提交提交(同时使用H
作为我们的常规)。也就是说,Git将对C
,对H
的回复承诺进行分析,看看我们做了什么。然后,它将使C
,恢复的承诺,与P
相比,看看他们做了什么 - 当然,这与他们实际做的相反。然后,合并引擎(实现合并作为动词的部分)将组合这两组更改,将组合的更改应用于C
并将结果放入索引和工作树中。综合结果保持我们的变化(C
vs H
)并撤消他们的变化(C
vs P
是反向差异)。
如果一切顺利,我们最终会得到一个完全普通的新提交:
...---o--P--C---o--...
. .
. .
. .
...--o---o---H--I <-- branch (HEAD)
从H
到I
的差异,这是我们将在git show
看到的,是P
到C
变化(樱桃挑选)的副本或P
到C
变化的逆转(恢复)。
除非索引和工作树与当前提交匹配,否则可以选择并恢复拒绝运行,尽管它们确实具有允许它们不同的模式。 “允许不同”只是调整期望的问题。而且如果拣选或恢复失败,则可能无法彻底恢复。如果工作树和索引与提交匹配,则很容易从失败的操作中恢复,因此这就是存在此要求的原因。