繁体   English   中英

为什么合并分支的总提交数?

[英]Why does merging branches sum total commits?

如果分支A有5次提交,分支B有7次提交,则当我将B合并到A中时,A现在有12次提交。

那是预期的结果吗? 我本以为合并将被视为单个提交?

当您说“分支A有五个提交”时,您可能没有计算分支A包含的所有提交。 同样适用于分支B中的七个提交。要真正理解这一点,重要的是要意识到在Git中,分支(或更确切地说,分支名称)实际上没有任何意义。 只是提交很重要。

为了了解它是如何工作的,让我们从一个只有三个提交的非常小的存储库开始。 这三个提交具有长而丑陋的Git-hash-ID名称,但我们仅将它们称为提交ABC ,就好像这些提交的真实姓名只有一个大写字母一样。 (我们很快就会用完,这是Git使用那些大的丑陋哈希ID的原因之一。)

Git的第一个重要重要秘密是每个提交都在其中存储其先前提交的哈希ID。 每当您拥有提交的哈希ID时,我们都说您指向该提交。 因此,我们的三个提交如下:

A <-B <-C

提交C存储B的哈希ID,因此C指向B B存储A的哈希ID,因此B指向A 当然, A是我们做过的第一个提交:它不能再指向其他任何地方。 这是一个特例- 提交,如果存储库不为空,则至少有一个。 通常,仅存在一个根提交,而该根提交是第一个提交。

分行名称

下一个重要的重要秘密是对第一个秘密的简单后续操作,即像masterdevelop这样的分支名称仅指向一个commit 在这种情况下,我们的master指向的一个提交将是提交C

A--B--C   <-- master

由于种种原因,我总是懒于在提交之间绘制内部箭头。 一个是,一旦我们提交了一个提交,就没有人可以更改提交,甚至没有人(甚至Git本身)。 提交C被永久冻结,总是指向B ,后者被冻结并指向A ,依此类推。 因此,内部箭头始终指向 Git的调用这些父母的承诺:父CB ,和父BA

分支名称指针是不同的。 与每个提交的冻结内容不同,分支名称指针可以并且确实会发生变化。

