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

时间:2011-05-30 03:28:50

标签: git svn merge

Stack Overflow问题 How and/or why is merging in Git better than in SVN? 是一个很棒的问题,有一些很棒的答案。但是,它们都没有显示一个简单的例子,其中Git中的合并比SVN更好。

关于这个问题将被重复关闭的可能性是什么

  1. 具体的合并方案
  2. SVN有多难?
  3. 在Git中如何更容易合并?
  4. 几点:

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

7 个答案:

答案 0 :(得分:22)

实践的角度来看,由于我称之为“大爆炸合并”问题,合并传统上一直很“困难”。假设一个开发人员一段时间以来一直在研究某些代码并且还没有提交他们的工作(也许开发人员习惯于在针对trunk的Subversion中工作,并且不提交未完成的代码)。当开发人员最终提交时,将会有很多更改汇总到一个提交中。对于想要将他们的工作与这个“大爆炸”提交合并的其他开发人员来说,VCS工具不会有足够的信息来了解第一个开发人员如何达到他们所提交的点,所以你只是得到“这是一个巨大的冲突在整个功能中,请修复它。“

另一方面,使用Git和其他具有廉价本地分支的DVCS的常用风格是定期提交的。一旦你完成了一些非常有意义的工作,你就可以了。它不一定是完美的,但它应该是一个连贯的工作单元。当您返回合并时,您仍然拥有较小提交的历史记录,显示您从原始状态到当前状态的如何。当DVCS将其与其他人的工作合并时,它有更多关于何时做出更改的信息,并且最终得到更小的更少的冲突。

关键是你仍然可以通过在你完成某些事情之后制作一个大爆炸提交来与Git合并一个难题。 Git鼓励你做出较小的提交(尽可能使它们尽可能轻松),这将使未来的合并变得更容易。

答案 1 :(得分:15)

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

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

似乎给出合并冲突。 我希望Git能够认识到" blub()"在mytest1中被移动,然后能够将mytest2中的修改与mytest1中的移动自动合并。但至少在我尝试时这并没有自动起作用......

因此,虽然我完全理解Git在跟踪已合并的内容以及尚未合并的内容方面要好得多,但我也想知道是否有" pure"合并Git优于SVN的情况...

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

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

所以这是一个SVN示例,基本上尝试这个:

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

