[英]Why does merging branches sum total commits?
如果分支A有5次提交,分支B有7次提交,则当我将B合并到A中时,A现在有12次提交。
那是预期的结果吗? 我本以为合并将被视为单个提交?
当您说“分支A有五个提交”时,您可能没有计算分支A包含的所有提交。 同样适用于分支B中的七个提交。要真正理解这一点,重要的是要意识到在Git中,分支(或更确切地说,分支名称)实际上没有任何意义。 只是提交很重要。
为了了解它是如何工作的,让我们从一个只有三个提交的非常小的存储库开始。 这三个提交具有长而丑陋的Git-hash-ID名称,但我们仅将它们称为提交A
, B
和C
,就好像这些提交的真实姓名只有一个大写字母一样。 (我们很快就会用完,这是Git使用那些大的丑陋哈希ID的原因之一。)
Git的第一个重要重要秘密是每个提交都在其中存储其先前提交的哈希ID。 每当您拥有提交的哈希ID时,我们都说您指向该提交。 因此,我们的三个提交如下:
A <-B <-C
提交C
存储B
的哈希ID,因此C
指向B
B
存储A
的哈希ID,因此B
指向A
当然, A
是我们做过的第一个提交:它不能再指向其他任何地方。 这是一个特例- 根提交,如果存储库不为空,则至少有一个。 通常,仅存在一个根提交,而该根提交是第一个提交。
下一个重要的重要秘密是对第一个秘密的简单后续操作,即像master
或develop
这样的分支名称仅指向一个commit 。 在这种情况下,我们的master
指向的一个提交将是提交C
:
A--B--C <-- master
由于种种原因,我总是懒于在提交之间绘制内部箭头。 一个是,一旦我们提交了一个提交,就没有人可以更改提交,甚至没有人(甚至Git本身)。 提交C
被永久冻结,总是指向B
,后者被冻结并指向A
,依此类推。 因此,内部箭头始终指向后 。 Git的调用这些父母的承诺:父C
是B
,和父B
是A
。
分支名称指针是不同的。 与每个提交的冻结内容不同,分支名称指针可以并且确实会发生变化。
让我们使用git checkout master
,它将提交C
提取到我们的工作树中 ,为我们提供可以查看和使用的文件。 然后,我们将进行一些更改, git add
更新的文件,然后git commit
进行新的提交,我们将其称为D
Git将打包我们的新文件1并进行新的提交D
,指向我们已经提交的提交(即C
,这样我们现在有了:
A--B--C--D
然后作为最后的动作, git commit
将D
的哈希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 add
和git 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必须找到合并基础。 这就是为什么我们在合并提交I
和P
时将所有这些工作作为动词的一部分进行合并的原因 。
现在,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: A
到D
加E
到I
加J
到P
加上Q
如果我们问branch-a
多少个提交而不是 master
上的提交,则得到13: E
到I
五个提交, J
到P
七个提交, Q
。
这是绘制发生的情况的另一种方法:
...--D--E--F--G--H--I------Q <-- branch-a (HEAD)
\ /
J--K--L--M--N--O--P <-- branch-b
但是, 可到达的提交数保持不变:Git从Q
开始,移回到I
和P
,然后又移回到H
和O
,依此类推,直到到达D
,然后它又回到共享提交D
之前的状态。
如果您有git log
绘制图形,请使用git log --graph
或git 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
因为Q
和P
之间没有联系,所以后来有人(包括您自己或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
以及S
和T
并查看其中哪些提交引入了bug 。 然后,您可以比较R
与D
, S
与R
或T
与S
,以帮助您找出错误的产生原因,并找出解决方法。
归结为,壁球合并还不错,它们只是一个工具。 使用您的工具来做事,使自己将来的生活更轻松。 如果那意味着压扁,那就继续压扁。 如果没有,那就不要。
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在这里所做的是,它像进行真正的合并一样进行了基于合并的查找,并且发现master
和feature
之间最好的通用提交是commit D
但是,名称master
指向commit D
,因此,如果Git照常进行合并动词,它将运行:
git diff --find-renames *hash-of-D* *hash-of-D* # what we changed
答案当然是我们什么都不会改变! 然后,Git将需要比较D
与H
来找出它们的变化,当然,这就是它们的变化。 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.