Wednesday, January 19, 2011

Subversion and feature branches

I've used subversion for a long time (probably around five or six years), but it's only recently I've started to use feature branches actively.

Mostly it was because it was a huge pain to do. You used to have to manually track revisions for all merges for it to work at all. Fortunately that was greatly improved with subversion 1.6, but it was still quite possible to mess it up.

Usual symptoms were crazy conflicts when merging from files not even touched in the branch.

These days, I happily use feature branches, because I finally figured out how to manage them properly. I can't remember exactly who I got this from, it was quite possibly several different people independently from each other.

Basic rules to avoid pain:
  • Always stand in the project root (/trunk/ or /branches/branch/) when doing subversion operations.
  • Update often.
  • Commit often.
  • Sync trunk -> branch often.
  • Merge branch -> trunk only once, and then delete it.
Here are the general operations I use nowadays.

To create a branch:

Start using the branch:
svn switch BRANCH_URL

List existing branches:
svn list BASE_URL/branches

Sync trunk to branch:
svn up
svn merge TRUNK_URL .
svn commit -m "Merged trunk to branch BRANCH"

Reintegrate branch (make sure it's commited and synced recently):
svn switch TRUNK_URL
svn merge --reintegrate BRANCH_URL .
svn commit -m "Reintegrated BRANCH"
svn delete -m "Deleting reintegrated BRANCH" BRANCH_URL

It doesn't look so hard, right?
The trick is that if you do something else at some point, like cherry picking revisions into the branch, subversion tends to get confused. I am not sure if it's subversions fault or the users fault, but in either case, cleaning it up usually gets messy.

An annoying thing is all those URL:s you need to enter.
To simplify things for my most common usage, I wrote a simple bash script:

It mostly just ensures that I don't forget any steps, does some basic error checking, and most importantly, it lets me skip writing the full URL all the time.

Here's what it looks like:

And here is the actual script if anyone is interested.


FULLURL=`svn info | grep -E "^URL: "| cut -d " " -f 2`
if [ "$FULLURL" == "" ]; then exit 1; fi
TRUNKURL=$(echo $FULLURL | grep -E -o "^.*/trunk")
BRANCHURL=$(echo $FULLURL | grep -E -o "^.*/branches/[^/]+")

set -e

function error() {
echo -e "${ERROR}$1${ENDCOL}"
exit 1

function execute() {
echo -e "> ${EXECUTE}$@${ENDCOL}"

if [ "$TRUNKURL" != "" ]; then
if [ "$FULLURL" != "$TRUNKURL" ]; then
error "Please stand in the project root.\n Expected $TRUNKURL\n but was $FULLURL"
BASEURL=$(echo $TRUNKURL | sed "s/\\/trunk//")
elif [ "$BRANCHURL" != "" ]; then
if [ "$FULLURL" != "$BRANCHURL" ]; then
error "Please stand in the project root.\n Expected $BRANCHURL\n but was $FULLURL"
BRANCH=$(echo $FULLURL | grep -E -o "[^/]+$")
BASEURL=$(echo $BRANCHURL | sed "s/\\/branches\\/$BRANCH//")
error "Could not determine trunk or branch from $FULLURL"

function sync() {
if [ "$BRANCH" == "trunk" ]; then error "Must be on a branch to sync or reintegrate!"; fi
CLEAN=`svn status|grep -v "^\\?"`
if [ "$CLEAN" != "" ]; then error "Working copy is not clean. Revert or commit first!"; fi

execute svn up
execute svn merge $BASEURL/trunk .

UPDATED_FILES=`svn status|grep -E -v "^\\?"|cut -b 9-`
if [ "$UPDATED_FILES" == "" ]; then
echo "Nothing changed, skipping sync."
elif [ "$UPDATED_FILES" == "." ]; then
echo "Only trivial changes merge - reverting merge"
execute svn revert -R .
execute svn commit -m "Merged trunk to branch $BRANCH"


case $1 in
if [ "$2" == "" ]; then error "Missing parameter to switch: branch"; fi
execute svn switch $BASEURL/branches/$2
execute svn switch $BASEURL/trunk/
echo -e "> ${EXECUTE}svn list $BASEURL/branches${ENDCOL}"
echo "Available branches:"
BRANCHES=`svn list $BASEURL/branches | grep -E -o "^[a-zA-Z]*"`
for b in $BRANCHES; do
echo -e " ${CBRANCH}$b${ENDCOL}"
execute svn switch $BASEURL/trunk
execute svn merge --reintegrate $BASEURL/branches/$BRANCH .
execute svn commit -m "Reintegrated $BRANCH"
execute svn delete -m "Deleting reintegrated $BRANCH" $BASEURL/branches/$BRANCH
if [ "$2" == "" ]; then error "Missing parameter to create: branch"; fi
execute svn copy $BASEURL/trunk $BASEURL/branches/$2 -m "Creating branch $2 from trunk"
execute svn switch $BASEURL/branches/$2
if [ "$2" == "" ]; then error "Missing argument to delete: branch"; fi
if [ "$BRANCH" == "$2" ]; then error "Can not delete active branch. Switch first!"; fi
execute svn delete -m "Deleting branch $2" $BASEURL/branches/$2
echo -e "Repository: ${REPO}$BASEURL${ENDCOL}"
echo -e "Active branch: ${CBRANCH}$BRANCH${ENDCOL}"
echo "Usage: ./branch <command> [option]"
echo "Commands:"
echo -e " ${COMMAND}switch${ENDCOL} ${CBRANCH}<branch>${ENDCOL} -- switches to the branch"
echo -e " ${COMMAND}trunk${ENDCOL} -- switches to trunk"
echo -e " ${COMMAND}list${ENDCOL} -- list available branches"
echo -e " ${COMMAND}reintegrate${ENDCOL} or ${COMMAND}reint${ENDCOL} -- reintegrate active branch to trunk"
echo -e " ${COMMAND}sync${ENDCOL} -- syncs current branch with trunk"
echo -e " ${COMMAND}create${ENDCOL} ${CBRANCH}<branch>${ENDCOL} -- creates a new branch from trunk and switches to it"
echo -e " ${COMMAND}delete${ENDCOL} ${CBRANCH}<branch>${ENDCOL} -- deletes a branch without reintegrating it"


fafkulec said...

Have you tried any DVCS like git, mercurial, bazaar?

Kristofer said...

Sure, i've used git (and very little mercurial), and I like that approach. In fact, I've tried to model the script somewhat after git. (Listing of branches / switching)

However, you can not always choose the source control tool yourself.

This is just my attempt at making life with svn a bit easier.

fafkulec said...

Wouldn't git-svn solve your problems?

I haven't used it myself but seems that merging with git-svn should be as easy as merging with git itself.

Kristofer said...

I would consider that if I knew the best mappings in git-svn for the functions I described.

Post a Comment