当前位置: 首页 > 知识库问答 >
问题:

git中“提交引入的更改”的含义

暨正真
2023-03-14

我看到的每一处都是这样的:“…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 **

现在我明显的问题是,我对cherry pick的工作原理有什么误解,具体来说,为什么这里会有合并冲突,这将是git merge的一个快速发展?

重要通知:这不是一个关于合并冲突的问题,我感兴趣的是樱桃选择实际上在这里做什么。我不是出于好奇/随便问,而是因为我在工作中使用git遇到了麻烦。

共有1个答案

仲鸿风
2023-03-14

正如一些人在评论中指出的那样(并链接到其他问题),git cherry pick实际上是三方合并。cherry pick和revert是如何工作的?描述了这一点,但更多的是从内容而不是机制方面。

我在《为什么我要用git rebase interactive获得这个合并冲突?》中描述了一组特定合并冲突的来源,并概述了cherry pick and revert,但我认为退一步问一下您所做的机制问题是个好主意。不过,我会将其重新框定为以下三个问题:

  • 提交真的是快照吗?

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

  • Git如何执行git合并

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

是的——尽管从技术上来说,提交指的是快照,而不是快照。这非常简单明了。要使用Git,我们通常从运行git clone开始,这为我们提供了一个新的存储库。有时,我们从创建一个空目录开始,并使用git init创建一个空存储库。不管怎样,我们现在有三个实体:

>

Git调用索引或暂存区域,有时调用缓存。它被调用的内容取决于谁调用。索引本质上就是让Git构建下一次提交的地方,尽管它在合并过程中扮演着扩展的角色。

工作树,您可以在其中实际查看文件并处理它们。

对象数据库包含四种类型的对象,Git调用提交、树、blob和带注释的标记。树和blob主要是实现细节,我们可以在这里忽略带注释的标记:为了我们的目的,这个大型数据库的主要功能是保存所有提交。这些提交然后引用保存文件的树和blob。最后,实际上是树和斑点的组合,这就是快照。尽管如此,每个提交都只有一棵树,而这棵树正是我们获取快照的其余部分的原因,因此,除了许多可怕的实现细节外,提交本身也可能是一个快照。

我们还不会深入研究,但是我们会说索引是通过保存每个文件的压缩、Git化、大部分冻结的副本来工作的。从技术上讲,它保存了对实际冻结副本的引用,存储为Blob。也就是说,如果您从执行git clone url开始,Git将运行git签出分支作为克隆的最后一步。此签出从分支顶端的提交中填充索引,以便索引具有该提交中每个文件的副本。

实际上,大多数1git checkout操作都会从提交中填充索引和工作树。这使您可以查看和使用工作树中的所有文件,但工作树副本并不是提交中的副本。提交中的内容是(是?)冻结、压缩、Git-ified,永远无法更改所有这些文件的blob快照。这样可以永久保存这些文件的版本,或者只要提交本身存在,就可以保存这些文件的版本,这对于存档非常有用,但对于执行任何实际工作都是无用的。这就是Git de Git将文件归档到工作树中的原因。

Git可以在这里停止,只使用提交和工作树。Mercurial在许多方面与Git相似,但它确实止步于此:你的工作树是你提议的下一个提交。您只需更改工作树中的内容,然后运行hg提交,它就会从工作树中生成新的提交。这有一个明显的好处,那就是没有讨厌的指数制造麻烦。但是它也有一些缺点,包括天生比Git的方法慢。无论如何,Git所做的是从保存在索引中的前一次提交信息开始,准备再次提交。

然后,每次运行git add,git都会压缩并验证您添加的文件,并立即更新索引。如果您只更改了几个文件,然后git添加了这些文件,那么git只需要更新几个索引项。因此,这意味着索引在任何时候都有下一个快照在它里面,以特殊的Git-only压缩并准备冻结的形式。

这反过来意味着git提交只需要冻结索引内容。从技术上讲,它将索引转换成一个新的树,为新的提交做好准备。在一些情况下,例如在一些还原之后,或者对于一个git提交--let-空,新树实际上与以前的一些提交是相同的树,但是你不需要知道或关心这个。

此时,Git将收集日志消息和进入每次提交的其他元数据。它将当前时间添加为时间戳,这有助于确保每次提交都是完全唯一的,并且通常是有用的。它使用当前提交作为新提交的父散列ID,使用通过保存索引生成的树散列ID,并写出新提交对象,该对象获得新的唯一提交散列ID。因此,新提交包含您先前签出的任何提交的实际散列ID。

