真的,在Git中合并的一个具体例子比SVN容易吗?

堆栈溢出问题如何和/或为什么合并在Git比SVN更好? 是一个伟大的问题,有一些很好的答案 。 然而,他们没有一个显示了一个简单的例子,在Git中合并比SVN更好。

这个问题的机会将被作为一个重复被closures,是什么

  1. 具体合并scheme
  2. SVN有多困难?
  3. 在Git中如何进行相同的合并更容易?

几点:

  • 没有哲学或DVCS是什么请深入解释。 这些真的很棒,但我不希望他们的细节混淆这个(恕我直言)重要的答案
  • 我现在不关心“历史SVN”。 请将现代G​​it(1.7.5)与现代SVN(1.6.15)进行比较。
  • 没有重命名 – 我知道Git检测重命名和移动,而SVN不。 这很好,但是我正在寻找更深层次的东西,而且不涉及重命名或移动。
  • 没有rebase或其他'高级'Git操作。 请给我看看合并。

实践angular度来看,由于我所说的“大爆炸”问题,合并传统上是“硬”的。 假设开发者已经在一段代码上工作了一段时间,还没有完成他们的工作(也许开发人员习惯于在Subversion中工作,而不会提交未完成的代码)。 当开发人员最终提交时,将会有很多变更汇总成一个提交。 对于其他希望将自己的工作与“大爆炸”提交合并的开发人员,VCS工具将不会获得关于第一个开发人员如何达到他们所承诺的程度的足够信息,因此,您只需要得到“这是一个巨大的冲突在这整个function,去修复它“。

另一方面,与Git和其他具有便宜本地分支的DVCS一起工作的常规方式是定期提交。 一旦你完成了一些非常有意义的工作,你就可以做到这一点。 它不一定是完美的,但它应该是一个连贯的工作单位。 当你回来合并时,你仍然有更小的提交历史,显示你从原始状态到当前状态。 当DVCS将其与其他人的工作合并时,它会提供更多关于什么时候进行更改的信息,并且最终会变得更更less的冲突。

关键是,你仍然可以通过做一个单一的大爆炸,只有在你完成了一些事情后才能合并Git的一个难题。 Git鼓励你做更小的提交(通过尽可能无痛苦),这使未来的合并更容易。

我只能告诉你一个小实验,Git并不比Subversion好(同样的问题)。

我想知道这种情况:你从两个分支“mytest1”和“mytest2”都基于相同的提交开始。 你有一个C文件,其中包含一个函数blub()。 在mytest1分支中,将“blub()”移动到文件中的不同位置并提交。 在分支mytest2你修改blub()并提交。 在分支mytest2你尝试使用“git merge mytest1”。

似乎给一个合并冲突。 我希望Git能够认识到“blub()”在mytest1中被移动,然后能够将mytest2中的修改与mytest1中的移动进行自动合并。 但至less当我尝试这个没有自动工作…

所以,虽然我完全理解Git在跟踪合并和未合并的情况下好得多,但我还想知道Git是否比SVN好的“纯粹”合并案例…

现在因为这个问题一直在困扰我很长一段时间,我真的想创造一个Git 更好的具体例子,而在SVN合并失败。

我在这里find了一个https://stackoverflow.com/a/2486662/1917520 ,但这包括重命名,这里的问题是没有重命名的情况。

所以这里是一个基本上尝试这个SVN的例子:

 bob +-----r3----r5---r6---+ / / \ anna / +-r2----r4----+--+ \ / / \ \ trunk r1-+-------------------r7-- Conflict 

这里的想法是:

  • 安娜和鲍勃都是拥有自己的分支的开发者(创build于r2,r3)。
  • 安娜做了一些修改(r4),
  • 鲍勃做了一些修改(r5)。
  • Bob将Anna的修改合并到他的分支中; 这产生了冲突,Bob修复并提交(r6)。
  • Annas的修改被合并回主干(r7)。
  • 鲍勃试图合并他的修改回到主干,这再次发生冲突。

这里是一个Bash脚本,它产生了这个冲突(使用SVN 1.6.17和SVN 1.7.9):

 #!/bin/bash cd /tmp rm -rf rep2 wk2 svnadmin create rep2 svn co file:///tmp/rep2 wk2 cd wk2 mkdir trunk mkdir branches echo -e "A\nA\nB\nB" > trunk/f.txt svn add trunk branches svn commit -m "Initial file" svn copy ^/trunk ^/branches/anna -m "Created branch anna" svn copy ^/trunk ^/branches/bob -m "Created branch bob" svn up echo -e "A\nMA\nA\nB\nB" > branches/anna/f.txt svn commit -m "anna added text" echo -e "A\nMB\nA\nB\nMB\nB" > branches/bob/f.txt svn commit -m "bob added text" svn up svn merge --accept postpone ^/branches/anna branches/bob echo -e "A\nMAB\nA\nB\nMB\nB" > branches/bob/f.txt svn resolved branches/bob/f.txt svn commit -m "anna merged into bob with conflict" svn up svn merge --reintegrate ^/branches/anna trunk svn commit -m "anna reintegrated into trunk" svn up svn merge --reintegrate --dry-run ^/branches/bob trunk 

