English 中文(简体)
真的吗,Git中的合并比SVN更容易的一个具体例子?
原标题:Really, a concrete example that merging in Git is easier than SVN?
  • 时间:2011-05-30 03:28:49
  •  标签:
  • git
  • svn
  • merge

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

如果这个问题有可能被重复关闭,什么是

  1. A concrete merge scenario
  2. How it is difficult in SVN?
  3. How the same merge is easier in Git?

以下几点:

  • No philosophy or deep explanations of what a DVCS is please. These are great, really, but I don t want their details to obfuscate the answer to this (IMHO) important
  • I don t care about "historic SVN" at the moment. Please compare modern Git (1.7.5) to modern SVN (1.6.15).
  • No renames please - I know that Git detects renames and moves, while SVN doesn t. This is great, but I am looking for something deeper, and example that doesn t involve renames or moves.
  • No rebase or other advanced Git operation. Just show me the merge please.
问题回答

实用的角度来看,合并传统上是“困难的”,因为我称之为“大爆炸合并”问题。假设一个开发人员已经处理了一些代码一段时间,但还没有提交他们的工作(也许该开发人员习惯于在Subversion中针对<code>trunk</code>进行工作,并且没有提交未完成的代码)。当开发人员最终提交时,将会有很多更改都集中到一个提交中。对于其他想要将他们的工作与这种“大爆炸”提交合并的开发人员来说,VCS工具将没有足够的信息来说明第一个开发人员是如何达到他们提交的地步的,所以你只会得到“整个功能中存在巨大冲突,去解决它”。

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

关键是,你仍然可以通过在完成某件事后进行一次大爆炸提交,使Git的合并成为一个难题。Git鼓励您进行较小的提交(通过尽可能无痛地进行),这使得未来的合并更加容易。

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

I was wondering about this case: You start with two branches "mytest1" and "mytest2" both based on the same commit. You have got a C file which contains a function blub(). In branch mytest1 you move "blub()" to a different position in the file and commit. In branch mytest2 you modify blub() and commit. On branch mytest2 you try to use "git merge mytest1".

Seems to give a merge conflict. I hoped that Git would recognize that "blub()" was moved in mytest1 and then be able to auto-merge the modification in mytest2 with the move in mytest1. But at least when I tried this did not work automatically...

因此,虽然我完全理解Git在跟踪已经合并和尚未合并的内容方面要好得多,但我也想知道是否存在Git比SVN更好的“纯”合并情况。。。

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

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

这里有一个SVN的例子,它基本上是这样尝试的:

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

这里的想法是:

  • Anna and Bob are both developers with their own branches (created in r2,r3).
  • Anna does some modifications (r4),
  • Bob does some modifications (r5).
  • Bob merges the modifications from Anna into his branch; this gives conflicts, which Bob fixes and then commits (r6).
  • Annas modifications are merged back into the trunk (r7).
  • Bob tries to merge his modification back into the trunk and this again gives a conflict.

这是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
A
B
B" > 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
MA
A
B
B" > branches/anna/f.txt
svn commit -m "anna added text"
echo -e "A
MB
A
B
MB
B" > branches/bob/f.txt
svn commit -m "bob added text"
svn up
svn merge --accept postpone ^/branches/anna branches/bob
echo -e "A
MAB
A
B
MB
B" > 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
A
B
B" > f.txt
git add f.txt
git commit -m "Initial file"
git branch anna
git branch bob
git checkout anna
echo -e "A
MA
A
B
B" > f.txt
git commit -a -m "anna added text"
git checkout bob
echo -e "A
MB
A
B
MB
B" > f.txt
git commit -a -m "bob added text"
git merge anna
echo -e "A
MAB
A
B
MB
B" > 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

Bob的修改

A
MB
A
B
MB
B

安娜的分店并入鲍勃的分店后

A
MAB
A
B
MB
B

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

Git的工作方式完全不同。下面是git的一些图形表示

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

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

