The troublesome Git-Svn Marriage

If you are like me and you are asked to implement a workflow that interfaces with the developers using Git and allows changes to flow through to the SVN repo seamlessly, then this post might be helpful.

The Idea

the-idea

The Setup

Create the following folder structure on the disk locally.

the-setup

Create readme.text inside 1-Svn-repo and check it to svn server. This is how it looks now:

the-setup

Now clone this into 2-Git-Svn-Repo using the git svn clone command:

git svn clone -r2:HEAD -s <<Path to svn repo>> 2-Git-Svn-Repo

Now create a bare repo in 3-Git-Bare-Repo that all developers will sync with. This bare repo will have hooks defined on it which will trigger the 2-Git-Svn-Repo to pull changes from this repo when a developer pushes changes to the bare repo and also these changes will be pushed out to svn using the git svn dcommit command later on.

Now lets create a sub directory under 3-Git-Bare-Repo called MyGitsvnWorkflow.git and initialize it to be a bare repo by issuing the following command:

the-setup

Now lets push changes from the Git svn repo to this newly create bare repo, so that the devs can start checking out files.

This can be achieved by adding a remote to the git svn repo that points to the bare repo.

the-setup

Now lets push the master branch from Git svn to the bare repo by issuing the following command:

git push –u git-bare-repo master

Now at this point we should have 2 remote branches on our Git svn repo:

git-bare-repo/master = This branch will be the holding branch when we fetch changes from 3-Git-Bare-Repo trunk = This will be the holding branch when we fetch changes from svn

Now do a git log on 3-Git-Bare-Repo to make sure we have the changes from the svn branch

the-setup

By the way, lga is a git alias which does git log -10 –online –graph –decorate –all

Now finally as a developer, we clone the 3-Git-Bare-Repo into 4-Git-Dev-Repo by using visual tools like TortoiseGit.

the-setup

This concludes the setup part and now to make this process automated, so that when devs check into the bare repo and those changes propagate all the way back to the actual SVN server.

Syncing repos with Hooks

This can be done by using the post-receive hook on the bare repo. All the hooks live under the hooks folder of the bare repo, so we switch to that directory and edit the post-receive hook file. By default all hook files end with .sample suffix, if you get rid of this suffix, git will start calling these hook files when actions occur on the repo.

the-sync

Now lets edit post-receive.sample and save it as post-receive

#!/bin/sh
 
# An example hook script for the "post-receive" event.
#
# The "post-receive" script is run after receive-pack has accepted a pack
# and the repository has been updated.  It is passed arguments in through
# stdin in the form
#  <oldrev> <newrev> <refname>
# For example:
#  aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master
#
# see contrib/hooks/ for a sample, or uncomment the next line and
# rename the file to "post-receive".
 
echo " ----------------------------- "
echo "| Inside post-receive hook file |"
echo " ----------------------------- "
 
read oldRev newRev refName
echo " Printing arguments passed to this file:"
echo "  Old Revision   = $oldRev"
echo "  New Revision   = $newRev"
echo "  Reference Name = $refName"
 
newCommitMsg=$(git log -n 1 --pretty=format:%s $newRev)
echo "  New Commit Message = " $newCommitMsg
 
cd D:/Code/Learning/GitSvnWorkflow/2-Git-Svn-Repo
echo "Directory changed to 2-Git-Svn-Repo"
 
unset GIT_DIR
 
case "$refName" in
 
        refs/heads/*) # This is a head push
                echo "Head Push"
 
                echo "  Fetching from git-bare-repo..."
                git fetch git-bare-repo                
                echo "  Changes fetched and are placed inside remote branch named git-bare-repo/master"
 
                echo "  Merging master with remote branch named git-bare-repo/master"
                git merge git-bare-repo/master -Xtheirs -m "$newRev: $newCommitMsg"
                echo "  Merge complete."
                 
                echo "  Starting svn dcommit ..."
                git svn dcommit --add-author-from                
                echo "Svn dcommit was successful."
        ;;
         
esac
 
echo " ------------------------------------------ "
echo "| post-receive hook file execution complete |"
echo " ------------------------------------------ "

Now, if we did everything correctly, changes we push from our dev repo should end up in SVN. So lets make a change and push into out to our bare repo and see the hook file getting executed.

the-sync

and SVN looks like:

the-sync

Notice, the SVN commit message contains the SHA1 hash of the commit that originated from the dev box, which is evident as follows:

the-sync

This provides us end-to-end traceability for the commits.

Making things robust

Now lets make the process a bit more robust and stop the developers from checking in code to the bare repo, if the SHA1 of the HEAD pointer in the bare repo does not match with the SHA1 hash embedded in the commit message of the git svn repo.

For that we need to modify the pre-receive hook.

#!/bin/sh
 
echo " ----------------------------- "
echo "| Inside pre-receive hook file |"
echo " ----------------------------- "
 
read oldRev newRev refName
echo " Printing arguments passed to this file:"
echo "  Old Revision   = $oldRev"
echo "  New Revision   = $newRev"
echo "  Reference Name = $refName"
 
cd D:/Code/Learning/GitSvnWorkflow/2-Git-Svn-Repo
unset GIT_DIR
 
lastCommitSHA=$(git log -1 --pretty=format:%s | cut -d \: -f 1)
echo "  Last Commit SHA = $lastCommitSHA"
 
exitCode=1
if [ "$oldRev" == "$lastCommitSHA" ]; then
    echo "Old Rev and Last Commit SHA1 hash match. Exiting with code 0"
    exitCode=0
else
    echo "Old Rev and Last Commit SHA1 hash DO NOT match. Exiting with code 1"   
fi
 
echo " ------------------------------------------ "
echo "| pre-receive hook file execution complete |"
echo " ------------------------------------------ "
 
exit $exitCode

Now lets test to see if the pre-receive hook output shows when a push a new change:

the-sync

That worked.

Next lets deliberately mess up things and add some random character to the oldRev varaible and have it fail.

Adding the following line to out pre-receive file

oldRev=$oldRev”XXXXXXXXXXXXXXXXXXXX”

the-sync

That failed, great! This will deter things getting from bad to worse as it’ll stop people from checking in when the bare repo and the git svn repo are out of sync.

Recovery

Next, lets look at a scenario when someone accidently/deliberately makes changes to the 2-Git-Svn-Repo directly thereby short-circuiting the whole bare repo integration.

Developer A decides to make changes to the readme file in 2-Git-Svn-Repo and dcommit those changes to svn directly, leaving 3-Git-Bare-Repo and 4-Git-dev-Repo out of sync.

the-sync

Now Developer B makes an honest change to their 4-Git-Dev-Repo and tries to commit.

the-sync

As expected the commit SHA1 hashes won’t match and no one will be able to check-in code anymore. So to workaround this issue we can do the following:

git rev-parse HEAD on the bare repo to grab the head SHA of the bare repo.

Now from the 2-Git-Svn-Repo make a small change to any file (say add a whitespace). Then commit this change

git commit -am "<<Append the head SHA>>: Dev A being Sneaky"

and then finally do a dcommit. This will allow devs to commit code as usual again as next time the -Xtheirs flag in the merge command of the post-receive hook will overwrite changes from the Dev’s machine and will not result in a merge conflict.

the-sync

SVN tagging (Optional)

Also, if you decide that you want to create svn tags from Git then this can be achieved by modifying the post-receive file as follows:

case "$refName" in
 
        refs/tags/*) # This is a tag push
                tagName=$(echo $refName | cut -d \/ -f 3)
                echo "Tag Push. Creating svn tag with name = $tagName"               
                git svn tag $tagName                
                echo "Svn Tag created successfully."
        ;;

and now from the dev box create a git tag by issuing the following command:

git tag -a v0.1 -m "Creating tag 0.1"

verify the tag by issuing command

git tag

finally, push the tag to bare repo and a tag should be created in SVN with the same name.

git push origin v0.1       

the-sync

and SVN looks like:

the-sync

Downloads:

March 2, 2015 · SourceControl · GIT · SVN


Previous:GTD (Getting things done) — A simplified view
Next:WCF service NETBIOS name resolution woes