最后一个“干扰”告诉你,会有冲突。 如果你首先尝试将安娜的重新整合融入鲍勃的分支,那么你也会发生冲突; 所以如果你replace最后的svn merge

 svn merge ^/trunk branches/bob 

这也显示出冲突。

这和Git 1.7.9.5一样:

 #!/bin/bash cd /tmp rm -rf rep2 mkdir rep2 cd rep2 git init . echo -e "A\nA\nB\nB" > f.txt git add f.txt git commit -m "Initial file" git branch anna git branch bob git checkout anna echo -e "A\nMA\nA\nB\nB" > f.txt git commit -a -m "anna added text" git checkout bob echo -e "A\nMB\nA\nB\nMB\nB" > f.txt git commit -a -m "bob added text" git merge anna echo -e "A\nMAB\nA\nB\nMB\nB" > f.txt git commit -a -m "anna merged into bob with conflict" git checkout master git merge anna git merge bob 

f.txt的内容像这样改变。

初始版本

 A A B B 

安娜的修改

 A MA A B B 

鲍勃的修改

 A MB A B MB B 

安娜的分支合并到鲍勃的分支之后

 A MAB A B MB B 

正如很多人已经指出的那样:问题是,颠覆者不记得鲍勃已经解决了冲突。 所以当你试图将Bob的分支合并到主干时,你必须重新解决冲突。

Git的作品完全不同。 这里有一些graphics表示git在做什么

 bob +--s1----s3------s4---+ / / \ anna / +-s1----s2----+--+ \ / / \ \ master s1-+-------------------s2----s4 

s1 / s2 / s3 / s4是git所用工作目录的快照。

笔记:

  • 当anna和bob创build他们的开发分支时,这不会在git下创build任何提交。 git会记住两个分支最初引用与主分支相同的提交对象。 (这个提交将会引用s1快照)。
  • 当anna实现她的修改时,这将创build一个新的快照“s2”+一个提交对象。 一个提交对象包括:
    • 对快照的引用(这里是s2)
    • 提交消息
    • 有关祖先的信息(其他提交对象)
  • 当bob实现他的修改时,这将创build另一个快照s3 +一个提交对象
  • 当bob将annas修改合并到他的开发分支时,这将创build另一个快照s4(包含他的更改和anna的更改合并),还有另一个提交对象
  • 当anna合并她的变化回到主分支,这将是一个“快进”的合并,在所示的例子中,因为主人没有改变在此期间。 这里的“快进”意味着,主人只需指向来自anna的s2快照而不合并任何东西。 有了这样的“快进”,甚至不会有另一个提交对象。 “主”分支将直接引用“anna”分支的最后一个提交
  • 当鲍勃现在合并他的变化到后备箱,将发生以下事情:
    • git会发现创builds2快照的anna的提交是bobs提交(直接)的祖先,它创build了s4快照。
    • 因为这个git会再次将主分支“快进”到“bob”分支的最后一个提交。
    • 再次,这甚至不会创build一个新的提交对象。 “master”分支将简单地指向“bob”分支的最后一个提交。

这里是“git ref-log”的输出,它显示了所有这些:

 88807ab HEAD@{0}: merge bob: Fast-forward 346ce9f HEAD@{1}: merge anna: Fast-forward 15e91e2 HEAD@{2}: checkout: moving from bob to master 88807ab HEAD@{3}: commit (merge): anna merged into bob with conflict 83db5d7 HEAD@{4}: commit: bob added text 15e91e2 HEAD@{5}: checkout: moving from anna to bob 346ce9f HEAD@{6}: commit: anna added text 15e91e2 HEAD@{7}: checkout: moving from master to anna 15e91e2 HEAD@{8}: commit (initial): Initial file 

正如你所看到的:

  • 当我们去anna的开发分支(HEAD @ {7})时,我们不会改变到另一个提交,我们保持提交; git只是记得我们现在在不同的分支上
  • 在HEAD @ {5}我们移动到鲍勃的初始分支; 这会将工作副本移到与master分支相同的状态,因为bob还没有改变任何东西
  • 在HEAD @ {2}我们移回到主分支,所以从同一个提交对象开始。
  • Head @ {1},HEAD @ {0}显示“快进”合并,不会创build新的提交对象。