最后,Git将新提交的哈希ID写入当前的分支名称中,这样分支名称现在引用新提交,而不是像以前那样引用新提交的父级。也就是说,无论提交是分支的顶端,现在提交是分支顶端之后的一步。新的提示是你刚刚做出的promise。

1您可以使用git签出提交-path从一个特定的提交中提取一个特定的文件。这仍然会先将文件复制到索引中,所以这并不是真正的例外。但是,您也可以使用git check out将文件从索引复制到工作树,并且您可以使用git check out-p选择性地交互式修补文件。这些变体中的每一个都有自己的一组特殊规则,关于它如何处理索引和/或工作树。

由于Git从索引中构建新的提交,因此经常重新检查文档可能是明智的,尽管是痛苦的。幸运的是,git status通过比较当前提交和索引,然后比较索引和工作树,告诉您现在索引中有哪些内容,并且对于每个这样的比较,告诉您有哪些不同。因此,很多时候,您不必在头脑中考虑每个Git命令对索引和/或工作树的影响的各种各样的细节:您只需运行该命令,然后使用Git status

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

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

我们只需要找到找到最后一次提交的方法。这种方式是:分支名称(如此处的master)标识最后一次提交。如果最后一次提交的哈希ID是H,Git会在对象数据库中找到commitHH存储G的哈希ID,Git从中查找G,它存储F的哈希ID,Git从中查找F,依此类推。

这也是将提交显示为补丁的指导原则。我们让Git查看提交本身,找到其父级,并提取提交的快照。然后我们让Git也提取提交的快照。现在我们有两个快照,现在我们可以比较它们,从后面的快照中减去前面的快照。不管有什么不同,这一定是快照中发生的变化。

请注意,这仅适用于非合并提交。当我们让Git构建合并提交时,我们让Git存储的不是一个而是两个父散列ID。例如,在master上运行git合并功能后,我们可能会:

       G--H--I
      /       \
...--F         M   <-- master (HEAD)
      \       /
       J--K--L   <-- feature

CommitM有两个父级:它的第一个父级是I,这是刚才在master上的提示提交。它的第二个父级是L,它仍然是特性上的提示提交。很难-嗯,不可能,真的-将提交M呈现为从IL的简单更改,并且默认情况下,git log根本不会在这里显示任何更改!

(您可以告诉git loggit show实际上拆分合并:使用git log-M-pgit show-M显示从IM的差异,然后显示第二个,使用git log-M-pgit show-M将差异从L分离到,Git称之为组合的diff,这有点奇怪和特殊:实际上,它是通过运行两个diff作为-m,然后忽略它们所说的大部分内容,只显示来自两个提交的一些更改。这与合并的工作方式密切相关:其思想是显示可能已合并的部分冲突。)

这就引出了我们的内在问题,在我们开始认真挑选和还原之前,我们需要先讨论这个问题。我们需要讨论git merge的机制,也就是说,我们首先是如何获得提交M的快照的。

让我们首先注意到合并的目的——不管怎样,大多数合并的目的——是合并工作。当我们做了git签出master,然后做了git合并特性,我们的意思是:我在master上做了一些工作。其他人做了一些关于特性的工作。我想把他们做的工作和我做的工作结合起来。有一个进行这种组合的过程,然后有一个更简单的保存结果的过程。

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

这里还值得一提的是,git merge并不总是进行合并。命令本身很复杂,并且有很多有趣的标志参数以各种方式控制它。在这里,我们只考虑一个实际合并的情况,因为我们正在研究合并,以便理解Cyt Poice和Realt。

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

例如,对于我们在master上的git合并功能,第一个父项将是commitI——我们通过运行git checkout master签出的commit。第二个父级将是commitL,即功能所指向的父级。这就是合并的全部内容:合并提交只是一个至少有两个父级的提交,标准合并的标准两个父级是第一个父级与任何提交的父级相同,第二个父级是我们通过运行git merge something选择的父级。

合并为动词是比较难的部分。我们在上面提到,Git将从索引中的任何内容进行新的提交。因此,我们需要将组合工作的结果放入索引中,或者将Git放入索引中。

我们在上面声明,我们对主机进行了一些更改,并且他们对功能进行了一些更改。但我们已经看到Git不存储更改。Git存储快照。我们如何从快照转变为更改?

我们已经知道这个问题的答案了!当我们查看git show时,我们看到了它。Git比较两个快照。所以对于git合并,我们只需要选择正确的快照。但是哪些是正确的快照呢?

