如何和/或为什么Git合并比SVN更好?
我曾在几个地方听说分布式版本控制系统发光的主要原因之一,与SVN等传统工具相比,要好得多。 这实际上是由于两个系统工作方式的固有差异,还是像Git / Mercurial这样的具体DVCS实现比SVN拥有更聪明的合并算法?
关于为什么合并在DVCS中比在Subversion中更好的主张很大程度上取决于前一段时间Subversion如何进行分支和合并。 1.5.0之前的Subversion没有存储任何有关合并分支的信息,因此,当您想要合并时,必须指定必须合并的修订版本的范围。
那么为什么Subversion会合并?
思考这个例子:
1 2 4 6 8
trunk o-->o-->o---->o---->o
3 5 7
b1 +->o---->o---->o
当我们想要将b1的更改合并到主干中时,我们会发出以下命令,同时站在检出主干的文件夹中:
svn merge -r 2:7 {link to branch b1}
...将尝试将b1
的更改合并到本地工作目录中。 然后在解决任何冲突并测试结果后再提交更改。 当你提交修订树时,看起来像这样:
1 2 4 6 8 9
trunk o-->o-->o---->o---->o-->o "the merge commit is at r9"
3 5 7
b1 +->o---->o---->o
然而,当版本树增长时,这种指定版本范围的方式很快就会失控,因为Subversion没有关于何时和何种版本合并在一起的元数据。 思考后来会发生什么:
12 14
trunk …-->o-------->o
"Okay, so when did we merge last time?"
13 15
b1 …----->o-------->o
这在很大程度上是Subversion存储库设计的一个问题,为了创建一个分支,您需要在存储库中创建一个新的虚拟目录,该目录将存放trunk的副本,但不会存储任何关于何时和什么的信息事情被合并回来。这会导致有时令人讨厌的合并冲突。 更糟糕的是,Subversion默认使用双向合并,当两个分支头与其共同祖先相比时,它在自动合并中存在一些削弱限制。
为了缓解这个问题,Subversion现在为分支和合并存储元数据。 那会解决所有问题吗?
顺便说一句,Subversion仍然很糟糕......
在像颠覆这样的集中式系统上,虚拟目录很糟糕。 为什么? 因为每个人都可以查看它们,甚至是垃圾实验的。 如果你想实验,分支是好的, 但你不想看到每个人和他们的阿姨试验 。 这是严重的认知噪音。 你添加的分支越多,你就会看到更多的垃圾。
您在储存库中拥有的公共分支越多,就越难追踪所有不同的分支。 因此,您将面临的问题是,如果分支机构仍在开发中,或者它真的死了,这在任何中央版本控制系统中都很难说清楚。
大多数情况下,从我看到的情况来看,一个组织会默认使用一个大分支。 这是一种遗憾,因为这反过来将难以跟踪测试和发布版本,而其他任何好的东西都来自分支。
那么为什么DVCS,比如Git,Mercurial和Bazaar,在分支和合并方面比Subversion更好?
有一个非常简单的原因: 分支是一流的概念 。 没有按设计的虚拟目录,分支是DVCS中的硬对象,为了简单地与存储库的同步(即,推和拉)一起工作,它们需要这样做。
你使用DVCS时要做的第一件事就是克隆版本库(git的clone
,hg的clone
和bzr的branch
)。 克隆在概念上与在版本控制中创建分支是一回事。 有些人称这种分叉或分支(尽管后者通常也用于指称位于同一地点的分支机构),但它只是同样的事情。 每个用户都运行自己的存储库,这意味着您的每个用户分支正在进行。
版本结构不是一棵树 ,而是一个图形 。 更具体地说是一个有向无环图(DAG,意思是没有任何周期的图)。 除了每个提交有一个或多个父引用(提交所基于的内容)之外,您实际上不需要深入研究DAG的具体细节。 因此,下面的图表会因此而显示相反版本之间的箭头。
合并的一个非常简单的例子就是这个; 设想一个名为origin
的中央仓库和一个用户Alice,将仓库克隆到她的机器上。
a… b… c…
origin o<---o<---o
^master
|
| clone
v
a… b… c…
alice o<---o<---o
^master
^origin/master
在克隆过程中发生的情况是,每个修订版本都完全按照原样复制到Alice(它由唯一可识别的哈希标识符进行验证),并标记原始分支所在的位置。
然后,爱丽丝在她的回购中工作,在自己的存储库中提交并决定推进她的更改:
a… b… c…
origin o<---o<---o
^ master
"what'll happen after a push?"
a… b… c… d… e…
alice o<---o<---o<---o<---o
^master
^origin/master
解决的方法是相当简单的,那唯一origin
仓库需要做的是采取了所有的新修订和移动它的分支到最新版本(这混帐呼叫“快进”):
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
a… b… c… d… e…
alice o<---o<---o<---o<---o
^master
^origin/master
我在上面说明的用例甚至不需要合并任何东西 。 所以这个问题实际上不是合并算法,因为三路合并算法在所有版本控制系统中几乎相同。 问题更多的是结构而不是任何事情 。
那么你如何向我展示一个真正合并的例子?
无可否认,上面的例子是一个非常简单的用例,所以让我们做一个更加扭曲的例子,尽管它更常见。 请记住, origin
有三个修订版吗? 那么,做他们的人,让他叫他鲍勃,一直在自己的工作,并在他自己的仓库作出承诺:
a… b… c… f…
bob o<---o<---o<---o
^ master
^ origin/master
"can Bob push his changes?"
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
现在Bob不能直接将他的更改推送到origin
存储库。 系统如何检测到这一点是通过检查鲍勃的修改是否直接从origin
下降,在这种情况下不是。 任何尝试推动都会导致系统发出类似于“呃......我恐怕不能让你这样做的鲍勃”。
所以Bob必须拉入,然后合并更改(使用git的pull
;或者hg的pull
和merge
;或者bzr的merge
)。 这是一个两步过程。 首先鲍勃必须获取新的修订版本,它们将从origin
库中复制它们。 我们现在可以看到图形发散:
v master
a… b… c… f…
bob o<---o<---o<---o
^
| d… e…
+----o<---o
^ origin/master
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
拉动过程的第二步是合并分歧的提示并作出结果的提交:
v master
a… b… c… f… 1…
bob o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
^ origin/master
希望合并不会发生冲突(如果您预计他们可以在git中通过fetch
和merge
手动执行这两个步骤)。 之后需要做的是将这些更改再次推送到origin
,这将导致快速合并,因为合并提交是origin
存储库中最新的直接后代:
v origin/master
v master
a… b… c… f… 1…
bob o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
v master
a… b… c… f… 1…
origin o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
还有一种选择是在git和hg中进行合并,称为rebase,它将在最新的更改之后移动Bob的更改。 由于我不想让这个答案变得更加冗长,我会让你阅读有关这个的git,mercurial或bazaar文档。
作为读者的练习,请尝试绘制如何与其他涉及的用户合作。 这与上面鲍勃的例子类似。 库之间的合并比您想象的更容易,因为所有的修订/提交都是唯一可识别的。
在每个开发者之间也有发送补丁的问题,这在Subversion中是一个巨大的问题,在git,hg和bzr中被唯一可识别的修订所缓解。 一旦有人合并了他的更改(即进行了合并提交),并将其发送给团队中的其他人使用,方法是推送到中央存储库或发送补丁,然后他们不必担心合并,因为它已经发生。 马丁福勒称这种工作方式混杂整合。
由于结构与Subversion不同,通过代替使用DAG,它使分支和合并不仅可以用于系统,也可以用于用户。
从历史上看,Subversion只能执行直接双向合并,因为它没有存储任何合并信息。 这包括进行一系列更改并将其应用于树。 即使有合并信息,这仍然是最常用的合并策略。
默认情况下,Git使用3路合并算法,其中包括找到合并头部的共同祖先,并利用合并两侧存在的知识。 这让Git在避免冲突方面更加聪明。
Git也有一些复杂的重命名查找代码,这也有帮助。 它不存储更改集或存储任何跟踪信息 - 它只是在每次提交时存储文件的状态,并根据需要使用启发式来定位重命名和代码移动(磁盘存储比此更复杂,但接口它呈现给逻辑层没有跟踪)。
简而言之,合并实现在Git中比在SVN中更好。 在1.5 SVN没有记录合并操作之前,因此无需用户提供SVN没有记录的信息就无法进行未来合并。 随着1.5的增加,SVN存储模型的能力稍强于Git的DAG。 但SVN以相当复杂的形式存储合并信息,可以让合并的时间比Git大得多 - 我在执行时间中观察到了300的因子。
另外,SVN声称跟踪重命名以帮助合并移动的文件。 但实际上它仍然将它们存储为副本和单独的删除操作,并且合并算法仍然在修改/重命名情况下绊倒它们,也就是说,文件在一个分支上被修改并在另一个分支上重命名,并且这些分支是被合并。 这种情况仍然会产生虚假的合并冲突,而在目录重命名的情况下,甚至会导致无声的修改损失。 (然后SVN的人倾向于指出修改仍然在历史中,但是当它们没有出现在合并结果中时,这并没有什么帮助。
另一方面,Git甚至没有对重命名进行跟踪,而是在事实之后(合并时)将它们计算出来,并且非常神奇。
SVN合并表示也有问题; 在1.5 / 1.6中,你可以自动地从树干合并到分支,但是需要公布另一个方向的合并( --reintegrate
),并且使分支处于不可用状态。 很久以后,他们发现,这其实并非如此,而且一) --reintegrate
可以自动计算出,和b)在两个方向上重复的合并是可能的。
但是,在所有这些(恕我直言表明他们对自己的行为缺乏了解)之后,我会(很好,我)非常小心地在任何不平凡的分支场景中使用SVN,并且理想地尝试看看Git认为的是什么合并结果。
答案中提出的其他观点,因为SVN中分支机构的强制全局可见性与合并能力(但是可用性)无关。 此外,'Git商店的变化,而SVN商店(不同的东西)'大多是关键点。 Git在概念上将每个提交作为一个单独的树存储(像一个tar文件),然后使用相当多的启发式来有效地存储它。 计算两个提交之间的更改与存储实现是分开的。 什么是真正的是,Git存储历史DAG在一个更简单的形式,SVN的mergeinfo。 任何想了解后者的人都会明白我的意思。
简而言之:Git使用比SVN更简单的数据模型来存储修订版,因此它可以为实际的合并算法投入大量精力,而不是试图处理表示法=>实际上更好的合并。
链接地址: http://www.djcxy.com/p/45725.html