注意事项:

  • When anna and bob create their development branches, this will NOT create any commits under git. git will just remember that both branches initially refer to the same commit object as the master branch. (This commit in turn will refer to the s1 snapshot).
  • When anna implements her modification, this will create a new snapshot "s2" + a commit object. A commit object includes:
    • A reference to the snapshot (s2 here)
    • A commit message
    • Information about ancestors (other commit objects)
  • When bob implements his modification, this will create another snapshot s3 + a commit object
  • When bob merges annas modifications into his development branch this will create yet another snapshot s4 (containing a merge of his changes and anna s changes) + yet another commit object
  • When anna merges her changes back into the master branch, this will be a "fast-forward" merge in the shown example, because the master has not changed in the meantime. What "fast-forward" here means is, that the master will simply point to the s2 snapshot from anna without merging anything. With such a "fast-forward" there will not even be another commit object. The "master" branch will just directly now refer to the last commit from the "anna" branch
  • When bob now merges his changes into the trunk, the following will happen:
    • git will find out that the commit from anna which created the s2 snapshot is a (direct) ancestor for bobs commit, which created the s4 snapshot.
    • because of this git will again "fast-forward" the master branch to the last commit of the "bob" branch.
    • again this will not even create a new commit object. The "master" branch will simply be pointed to the last commit of the "bob" branch.

下面是“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

从中可以看出:

  • when we go to anna s development branch (HEAD@{7}) we do not change to a different commit, we keep the commit; git just remembers that we are now on a different branch
  • At HEAD@{5} we move to bob s initial branch; this will move the working copy to the same state as the master branch, because bob has not changed anything yet
  • At HEAD@{2} we move back to the master branch, so to the same commit object everything started from.
  • Head@{1},HEAD@{0} show the "fast-forward" merges, which do not create new commit objects.

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

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

Initial file

The "tree" line identifies the snapshot s1 (==b634f7c9c819bb524524bcada067a22d1c33737f) to which this commit refers.

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

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

anna merged into bob with conflict

上面显示了bob在合并anna的开发分支时创建的commit对象。同样,“树”行指的是创建的快照(此处为s3)。此外,请注意“父”行。第二个以“parent 346ce9f”开头,稍后告诉git,当您试图将bob的开发分支合并回master分支时,bob的最后一个提交将anna的最后一次提交作为祖先。这就是为什么git知道将bob的开发分支合并到master分支是一种“快进”。

我没有具体的例子,但任何类型的重复合并都很困难,尤其是所谓的交叉合并

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

合并b2和c2


Subversion wiki上的wiki页面描述了DVCS中基于mergeinfo的不对称Subversion merge(具有同步和重新整合方向)和基于merge的跟踪的对称合并之间的差异,该页面有一个“具有Criss交叉合并的对称合并

我能想到的最具体的例子是最简单的合并,它不会导致合并冲突。然而,(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

Conclusion

在这个简单的场景中,你可以断言Subversion和Git之间的区别有两件事:

  • SVN requires at least a couple of commands to finalize the merge and forces you to publish your merge as a commit.
  • Git only requires one command and does not force you to publish your merge.

考虑到这是最简单的合并场景,很难说subversion比git更容易。

在更困难的合并场景中,git还为您提供了历史重写为代价,为生成更简单的修订图的分支重新设置基础。不过,一旦你掌握了窍门,避免发布已发布内容的历史重写,这并不是一件坏事。

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

简而言之——在DVCS中,由于您有本地源代码管理,如果合并过程中出现问题(这可能会发生在大型合并中),您总是可以回滚到以前的本地版本,该版本包含您在合并前所做的更改,然后重试。

因此,基本上,您可以进行合并,而不必担心您的本地更改可能在合并过程中受损。

Git中的合并比SVN中的容易,这似乎是一个神话。。。

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

Consider the following simple scenario: you have some changes in your working tree and want to integrate remote changes without committing your own.

SVN更新,[解决冲突]。

Gitstashfetchrebasestashpop

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


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

如果排除“合并重构地狱”,则将无法获得公平样本,因为它们根本不存在





相关问题
Best practices for Subversion and Visual Studio projects

I ve recently started working on various C# projects in Visual Studio as part of a plan for a large scale system that will be used to replace our current system that s built from a cobbling-together ...

Changing username in SVN+SSH URI on the fly in working copy

I am using SVN+SSH to check out a working copy of repository from an SVN server on which all developers are members of a developer group and have full read/write permissions on the repository ...

ASP.NET MVC: How should it work with subversion?

So, I have an asp.net mvc app that is being worked on by multiple developers in differing capacities. This is our first time working on a mvc app and my first time working with .NET. Our app does not ...

How to search for file in subversion server?

Is there a way to search for a file in a subversion repository? Something similar to Unix find command, with which I can find the location of a file in a repository. I know there is svn list, but ...