在git中它意味着“提交引入的更改”

问题描述 投票:1回答:1

在任何地方我都看到了这一点:“... 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
1个回答
2
投票

正如几位人士在评论中指出的那样(并与其他问题建立了联系),git cherry-pick实际上进行了三方合并。 How do cherry-pick and revert work?描述了这一点,但在内容方面比机制更多。

我描述了Why do I get this merge conflict with git rebase interactive?中一组特定合并冲突的来源,以及樱桃挑选和还原的一般概述,但我认为退一步并询问你所做的机制问题是个好主意。不过,我会重新构建它,因为这三个问题:

  • 提交真的是快照吗?
  • 如果提交是快照,git showgit log -p如何将其显示为更改?
  • 如果提交是快照,git cherry-pickgit revert如何工作?

回答最后一个问题需要先回答一个问题:

  • Git如何执行git merge

那么,让我们按照正确的顺序采取这四个问题。这将是相当长的,如果你愿意,你可以直接跳到最后一节 - 但请注意它建立在第三部分的基础上,第三部分建立在第二部分的基础上,它建立在第一部分之上。

Is a commit really a snapshot?

是的 - 但是,从技术上讲,提交是指快照,而不是一个快照。这非常简单明了。要使用Git,我们通常首先运行git clone,它会为我们提供一个新的存储库。有时,我们首先创建一个空目录并使用git init创建一个空存储库。无论哪种方式,我们现在有三个实体:

  1. 存储库本身,其是对象的大数据库,加上较小的名称到哈希ID映射的数据库(例如,用于分支名称),以及许多实现为单个文件的其他小型数据库(例如,每个reflog一个)。
  2. Git调用索引,暂存区域或有时缓存。它被称之为取决于谁在做什么。索引基本上是你让Git构建你将要进行的下一次提交的地方,尽管它在合并期间需要扩展角色。
  3. 工作树,您可以在其中实际查看文件并使用它们进行工作。

对象数据库包含四种类型的对象,Git调用提交,树,blob和带注释的标记。树和blob主要是实现细节,我们可以在这里忽略带注释的标签:为了我们的目的,这个大数据库的主要功能是保存我们所有的提交。然后,这些提交引用保存文件的树和blob。最后,它实际上是树 - 加 - blob的组合,它是快照。尽管如此,每个提交只有一棵树,而这棵树是让我们完成快照的其余部分,所以除了大量的恶魔实现细节之外,提交本身也可能是一个快照。

How we use the index to make new snapshots

我们还不会深入研究杂草,但我们会说索引的工作原理是保存每个文件的压缩的,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


How does git show or git log -p show a commit as a change?

每个提交都包含其父提交的原始哈希ID,这反过来意味着我们总是可以从最后一次提交的某些提交开始,然后向后工作以查找所有以前的提交:

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

我们只需要找到最后一次提交的方法。这样的方式是:分支名称,例如master,标识最后一次提交。如果最后一个提交的哈希ID是H,Git会在对象数据库中找到提交HH存储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作为IL的简单更改是很难的,不可能的,真的,并且默认情况下,git log根本不打算在这里显示任何更改!

(你可以告诉git loggit show,实际上,拆分合并:显示从IM的差异,然后使用LM显示从git log -m -pgit show -m的第二个独立差异.git show命令产生,默认情况下,Git称之为组合差异,这有点奇怪和特殊:它实际上是由-m运行两个差异,然后忽略他们所说的大部分内容,并且只显示来自的一些变化这两个提交。这与合并的工作方式有很大关系:想法是显示可能存在合并冲突的部分。)

这引出了我们的嵌入式问题,在我们进行挑选和回复之前,我们需要讨论这个问题。我们需要讨论git merge的机制,即我们如何获得提交M的快照。

How does Git perform git merge?

让我们首先注意到大多数合并的合并点,无论如何都要结合工作。当我们做git checkout master然后git merge feature时,我们的意思是:我做了一些关于master的工作。其他人在feature做了一些工作。我想将他们所做的工作与我所做的工作结合起来。有一个进行组合的过程,然后是一个更简单的过程来保存结果。

因此,真正的合并有两个部分导致像上面的M这样的提交。第一部分是我喜欢称之为动词部分,合并。这部分实际上结合了我们不同的变化第二部分是合并或合并提交:这里我们使用“合并”这个词作为名词或形容词。

这里也值得一提的是,git merge并不总是合并。命令本身很复杂,并且有很多有趣的标志参数来以各种方式控制它。在这里,我们只考虑它确实进行实际合并的情况,因为我们正在寻找合并以理解樱桃挑选和还原。

Merge as a noun or adjective