这个问题的答案在于提交图。在运行git merge之前,图形如下所示:

       G--H--I   <-- master (HEAD)
      /
...--F
      \
       J--K--L   <-- feature

我们坐在提交I上,这是master的提示。他们的提交是提交L,这是特性的提示。从I,我们可以向后工作到H,然后是G,然后是F,然后大概是E等等。同时,从L,我们可以向后工作到K,然后是J,然后是F,大概是E等等。

当我们真的向后做这项工作时,我们收敛于提交F。显然,无论我们做了什么更改,我们都从F中的快照开始。。。无论他们做了什么更改,他们也从F中的快照开始!因此,我们所要做的是,结合我们的两组更改:

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

本质上,我们将让Git运行两个Git diffs。一个人会发现我们改变了什么,一个人会发现他们改变了什么。CommitF是我们共同的起点,或者说是版本控制中的合并基础。

现在,为了实际完成合并,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运行一个低级合并驱动程序,我们可以在.gittributes中设置一个,或者使用默认的。

默认的低级合并接受两个不同,从基到我们的,从基到他们的,并试图通过接受两组更改来组合它们。每当我们触摸文件中的不同行时,Git都会接受我们或他们的更改。当我们触摸相同的行时,Git声明合并冲突。3Git将结果文件作为file.txt写入工作树,如果有冲突,则使用冲突标记。如果您将merge.conflictStyle设置为sky3,则冲突标记包括来自槽1的基本文件,以及来自槽2和3中的文件的行。我喜欢这种冲突样式比默认样式好得多,默认样式省略了插槽1上下文,只显示了插槽2和插槽3的冲突。

当然,如果存在冲突,Git会声明合并冲突。在这种情况下,(最终,在处理所有其他文件之后)在合并的中间停止,在工作树中留下冲突标记混乱,索引中的所有三个副本<代码>文件.txt < /C> >在槽1, 2和3中。但是,如果Git能够自行解决两个不同的更改集,它将继续删除插槽1-3,将成功合并的文件写入工作树,4将工作树文件复制到正常插槽0处的索引中,并照常处理其余文件。

如果合并确实停止了,你的工作就是修复混乱。许多人通过编辑冲突的工作树文件,找出正确的结果,写出工作树文件,然后运行git add将该文件复制到索引中。5复制到索引步骤删除阶段1-3条目,并写入正常的阶段零条目,这样冲突就解决了,我们准备好了。然后告诉合并继续,或者直接运行git commit,因为git merge--continue只是运行git commit

这个合并过程虽然有点复杂,但最终非常简单:

  • 选择一个合并基地。
  • 将合并基与当前提交进行区分,我们已经检查了要通过合并来修改的提交,以查看我们更改了什么。
  • 将合并基与另一个提交(我们选择合并的提交)进行比较,看看它们发生了什么变化。
  • 合并更改,将合并的更改应用到合并基中的快照。这是结果,放在索引中。我们可以从合并基础版本开始,因为合并后的更改包括我们的更改:我们不会丢失它们,除非我们说只取它们的文件版本。

这个合并或作为动词合并的过程之后是作为名词合并的步骤,进行合并提交,合并完成。

2如果三个输入提交没有所有相同的文件,事情就会变得棘手。我们可以有添加/添加冲突,修改/重命名冲突,修改/删除冲突,等等,所有这些都是我所说的高级冲突。这些也在中间停止合并,留下适当填充索引的插槽1-3。-X标志,-X our-X theys不会影响高层冲突。

3您可以使用-X我们的-X他们的让Git选择“我们的更改”或“他们的更改”,而不是因为冲突而停止。请注意,您将此指定为git merge的一个参数,因此它适用于所有有冲突的文件。在冲突发生后,可以使用git merge file,以更智能、更选择性的方式一次处理一个文件,但git并没有让这件事变得简单。

4至少,Git认为文件被成功合并。Git的基础仅仅是合并的两边触摸了同一个文件的不同行,这一定是可以的,而实际上这根本不一定是可以的。不过,它在实践中运行得很好。

5有些人更喜欢合并工具,它通常会显示所有三个输入文件,并允许您以某种方式构造正确的合并结果,具体方式取决于工具。合并工具可以简单地从索引中提取这三个输入,因为它们就在三个插槽中。

这些也是三向合并操作。他们使用提交图的方式与git show使用提交图的方式类似。它们不像git merge那样花哨,尽管它们将merge用作合并代码的动词部分。

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

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

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

