Summary for busy readers

Working in big enterprise company, you have to justify before moving from one version control tool to another. But why should I replace Subversion with Git or Mercurial.
I took the time to analyse a couple of Subversion issues and would like to show when other version control system like Mercurial or Git work whereas SVN fails.

The summary explains issues you can encounter with subversion and explains how other version control system treat the situation. You will find the prove with real examples below.

Subversion is not aware of branches

Subversion is not aware of branches and can cause complex and time consuming merging problems, if you try to use branching. Subversion can just copy directories efficiently to a directory which is called branches. If you create a branch, do some changes, merge the changes back and delete the branch, then you won’t be impacted that much.

But if

then you should

If you do not want to delete your branch, then you need to update the merge information in your branch else you might get your own changes back. You can use the —record-only merge command to achieve this. It sounds complex and it is complex.

Do not believe that cherry picking is something sweet.

Inconsistencies with broken merge infos

Subversion does not distinguish between conflict free merges and merges which needed a conflict resolution. Using record only indicates a conflict free merge. It either leads to another conflict or in the worst case, leads to a broken branches having different content but mergeinfo telling that everything is merged. Does this sound complex? Well, it sounds complex and it is complex.

Subversion does not track changes to files but revisions in directories (SVN branches)

If you create a directory with the same name in two branches or the trunk and your working copy, then be prepared for a merge session with tree conflicts. Tree conflicts basically mean that subversion stops merging files.

If you refactor code, move files to better locations and another branch has changes in this files, you will see tree conflicts. So again Subversion stops merging the files and you end up with manually copying your changes. You should be aware that this failing is Subversion specific.

If you refactor code, move files to better locations and somebody else update his/her workingcopy, he/she will encounter tree conflicts if the local files are changes as well. So again, you have to manually somehow get the changes to your working copy.

Subversion does not know about conflict free merges

The merger always have to treat merged changes as his own changes. They need to be committed even if they where conflict free. Apart from wasting your time, it can be very handsome not having new commits with new commit messages for merged changes.

It makes it even with additional tools nearly impossible to track who has changed which code.

Staying in the rain

You have changed code in a moved file. Do not expect to just merge the change to the new location of the file.

You cannot use svn without additional tools

Experiment one:

Let’s not see what was changed in your last two changes.

svn log

------------------------------------------------------------------------
r9 | hennebrueder | 2012-08-08 14:57:42 +0200 (Mi, 08 Aug 2012) | 1 line

merged team b
------------------------------------------------------------------------
r8 | hennebrueder | 2012-08-08 14:57:35 +0200 (Mi, 08 Aug 2012) | 1 line

merged team a
------------------------------------------------------------------------
r3 | hennebrueder | 2012-08-08 14:56:40 +0200 (Mi, 08 Aug 2012) | 1 line

paint middle wall blue
------------------------------------------------------------------------
r2 | hennebrueder | 2012-08-08 14:55:22 +0200 (Mi, 08 Aug 2012) | 1 line

prepared demo
------------------------------------------------------------------------
r1 | hennebrueder | 2012-08-08 14:50:00 +0200 (Mi, 08 Aug 2012) | 1 line

prepared template

Success, we successfully could not see what was done in team a and b’s branches.

The subversion command svn log fails. You need to use tools to achieve this.

Experiment two:

Let’s see what was changed in your last two changes not using subversion.

git log
commit 0b1068c2fffb549dbb5fc01eaf09bd0fea3b35c0
Merge: e69dc58 aadddf9
Author: Laliluna Admin <hennebrueder@laliluna.de>
Date:   Wed Aug 8 15:01:26 2012 +0200

    Merge branch 'team-b'

commit e69dc58b940d7f7aa4c86fd2f7026af1974e83ff
Author: Laliluna Admin <hennebrueder@laliluna.de>
Date:   Wed Aug 8 15:01:12 2012 +0200

    paint left wall red

commit aadddf960c25350f547d36d96fae1b87ba9921ce
Author: Laliluna Admin <hennebrueder@laliluna.de>
Date:   Wed Aug 8 15:01:12 2012 +0200

    paint right wall green