这里的想法是:

  • Anna和Bob都是拥有自己分支的开发人员(在r2,r3中创建)。
  • Anna做了一些修改(r4),
  • Bob做了一些修改(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

最后一次" - 干跑"告诉你,会有冲突。如果您首先尝试将Anna的重新整合合并到Bob的分支中,那么您也会遇到冲突;所以如果你用

替换最后一个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

Anna的分支合并到Bob的分支

之后
A
MAB
A
B
MB
B

正如许多人已经指出的那样:问题是,颠覆不记得Bob已经解决了冲突。因此,当您尝试将Bob的分支合并到主干中时,您必须重新解决冲突。

Git完全不同。这里有一些图形表示git正在做什么

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

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

注意:

  • 当anna和bob创建他们的开发分支时,这将会 在git下创建任何提交。 git会记得那个 两个分支最初都引用相同的提交对象 主分支。 (此提交依次将引用s1快照)。
  • 当anna实施她的修改时,这将创建一个 新快照" s2" +一个提交对象。 提交对象包括:
    • 对快照的引用(此处为s2)
    • 提交消息
    • 有关祖先(其他提交对象)的信息
  • 当bob实施他的修改时,这将创建另一个快照 s3 +提交对象
  • 当bob将annas修改合并到他的开发分支中时 这将创建另一个快照s4(包含他的合并 更改和anna的更改)+另一个提交对象
  • 当anna将她的更改合并回主分支时,这将会 成为一个快速前进的"在显示的示例中合并,因为主人 在此期间没有改变。什么"快进"这意味着, 主人只会指向来自安娜的s2快照 没有合并任何东西。有了这样的"快进"这里将 甚至不是另一个提交对象。 "主人"分支只会 现在直接引用" anna"的最后一次提交分支
  • 当bob现在将他的更改合并到主干时,将发生以下情况:
    • git会发现anna创建的提交 s2快照是bobs commit的(直接)祖先, 这创建了s4快照。
    • 因为这个git会再次"快进"主分支到 " bob"的最后一次提交分支。
    • 再次,这甚至不会创建一个新的提交对象。 "主人" branch将简单地指向最后一次提交 " 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

从中可以看出:

  • 当我们去安娜的开发分支(HEAD @ {7})时,我们更改 对于不同的提交,我们保持提交; git只记得那个 我们现在在另一个分支
  • 在HEAD @ {5},我们转移到bob的初始分支;这将推动工作 复制到与master分支相同的状态,因为bob没有改变
  • 在HEAD @ {2},我们回到主分支,所以回到同一个提交 对象一切都从。
  • 开始
  • Head @ {1},HEAD @ {0}显示"快进"合并,不创建 新的提交对象。

使用" git cat-file HEAD @ {8} -p"您可以检查初始提交对象的完整详细信息。对于上面的例子,我得到了:

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

Initial file

&#34;树&#34;线标识 快照s1(== b634f7c9c819bb524524bcada067a22d1c33737f) 这个提交所涉及的内容。

如果我这样做&#34; git cat-file HEAD @ {3} -p&#34;我明白了:

tree f8e16dfd2deb7b99e6c8c12d9fe39eda5fe677a3
parent 83db5d741678908d76dabb5fbb0100fb81484302
parent 346ce9fe2b613c8a41c47117b6f4e5a791555710
author Ingo <***> 1475066831 +0200
committer Ingo <***> 1475066831 +0200

anna merged into bob with conflict

上面显示了提交对象,bob在合并anna的开发分支时创建的。再次&#34;树&#34; line指的是创建的快照(此处为s3)。另外请注意&#34;父母&#34;线。第二个以&#34;父母346ce9f&#34;开头。后来告诉git,当你试图将bob的开发分支合并到master分支中时,bob的最后一次提交将anna的最后一次提交作为祖先。这就是为什么git知道将bob的开发分支合并到主分支中是一个快速前进的&#34;。

答案 2 :(得分:6)

我没有具体的例子,但任何类型的 重复合并 都很困难,特别是所谓的 criss-cross merge

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

合并b2和c2


Subversion Wiki上的wiki页面描述了基于 merge info 的不对称Subversion合并(带有'sync'和'reintegrate'指示)和 merge 的对称合并有一个“Symmetric Merge with Criss-Cross Merge

部分

答案 3 :(得分:4)

我能想到的最具体的例子是最简单的合并,不会导致合并冲突。然而(TL; DR)与该示例相比,Git本质上仍然是比Subversion更简单的过程。让我们回顾一下原因:

的Subversion

在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!"

在subversion中合并更改需要另一次提交。这是为了发布合并所做的更改,将功能分支虚拟目录上的更改应用回主干。这样每个使用主干的人现在都可以使用它,修订图看起来像这样:

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

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

GIT中

在Git中,这个合并提交实际上没有必要,因为分支是修订图上的美化书签。因此,使用相同类型的修订图结构,它看起来像这样:

         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

...它将快速转发分支到功能分支所指向的提交。这是可能的,因为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至少需要几个命令来完成合并,强制您将合并发布为提交。
  • Git只需要一个命令不强迫您发布合并。

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

在更困难的合并方案中,git还为您提供rebase the branch能够以历史记录重写为代价生成更简单的修订图的功能。一旦掌握了它,并避免发布已经发布的事物的历史记录重写,那么事情并不是那么糟糕。

Interactive rebases超出了问题的范围,但说实话;它使您能够重新排列,挤压和删除提交。我不愿意切换回Subversion,因为历史重写是不可能的。

答案 4 :(得分:2)

保持答案简短 - 在DVCS中,由于你有一个本地源代码控制,如果在合并过程中出现问题(可能会在大型合并中发生),你可以随时回滚到以前的本地版本您在合并之前所做的更改,然后再试一次。

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

答案 5 :(得分:1)

如果排除“Merge-Refactored Hell”,将无法获得 合格样本,因为它们不存在

答案 6 :(得分:0)

在Git中合并比在SVN中更容易看起来是一个神话......

例如,与SVN不同,Git无法合并到具有更改的工作树中。

考虑以下简单场景: you have some changes in your working tree and want to integrate remote changes without committing your own

SVN update,[解决冲突]。

Git stashfetchrebasestash pop,[解决冲突],[stash drop如果有冲突。

或者你知道Git中更简单的方法吗?

Btw,这个用例似乎非常重要,IntelliJ甚至为Git实现了缺失的“更新项目”功能(类似于SVN更新),可以自动执行上述手动步骤:

IntelliJ IDEA - Update Project