Thursday, March 24, 2011

SVN checkout without branches and tags

Subversion (SVN) is a decent step up from CVS, and Maven (MVN) likewise from Ant. However one thing continues to bug me about how the two work together. Since MVN requires a fixed directory layout of /trunk, /tags and /branches, it becomes more and more painful to checkout or update a complete corporate source tree from one of the lower roots.

In the perfect world, every project may be checked in at the root of the tree, but we all know the world ain't perfect and it's not unusual to require checkout and build of up to a dozen SNAPSHOT dependencies spread around deeper in the hierarchy. There is no (to my knowledge) easy way around this, either you have to live with insanely long checkout times and the associated waste of disk space, or meticulously checkout each every project trunk individually.

As unimpressed I am by the Java language itself, the hat must come off to the plethora of readily available libraries on this platform. One of these libraries is SVNKit, a complete client API for accessing SVN servers. So I wondered just how hard it would be to put a client together, doing full tree checkouts, *without* /tags and /branches. Turns out it only took a few hours and under 100 lines of code. :)



/* Copyright (C) 2011 Casper Bang (casper.bang@gmail.com) */

package com.blogspot.coffeecokeandcode;

import java.io.File;
import java.util.Collection;
import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.io.*;
import org.tmatesoft.svn.core.wc.*;
import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;

public class svntrunks
{
public static void main(String... args) throws SVNException
{
String checkoutPath = null;
String username = null;
String password = null;
String checkoutRootPath = new File("").getAbsolutePath();

if(args.length > 2){
checkoutPath = args[0].trim();
username = args[1].trim();
password = args[2].trim();

if(args.length == 4)
checkoutRootPath = args[3].trim();
}
else
{
System.out.println("Usage: java -jar svntrunks.jar URL USERNAME PASSWORD [PATH]\n");
System.out.println("URL: The mandatory path to the SVN reposatory");
System.out.println("USERNAME: The username to log in as");
System.out.println("PASSWORD: Password for the username");
System.out.println("[PATH] Optional destination folder (defaults to current)\n\n");
System.out.println("Example: java -jar svntrunks.jar https://svn.java.net/svn/jersey~svn david secret jersey-checkout");
System.exit(1);
}

DAVRepositoryFactory.setup();

final SVNRepository repository = SVNRepositoryFactory.create( SVNURL.parseURIDecoded(checkoutPath) );
repository.setAuthenticationManager( SVNWCUtil.createDefaultAuthenticationManager( username , password ) );

final SVNClientManager clientManager = SVNClientManager.newInstance(null, repository.getAuthenticationManager());
final SVNUpdateClient updateClient = clientManager.getUpdateClient();
updateClient.setIgnoreExternals(false);

final SVNNodeKind nodeKind = repository.checkPath( "" , -1 );

if ( nodeKind == SVNNodeKind.NONE ) {
System.err.println( "There is no entry at '" + checkoutPath + "'." );
System.exit( 1 );
} else if ( nodeKind == SVNNodeKind.FILE ) {
System.err.println( "The entry at '" + checkoutPath + "' is a file while a directory was expected." );
System.exit( 1 );
}

System.out.println("Checkout source: "+ checkoutPath );
System.out.println("Checkout destination: "+ checkoutRootPath );
traverse(updateClient, repository , checkoutPath, checkoutRootPath, "");
System.out.println( "Repository latest revision: " + repository.getLatestRevision( ) );
}

public static void traverse(SVNUpdateClient updateClient, SVNRepository repository, String checkoutRootPath, String destRootPath, String repoPath ) throws SVNException {

System.out.println(repoPath);

updateClient.doCheckout(
SVNURL.parseURIDecoded(checkoutRootPath + "/" + repoPath),
new File(destRootPath + (!repoPath.isEmpty() ? "/":"") + repoPath),
SVNRevision.UNDEFINED,
SVNRevision.HEAD,
SVNDepth.FILES, false);

final Collection<SVNDirEntry> entries = repository.getDir( repoPath, -1 , null , (Collection) null );
for(SVNDirEntry entry : entries){
if(!entry.getName().equalsIgnoreCase("branches") && !entry.getName().equalsIgnoreCase("tags")){
if ( entry.getKind() == SVNNodeKind.DIR ) {
traverse(
updateClient,
repository,
checkoutRootPath,
destRootPath,
( repoPath.equals( "" ) ) ? entry.getName( ) : repoPath + "/" + entry.getName( ) );
}
}
}
}
}




Note that I do not subscribe to the legacy policy of 80 columns per line! This is 2011, I have more monitors than I have shoes and none are below 22".

You may download the complete source, or just the compiled binary I have build. The source code is licenced under GPL.



casper@workstation:~/Development/Code/Java/$ java -jar svntrunks.jar
Usage: java -jar svntrunks.jar URL USERNAME PASSWORD [PATH]

URL: The mandatory path to the SVN reposatory
USERNAME: The username to log in as
PASSWORD: Password for the username
[PATH] Optional destination folder (defaults to current)


Example: java -jar svntrunks.jar https://svn.java.net/svn/jersey~svn david secret jersey-checkout
casper@workstation:~/Development/Code/Java/$




Now it's considerably easier to do a new clean checkout of trees, in fact the tool cut down checkout time from the corporate tree I work in, with a factor of about 10! Your mileage may of course vary, depending on the amount of snapshots and branches in the tree.

There are some obvious improvements I'd like to add, i.e. I never understood why the vanilla SVN client doesn't include commands to detach previously checked out source from its repository. Of course, the true solution to the above problems may simply be to switch to a distributed SCM/VCS like Mercurial or Git. :)