真正合并的第二部分是更容易的部分。一旦我们完成了合并过程,merge-as-a-verb,我们让Git以通常的方式使用索引中的任何内容进行新的提交。这意味着索引需要以其中的合并内容结束。 Git会像往常一样构建树并像往常一样收集日志消息 - 如果我们感觉特别勤奋,我们可以使用不那么好的默认值merge branch B,或构建一个好的默认值。 Git会像往常一样添加我们的名字,电子邮件地址和时间戳。然后Git将写出一个提交 - 但不是存储,在这个新提交中,只是一个父,Git将存储一个额外的第二个父,这是我们运行git merge时选择的提交的哈希ID。

例如,对于我们在git merge feature上的master,第一个父级将提交I - 我们通过运行git checkout master检查的提交。第二个父母将提交Lfeature指向的那个。这就是合并的全部内容:合并提交只是一个至少有两个父项的提交,标准合并的标准两个父项是第一个与任何提交相同,第二个是我们的通过运行git merge something挑选。

Merge as a verb

合并为动词是更难的部分。我们在上面提到过,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上。他们的承诺是承诺Lfeature的尖端。从I,我们可以向后工作到H然后G然后F然后推测E等等。同时,从L,我们可以倒退到K然后J然后F和大概E等等。

当我们真的做这个倒退技巧时,我们会聚在提交F。显然,那么,无论我们做了什么改变,我们都从F中的快照开始......无论他们做了什么改变,他们也从F的快照开始!因此,我们要做的就是将两组变化结合起来:

  • 比较FI:这就是我们改变的
  • 比较FL:这就是他们改变的

从本质上讲,我们将让Git运行两个git diffs。人们会弄清楚我们改变了什么,并且会弄清楚他们改变了什么。提交F是我们的共同起点,或者在版本控制方面,是合并基础。

现在,为了实际完成合并,Git扩展了索引。 Git现在让索引保存每个文件的三个副本,而不是保存每个文件的一个副本。一份副本将来自合并基地F。第二份副本将来自我们的提交I。最后一个,第三个副本来自他们的提交L

同时,Git还会逐个文件地查看两个差异的结果。只要提交FIL都拥有所有相同的文件,2只有这五种可能性:

  1. 没有人碰过这个档案。只需使用任何版本:它们都是一样的。
  2. 我们更改了文件但他们没有。只需使用我们的版本。
  3. 他们改变了文件而我们没有。只需使用他们的版本。
  4. 我们和他们都改变了文件,但我们做了同样的改动。使用我们的或他们两者都是相同的,所以无论哪个都没关系。
  5. 我们和他们都改变了同一个文件,但我们做了不同的改动。

案例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有些人更喜欢合并工具,它们通常会显示所有三个输入文件,并允许您以某种方式构建正确的合并结果,以及如何依赖于工具。合并工具可以简单地从索引中提取这三个输入,因为它们就在三个插槽中。

How do git cherry-pick and git revert work?

这些也是三方合并操作。他们使用提交图,方式类似于git show使用它的方式。它们并不像git merge那样华丽,即使它们使用合并作为合并代码的动词部分。

相反,我们从你可能拥有的任何提交图开始,例如:

...---o--P--C---o--...
      .      .
       .    .
        .  .
 ...--o---o---H   <-- branch (HEAD)

HP之间以及HC之间的实际关系(如果有的话)并不重要。这里唯一重要的是当前(HEAD)提交是H,并且有一些提交C(孩子)与(一个,单个)父提交P。也就是说,PC直接是我们想要选择或还原的提交的父级和提交。

由于我们正在提交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过程,你会发现这意味着:

  • diff commit P vs C:这就是他们所改变的
  • diff commit P vs H:这就是我们所改变的
  • 结合这些差异,将它们应用于P中的内容

所以git cherry-pick是一个三方合并。只是他们所改变的东西与git show所展示的一样!与此同时,我们改变的是将P变成H所需的一切 - 我们确实需要这样做,因为我们希望将H作为我们的起点,并且只将它们的变化添加到那里。

但这也是为什么樱桃挑选有时看到一些奇怪的 - 我们认为 - 冲突。它必须将整套P-vs-H变化与P-vs-C变化结合起来。如果PH相距很远,那么这些变化可能是巨大的。

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)

HI的差异,这是我们将在git show看到的,是PC变化(樱桃挑选)的副本或PC变化的逆转(恢复)。


除非索引和工作树与当前提交匹配,否则可以选择并恢复拒绝运行,尽管它们确实具有允许它们不同的模式。 “允许不同”只是调整期望的问题。而且如果拣选或恢复失败,则可能无法彻底恢复。如果工作树和索引与提交匹配,则很容易从失败的操作中恢复,因此这就是存在此要求的原因。

© www.soinside.com 2019 - 2024. All rights reserved.