让我们使用git checkout master ,它将提交C提取到我们的工作树中 ,为我们提供可以查看和使用的文件。 然后,我们将进行一些更改, git add更新的文件,然后git commit进行新的提交,我们将其称为D Git将打包我们的新文件1并进行新的提交D ,指向我们已经提交的提交(即C ,这样我们现在有了:

A--B--C--D

然后作为最后的动作, git commitD的哈希ID写入名称master ,因此master现在不指向C而是指向D

A--B--C--D   <-- master

这是怎么分支机构成长为你添加新的提交:每个新提交点回,这就是一个分支中的最后一个,然后更新的Git分支名称,以便该名称现在确定新的提示。 每当Git查找历史记录(随时间推移发生的事情)时,它都会从最后一次提交(名称指向的提交)开始,然后向后工作,一次提交。

新建分支

要创建一个新分支 ,Git所做的只是添加一个指向现有提交的新名称 现在,在我们的四提交存储库中,将branch branch-a为:

A--B--C--D   <-- master, branch-a (HEAD)

除了添加名称branch-a指向D ,我还为两个分支名称之一附加了特殊名称HEAD (尽管大写,但可以使用@ ,大写)。 这就是Git记住当前分支的方式

在进行任何新提交之前,请自己回答: master中有多少个提交,而branch-a多少个? 如果您没有每次都回答“四个”,那为什么不呢? 如果您问Git,答案是四个:在两个分支上有四个提交,依次是D C B A

现在,通过更改内容并以常规方式使用git addgit commit ,将五个提交添加到我们的新branch-a git commit中。 Git将构造五个新的,唯一的,丑陋的大哈希ID,但是我们将新的提交EFGHI并将其绘制在:

           E--F--G--H--I   <-- branch-a (HEAD)
          /
A--B--C--D   <-- master

当我们创建E ,Git与父D一起创建它,然后将名称 branch-a更改为指向E 当我们制作F ,它的父代是E ,而Git更新了branch-a指向F 我们重复了五次,在branch-a上有五个提交( 不在 master上),在两个分支都有四个提交。 因此, branch-a -a没有五个提交,而只有九个提交。 只是其中五个branch-a

现在,通过首先切换回master ,然后创建新名称branch-b指向commit D ,使它成为branch-b

           E--F--G--H--I   <-- branch-a
          /
A--B--C--D   <-- master, branch-b (HEAD)

请注意,存储库本身内部的其他内容在这里都没有改变。 我们的工作树 (和索引)已经更改-他们又回到了提交D ,并且添加了一个新的名称branch-b ,它像master一样标识提交D ,但是所有提交都不受干扰。

现在,让我们添加branch-b特有的七个提交:

           E--F--G--H--I   <-- branch-a
          /
A--B--C--D
          \
           J--K--L--M--N--O--P   <-- branch-b (HEAD)

实际上在branch-b上有11个提交,但其中四个共享(与master共享,我不再出于懒惰而退出,而与branch-a共享)。

合并

现在您想将branch-b合并为branch-a 因此,您运行的命令将是:

git checkout branch-a
git merge branch-b

第一步选择提交I作为当前提交,选择branch-a HEAD作为HEAD附加的名称。 它将提交I的内容复制到工作树(以及索引/登台区域)中。 图形本身没有变化,但是现在HEAD表示branch-a ,因此提交I

           E---F----G---H----I   <-- branch-a (HEAD)
          /
A--B--C--D
          \
           J--K--L--M--N--O--P   <-- branch-b

(由于我打算稍后绘制一些内容,所以我也拉长了第一行。提交在图中的位置是可伸缩的,因为Git不在乎提交的实际时间 ,只在乎提交及其连接弧的形状,只要您不破坏任何连接或组成不存在的新连接,就可以根据需要弯曲和扭曲图形。)

git merge命令然后会有些棘手。 首先,它找到当前提交I和另一个提交P之间的合并基础 粗略地说,合并基点是两个分支分开的点。 在这种情况下,这在图中非常明显:它是提交D

现在,Git通过执行以下操作来弄清branch-a上的“我们”的变化:

git diff --find-renames <hash-of-D> <hash-of-I>   # what we changed

第二个区别是找出它们branch-b上所做的更改:

git diff --find-renames <hash-of-D> <hash-of-P>   # what they changed

然后,Git合并两组更改,并将合并的更改应用于提交D快照中的任何内容。

这种“制作两个差异,将它们组合,然后将它们应用于合并基础”的过程是合并的动作形式。 我喜欢将其称为合并 (即合并更改)的动词。 因为提交是快照,而不是变更集,所以Git必须做两个比较。 为了有一个合理的起点,Git必须找到合并基础。 这就是为什么我们在合并提交IP时将所有这些工作作为动词的一部分进行合并的原因

合并提交

现在,Git完成了所有这些合并工作,Git将进行合并提交 好吧,它通常通常会成为一个—我们将在稍后看到例外。 请注意,尽管使用了merge一词作为形容词,但修改了commit词。 我们还可以将此合并合并提交称为merge ,使用单词merge作为名词。 我喜欢将此称为合并作为一种名词或合并-AS-AN-形容词,从工艺区分开,所述合并动词。 对于git merge命令,我们首先执行该过程,然后最后进行合并提交。 但让我们来画一下:

           E---F----G---H----I
          /                   \
A--B--C--D                     Q   <-- branch-a (HEAD)
          \                   /
           J--K--L--M--N--O--P   <-- branch-b

这种新的提交,即合并提交Q ,恰恰是一种特殊的方式:它有两个父代而不是一个。 这点背的第一承诺I ,说I是在尖branch-a刚才,是犯的父母Q ,但随后指回犯P ,说P也是父提交Q

如果我们现在要问的Git多少和哪些-提交上branch-a ,Git会开始于Q ,然后通过两个倒过来I P ,最终到达D (到master还有点),然后一路回到A 所以现在的提交次数为17: ADEIJP加上Q 如果我们问branch-a多少个提交而不是 master上的提交,则得到13: EI五个提交, JP七个提交, Q

有很多绘制方法

这是绘制发生的情况的另一种方法:

...--D--E--F--G--H--I------Q   <-- branch-a (HEAD)
      \                   /
       J--K--L--M--N--O--P   <-- branch-b

但是可到达的提交数保持不变:Git从Q开始,移回到IP ,然后又移回到HO ,依此类推,直到到达D ,然后它又回到共享提交D之前的状态。

如果您有git log绘制图形,请使用git log --graphgit log --graph --oneline ,Git将垂直绘制它,并在顶部提交Q并将分支结构表示为单独的行:

* hashofQ (HEAD -> branch-a) Merge ..
|\
| * hashofP commit message for P
* | hashofI commit message for I
...

或类似内容-每行*和每行的确切位置取决于您可能传递给git log其他排序选项,例如--graph --author-date-order ,尽管--graph总是至少强制使用--topo-order选项。 诸如gitk图形查看器和各种GUI可能会模仿git log --graph --oneline但它们都更漂亮(尽管一如既往,美丽在旁观者的眼中)。

壁球合并: git merge并不总是合并

git merge命令除了使用to merge (动词)过程构建合并 (名词)以外,还可以做更多的事情。 arkus提到了git merge --squash ,它执行合并过程的一部分,但随后只是停止,不进行提交,也没有记录下一个提交应该是合并的事实。 在这种情况下,我们自己运行git commit来使提交Q 新的提交Q将是一个普通的提交 ,而不是合并的提交,我们可以像这样绘制它:

...--D--E--F--G--H--I--Q   <-- branch-a (HEAD)
      \
       J--K--L--M--N--O--P   <-- branch-b

因为QP之间没有联系,所以后来有人(包括您自己或Git)进入该图,可能不知道提交Q是合并的结果。 branch-b独占的七个提交仍然是branch-b独占的。 通常,如果已执行此操作, 则应立即从此存储库以及该存储库的每个克隆中 删除名称branch-b ,以完全忘记提交JKLMNOP的存在。

有时但并非总是如此,这是可行,有用和良好的工作流程。 当在branch-b上的单个提交从未在其他地方看到过,这样您就知道其他人都没有它们, 并且仅将它们作为临时提交并打算用一个“添加功能”替换所有提交时,它特别有用。提交,即最后提交Q 完成壁球合并后,您迫使Git删除您的branch-b名称,而您忘记了曾经进行过任何单个提交。 您有一个最后的良好承诺,并且您假装自己知道如何立即进行所有承诺的世界。

有时候,即使您要引入一项功能,也最好将其保留为一系列单独的提交。 特别是,如果您还引入了错误,该怎么办? 在这种情况下,如果将特征缩小到一系列简单但清晰的提交(假设其中三个),然后将它们与真实的合并合并,则会得到如下图:

...--D--E--F--G--H--I--Q   <-- branch-a (HEAD)
      \               /
       R-------S-----T   <-- branch-b

如果现在证明您已经引入了错误,则可能可以检出提交R以及ST并查看其中哪些提交引入了bug 然后,您可以比较RDSRTS ,以帮助您找出错误的产生原因,并找出解决方法。

归结为,壁球合并还不错,它们只是一个工具。 使用您的工具来做事,使自己将来的生活更轻松。 如果那意味着压扁,那就继续压扁。 如果没有,那就不要。

快进: git merge并不总是合并

我们还应该介绍快进操作。 考虑一个分支的情况:

...--C--D   <-- master, feature (HEAD)

然后,您该分支进行一些提交:

...--C--D   <-- master
         \
          E--F--G--H   <-- feature (HEAD)

一切看起来都很不错,您现在就想介绍该功能,并保持所有这四个提交不变。 如果现在运行:

git checkout master
git merge feature

Git会说一些关于fast-forward的信息 ,您将得到以下图表:

...--C--D--E--F--G--H   <-- master (HEAD), feature

名称feature尚未移动-它仍然指向提交H但名称master 移动,现在也指向提交H 没有新的合并提交!

Git在这里所做的是,它像进行真正的合并一样进行了基于合并的查找,并且发现masterfeature之间最好的通用提交是commit D 但是,名称master指向commit D ,因此,如果Git照常进行合并动词,它将运行:

git diff --find-renames *hash-of-D* *hash-of-D*   # what we changed

答案当然是我们什么都不会改变! 然后,Git将需要比较DH来找出它们的变化,当然,这就是它们的变化。 Git会将这些更改应用于D并再次提交H

如果Git对此进行了真正的合并,它将看起来像:

...--C--D------------I   <-- master (HEAD)
         \          /
          E--F--G--H   <-- feature

提交的快照I将与提交H匹配。

您可以强制 Git进行此合并提交:

git checkout master; git merge --no-ff feature

这样,您将获得与D相同的,如果master进行了某些提交就可以得到的真正合并。 如果你想强调到未来的观众,谁可能是自己在一个或两个,当年承诺可以做到这一点EFGH作了作为一个群体,和他们一起实现一些功能。 或者您可能不在乎:您和您以及未来的一年(从现在开始)可能更愿意将EFGH作为master的逻辑扩展,而无需记住这四个是专门针对某些特定功能完成的。

同样,这确实归结为以下事实: 快进合并与真实合并是一个工具,您可以使用该工具将信息传达给此存储库的未来用户。 使用您的工具来安排事情,使您的未来生活变得更轻松。

如果您认为希望在git log --graph或图形查看器中查看合并,请使用git merge --no-ff强制进行非快进合并。 如果您认为自己不想看到合并,甚至可以使用git merge --ff-only来确保Git仅在需要进行真正合并时才会失败 (此后您需要做一些不同的事情,并且这超出了本已太长的答案的范围)。

这取决于分支机构的历史...可能介于7个修订版(快速转发)和13个修订版(合并2个完全无关的故事)之间。 这完全取决于故事以及您在谈论多少不同的修订版(或者是否要强制使用--no-ff )。 获得12个修订版本的一种可能方法是在两个分支之间拥有一个共同的祖先,因此您拥有共同的祖先,一个分支拥有4个修订,而另一个分支拥有6个修订(在共同祖先之后)加上合并修订:1 + 4 + 6 +1 =12。但是正如我所说,这完全取决于历史。 通过将一个分支的所有5个修订版作为另一个分支的前5个修订版,然后合并--no-ff,可以实现8个。 这将为原先的ff创建合并提交。 结果:8次修订。 与4个共同祖先进行合并,您将获得9个修订版本……依此类推。

如果要合并一次提交的分支,可以使用--squash选项fo git merge

它的作用是从git merge --squash <branch>传递的分支创建一个提交,您可以提交。

默认的git merge branch

git merge --squash branch

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM