At work, we use Subversion for source control. This is quite the popular VCS, but I’ve grown accustomed to (and much prefer) Git. Don’t get me wrong, SVN has its advantages, but since using Git my workflow has changed quite radically, and it’s difficult to revert to the rather inflexible and tedious SVN workflow. Anyway, I’ve been using git-svn for the past month or so, and thought I’d share some of my practices.
In my clone, master is my “local” storage branch. I use it to version things like my .gitignore, and my commit message template. I would also use it for my dcommit/rebase scripts too, but since this is on Windows, Git becomes angry when it attempts to remove scripts that are executing.
Master is then the common root for my topic branches. I’ll do some work and commit, then do more work, as the usual Git workflow goes. The ability to create local branches and commits is great for several reasons:
First, I can commit much more often, without fearing that I will break somebody else’s working copy — I do frequently commit broken code now, because the commits don’t get sent to SVN automatically.
Second, and a side effect of the above, I am much more agile. Sometimes I’ll be working on two projects at once, and keeping separate branches for them means that the broken state of one branch does not affect my ability to build/debug another. This means I can even drop everything (after a commit or stash) and help out with an urgent QA or support issue, without either having lots of uncommitted work interfering or committing broken code to a production repository first.
Third, I can version changes I make to code that might make my life easier, but that would require approval to commit. I have not yet done this, but thanks to my local repository, it’s an option.
Fourth, I can develop against a stable codebase. If I need a specific fix from SVN to work, I can cherry-pick it into my topic branch — fetching commits from SVN does not mandate that my code must be merged with them. Along the same lines, I also have the ability to rebase all my topic branches against a specific SVN commit, which is great when somebody commits broken code that causes build errors or incorrect runtime behavior. If I were using SVN, I would either have to fix it myself, wait for the owner of the code to fix it, or revert to an earlier commit. And since commits are linear, that might mean I would lose some of my code. Git allows me to retain my commits while backing out changes made by others. And if I did already commit some of my changes after the broken code, I can still rebase master and cherry-pick my subsequent commits onto a new branch and continue my work.
And finally, I can review all my commits before pushing them out. I’ve had several occasions to fix up commit messages already.
Of course, I do eventually need to commit to SVN. I have a few scripts to help here. Obviously I don’t want to commit my local-only stuff in master. So I have one script that simply does
git svn fetch && git rebase --onto git-svn master. For the Git-impaired, this fetches any new commits from SVN and adds corresponding Git commits on the git-svn branch. The rebase command then takes all the commits on the current branch since master’s latest commit, and applies them as patches to the tip of the git-svn branch, creating a new commit for each. This effectively removes the local changes I’ve made in master from the commits, as well as merges the changes in SVN into my commits. (If one of my commits fails to apply as a patch, then I have to resolve the conflicts manually, just like an SVN update. But in this case, I still retain all my individual commits.)
After running this script, I then
git svn dcommit, which commits my local commits to SVN, one by one.
At this point, I usually run my post-commit script, which rebases master on to git-svn, then rebases all my other branches on top of master. I might not do this if I don’t want to rebase some of my other topic branches yet.
In closing, I leave you with my trick for changing commit messages. I used to use git filter-branch for this, which is like using a sledgehammer for surgery. (My similes are awesome, I know.) Now I use this process:
git checkout -b temp $COMMIT_TO_CHANGE (Create a new branch called “temp” at the commit I’m changing, and switch to it.)
git commit --amend (Open an editor to amend my commit. I change to message here, then save and quit. The temp branch now has the new commit, whose parent commit is the same as it was before being amended.)
git checkout $ORIGINAL_BRANCH (Switch back to the branch we are amending.)
git rebase temp (Rebase the branch on top of the amended commit. Since the original commit will be applied on top of the amended commit, it is dropped during the rebase. The other commits will apply with no conflicts. The history is now corrected.)
git branch -d temp (Remove our temporary branch.)
And there you have it. In a history with many thousands of commits, this is much faster than git filter-branch, and for those relatively fluent in Git, is also easier to remember how to do.