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
- you want to keep your branch alive,
- you want to merge changes from one branch to another (sample: a fix in branch integration to branch pre-production, a hot fix from production to integration) (it is named cherry picking)
then you should
- not expect to be able to track this,
- expect to encounter merging orgies because Subversion is not aware that changes has already been applied,
- expect to waste a lot of time.
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.
- Trunk
- middle (blue)
- Team a branch
- middle (blue)
- left (red)
- Team b branch
- middle (blue)
- right (green)
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.
- merge
- commit
- switch to merged branch
- merge with record only
- 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.
- merge
- solve conflict
- commit
- switch to merged branch
- merge with record only
- commit
- merge again
- solve conflict
- commit
Other SCM need only 6 steps and one conflict solving.
- git merge team-b
- openWithEditor middle
- git add middle
- git commit -m ‘merged middle’
- git co team-b
- 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
- 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’ - [team-a 3e4c5ce] added a test for the left wall
- 1 files changed, 1 insertions(+), 0 deletions(-)
- create mode 100644 test/leftWallTest
git co team-b - 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’ - [team-b 83d1b05] added a test for the right wall
- 1 files changed, 1 insertions(+), 0 deletions(-)
- 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