commit 8c4078e8850623230b8088ed0a3376bdcbb7fee7
Author: Laliluna Admin <hennebrueder@laliluna.de>
Date:   Wed Aug 8 15:01:12 2012 +0200

    paint middle wall blue

Success, we successfully could see what was done in team a and b’s branches not using subversion.

Let’s now prove what we have just learned.

Exchange changes between branches

User story

I have a trunk with a blue wall in the middle. There is a branch of this trunk for team a. Team a painted the left wall red. Furthermore, I split up a branch for team b. This team painted the right wall green.
A wall is a file and the color is text in the file.

I would like to get team a’s changes into the trunk and into team b.

Using Git

Setup the project

git init
# Initialized empty Git repository in /Users/hennebrueder/workspaces/default-workspace/source-code-sample/git/.git/

echo "blue" > middle
git add middle 
git commit -m 'paint middle wall blue'
# [master (root-commit) 3bb180e] paint middle wall blue
# 1 file changed, 1 insertion(+)
# create mode 100644 middle

git branch team-a
git branch team-b

git co team-a
# Switched to a branch 'team-a'

echo "red" > left
git add left
git commit -m 'paint left wall red'
# [team-a 792326f] paint left wall red
#  1 file changed, 1 insertion(+)
#  create mode 100644 left

git  co team-b
# Switched to branch 'team-b'
echo "green" > right
git add right 
git commit -m 'paint right wall green'
# [team-b 17236cc] paint right wall green
#  1 file changed, 1 insertion(+)
#  create mode 100644 right

The project is setup now. We can start merging.

Merging

The target is to get team a’s changes into the trunk(alias master) and into team b’s branch.

The approach is straight forward, we need exactly 4 commands.

git co master
# Switched to branch 'master'

git merge team-a
# Updating ecfda15..b763ead
# Fast-forward
#  left |    1 +
#  1 file changed, 1 insertion(+)
#  create mode 100644 left

git co team-b
# 	Switched to branch 'team-b'

git merge team-a
# 	Merge made by the 'recursive' strategy.
# 	 left |    1 +
#	 1 file changed, 1 insertion(+)
#	 create mode 100644 left
#	sebastian-mac:git hennebrueder$

Using Mercurial

Setup the project

mkdir master
cd master/
hg init
echo 'blue' > middle
hg add middle 
hg commit -m 'Paint middle wall blue'
cd ..
hg clone master/ team-a
# updating to branch default
# 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
cd team-a/
echo 'red' > left
hg add
# adding left
hg commit -m 'paint left wall red'
cd ..
hg clone master/ team-b
# updating to branch default
# 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
cd team-b
echo 'green' > right
hg add
adding right
hg commit -m 'paint right wall green'

Merging

cd ../master/
hg pull ../team-a
pulling from ../team-a
# searching for changes
# adding changesets
# adding manifests
# adding file changes
# added 1 changesets with 1 changes to 1 files
# (run 'hg update' to get a working copy)
hg update tip
# 1 files updated, 0 files merged, 0 files removed, 0 files unresolved

cd ../team-b/
hg pull ../team-a
# pulling from ../team-a
# searching for changes
# adding changesets
# adding manifests
# adding file changes
# added 1 changesets with 1 changes to 1 files (+1 heads)
# (run 'hg heads' to see heads, 'hg merge' to merge)
hg merge
# 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
# (branch merge, don't forget to commit)
hg commit -m 'merged team a changes'	

Mercurial is really user friendly. It tells you what to do when you pull in changes from another branch.

Using Subversion

Setup the project

cd $HOME
svnadmin create ./repo
svn co file:///$HOME/repo checkout
cd checkout
mkdir -p {trunk,branches}
svn add *
svn ci -m 'prepared trunk and branches'
cd ..
rm -rf checkout
export base=file:///$HOME/repo

svn co $base/trunk
# Checked out, Revision 72.
cd trunk/
echo 'blue' > middle
svn add middle
# A         middle
svn ci -m 'paint middle wall blue'
# Add     middle
# Transmitting file data .
# Committed Revision 73.

svn cp $base/trunk $base/branches/team-a -m 'create branch for team a'
# Committed Revision 76.

svn cp $base/trunk $base/branches/team-b -m 'create branch for team b'
# Committed Revision 77.

svn switch $base/branches/team-a
# A    middle
# Updated to Revision 77.
echo 'red' > left
svn add left 
# A         left
svn ci -m 'paint left wall red'
# Add     left
# Transmitting file data .
# Committed Revision 78.
svn switch $base/branches/team-b
# D    left
# Updated to Revision 78.
echo 'green' > right
svn add right 
# A         right
svn ci -m 'paint right wall green'
# Add     right
# Transmitting file data .
# Revision 79 created.

Waste time merging

I will introduce to you the waste time merge.

The target is to get team a’s changes into the trunk(alias master) and into team b’s branch.

svn switch $base/trunk
# At revision 81.
svn merge $base/branches/team-a
# -- Merging r76 through r80 into ».«:
# A    left
# --- Recording mergeinfo for merge of r76 through r80 into '.':
# U   .
svn ci -m 'merged team a into trunk'
## Add     left
# Transmitting file data .
# Revision 81 created.

Apart from the requirement to make a commit in addition to the merge operation, it looks the same as the merge with Git.

Failure number one

But sadly at this point we have caused our first inconsistency. We can easily proof it by trying to merge back the trunk into the team-a branch.
The expected result of this operation is that we get the information that the changes are already there.

svn switch $base/branches/team-a
# At revision 83.
svn merge $base/trunk
# --- Merging r76 through r83 into '.':
#    C left
# --- Recording mergeinfo for merge of r76 through r83 into '.':
#  U   .
# Summary of conflicts:
#  Tree conflicts: 1
svn status
# M      .
#      C left
#      >   local add, incoming add upon merge
# Summary of conflicts:
#  Tree conflicts: 1

We got a conflict, naturally because the left wall is is red and you merged this change back to a branch where the left wall is red, which is not the same because it is red. Ha ha

Why did this happen?

SVN has not the notion of branches but in fact only copies directories recursively to other directories. svn cp $base/trunk $base/branches/team-b -m ‘create branch for team b’

When we merge, it applies the changes to the files and writes the revision it has merged into the svn property mergeinfo. The first merge did record those information into the trunk directory.

svn propget svn:mergeinfo $base/trunk
# /demo/branches/team-a:76-80

But it does not record anything into the branch of team-a. As a consequence team-a’s branch is not aware which of its own changes has already been merged into the trunk. It try to merge trunk changes from its own to its own branch, which is of course nonsense. Remember, that a merge is always a new commit.

svn propget svn:mergeinfo $base/branches/team-a
# prints nothing

The failed merge operation tried to merge all changes since the branch of team-a was created. It included the changes in the commit of revision 81. These are its own changes. As a consequence we get a conflict.

Solution

As the SVN documentation indicates, you need to tell a branch which of its changes have already been applied.

We will revert the failed merge and fix the mergeinfos.

svn revert -R .
# Reverted '.'
# Reverted 'left'
svn merge -c 81 --record-only $base/trunk
# --- Recording mergeinfo for merge of r81 into '.':
#  U   .
svn ci -m 'recorded trunk merge'
# Sending        .
# Committed revision 84.

This operation only updates the mergeinfo of the team-a branch.

svn propget svn:mergeinfo $base/branches/team-a
# /demo/trunk:81	

With SVN you need 5 steps to merge a non conflicting branch into another. The other SCM need only 1 step.

  1. merge
  2. commit
  3. switch to merged branch
  4. merge with record only
  5. commit

Your SVN repository is now consistent but the next use case will demonstrate why recording merge information can be dangerous.

Decision graph for business people

                                              / (yes): You can use subversion if you have enough time or money
                                            / 
Did you understand the subversion example? /
                                           \
                                            \ (no): You should use something else

Breaking branches using record-only

We will change the color of the middle wall to orange in the trunk and will add a black border to the middle wall in the team-a branch. Naturally this should cause a conflict.

svn switch $base/trunk
# U   .
# Updated to revision 85.
echo 'orange' > middle 
svn ci -m 'changed middle wall color to orange'
# Sending        middle
# Transmitting file data .
# Committed revision 86.

svn switch $base/branches/team-a
# U    middle
#  U   .
# Updated to revision 86.
echo 'blue with black border' > middle 
svn ci -m 'added black border to middle wall'
# Sending        middle
# Transmitting file data .
# Committed revision 87.
svn switch $base/trunk
# U    middle
#  U   .
# Updated to revision 87.

The merge leads naturally to a conflict, which we can resolve having an orange wall with a black border.

svn merge $base/branches/team-a
# Conflict discovered in '/Users/hennebrueder/workspaces/db-workspace/demo-svn/middle'.
# Select: (p) postpone, (df) diff-full, (e) edit,
#         (mc) mine-conflict, (tc) theirs-conflict,
#         (s) show all options: e
# Select: (p) postpone, (df) diff-full, (e) edit, (r) resolved,
#         (mc) mine-conflict, (tc) theirs-conflict,
#         (s) show all options: r
# --- Merging r81 through r87 into '.':
# U    middle
# --- Recording mergeinfo for merge of r81 through r87 into '.':
#  U   .
# cat middle 
# orange with black border
svn ci -m 'after merge middle wall is now orange with a black border'
# Sending        .
# Sending        middle
# Transmitting file data .
# Committed revision 88.
svn switch $base/branches/team-a
U    middle
 U   .
Updated to revision 88.
svn merge -c 88 --record-only $base/trunk
--- Recording mergeinfo for merge of r88 into '.':
 U   .
svn ci -m 'recorded trunk merge'
# Sending        .
# Committed revision 89.

The merge is done and we have even fixed the mergeinfo. Now we should actually be able to merge the solution without conflict to the team a branch.

Second failure using record-only

Let’s give it a try.


svn up
Updating ‘.’:
At revision 89.
svn merge $base/trunk
Conflict discovered in ‘/Users/hennebrueder/workspaces/db-workspace/demo-svn/middle’.
Select: (p) postpone, (df) diff-full, (e) edit,
(mc) mine-conflict, (tc) theirs-conflict,
(s) show all options: df
- /var/folders/jp/mdf5fyx50n194xd_p4f8pr2w0000gn/T/svn-xBadNM Mon Jun 18 08:41:11 2012
+ /Users/hennebrueder/workspaces/db-workspace/demo-svn/.svn/tmp/middle.tmp Mon Jun 18 08:41:11 2012
@ -1 +1,5 @
-blue
+<<<<<<< .working
+blue with black border
+===
+orange
+>>>>>>> .merge-right.r87

Sadly we failed. We have to solve the conflict again and SVN is telling us that we have orange in the trunk, though we have orange with black border since revision 88.

We need to solve the conflict again and are finally done.

SVN needs 9 steps and is having merge conflicts two times.

  1. merge
  2. solve conflict
  3. commit
  4. switch to merged branch
  5. merge with record only
  6. commit
  7. merge again
  8. solve conflict
  9. commit

Other SCM need only 6 steps and one conflict solving.

  1. git merge team-b
  2. openWithEditor middle
  3. git add middle
  4. git commit -m ‘merged middle’
  5. git co team-b
  6. git merge master

Decision graph for business people

	 	 	 	 	           
                         	      / (yes): You can use subversion if you have enough time or money 
Did you understand the problem   /
to twiddle with merge infos      \
                                   \ (no): You should use something else

Creating and deleting directories

Team a is adding a test directory and write a test for the left wall. Team b adds the same directory and test the right wall.

Setup with Subversion

svn switch $base/branches/team-a
# A    team-a/left
# A    team-a/middle
#  U   team-a
# Checked out revision 97.
mkdir test
echo 'test left wall' > test/leftWallTest
svn add test/
# A         test
# A         test/leftWallTest
svn ci -m 'added test for left wall'
# Adding         test
# Adding         test/leftWallTest
# Transmitting file data .
# Committed revision 98.

svn switch $base/branches/team-b
# D    test
# D    left
# A    right
# U    middle
#  U   .
# Updated to revision 98.

mkdir test
echo 'test right wall' > test/rightWallTest
svn add test/
# A         test
# A         test/rightWallTest
svn ci -m 'added test of right wall'
# Adding         test
# Adding         test/rightWallTest
# Transmitting file data .
# Committed revision 99.

Merging with subversion

svn switch $base/trunk
# D    test
# D    right
# A    left
# U    middle
#  U   .
# Updated to revision 99.
svn merge $base/branches/team-a
# --- Merging r94 through r99 into '.':
# A    test
# A    test/leftWallTest
#  U   .
# --- Recording mergeinfo for merge of r94 through r99 into '.':
#  U   .
svn ci -m 'merged team a into trunk'
# Sending        .
# Adding         test
# Transmitting file data .
# Committed revision 100.

svn merge $base/branches/team-b
# Hoops, I forgot to get a consistent working copy
# svn: E195020: Cannot merge into mixed-revision working copy [99:100]; try updating first
svn up
# Updating '.':
# At revision 100.
svn merge $base/branches/team-b
# --- Merging r77 through r100 into '.':
#    C test
# A    right
# --- Recording mergeinfo for merge of r77 through r100 into '.':
#  U   .
# Summary of conflicts:
#   Tree conflicts: 1
svn status
#  M      .
# A  +    right
#       C test
#       >   local add, incoming add upon merge
# Summary of conflicts:
#   Tree conflicts: 1

At this point, I have a SVN tree conflict. The only way to resolve this, is to accept the working version. You will not receive the files created by team b (rightWallTest) but have to copy them on your own.

svn resolve --accept=working  test/
# Resolved conflicted state of 'test'	
ls test/
# leftWallTest

Let’s get the missing file

svn ci -m 'merged team b'
# Sending        .
# Adding         right

# Committed revision 101.
svn cp $base/branches/team-b/test/rightWallTest test/
# A         test/rightWallTest
svn ci -m 'copied missing rightWallTest of team b branch after tree conflict failurewq'
# Adding         test/rightWallTest

# Committed revision 102.

# We ommit the record only step here.

We are done after 14 steps.

Merging with Git

Setup with git

git co team-a

  1. Switched to branch ‘team-a’
    mkdir test
    echo ‘test left wall’ > test/leftWallTest
    git add test/leftWallTest
    git commit -m ‘added a test for the left wall’
  2. [team-a 3e4c5ce] added a test for the left wall
  3. 1 files changed, 1 insertions(+), 0 deletions(-)
  4. create mode 100644 test/leftWallTest
    git co team-b
  5. Switched to branch ‘team-b’
    mkdir test
    echo ‘test right wall’ > test/rightWallTest
    git add test/rightWallTest
    git commit -m ‘added a test for the right wall’
  6. [team-b 83d1b05] added a test for the right wall
  7. 1 files changed, 1 insertions(+), 0 deletions(-)
  8. create mode 100644 test/rightWallTest

Merging with git

We need only 3 steps as opposed to 14.

git co master
# Switched to branch 'master'
git merge team-a
# Merge made by the 'recursive' strategy.
#  test/leftWallTest |    1 +
#  1 file changed, 1 insertion(+)
#  create mode 100644 test/leftWallTest
git merge team-b
# Merge made by the 'recursive' strategy.
#  test/rightWallTest |    1 +
#  1 file changed, 1 insertion(+)
#  create mode 100644 test/rightWallTest

Decision graph for business people

Did you understand that you have to sent the majority of the team to holiday when you refactor code to avoid destroying their local working copies?

In addition did you understand that you need a directory creation responsible, a directory creation process and a directory creation application form?

	 	 	 	 	 	      
                         	/ (yes): You can use subversion if you have enough time or money
I understood the issue    /
                          \
                            \ (no): You should use something else

I hope you enjoyed reading.

Best Regards / Viele Grüße

Sebastian