用“git cat-file HEAD @ {8} -p”你可以检查初始提交对象的完整细节。 对于上面的例子,我得到了:

 tree b634f7c9c819bb524524bcada067a22d1c33737f author Ingo <***> 1475066831 +0200 committer Ingo <***> 1475066831 +0200 Initial file 

“树”行标识此提交所引用的快照s1(== b634f7c9c819bb524524bcada067a22d1c33737f)。

如果我做“git cat-file HEAD @ {3} -p”我得到:

 tree f8e16dfd2deb7b99e6c8c12d9fe39eda5fe677a3 parent 83db5d741678908d76dabb5fbb0100fb81484302 parent 346ce9fe2b613c8a41c47117b6f4e5a791555710 author Ingo <***> 1475066831 +0200 committer Ingo <***> 1475066831 +0200 anna merged into bob with conflict 

上面显示了合并anna的开发分支时创build的提交对象,bob。 “树”行再次指的是创build的快照(这里s3)。 另外请注意“父母”行。 第二个以“parent 346ce9f”开头的告诉git,当你试图将bob的开发分支合并到master分支中时,bob的最后一个提交将anna的最后提交作为祖先。 这就是为什么git知道bob开发分支合并到主分支中是“快进”的原因。

我没有具体的例子,但是任何一种重复的合并都是困难的,特别是所谓的纵横交错

  a / \ b1 c1 |\ /| | X | |/ \| b2 c2 

合并b2和c2


Subversion Wiki上的wiki页面描述了基于合并信息的Subversion合并(“同步”和“重新合并 ”方向)与基于合并跟踪的DVCS中的对称合并之间的差异,其中有一个部分“ Criss-Cross Merge的对称合并 ”

我能想到的最具体的例子是最简单的合并,不会导致合并冲突。 然而,使用这个例子的Git(TL; DR)本质上比Subversion更简单。 让我们看看为什么:

颠覆

在颠覆中考虑以下情况; 树干和特征分支:

  1 2 3 …--o--o--o trunk \4 5 o--o branches/feature_1 

要合并,你可以在subversion中使用下面的命令:

 # thank goodness for the addition of the --reintegrate flag in SVN 1.5, eh? svn merge --reintegrate central/repo/path/to/branches/feature_1 # build, test, and then... commit the merge svn commit -m "Merged feature_1 into trunk!" 

在颠覆合并的变化需要另一个提交。 这是为了发布合并在function分支虚拟目录上应用更改后所做的更改。 这样,每个使用trunk的人都可以使用它,修订图看起来就像这样:

  1 2 3 6 …--o--o--o------o /trunk \4 5/ o--o /branches/feature_1 

让我们看看这是如何在git中完成的。

混帐

在Git中,这个合并提交实际上并不是必须的,因为分支是修订图上的书签。 因此,使用相同types的修订图结构,看起来像这样:

  v-- master, HEAD 1 2 3 …--o--o--o \4 5 o--o ^-- feature_branch 

当前在主分支上的头部,我们可以执行与特征分支的简单合并:

 # Attempt a merge git merge feature_branch # build, test, and then... I am done with the merge 

…它会将分支快进到function分支指向的提交。 这是可能的,因为Git知道合并的目标是一个直接的后代,当前分支只需要接受所有发生的变化。 修订图最终会看起来像这样:

  1 2 3 4 5 …--o--o--o--o--o ^-- feature_branch, master, HEAD 

因为所有的git所做的更改不需要一个新的提交就是将分支引用进一步移动到前面。 剩下的就是把这个发布到公共仓库,如果你有任何的话:

 # build, test, and then... just publish it git push 

结论

鉴于这个简单的情况,你可以断言Subversion和Git之间的区别有两点:

  • SVN至less需要一些命令才能完成合并,并强制您将合并发布为提交。
  • Git只需要一个命令 ,不会强制你发布你的合并。

鉴于这是最简单的合并scheme,很难说颠覆比git更容易。

在更困难的合并情况下,git还为您提供了以重写历史logging为代价来生成更简单版本图的分支的能力。 一旦你掌握了它的内容,并且避免发布已经发布的事物的历史重写,那么这件事情就不是那么糟糕了。

交互式资产重置不在问题的范围之内,但说实话; 它使您能够重新排列,挤压和删除提交。 我不愿意切换回Subversion的历史重写是不可能的devise。

保持答案的简短 – 在DVCS中,因为你有一个本地的源代码控制,如果在合并过程中(这可能会发生在大的合并)中的东西搞砸,你总是可以回滚到以前的本地版本,在合并之前做了,然后再试一次。

所以基本上你可以合并,而不用担心在这个过程中你的本地变化可能会受到损害。

如果你排除“合并重构地狱”,你将不会得到 公平的样本,因为它们不存在