首页 文章

如何和/或为什么在Git中合并比在SVN中更好?

提问于
浏览
395

我在一些地方听说分布式版本控制系统闪耀的主要原因之一是比SVN等传统工具更好地融合 . 这实际上是由于两个系统如何工作的固有差异,或者像Git / Mercurial这样的特定DVCS实现是否只有比SVN更聪明的合并算法?

7 回答

  • 549

    为什么合并在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
    

    当我们想要merge b1 's changes into the trunk we' d发出以下命令时,站在已签出中继的文件夹上:

    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仍然很糟糕......

    在集中式系统上,如颠覆,虚拟目录很糟糕 . 为什么?因为每个人都可以查看它们......甚至是垃圾实验的 . 如果你想试验 but you don't want to see everyones' and their aunts experimentation ,分支是好的 . 这是严重的认知噪音 . 你添加的分支越多,你就会看到越多的垃圾 .

    您在存储库中拥有的公共分支越多,跟踪所有不同分支的难度就越大 . 因此,您将遇到的问题是,如果分支仍在开发中,或者它是否真的死了,这在任何集中式版本控制系统中都很难说清楚 .

    大多数时候,从我看到的情况来看,组织无论如何都会默认使用一个大分支 . 这是一种耻辱,因为反过来很难跟踪测试和发布版本,以及其他任何好处来自分支 .

    那么为什么DVCS,比如Git,Mercurial和Bazaar,在分支和合并方面比Subversion更好?

    有一个非常简单的原因: branching is a first-class concept . 设计中没有虚拟目录,并且分支是DVCS中的硬对象,它需要这样才能简单地与存储库的同步(即推和拉)一起工作 .

    使用DVCS时,您要做的第一件事就是克隆存储库(git的clone,hg的clone和bzr的branch) . 克隆在概念上与在版本控制中创建分支相同 . 有些人称之为分叉或分支(虽然后者通常也用于指代同位分支),但它也是一样的 . 每个用户都运行自己的存储库,这意味着您可以进行每用户分支 .

    版本结构是 not a tree ,而是 graph . 更具体地说是一个directed acyclic graph(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(由唯一可识别的哈希id验证),并标记原点的分支是在 .

    爱丽丝然后处理她的回购,在她自己的存储库中提交并决定推送她的更改:

    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调用"fast-forward"):

    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
    

    用例,我在上面说明, doesn't even need to merge anything . 所以问题实际上不是合并算法,因为三向合并算法在所有版本控制系统之间几乎相同 . The issue is more about structure than anything .

    那你怎么样给我看一个真正合并的例子呢?

    不可否认,上面的例子是一个非常简单的用例,所以让我们做一个更加扭曲的例子,尽管是一个更常见的例子 . 请记住 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, which in this case doesn' t下降 . 任何推动的尝试都会导致系统说出类似于“Uh... I'm afraid can't let you do that Bob”的内容 .

    所以Bob必须拉入然后合并更改(使用git的pull;或者hg的pullmerge;或者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中使用fetchmerge手动执行这两个步骤) . 以后需要做的是再次将这些更改推送到 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,'ll move Bob'在最新的更改后更改为 . 因为我不允许你阅读gitmercurialbazaar文档 .

    作为读者的练习,试着弄清楚它将如何与其他用户合作 . 它与Bob的上述示例类似 . 存储库之间的合并比您想象的更容易,因为所有修订/提交都是唯一可识别的 .

    还有在每个开发人员之间发送补丁的问题,这是Subversion中的一个巨大问题,它通过唯一可识别的修订在git,hg和bzr中得到缓解 . 一旦有人合并了他的更改(即进行合并提交)并通过推送到中央存储库或发送补丁将其发送给团队中的其他人消费,那么他们就不必担心合并,因为它已经发生了 . Martin Fowler称这种工作方式为promiscuous integration .

    因为该结构与Subversion不同,所以通过使用DAG,它使得分支和合并能够以更容易的方式完成,不仅对于系统而且对于用户也是如此 .

  • 28

    从历史上看,Subversion只能执行直接的双向合并,因为它没有存储任何合并信息 . 这涉及进行一系列更改并将其应用于树 . 即使使用合并信息,这仍然是最常用的合并策略 .

    默认情况下,Git使用3向合并算法,其中包括找到要合并的头部的共同祖先,并利用合并两侧存在的知识 . 这使Git能够更加智能地避免冲突 .

    Git还有一些复杂的重命名查找代码,这也有帮助 . 它不存储更改集或存储任何跟踪信息 - 它只是存储每次提交时文件的状态,并使用启发式方法根据需要定位重命名和代码移动(磁盘存储比这更复杂,但界面它呈现给逻辑层暴露没有跟踪) .

  • 16

    简而言之,合并实现在Git中比在SVN中做得更好 . 在1.5 SVN没有记录合并动作之前,没有用户需要提供SVN没有记录的信息的帮助就无法进行未来的合并 . 随着1.5它变得更好,实际上SVN存储模型的能力略高于Git 's DAG. But SVN stored the merge information in a rather convoluted form that lets merges take massively more time than in Git - I' ve在执行中观察到300的因素时间 .

    此外,SVN声称跟踪重命名以帮助合并已移动的文件 . 但实际上它仍然将它们存储为副本和单独的删除操作,并且合并算法仍然在修改/重命名情况下偶然发现它们,即,在一个分支上修改文件并在另一个分支上重命名,并且这些分支是合并 . 这种情况仍然会产生虚假的合并冲突,并且在目录重命名的情况下,它甚至会导致无声的修改丢失 . (然后SVN人员倾向于指出修改仍然在历史中,但是当它们不在合并结果中时它们应该出现时没有多大帮助 .

    另一方面,Git甚至不跟踪重命名,而是在事后(合并时)将它们计算出来,并且非常神奇 .

    SVN合并表示也存在问题;在1.5 / 1.6中你可以自动地从主干到分支合并,但是需要宣布另一个方向的合并( --reintegrate ),并使分支处于不可用状态 . 很久以后他们发现事实并非如此,并且a) --reintegrate 可以自动计算出来,并且b)可以在两个方向上重复合并 .

    但毕竟这个(IMHO表示对他们正在做的事情缺乏了解),我会(好吧,我)非常谨慎地在任何非平凡的分支场景中使用SVN,理想情况下会尝试看看Git的想法合并结果 .

    在答案中提出的其他观点,作为SVN中分支的强制全局可见性,并不是Git存储变化,而SVN存储(不同的东西)'大部分都不合适 . Git在概念上将每个提交存储为一个单独的树(如tar文件),然后使用相当一些启发式来有效地存储它 . 计算两次提交之间的更改与存储实现是分开的 . 真实的是,Git以更直接的形式存储历史DAG,SVN执行其mergeinfo . 任何试图理解后者的人都知道我的意思 .

    简而言之:Git使用比SVN更简单的数据模型来存储修订,因此它可以将大量精力投入到实际的合并算法中,而不是试图应对表示=>实际上更好的合并 .

  • 10

    其他答案中没有提到的一件事,那就是DVCS的一大优势,就是你可以在推送更改之前在本地提交 . 在SVN中,当我进行一些更改时,我想要检查,并且有人在此期间已经在同一分支上完成了提交,这意味着我必须先做 svn update 才能提交 . 这意味着我的更改以及来自其他人的更改现在混合在一起,并且无法中止合并(例如 git resethg update -C ),因为没有提交可以返回 . 如果合并非常重要,则意味着在清理合并结果之前无法继续处理功能 .

    但是,对于那些过于愚蠢而无法使用单独分支的人来说,这可能只是一个优势(如果我没记错的话,我们在使用SVN的公司中只有一个用于开发的分支) .

  • 8

    我读了接受的答案 . 这是完全错的 .

    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中使用 .

    这并不是说他们不是更好的工具!

  • 10

    当Git跟踪内容更改时,SVN会跟踪文件 . 它足够聪明地跟踪从一个类/文件重构到另一个类/文件的代码块 . 他们使用两种完全不同的方法来跟踪您的来源 .

    我仍然大量使用SVN,但我很高兴我几次使用Git .

    如果你有时间,这是一个很好的阅读:Why I chose Git

  • 6

    刚读一篇关于Joel博客的文章(遗憾的是他的最后一篇) . 这个是关于Mercurial的,但它实际上讨论了像Git这样的分布式VC系统的优点 .

    使用分布式版本控制,分布式部分实际上不是最有趣的部分 . 有趣的是,这些系统考虑的是变化,而不是版本 .

    阅读文章here .

相关问题