Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

git

merge

svn

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 a simple example where merging in Git works better than SVN.

On the chance this question will be closed as a duplicate, what is

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

A few points:

  • 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.
like image 562
ripper234 Avatar asked May 30 '11 03:05

ripper234


People also ask

How is Git better than SVN?

Many people prefer Git for version control for a few reasons: It's faster to commit. Because you commit to the central repository more often in SVN, network traffic slows everyone down. Whereas with Git, you're working mostly on your local repository and only committing to the central repository every so often.

What is the best Git merge strategy?

The most commonly used strategies are Fast Forward Merge and Recursive Merge. In this most commonly used merge strategy, history is just one straight line. When you create a branch, make some commits in that branch, the time you're ready to merge, there is no new merge on the master.

Which feature of Git makes it attractive over SVN?

Git has the advantage that it's MUCH better suited if some developers are not always connected to the master repository. Also, it's much faster than SVN. And from what I hear, branching and merging support is a lot better (which is to be expected, as these are the core reasons it was written).

What are some merge strategies you can use with Git?

Recursive Git Merge Strategy Options Changes from the 'theirs' side are automatically incorporated if they do not conflict. The opposite of the 'ours' strategy. the "theirs" option favors the foreign merging tree in conflict resolution. This option spends extra time to avoid mis-merges on unimportant matching lines.


2 Answers

From a practical perspective, merging has traditionally been "hard" because of what I call the "big bang merge" problem. Suppose a developer has been working away on some code for a while and hasn't committed their work yet (maybe the developer is accustomed to working in Subversion against trunk, and doesn't commit unfinished code). When the developer finally commits, there is going to be a lot of changes all rolled up into one commit. For the other developers who want to merge their work with this "big bang" commit, the VCS tool isn't going to have enough information about how the first developer got to the point they committed, so you just get "here's a giant conflict in this whole function, go fix it".

On the other hand, the usual style of working with Git and other DVCS that have cheap local branches, is to commit regularly. Once you've done one bit of work that pretty much makes sense, you commit it. It doesn't have to be perfect but it should be a coherent unit of work. When you come back to merge, you still have that history of smaller commits that shows how you got from the original state to the current state. When the DVCS goes to merge this with the work of others, it has a lot more information about what changes were made when, and you end up getting smaller and fewer conflicts.

The point is that you can still make merging a hard problem with Git by making a single big bang commit only after you've finished something. Git encourages you to make smaller commits (by making them as painless as possible), which makes future merging easier.

like image 95
Greg Hewgill Avatar answered Sep 27 '22 01:09

Greg Hewgill


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

like image 45
Ingo Blackman Avatar answered Sep 27 '22 01:09

Ingo Blackman