git - merge 同一分支上的两个GIT提交

标签 git

我重新遇到了几次问题。这是基本情况:

创建一个文件,file1.txt,其内容如下:

Hello,
Welcome to my file.
Goodbye.


$ git add file1.txt
$ git commit -m “Initial commit”

将第二行正文添加到file1.txt。 **注意:添​​加此行时,“意外”删除“欢迎使用我的文件”。

Hello,
This is the second line.
Goodbye.


$ git add file1.txt
$ git commit -m “Added second line”


$ git log
commit ccd8.. (HEAD -> master)
Author: ____
Date:   Tue Jan 28 11:50:11 2020 -0800

Added second line

commit 6d83..
Author: ___
Date:   Tue Jan 28 11:49:36 2020 -0800

Initial commit

合并这两个提交的最佳方法是什么?目标是拥有文件file1.txt,其中包含以下内容:

Hello,
Welcome to my file.
This is the second line.
Goodbye.



到目前为止,我尝试过的是:
$ git checkout 6d83..
$ git branch tmp
$ git checkout master
$ git merge tmp

但是我收到消息“已经更新”。 git rebase在这里最好的做法吗?为什么创建临时分支然后合并无效?

最佳答案

这里的问题是,就Git而言,删除已删除的行是正确的答案。

请记住,Git的基本存储单位是提交。每个提交都有:

  • 一些数据:所有文件的快照;和
  • 一些元数据:有关提交的信息。这包括创建者,时间(日期和时间戳)以及原因(您或提交者的日志消息)。不过,对Git来说,最后一个也是最重要的元数据是父提交哈希ID。

  • 每个提交都有唯一的哈希ID。哈希ID会在您提交后立即分配给提交。从那时起,该哈希ID保留给该提交。只有该提交可以具有该ID.1

    同时,正如我们刚刚指出的,每个提交都必须在其元数据中存储一个哈希ID。从技术上讲,每个提交都可以存储Git所需的哈希ID,但是它们必须是已经存在的提交的哈希ID。2大多数提交仅存储另一个提交的哈希ID:提交的父(单数)。 (合并提交存储两个,这就是使它们合并提交的原因,而某人在新的,完全为空的存储库中进行的第一个提交不能有父级(没有更早的提交可以引用回去,因此它只是没有) t。)

    那么,就您而言,您可能已经进行了一些较早的提交。我们只绘制一个假设您做过的图:
    ... <-F <-G <-H
    

    哈希ID为H的提交(H代表真实的哈希ID,看起来是随机的)会记住其父级的哈希ID,先前存在的commit G会记住其父级F的哈希ID,依此类推。嵌入在每个提交的元数据中的这些向后箭头是Git查找提交的方式-提交H本身(最后的提交)除外。

    Git查找任何分支的最后提交的方式是分支名称(例如master)保存提交的哈希ID。因此,为了使绘图更加完整,让我们将其绘制进去。由于在进行任何提交后,任何提交都无法更改,因此我们可以偷懒并停止将那些箭头绘制为箭头,只要我们记得它们指向后即可:
    ...--F--G--H   <-- master
    

    现在,让我们进行新的提交,以添加此新文件file1.txt。提交H根本没有file1.txt -它具有一些其他文件,但是没有file1.txt。我们git add file1.txt并运行git commit并提供一条日志消息。 Git创建一个新提交,该提交将获得一个新的唯一的大丑陋哈希ID,但我们将其称为I。 Git将父对象设置为H,以便I指向H:
    ...--F--G--H   <-- master
                \
                 I
    

    然后,作为git commit的最后一步,Git将I的实际哈希ID写入名称master中:
    ...--F--G--H
                \
                 I   <-- master
    

    (没有理由继续在单独的行上绘制I,因此我们不会这样做。)

    现在,您可以编辑文件,并按照通常的过程制作新的提交J。 Commit JI作为其父级,而Git将J的哈希ID写入名称master中:
    ...--F--G--H--I--J   <-- master
    

    这里没有要合并的内容,也就是说,您不能使用git merge来完成所需的操作。您有一个线性的提交链,以J结尾。从J我们回到I,从IH,依此类推。

    1从某种意义上说,哈希ID是在提交之前保留给该提交的,但哈希ID本身取决于您进行提交的确切时间(精确到秒)。因此,如果您提前一秒钟或稍后一秒钟进行提交,则该提交将具有不同的哈希ID。无论如何,哈希ID是唯一的:只有该提交才能具有该哈希ID。

    如果Git无法提供唯一的哈希ID,它就不会让您进行提交!尽管这在理论上是可能的,但从未真正发生过。另请参阅How does the newly found SHA-1 collision affect Git?

    2我们将要创建的新提交的哈希ID取决于其父提交的哈希ID。因此,即使我们弄清楚如果其父项是现有的提交X,则新创建的提交将具有什么哈希ID,对于任何X,如果我们在创建之前将该哈希ID插入到提交的元数据中,那么它将获得一个不同的哈希ID。毕竟。因此,提交不可能引用自身,并且不允许在其中放置一些随机的垃圾。因此,每次提交总是引用一些较早的提交。

    简而言之,在提交后,您可以在时间上向后退至其父级,但是您只能在时间上向后退。您不能继续寻找其 future 的孩子。

    因此,您不能更改任何提交,也不能删除任何较早的提交而又不删除所有较晚的提交。 (Git使得删除提交特别困难。与Mercurial相比,您在其中运行hg strip -r <rev>并删除该提交及其所有子项。您仍然无法选择子项,但是很容易取消提交。)

    合并中

    当我们有多个分支名称时,通常发生在Git中的合并。让我们回到刚才提交H作为master的最后一次提交的情况。 (我们可以使用git reset --hard HEAD~2来实现这一点-使master再次直接指向H,并设置工作区域(Git的索引以及我们可以在其中看到文件的工作树)来再次反映H的提交。IJ将继续存在,并且默认情况下可以再检索至少30天。但是我们只是假装根本没有制作IJ。)因此,我们有:
    ...--G--H   <-- master
    

    现在,我们将创建一个或两个新分支。在执行此操作时,我们需要在工程图中添加更多内容。如果只有一个分支名称master,则可能是我们正在使用的分支。但是,如果我们添加了dev作为第二个名字怎么办?我们使用哪个名字?

    Git的答案是使用特殊名称HEAD。通常,此特殊名称附加到您的分支机构名称之一。 (它只能附加到一个或不附加:最多不能附加一个。)我们将添加第二个分支名称dev,但将HEAD附加到master:
    ...--G--H   <-- master (HEAD), dev
    

    现在,我们将以通常的方式创建新的提交IJ。让我们来画它们:
              I--J   <-- master (HEAD)
             /
    ...--G--H   <-- dev
    

    请注意,dev尚未移动:它仍然指向现有的提交H。现在,名称master指向新的提交J

    现在,让我们在dev上创建两个提交。我们从做git checkout dev开始。这会将我们的HEAD附加到dev,还提取了H提交的内容以用于/在其上工作:
              I--J   <-- master
             /
    ...--G--H   <-- dev (HEAD)
    

    存储库中的提交未更改!但是我们看到并使用的文件都有,并且当前分支是dev,当前提交是H .3现在,我们再进行两个新提交。允许使用任何数字,但是使用两个数字可以简化图示:
              I--J   <-- master
             /
    ...--G--H
             \
              K--L   <-- dev (HEAD)
    

    现在我们可以运行git merge。我们选择一个要使用的分支(即git checkout mastergit checkout dev),然后运行git merge并为其指定另一个分支的名称。4让我们使用git checkout mastergit merge dev,以便HEAD和当前提交标识J而不是L:5
              I--J   <-- master (HEAD)
             /
    ...--G--H
             \
              K--L   <-- dev
    

    Git现在必须找到两个分支上的最佳提交。在这种情况下,这很明显:它是commit H。我们通过回溯两步从J到达那里,然后通过回溯两步从L到达那里。如果底部的链较长,则我们必须返回3或4或更多的步骤,但是只要我们能够提交H,commit H将是最好的共享提交。

    Git将此称为共享的最佳提交,从我们和他们俩开始,即合并基础。 合并基础提交是合并的关键。 您(或Git)通过查看图形来查找它,该图显示了提交如何连接。

    Git现在将运行两个git diff操作:
  • git diff --find-renames hash-of-H hash-of-J,找出由于共享提交master导致我们对H所做的更改;和
  • git diff --find-renames hash-of-H hash-of-L,以查找它们在dev上的更改,因为共享提交H
  • git merge的作用是合并这些更改,然后将合并的更改应用于commit H(合并基础)中的快照。这样,我们就可以保留更改并添加更改。

    这也是为什么合并大多是对称的。如果我们已经 checkout dev,即commit L并运行git merge master,Git仍会找到常见的commit H作为合并基础。它将运行相同的两个git diff命令(以其他顺序运行,但谁在乎?)。然后,它将这些差异组合成一个大的组合集,并将它们从commit H应用于快照。结果将是相同的。

    如果我们的更改及其更改以某种方式重叠,则Git将声明合并冲突。在这种情况下,Git不会自行完成合并。这将使您陷入混乱,必须手动清理。没关系:您只需清理git add,然后提交(或运行git merge --continue)即可完成工作。

    为了完成这项工作,Git将进行一次新提交-我们将其称为M进行合并,因为我们通过H巧妙地标记了每个先前的提交L-并像往常一样更新当前分支名称,因此无论我们拥有哪个分支现在 check out 结束于新的合并提交M。为了将其标记为合并提交,Git将其两个父对象依次设置为JL,因为开始时我们使用的是J。这样我们就可以得出结果:
              I--J
             /    \
    ...--G--H      M   <-- master (HEAD)
             \    /
              K--L   <-- dev
    

    我们有合并。合并附带的快照是将H -vs- HJ -vs- H的组合更改应用于L的结果。合并的父级是通常的上一个提交,而另一个是我们在运行git merge dev时选择的提交。

    现在已经存在此合并,无法将L甚至K合并为master了。原因是LM之间最好的共享提交是L ...,它已经成为M历史的一部分。如果我们从最底行的M后退,则到达L。历史记录(在Git中由提交(包括它们的连接)组成)表示L已在此处合并。

    3当您询问Git时:HEAD是什么?您可以通过两种方式对此进行表述。您可以询问Git:HEAD中的分支名称是什么?或者,您可以问:HEAD选择什么提交?这两个不同的问题得到两个不同的答案。在HEAD未附加到任何分支名称的“分离式HEAD”模式下,第一个会为您带来错误而不是答案。第二个问题几乎总是有效。

    Git还具有一个未出生分支的概念,当您从一个完全没有提交的新的完全空的存储库开始时,它需要它。在这种情况下,HEAD存在,并且具有分支名称,但是分支名称本身不存在且无效。因此,在这种特殊情况下,您可以问有关HEAD的“what name”(问题)问题,而不是“what ID”(what ID)问题:与分离的HEAD设置相反。

    4实际上,git merge通过提交哈希ID起作用,因此我们可以为它提供所需的任何提交的哈希ID。但是通常我们(人类)是按名字工作的。

    5除了第一个列出的父对象外,每种方法的合并结果通常是相同的。但是,如果我们对git merge使用特定的标志参数,则合并结果可能会有所不同。

    采摘樱桃

    但是我们可以做些事情。完全没有任何提交链-是否有类似fork的东西:
              o--P--C--o--o   <-- branch1
             /
    ...--o--o
             \
              o--o--H   <-- branch2 (HEAD)
    

    或只是像这样的线性链:
    ...--o--o--P--C--o--o--H   <-- branch (HEAD)
    

    我们可以选择一些commit C,它的父母是P的孩子,然后在上面运行git cherry-pick。 (通常,您将在此处使用C的哈希ID。)这是强制Git执行以下操作:
  • 查找提交P的父对象C:这很容易,因为C在其中包含P的哈希ID;
  • P视为合并基础,将C视为“其”提交,将当前提交H(由HEAD选择)作为“我们的”提交,并像往常一样进行全面的三向合并。

  • 因此,Git现在将diff PC进行比较以查看“他们”做了什么,diff PH进行了对比以了解我们做了什么,并将这两组更改组合在一起。然后,Git将组合的更改应用于P中的快照。如果一切顺利,Git将使用C'的原始提交消息,将结果文件作为新快照C提交(提交C的副本)。它不会使它成为合并提交,而只是一个普通的提交:
              o--P--C--o--o   <-- branch1
             /
    ...--o--o
             \
              o--o--H--C'  <-- branch2 (HEAD)
    

    要么:
    ...--o--o--P--C--o--o--H--C'  <-- branch (HEAD)
    

    如上图所示,从另一个分支中挑选一个提交往往更有意义。但您可以从自己的历史记录中挑选一个提交,以重新应用相同的更改。如果CC'之间的某些提交是对C .6中发生的任何事情都没有做的提交,则这特别有用。

    6Git有一个命令git revert进行此类提交。您将其指向某个孩子,并且Git进行与Cherry-pick相同的三向合并,只是这次合并的基础是C,而“他们的”提交是P。 (与往常一样,我们的/ HEAD提交是HEAD提交。)练习:尝试按该顺序获取CP的区别。如果按顺序将这组更改与CHEAD结合会发生什么?

    请注意,所有这些操作都是针对整个提交的

    您开始想大惊小怪。但是Git在这里所做的一切(或我们已经向Git展示的)都是基于整个提交的。那是因为提交确实是Git中的基本单元。确实可以提交存储文件,但是Git并不是真正的文件。 Git与提交有关。文件仅仅是使提交有用的东西。

    您可以从单个提交中提取单个文件,并对其进行处理:例如git diff,给定两个文件的名称,则仅可以区分这两个文件。但这是与Git合作的一种非典型方式。 Git专为一次提交操作而设计。

    关于git - merge 同一分支上的两个GIT提交,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59957967/