Really, a concrete example that merging in Git is easier than SVN?

前端 未结 7 1203
一向
一向 2020-12-22 20:08

Stack Overflow question How and/or why is merging in Git better than in SVN? is a great question with some great answers. However none of them show

相关标签:
7条回答
  • 2020-12-22 20:23

    Keeping the answer short - In DVCS , since you have a local source control, if something get screwed up in the merge process (which will probably happen in large merges), you can always rollback to a previous local version which has the changes you've made before merging, and then try again.

    So basically you can do merge without the fear that your local changes might get damaged during the process.

    0 讨论(0)
  • 2020-12-22 20:24

    If you exclude "Merge-Refactored Hell" you will not get fair samples, because they just doesn't exist

    0 讨论(0)
  • 2020-12-22 20:31

    I don't have concrete examples, but any kind of repeated merge is difficult, in particular so called criss-cross merge.

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

    merging b2 and c2


    The wiki page on Subversion Wiki describing differences between mergeinfo based assymetric Subversion merge (with 'sync' and 'reintegrate' directions) and merge tracking based symmetric merge in DVCS has a section "Symmetric Merge with Criss-Cross Merge"

    0 讨论(0)
  • 2020-12-22 20:31

    The most concrete example I can think of is the simplest merge that does not result in merge conflicts. However (TL;DR) with that example Git is still inherently a simpler procedure than with Subversion. Lets review why:

    Subversion

    Consider the following scenario in subversion; the trunk and the feature branch:

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

    To merge you can use the following command in 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!"
    

    In subversion merging the changes requires another commit. This is to publish the changes that the merge did with applying the changes on the feature branch virtual directory back into the trunk. That way everyone working with the trunk can now use it and the revision graph looks like sort of like this:

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

    Lets see how this is done in git.

    Git

    In Git this merge commit is really not necessary as branches are glorified bookmarks on the revision graph. So with the same kind of revision graph structure it sort of looks like this:

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

    With the head currently on the master branch we can perform a simple merge with the feature branch:

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

    ... and it will fast-forward the branch over to the commit where feature branch is pointing at. This is made possible because Git knows that the goal of the merge is a direct descendant and the current branch only needs to take in all the changes that happened. The revision graph will end up looking like this:

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

    The changes does not need a new commit as all git has done is to move the branch references further up to the front. All that is left is to publish this to the public repository if you have any:

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

    Conclusion

    Given this simple scenario you can assert two things in the difference between Subversion and 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.

    Given this to be the most simplest merge scenario it is difficult to argue that subversion is easier than git.

    In more difficult merge scenarios, git also provides you the ability to rebase the branch that produces a simpler revision graph at the cost of history rewriting. Once you get the hang of it though, and avoid publishing history rewrites of things already published, it isn't really that bad of a thing to do.

    Interactive rebases is outside the scope of the question but to be honest; it enables you the ability to rearrange, squish and remove commits. I wouldn't willingly want to switch back to Subversion as history rewriting is not possible by design.

    0 讨论(0)
  • 2020-12-22 20:34

    Looks like it's a myth that merging in Git is easier than in SVN...

    For example, Git cannot merge into a working tree that has changes, unlike SVN.

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

    SVN: update, [resolve conflicts].

    Git: stash, fetch, rebase, stash pop, [resolve conflicts], [stash drop if there were conflicts].

    Or do you know an easier way in Git?


    Btw, this use case seems to be so important that IntelliJ even implemented the missing "Update Project" functionality for Git (analogon to SVN update) which can automate the manual steps described above:

    0 讨论(0)
  • 2020-12-22 20:41

    I can only tell you of a small experiment were Git was NOT better than Subversion (same problems).

    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...

    So while I fully understand that Git is much better at tracking what has been merged and what has not been merged yet, I also wonder if there is "pure" merge case in which Git is better than SVN...

    Now because this question has been bugging me for a long time I was really trying to create a concrete example where Git is better, whereas merging in SVN fails.

    I found one here https://stackoverflow.com/a/2486662/1917520, but this includes a rename and the question here was for a case without a rename.

    So here is an SVN example which basically tries this:

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

    The idea here is:

    • 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.

    Here is a Bash script, which produces this conflict (using SVN 1.6.17 and also 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
    

    The last "--dry-run" tells you, that there will be a conflict. If you instead first try to merge Anna's reintegration into Bob's branch then you also get a conflict; so if you replace the last svn merge with

    svn merge ^/trunk branches/bob
    

    this also shows a conflicts.

    Here is the same with 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
    

    The contents of f.txt change like this.

    Initial version

    A
    A
    B
    B
    

    Anna's modifications

    A
    MA
    A
    B
    B
    

    Bob's modifications

    A
    MB
    A
    B
    MB
    B
    

    After Anna's branch is merged into Bob's branch

    A
    MAB
    A
    B
    MB
    B
    

    As so many people already pointed out: The problem is, that subversion cannot remember that Bob already resolved a conflict. So when you try to now merge Bob's branch into the trunk, then you have to re-resolve the conflict.

    Git works completely different. Here some graphical representation what git is doing

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

    s1/s2/s3/s4 are the snapshots of the working directory git takes.

    Notes:

    • 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.

    Here is the output of "git ref-log" which shows all of this:

    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
    

    As you can see from this:

    • 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.

    With "git cat-file HEAD@{8} -p" you can inspect the complete details of the initial commit object. For the example above, I got:

    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.

    If I do "git cat-file HEAD@{3} -p" I get:

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

    This above shows the commit object, bob created when merging anna's development branch. Again the "tree" line refers to the created snapshot (s3 here). Additionally note the "parent" lines. The second one which starts with "parent 346ce9f" later tells git, when you try to merge back bob's development branch into the master branch, that this last commit of bob has anna's last commit as an ancestor. This is why git knows that the merge of bob's development branch into the master branch is a "fast-forward".

    0 讨论(0)
提交回复
热议问题