#!/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 );
}