#!/usr/bin/perl # # VSS-to-Subversion migration script # Original Brett Wooldridge (brettw@riseup.com) # # Contributions: # Daniel Dragnea # Hermod Opstvedt $DEBUG = 1; $SSREPO = ""; $SSPROJ = ""; $SSHOME = ""; $SSCMD = ""; $REPOS = ""; $BATCH = ""; use Cwd; use File::Basename; $PWD = fastgetcwd; $PWD = "$PWD\/work"; if ( $DEBUG == 1 ) { open( STDERR, "> migrate.log" ); } $PHASE = 0; &parse_args(@ARGV); if ($BATCH) { open( CMDFILE, "> work.bat" ); } &setup(); if ($RESTART) { &restart(); } if ( $PHASE < 1 ) { &build_hierarchy(); } if ( $PHASE < 2 ) { &build_filelist(); } if ( $PHASE < 3 ) { &build_histories(); } if ($DUMPUSERS) { &dump_users(); } if ( $PHASE < 4 ) { &create_directories; &import_directories; } if ( $PHASE < 5 ) { &checkout_directories; } # &interleave_histories; &extract_and_import; # cleanup &recursive_delete; if ($DEBUG) { close(DEBUG); } exit; ############################################################## # Parse Command-line arguments # sub parse_args { $argc = @ARGV; if ( $argc < 1 ) { print "migrate: missing command arguments\n"; print "Try 'migrate --help' for more information\n\n"; exit -1; } if ( $ARGV[0] eq '--help' ) { print "Usage: migrate [options] project\n\n"; print "Migrate a Visual SourceSafe project to Subversion.\n\n"; print " --restart\t\trestart the migration from last checkpoint\n"; print " --ssrepo=\trepository path, e.g. \\\\share\\vss\n"; print " --sshome=\tVSS installation directory\n"; print " --repos=\t\tURL for the Subversion repository\n"; print " --force-user=\tforce the files to be checked into Subversion as\n"; print "\t\t\tas user \n"; print " --dumpusers\t\tafter pre-processing the VSS repository, create a\n"; print " --batch\t\tpipe commands to work.bat instead of executing\n"; print "\t\t\tusers.txt file which can be used to create comparable\n"; print "\t\t\taccounts in Subversion. The migration can be restarted\n"; print "\t\t\twithout penalty by using the --restart option\n\n"; exit -1; } for ( $i = 0 ; $i < $argc ; $i++ ) { $arg = $ARGV[$i]; if ( $arg eq '--restart' ) { $RESTART = 1; } elsif ( $arg eq '--dumpusers' ) { $DUMPUSERS = 1; } elsif ( $arg =~ /\-\-ssrepo\=/ ) { $SSREPO = $'; } elsif ( $arg =~ /\-\-sshome\=/ ) { $SSHOME = $'; } elsif ( $arg =~ /\-\-repos\=/ ) { $REPOS = $'; } elsif ( $arg =~ /\-\-force\-user\=/ ) { $FORCEUSER = $'; } elsif ( $arg =~ /\-\-batch\=/ ) { $BATCH = 1; } else { $SSPROJ = $arg; } } if ( $SSPROJ !~ /^\$\/\w+/ ) { print "Error: missing project specification, must be of the form \$/project\n\n"; exit -1; } } ############################################################## # Check environment and setup globals # sub setup { $SSREPO = $ENV{'SSDIR'} unless length($SSREPO) > 0; if ( $SSREPO eq '' || length($SSREPO) == 0 ) { die "Environment variable SSDIR must point to a SourceSafe repository."; } $SSHOME = $ENV{'SS_HOME'} unless length($SSHOME) > 0; if ( $SSHOME eq '' || length($SSHOME) == 0 ) { die "Environment variable SS_HOME must point to where SS.EXE is located."; } $REPOS = $ENV{'SVN_ROOT'} unless length($REPOS) > 0; $ENV{'SSDIR'} = $SSREPO; $SSCMD = "$SSHOME"; if ( $SSCMD !~ /^\".*/ ) { $SSCMD = "\"$SSCMD\""; } $SSCMD =~ s/\"(.*)\"/\"$1\\ss.exe\"/; my $banner = "Visual SourceSafe to Subversion Migration Tool.\n" . "Brett Wooldridge (brettw\@riseup.com)\n\n" . "SourceSafe repository: $SSREPO\n" . "SourceSafe directory : $SSHOME\n" . "SourceSafe project : $SSPROJ\n" . "Subversion repository: $REPOS\n" . "Ekstrakt to directory: $PWD\n\n"; print "$banner"; if ($DEBUG) { print STDERR "$banner"; } } ############################################################## # Build project directory hierarchy # sub build_hierarchy { if ($DEBUG) { print STDERR "\n#############################################################\n"; print STDERR "# Subroutine: build_hierarchy #\n"; print STDERR "#############################################################\n"; } my ( $cmd, $blank, $dir, @lines ); $blank = 1; $dir = ""; print "Building directory hierarchy..."; $* = 1; $/ = ':'; $cmd = $SSCMD . " Dir \"$SSPROJ\" -I- -R -F-"; if ($BATCH) { print CMDFILE "$cmd\n"; } $_ = `$cmd`; if ($DEBUG) { print STDERR "Build Hierarchy: raw data"; print STDERR "$_"; print STDERR "\n#####################################################\n"; } # what this next expression does is to merge wrapped lines like: # $/DeviceAuthority/src/com/eclyptic/networkdevicedomain/deviceinterrogator/excep # tion: # into: # $/DeviceAuthority/src/com/eclyptic/networkdevicedomain/deviceinterrogator/exception: s/\n((\w*\-*\.*\w*\/*)+\:)/$1/g; if ($DEBUG) { print STDERR "Build Hierarchy: post process"; print STDERR "$_"; } $* = 0; $/ = ""; @lines = split('\n'); foreach $line (@lines) { if ( $line =~ /(.*)\:/ ) { chop($line); push( @projects, $line ); } } sort(@projects); open( DIRS, "> directories.txt" ); foreach $line (@projects) { print DIRS "$line\n"; } close(DIRS); my $count = @lines; print "\b\b\b:\tdone ($count dirs)\n"; $PHASE = 1; } ############################################################## # Build a list of files from the list of directories # sub build_filelist { if ($DEBUG) { print STDERR "\n#############################################################\n"; print STDERR "# Subroutine: build_filelist #\n"; print STDERR "#############################################################\n"; } my ( $proj, $cmd, $i, $j, $count ); print "Building file list ( 0%): "; $count = @projects; $i = 0; $j = 0.0; foreach $proj (@projects) { $* = 1; $/ = ':'; $cmd = $SSCMD . " Dir -I- \"$proj\""; $_ = `$cmd`; if($DEBUG) { print CMDFILE "$cmd\n"; } # what this next expression does is to merge wrapped lines like: # $/DeviceAuthority/src/com/eclyptic/networkdevicedomain/deviceinterrogator/excep # tion: # into: # $/DeviceAuthority/src/com/eclyptic/networkdevicedomain/deviceinterrogator/exception: s/\n((\w*\-*\.*\w*\/*)+\:)/$1/g; $* = 0; $/ = ""; @lines = split('\n'); foreach $line (@lines) { if ( $line eq '' || length($line) == 0 ) { break; } elsif ($line !~ /(.*)\:/ && $line !~ /^\$.*/ && $line !~ /^([0-9]+) item.*/ && $line !~ /^No items found.*/ ) { push( @filelist, "$proj/$line" ); printf( "\b\b\b\b\b\b\b\b\b\b\b\b\b(%3d\%): %5d", ( ( $j / $count ) * 100 ), $i ); if ($DEBUG) { print STDERR "$proj/$line\n"; } $i++; } } $j++; } print "\b\b\b\b\b\b\b\b\b\b\b\b\b(100%):\tdone ($i files)\n"; } ############################################################## # Build complete histories for all of the files in the project # sub build_histories { if ($DEBUG) { print STDERR "\n#############################################################\n"; print STDERR "# Subroutine: build_histories #\n"; print STDERR "#############################################################\n"; } my ( $file, $pad, $padding, $oldname, $shortname, $diff, $i, $count ); #my $i, $count; print "Building file histories ( 0%): "; $versioncount = 0; $count = @filelist; $i = 0.0; $diff = 0; $pad = " "; $oldName = ""; $shortname = ""; foreach $file (@filelist) { # display sugar $oldname =~ s/./\b/g; $shortname = substr( $file, rindex( $file, '/' ) + 1 ); $diff = length($oldname) - length($shortname); $padding = ( $diff > 0 ) ? substr( $pad, 0, $diff ) : ""; print "$oldname"; $tmpname = substr( "$shortname$padding", 0, 45 ); printf( "\b\b\b\b\b\b\b\b(%3d\%): %s", ( ( $i / $count ) * 100 ), $tmpname ); $padding =~ s/./\b/g; print "$padding"; $oldname = substr( $shortname, 0, 45 ); # real work $cmd = $SSCMD . " History -I- \"$file\""; $_ = `$cmd`; if($DEBUG) { print CMDFILE "$cmd\n"; } # print STDERR "$_"; &proc_history( $file, $_ ); $i++; } @sortedhist = sort(@histories); open( HIST, ">histories.txt" ); foreach $hist (@sortedhist) { print HIST "$hist\n"; } close(HIST); $oldname =~ s/./\b/g; print "$oldname\b\b\b\b\b\b\b\b(100%):\tdone ($versioncount versions)" . substr( $pad, 0, 20 ) . "\n"; } ############################################################## # Restart from previously generated parsed project data # sub proc_history { my $file = shift(@_); my $hist = shift(@_); $hist =~ s/Checked in\n/Checked in /g; # print "Starting processing of history file\n"; use constant STATE_FILE => 0; use constant STATE_VERSION => 1; use constant STATE_USER => 2; use constant STATE_ACTION => 3; use constant STATE_COMMENT => 5; my $state = STATE_VERSION; local $proj = $SSPROJ; $proj =~ s/(\$)/\\$1/g; $proj =~ s/(\/)/\\$1/g; my ( $version, $junk, $user, $date, $time, $month, $day, $year, $hour, $minute, $path, $action ); #my $hour, $minute, $path, $action; $comment = ""; my @lines = split( '\n', $hist ); my $line_count = @lines; my $i = 0; foreach $line (@lines) { if ( $state == STATE_VERSION && $line =~ /^\*+ Version ([0-9]+)/ ) { $versioncount++; $version = $1; $state = STATE_USER; } elsif ( $state == STATE_USER && $line =~ /^User: / ) { ( $junk, $user, $junk, $date, $junk, $time ) = split( ' ', $line ); ( $month, $day, $year ) = split( '/', $date ); ( $hour, $minute ) = split( ':', $time ); $state = STATE_ACTION; } elsif ( $state == STATE_ACTION ) { if ( $line =~ /^Checked in / ) { if ( $' =~ /^$proj/ ) { $path = $'; $action = 'checkin'; $state = STATE_COMMENT; } else { $proj = $'; $action = 'checkin'; $state = STATE_COMMENT; } } elsif ( $line =~ /^Created/ ) { $action = 'created'; $state = STATE_COMMENT; } elsif ( $line =~ / added/ ) { $path = $`; $action = 'added'; $state = STATE_COMMENT; } elsif ( $line =~ / deleted/ ) { $path = $`; $action = 'deleted'; $state = STATE_COMMENT; } } elsif ( $state == STATE_COMMENT ) { if ( $line =~ /^Comment\:/ ) { $comment = $'; } elsif ( length($comment) > 0 && length($line) > 0 ) { $comment = $comment . ' ' . $line; } elsif ( length($line) == 0 ) { $comment =~ s/^\s+(.*)/$1/g; $comment =~ s/\"/\\\"/g; $state = STATE_FINAL; } } $i++; if ( $state == STATE_FINAL || $i == $line_count ) { $year = ( $year < 80 ) ? 2000 + $year : 1900 + $year; $hist = join( ',', $file, sprintf( "%04d", $version ), "$year/" . sprintf( "%02d/", $month ) . sprintf( "%02d ", $day ) . sprintf( "%02d:", $hour ) . $minute, $user, $action, "\"$comment\"" ); $comment = ""; if ($DEBUG) { print STDERR "$hist\n"; } push( @histories, $hist ); $state = STATE_VERSION; } } } ############################################################## # Dump the users from the repository into users.txt and exit # sub dump_users { for $hist (@histories) { local ( $file, $version, $datetime, $user, $action, $comment ) = split( ',', $hist ); $USERHASH{$user} = 1; } open( USERS, "> users.txt" ); foreach $user ( keys %USERHASH ) { print USERS "$user\n"; } close(USERS); print "\nUsers.txt file has been created. Use the list of users in this\n"; print "file to create matching user accounts in Subversion. Ensure that these\n"; print "accounts initially have NO AUTHENTICATION, otherwise the migration will\n"; print "likely fail. Alternatively, you can use the --force-user option to\n"; print "create all files with the same username. Either way, you can restart\n"; print "this migration, picking up from this point, by using the --restart\n"; print "option on the command line.\n\n"; exit 0; } ############################################################## # Restart from previously generated parsed project data # sub restart { local ($i) = 0; if ( -f "directories.txt" ) { print "Loading directories: "; open( DIRS, "< directories.txt" ); while () { $line = $_; chop($line); push( @projects, $line ); $i++; printf( "\b\b\b\b\b%5d", $i ); } close(DIRS); print "\b\b\b\b\b\t\tdone ($i dirs)\n"; $PHASE = 1; } if ( -f "histories.txt" ) { print "Loading file histories: "; $i = 0; open( HIST, "< histories.txt" ); while () { $line = $_; chop($line); push( @sortedhist, $line ); $i++; printf( "\b\b\b\b\b%5d", $i ); } close(HIST); print "\b\b\b\b\b\tdone ($i versions)\n"; $PHASE = 3; } if ( -f "extract_progress.txt" ) { local ( $file, $version ); print "Calculating extract progress:"; open( EXTRACT, "< extract_progress.txt" ); while () { $RESTARTFILE = $_; chop($RESTARTFILE); ( $file, $version ) = split( ',', $RESTARTFILE ); $created{$file} = "true"; } close(EXTRACT); $RESTARTFILE =~ s/(\$)/\\$1/g; $RESTARTFILE =~ s/(\/)/\\$1/g; if ($DEBUG) { print STDERR "Restart from: $RESTARTFILE\n"; } $file =~ s/^$proj(.*)/$1/g; $file = substr( $file, rindex( $file, '/' ) + 1 ); print "\trestart from $file (v.$version)\n"; $PHASE = 4; } } ############################################################## # Create the directory hierarchy in the local filesystem # sub create_directories { if ($DEBUG) { print STDERR "\n#############################################################\n"; print STDERR "# Subroutine: create_directories #\n"; print STDERR "#############################################################\n"; } my $proj = $SSPROJ; $proj =~ s/(\$)/\\$1/g; $proj =~ s/(\/)/\\$1/g; my ($basedir) = $SSPROJ; $basedir =~ s/^\$\///g; print "Creating local directories: $basedir"; &recursive_delete('./work'); if ($BATCH) { print CMDFILE "mkdir \"./work\"\n"; } else { mkdir('./work'); } my @dircomponents = split( '/', $basedir . '/' ); my $buildupdir = './work'; foreach $dir (@dircomponents) { $buildupdir = $buildupdir . '/' . $dir; if ($BATCH) { $buildupdir =~ s/\//\\/g; print CMDFILE "mkdir \"$buildupdir\"\n"; } else { mkdir($buildupdir); if ($DEBUG) { print STDERR "Creating base dir '$buildupdir'\n"; } } } #mkdir("./$basedir/$basedir"); #if ($DEBUG) #{ # print STDERR "Create top project dir './$basedir/$basedir'\n"; #} foreach $dir (@projects) { if ( $dir =~ /^$proj\// ) { my $rawdir = "./work/$basedir/$'"; if ($BATCH) { $rawdir =~ s/\//\\/g; print CMDFILE "mkdir \"$rawdir\"\n"; } else { mkdir($rawdir); if ($DEBUG) { print STDERR "Creating project dir '$rawdir'\n"; } } } } print "\tdone\n"; } ############################################################## # Delete a directory tree and all of its files recursively # sub recursive_delete { my ($parent) = @_; my ( @dirs, $dir ); opendir( DIR, $parent ); @dirs = readdir(DIR); closedir(DIR); foreach $dir (@dirs) { if ( $dir ne '.' && $dir ne '..' ) { recursive_delete("$parent/$dir"); } } if ( -d $parent ) { if ($BATCH) { print CMDFILE "rmdir \"$parent\"\n"; } else { rmdir($parent); } } elsif ( -f $parent ) { unlink($parent); } } ############################################################## # Import a directory hierarchy into Subversion # sub import_directories { if ($DEBUG) { print STDERR "\n#############################################################\n"; print STDERR "# Subroutine: import_directories #\n"; print STDERR "#############################################################\n"; } print "Importing directories: "; my ($basedir) = $SSPROJ; $basedir =~ s/^\$\///g; my $cmd = "svn --message \"initial import\" import . \"$REPOS\""; if ($DEBUG) { print STDERR "$cmd\n"; } if ($BATCH) { print CMDFILE "cd \"./work\"\n"; print CMDFILE "$cmd\n"; print CMDFILE "cd ..\n"; } else { chdir('./work'); `$cmd`; chdir('..'); } print "\t\tdone\n"; } ############################################################## # Checkout a copy of the directory hierarchy so that we have # a Subversion local working copy # sub checkout_directories { if ($DEBUG) { print STDERR "\n#############################################################\n"; print STDERR "# Subroutine: checkout_directories #\n"; print STDERR "#############################################################\n"; } print "Checking out directories: "; my ($basedir) = $SSPROJ; $basedir =~ s/^\$\///g; my $cmd = "svn --non-interactive checkout \"$REPOS$basedir\" \"./work/$basedir\""; #my $cmd = "svn --non-interactive checkout \"$REPOS\" \"./work/$basedir\""; if ($DEBUG) { print STDERR "$cmd\n"; } &recursive_delete('./work'); if ($BATCH) { print CMDFILE "md \"./work\"\n"; print CMDFILE "$cmd\n"; } else { mkdir('./work'); `$cmd`; } print "\tdone\n"; } ############################################################## # This is the meat. Extract each version of each file in the # project from VSS and check it into Subversion # sub extract_and_import { if ($DEBUG) { print STDERR "\n#############################################################\n"; print STDERR "# Subroutine: extract_and_import #\n"; print STDERR "#############################################################\n"; } my ( $file, $padding, $count, $cmd ); print "Extracting and creating ( 0\%): "; open( EXTRACT, "> extract_progress.txt" ); if ( defined($RESTARTFILE) ) { while ( $#sortedhist > 0 ) { $hist = shift(@sortedhist); last if ( $hist =~ /^$RESTARTFILE(.*)/ ); if ($DEBUG) { print STDERR "$hist\n"; } } } my $proj = $SSPROJ; $proj =~ s/(\$)/\\$1/g; #$proj =~ s/(\/)/\\$1/g; my ($basedir) = $SSPROJ; $basedir =~ s/\$//g; $count = @sortedhist; my $i = 0.0; my $diff = 0; my $pad = " "; my $oldName = ""; my $shortname = ""; my $oldname = ""; my $tempvalue = ""; my $tempproj = ""; my $tempfile = ""; if ($BATCH) { print CMDFILE "cd \"$PWD\"\n"; } else { chdir($PWD); } foreach $hist (@sortedhist) { my ( $file, $version, $datetime, $user, $action, $comment ) = split( ',', $hist ); $oldname =~ s/./\b/g; $shortname = substr( $file, &min( rindex( $file, '/' ) + 1, 40 ) ) . ' (v.' . int($version) . ')'; $diff = length($oldname) - length($shortname); $padding = ( $diff > 0 ) ? substr( $pad, 0, $diff ) : ""; print "$oldname"; $tmpname = substr( "$shortname$padding", 0, 46 ); printf( "\b\b\b\b\b\b\b\b(%3d\%): %s", ( ( $i / $count ) * 100 ), $tmpname ); $padding =~ s/./\b/g; print "$padding"; $oldname = substr( $shortname, 0, 46 ); # chdir to the proper directory $path = substr( $file, 0, rindex( $file, '/' ) ); $path =~ s/^$proj(.*)/$1/g; #$path =~ s/\//\\/g; $path = "$basedir$path"; my $hdir = "$PWD$path"; $ver = int($version); #Get the file from VSS $cmd = $SSCMD . " get -GF -W -I- \"-GL$hdir\" -V" . int($ver) . " \"$file\""; if ($BATCH) { print CMDFILE "$cmd\n"; } else { $out = `$cmd`; if ($DEBUG) { print STDERR "$cmd\n"; print STDERR "$out"; } } if ( defined($FORCEUSER) ) { $user = $FORCEUSER; } if ($BATCH) { print CMDFILE "cd \"$hdir\"\n"; } else { chdir("$hdir"); } if ( $created{$file} ) { if ( $file =~ /^$proj\// ) { $tempfile = basename($file); if ($tempfile) { $cmd2 = "svn commit --non-interactive --non-recursive --username $user --message $comment \"$tempfile\""; if ($BATCH) { print CMDFILE "$cmd2\n"; } else { $out = `$cmd2`; if ($DEBUG) { print STDERR "$cmd2\n"; print STDERR "$out"; } } print EXTRACT "$file,$version\n"; } } } else { if ( $file =~ /^$proj\// ) { $tempfile = basename($file); if ($tempfile) { $cmd3 = "svn add \"$tempfile\""; if ($BATCH) { print CMDFILE "$cmd3\n"; } else { $out = `$cmd3`; if ($DEBUG) { print STDERR "$cmd3\n"; print STDERR "$out"; } } $cmd3 = "svn commit --non-interactive --non-recursive --username $user --message $comment \"$tempfile\""; if ($BATCH) { print CMDFILE "$cmd3\n"; } else { $out = `$cmd3`; } $created{$file} = "true"; print EXTRACT "$file,$version\n"; } } } $i++; } close(CMDFILE); close(EXTRACT); $oldname =~ s/./\b/g; print "$oldname\b\b\b\b\b\b\b\b(100%):\t" . substr( "done$pad", 46 ) . "\n"; } ############################################################## # Find the minimum value between two integers # sub min { local $one = shift(@_); local $two = shift(@_); return ( $one < $two ? $one : $two ); }