因为我们正在提交H,这就是我们的索引和工作树中的内容。我们的头连接到名为branch的分支,branch指向提交H6现在,Git为Git cherry pick hash-of-C做的事情很简单:

  • 选择提交P作为合并基础。

这个动词合并过程发生在索引中,就像git merge一样。当一切都成功完成,或者您已经清理了混乱,如果没有成功,并且您已经运行了git cherry pick--continue-git继续进行普通的非合并提交。

如果回顾merge-as-a-verb过程,您会发现这意味着:

  • 差异提交PvsC:这就是他们所改变的
  • 差异提交PvsH:这就是我们所改变的
  • 结合这些差异,将它们应用于P

因此,git cherry pick是一种三方合并。只是他们所做的改变和git show所做的一样!同时,我们所改变的是我们需要将P转化为H——我们确实需要这样,因为我们想把H作为我们的出发点,并且只把它们的变化添加到这一点上。

但这也是为什么樱桃采摘有时会看到一些奇怪的——我们认为——冲突。它必须将整组P-vs-H更改与P-vs-C更改相结合。如果PH相距甚远,这些变化可能是巨大的。

git revert命令与git cherry pick一样简单,实际上是由git中相同的源文件实现的。它所做的就是使用commitC作为合并基础,commitP作为它们的提交(而像往常一样使用H)。也就是说,Git将区分C、提交还原和H,以查看我们做了什么。然后它将区分C、提交还原和P,以查看他们做了什么,当然,这与他们实际做的是相反的。然后,合并引擎(作为动词实现合并的部分)将合并这两组更改,将合并的更改应用于C,并将结果放入索引和我们的工作树中。组合结果保留我们的更改(CvsH)并撤消它们的更改(CvsP是反向差异)。

如果一切顺利,我们最终会得到一个非常普通的新promise:

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

HI的区别,这就是我们将在git show中看到的,要么是P-to-C更改(樱桃选择)的副本,要么是P-to-C更改(还原)。

6除非索引树和工作树与当前提交匹配,否则cherry pick和revert都拒绝运行,尽管它们的模式允许它们不同。“允许不同”只是一个调整预期的问题。事实上,如果拾取或还原失败,则可能无法干净地恢复。如果工作树和索引与提交匹配,则很容易从失败的操作中恢复,因此这就是为什么存在此需求。

 类似资料:
  • 在本文章教程中,我们将演示如何查看 Git 存储库的文件和提交文件记录,并对存储库中的文件作修改和提交。 注意:在开始学习本教程之前,先克隆一个存储库,有关如何克隆存储库,请参考: http://www.yiibai.com/git/git_clone_operation.html 在上一步中,我们已经修改了 main.py 文件中的代码,在代码中定义了两个变量并提交代码,但是要再次添加和修改ma

  • 本文向大家介绍Git 提交特定文件中的更改,包括了Git 提交特定文件中的更改的使用技巧和注意事项,需要的朋友参考一下 示例 您可以使用以下命令提交对特定文件的更改,并跳过暂存文件git add: 或者,您可以首先暂存文件: 稍后再提交:            

  • 提交是 Git 的精髓所在,你无时不刻不在创建和缓存提交、查看以前的提交,或者用各种Git命令在仓库间转移你的提交。大多数的命令都对同一个提交操作,而有些会接受提交的引用作为参数。比如,你可以给 git checkout 传入一个引用来查看以前的提交,或者传入一个分支名来切换到对应的分支。 知道提交的各种引用方式之后,Git 的命令就会变得更加强大。在这章中,我们研究提交的各种引用方式,来一窥 g

  • 当本地文件变更以后,可以通过VCS —> Git —> Commit File 弹出提交变更窗口. 当然,分支合并过后也会弹出提交变更窗口. 配置提交信息 提交变更窗口中你可以选择Change list,也可以选择要提交的变更文件,默认是全选的. 在Author中选择或者输入作者名字.选择Amend commit(修订提交)会在Commit Message中添加上一次的提交信息. 在提交之前,你还

  • 我有这样的promise,1是最新的,3是最老的: 提交1 提交2 提交3 如何删除提交 1 和 2,但保留更改并将其提交到?

  • 我正在尝试撤消自上次提交以来的所有更改。看过这篇文章后,我尝试了<code>git reset--hard。我回答说,头现在在18c3773…但当我查看我的本地源时,所有文件仍然在那里。我错过了什么?