我在一些地方听说过,分布式版本控制系统之所以如此出色的主要原因之一,是与诸如SVN之类的传统工具相比,合并起来要好得多。这是否真的是由于两个系统在工作方式上存在固有差异,还是像Git / Mercurial 这样的特定 DVCS实现仅具有比SVN更聪明的合并算法?
我在一些地方听说过,分布式版本控制系统之所以如此出色的主要原因之一,是与诸如SVN之类的传统工具相比,合并起来要好得多。这是否真的是由于两个系统在工作方式上存在固有差异,还是像Git / Mercurial 这样的特定 DVCS实现仅具有比SVN更聪明的合并算法?
Answers:
为什么在DVCS中合并比在Subversion中更好的说法主要是基于前一段时间Subversion中分支和合并的工作方式。1.5.0之前的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拥有的存储库设计在很大程度上是一个问题,为了创建分支,您需要在存储库中创建一个新的虚拟目录,该目录将存储主干副本,但不存储有关何时何地的任何信息。事情又重新融合了。有时会导致讨厌的合并冲突。更糟糕的是,Subversion默认情况下使用双向合并,当两个分支头未与其共同祖先进行比较时,它在自动合并方面存在一些严重的限制。
为了缓解这种情况,Subversion现在存储了用于分支和合并的元数据。那会解决所有问题吧?
在集中式系统(如Subversion)上,虚拟目录很烂。为什么?因为每个人都可以查看它们……甚至是垃圾实验的人。如果您想尝试,但不想看到每个人及其姨妈的实验,则分支是很好的。这是严重的认知噪音。您添加的分支越多,您看到的内容就越多。
您在存储库中拥有的公共分支越多,跟踪所有不同分支的难度就越大。因此,您将要问的问题是分支是否仍在开发中,或者它是否真的已经死了,这在任何集中式版本控制系统中都很难分辨。
从我所看到的大部分时间来看,组织将默认使用一个大分支。令人遗憾的是,这反过来将难以跟踪测试和发行版本,而分支带来的其他好处。
原因很简单:分支是一流的概念。设计上没有虚拟目录,而分支是DVCS中的硬对象,为了与存储库同步(即push和pull)简单地工作,就必须如此。
使用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(已通过唯一可识别的hash-id进行验证),并标记了原始分支的位置。
然后,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
存储库唯一需要做的就是获取所有新修订并将其分支移至最新修订(git称为“快进”):
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
从三个修订版开始吗?好吧,做这些的人叫他Bob,他一直在自己工作,并在自己的存储库中进行了提交:
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
存储库。系统如何通过检查Bob的修订版本是否直接从origin
s 下降而检测到的,在这种情况下不是这样。任何试图推动的尝试都会导致系统说出类似“ 呃...我怕不能让你那样做Bob”。
所以,鲍勃有吸合,然后合并更改(用Git的pull
;或HG的pull
和merge
;或BZR的merge
)。这是一个两步过程。首先,Bob必须获取新修订,它将从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的文档。
作为读者的练习,请尝试确定如何与其他相关用户一起工作。与上述Bob的示例类似。存储库之间的合并比您想象的要容易,因为所有修订/提交都是唯一可识别的。
在每个开发人员之间也存在发送补丁的问题,这在Subversion中是一个巨大的问题,可通过唯一可识别的修订版本在git,hg和bzr中缓解。一旦某人合并了他的更改(即进行合并提交)并将其发送给团队中的其他所有人,则可以通过推送到中央存储库或发送补丁来使用它,因此他们不必担心合并,因为合并已经发生了。马丁·福勒(Martin Fowler)称这种工作方式为混杂集成。
因为结构与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
),并使分支处于不可用状态。后来,他们发现实际上并非如此,并且a)--reintegrate
可以自动找出,b)可以在两个方向上重复合并。
但是经过了所有这些(恕我直言,这表明他们对自己的工作缺乏了解),我会(好吧,我很谨慎)在任何非平凡的分支场景中使用SVN,并且理想情况下会尝试了解Git的想法合并结果。
答案中提到的其他要点,例如SVN中分支的强制全局可见性,与合并功能无关(但为了可用性)。同样,“ Git存储更改而SVN存储(有所不同)”大多不可行。Git在概念上将每个提交存储为单独的树(如tar文件),然后使用一些启发式方法有效地存储该提交。计算两次提交之间的更改与存储实现是分开的。事实是,Git以比SVN进行mergeinfo更直接的方式存储历史DAG。任何试图理解后者的人都会知道我的意思。
简而言之:与SVN相比,Git使用了更简单的数据模型来存储修订,因此,它可以将大量精力投入到实际的合并算法中,而不是试图应对表示形式=>实际上更好的合并。
在其他答案中没有提到的一件事,而这确实是DVCS的一大优势,那就是您可以在进行更改之前本地提交。在SVN中,当我要进行一些更改时要签入,并且同时有人已经在同一分支上进行过提交,这意味着我必须先执行“ svn update
先提交”。这意味着我的更改和其他人的更改现在混合在一起,并且没有方法可以中止合并(如使用git reset
或hg update -C
),因为没有提交可以返回。如果合并非常重要,则意味着您无法在清理合并结果之前继续使用功能。
但是,那也许对那些笨拙而无法使用单独分支的人来说只是一个优势(如果我没记错的话,在我使用SVN的公司中,我们只有一个分支用于开发)。
编辑:这主要是解决问题的这一部分:
这实际上是由于两个系统在工作方式上的固有差异,还是Git / Mercurial之类的特定DVCS实现比SVN具有更聪明的合并算法?
TL; DR-那些特定的工具具有更好的算法。分发具有一些工作流程优势,但与合并优势正交。
结束编辑
我阅读了接受的答案。这是完全错误的。
SVN合并可能会很痛苦,也很麻烦。但是,请忽略它实际上如何工作一分钟。Git没有保留或可以得出的信息,SVN也没有保留或可以得出的信息。更重要的是,没有理由为什么保留单独的(有时是部分的)版本控制系统副本会为您提供更多实际信息。这两个结构是完全等效的。
假设您想做“一些聪明的事情”,Git是“更好的”。而您正在将事情签入SVN。
将您的SVN转换为等效的Git形式,在Git中进行处理,然后(可能使用多次提交)一些额外的分支来检查结果。如果您可以想象将SVN问题转换为Git问题的自动化方法,那么Git没有根本的优势。
归根结底,任何版本控制系统都会让我
1. Generate a set of objects at a given branch/revision.
2. Provide the difference between a parent child branch/revisions.
此外,对于合并而言,了解信息也非常有用(或至关重要)
3. The set of changes have been merged into a given branch/revision.
Mercurial,Git和Subversion(现在本机使用,以前使用svnmerge.py)都可以提供全部三项信息。为了从DVC根本上展示出更好的东西,请指出Git / Mercurial / DVC中提供的第四条信息,而SVN /集中式VC中没有这些信息。
这并不是说它们不是更好的工具!
git merge-base
。使用git时,您可以说“在修订版x处拆分了分支a和b”。但是svn会存储“文件已从foo复制到bar”,因此您需要使用试探法来确定复制到bar正在创建新分支,而不是在项目中复制文件。诀窍是svn中的修订版由修订版号和基本路径定义。即使大多数时候都可以假定是“主干”,但如果确实有分支,它就会被咬住。
SVN跟踪文件,而Git跟踪内容更改。跟踪从一个类/文件重构到另一个的代码块足够聪明。他们使用两种完全不同的方法来跟踪您的源。
我仍然大量使用SVN,但是对使用Git的几次感到非常满意。
如果您有时间,请读一读:为什么我选择Git
只需阅读Joel博客上的一篇文章(可悲的是他的最后一篇)。这是关于Mercurial的内容,但实际上是关于分布式VC系统(如Git)的优点的。
使用分布式版本控制,分布式部分实际上并不是最有趣的部分。有趣的是,这些系统是根据更改而不是版本来考虑的。
在这里阅读文章。