#!/usr/bin/perl # # VSS-to-Subversion migration script # Original Brett Wooldridge (brettw@riseup.com) # # Contributions: # Daniel Dragnea # Magnus Hyllander 2006-12-05 (http://www.hyllander.org/node/23) # Neil Sleightholm v3.xx (http://neilsleightholm.blogspot.com/) # # Version History # 1.xx - Original Brett Wooldridge # 2.xx - Magnus Hyllander # 3.00 - Changes by Neil Sleightholm: # - Improved generation of atoms to make resume more reliable. # - Handle pinned files. # - Print migration start/end date and time. # - Improve status ('sugar') feedback. # - Simplify extract status code. # - Supply username and password for admin functions. # - Code reformatting. # - Added support for cutoff date to allow only newer files to be migrated. # - Always dump user names during migration. # - Add support for usernames containing spaces. # - Exclude additional vss warning message. # - Change to UK date format. Add commented out code to allow for european # and US date formats. TODO: Make this a command line option. # - Add version number. # 3.01 - Don't import vss files e.g. files ending .vssscc, .vspscc etc. # 3.02 - Allow vss project $/ e.g. all of vss. # 3.03 - Don't import vss files - .vsscc # 3.04 - Improve handling of cuttoff date if it is not set. # 3.05 - Changed so that svn url is the root and the vss path is append to it # e.g. $/MyPath is imported in to url svn://Import as svn://Import/MyPath # - Remove redundant commented out code. # - Add support for multiple vss projects on the command line. # # Furture enhancements: # - Make date format an option or auto-detect. # - Cleanup resume files if full migration selected. # - Make working username and password parameters. # - Optionally read vss projects from a file. # my $VERSION = "3.05"; use strict; use POSIX; my $DEBUG = 1; my $RESUME = 0; my $RESUMEAFTERATOM = ''; my $MIGRATELATEST = 0; my $DUMPUSERS = 0; my $FORCEUSER = ''; my $SSREPO = ''; #my $SSPROJ = ''; my $SSHOME = ''; my $SSCMD = ''; my $REPOS = ''; my $CUTOFFDATE = 0; # This is the username and password used for migration operations my $USERNAME = 'admin'; my $PASSWORD = ''; my $PHASE = 0; my @ssprojectlist = (); my @directorylist = (); my @filelist = (); my @histories = (); my %atomlist; my @atoms; my $datestring; if ($DEBUG == 1) { open(STDERR, "> migrate.log"); } &parse_args(@ARGV); &setup(); $datestring = prettydate(); print "Migration started: $datestring\n"; print STDERR "Migration started: $datestring\n"; if ($MIGRATELATEST) { &get_latest_checkpoint(); } elsif ($RESUME) { &resume(); } if ($PHASE < 1) { # Repeat for each vss project foreach my $proj (@ssprojectlist) { print "Project: $proj\n"; &build_directorylist($proj); } } if ($PHASE < 2) { &build_filelist(); } if ($PHASE < 3) { &build_histories(); &dump_users(); } if ($DUMPUSERS) { &dump_users_and_exit(); } &build_atoms; if ($MIGRATELATEST) { print "\nHistory has now been refreshed. You can compare atoms.txt.1 with atoms.txt to\n"; print "see if new data to be migrated has been checked in to the VSS repository after\n"; print "the previous run. Also verify that the last line of extract-progress.txt lists\n"; print "the last atom that was processed. When satisfied, you can process new atoms\n"; print "with the --resume option.\n\n"; exit; } if ($PHASE < 5) { &create_directories; &import_directories; } if ($PHASE < 6) { &checkout_directories; } &extract_and_import; if ($DEBUG) { close(DEBUG); } $datestring = prettydate(); print "\nMigration complete: $datestring\n\n"; print STDERR "\nMigration complete: $datestring\n\n"; exit; ############################################################## # Parse Command-line arguments # sub parse_args { my $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 "Visual SourceSafe to Subversion Migration Tool - v$VERSION\n\n"; print "Usage: migrate [options] project [project [project]]\n\n"; print "Migrate a Visual SourceSafe project to Subversion.\n\n"; print " --resume\t\tresume the migration from last checkpoint\n"; print " --migrate-latest\trefresh history and atoms, and resume after\n"; print "\t\t\tlast checkpoint\n"; print " --ssrepo=\trepository path, e.g. \\\\share\\vss\n"; print " --sshome=\tVSS installation directory\n"; print " --repos=\t\tbase URL for the Subversion repository\n"; print "\t\t\tNote: VSS project will be appended to this\n"; print " --force-user=\tforce the files to be checked into Subversion as\n"; print "\t\t\tas user \n"; print " --cutoff-date=\tminimum date to import\n"; print " --dumpusers\t\tafter pre-processing the VSS repository, create a\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 resumed\n"; print "\t\t\twithout penalty by using the --resume option\n\n"; exit -1; } for (my $i = 0; $i < $argc; $i++) { my $arg = $ARGV[$i]; if ($arg eq '--resume') { $RESUME = 1; } elsif ($arg =~ /--migrate-latest/) { $MIGRATELATEST = 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 =~ /--cutoff-date=/) { # Convert parameter from yyyymmdd to a datetime my $dateparam = $'; $CUTOFFDATE = POSIX::mktime(0, 0, 0, substr($dateparam, 6, 2), substr($dateparam, 4, 2) - 1, substr($dateparam, 0, 4) - 1900, -1, -1, -1); } else { push @ssprojectlist, $arg; } } foreach my $proj (@ssprojectlist) { if ($proj !~ /^\$\/\w+/ && $proj ne '$/' ) { print "Error: missing or invalid project specification, must be of the form \$/project or \$/\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 - v$VERSION\n" . "Original by Brett Wooldridge (brettw\@riseup.com)\n" . "Modified by Magnus Hyllander 2006-12-05\n" . "Modified by Neil Sleightholm v3.xx\n\n" . "SourceSafe repository: $SSREPO\n" . "SourceSafe directory : $SSHOME\n" . "Subversion repository: $REPOS\n"; foreach my $proj (@ssprojectlist) { $banner .= "SourceSafe project : $proj\n"; } if (0 == $CUTOFFDATE){ $banner .= "History cut off : not set\n\n"; }else{ $banner .= "History cut off : " . POSIX::ctime($CUTOFFDATE) . "\n\n"; } print "$banner"; if ($DEBUG) { print STDERR "$banner"; } } ############################################################## # Build project directory hierarchy # sub build_directorylist { my($proj) = @_; if ($DEBUG) { print STDERR "\n#############################################################\n"; print STDERR "# Subroutine: build_directorylist #\n"; print STDERR "#############################################################\n"; } print "Building directory hierarchy..."; my $oldcount = @directorylist; recursive_build_directorylist($proj); sort(@directorylist); open(DIRS, "> directories.txt"); foreach my $dir (@directorylist) { print DIRS "$dir\n"; } close(DIRS); my $count = @directorylist - $oldcount; print "\b\b\b:\tdone ($count dirs)\n"; $PHASE = 1; } sub recursive_build_directorylist { my ($proj) = @_; push @directorylist, $proj; my $cmd = $SSCMD . " Dir \"$proj\" -I- -F-"; $_ = `$cmd`; if ($DEBUG) { print STDERR "\nDirectory listing of $proj:\n$_"; } my @lines = split('\n'); foreach my $line (@lines) { chomp($line); if ($line =~ /^\$([^\/][^:]+)$/) { recursive_build_directorylist("$proj/$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 = @directorylist; $i = 0; $j = 0.0; foreach $proj (@directorylist) { $* = 1; $/ = ':'; $cmd = $SSCMD . " Dir -I- \"$proj\""; $_ = `$cmd`; # 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; $/ = ''; my @lines = split('\n'); LOOP: foreach my $line (@lines) { last LOOP if ($line eq '' || length($line) == 0); if ($line !~ /(.*)\:/ && $line !~ /^\$.*/ && $line !~ /^([0-9]+) item.*/ && $line !~ /^No items found.*/) { # Pinned files are returned as "file;n" remove ";n" my @file = split(/;/,$line); # Exclude vss files e.g. files ending .vsscc, .vssscc, .vspscc etc if (@file[0] =~ /.*\.\w{2,3}scc$/) { print STDERR "Skipping VSS file: $proj/@file[0]\n"; } else { push(@filelist, "$proj/@file[0]"); printf("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b(%3d\%): %-6d", (($j / $count) * 100), $i); if ($DEBUG) { print STDERR "$proj/@file[0]\n"; } $i++; } } } $j++; } open(FILES,">files.txt"); for my $file (@filelist) { print FILES "$file\n"; } close(FILES); printf "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b: done ($i files)\n"; $PHASE = 2; } ############################################################## # 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); my ($i, $count, $versioncount, $tmpname, $cmd); print "Building file histories ( 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`; #print STDERR "$_"; # DEBUG ONLY &proc_history($file, $_); $i++; } open(HIST, ">histories.txt"); foreach my $hist (@histories) { print HIST "$hist\n"; } close(HIST); $oldname =~ s/./\b/g; $count = @histories; print "$oldname\b\b\b\b\b\b\b\b\b: done ($count versions)" . substr($pad, 0, 20) . "\n"; $PHASE = 3; } ############################################################## # Process the VSS history of a file. # 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 => 4; use constant STATE_FINAL => 5; my $state = STATE_VERSION; my $projre = '\$\/'; my ($version, $junk, $user, $date, $time, $month, $day, $year); my ($hour, $minute, $path, $action, $datetime, $timestamp); my $readhist = 0; my $comment = ''; my @lines = split('\n', $hist); my $line_count = @lines; my $i = 0; my $history_count = 0; #print STDERR ">>>> $file\n"; # DEBUG ONLY foreach my $line (@lines) { #print STDERR ">>>> state = $state: $line\n"; # DEBUG ONLY if ($state == STATE_VERSION && $line =~ /^\*+ Version ([0-9]+)/) { $version = $1; $readhist = 1; $state = STATE_USER; } elsif ($state == STATE_USER && $line =~ /^User: /) { # Example: "User: Neil Sleightholm Date: 9/03/01 Time: 8:15" $line =~ m/\w+:\s+(.*?)\s+\w+:\s+(.*?)\s+\w+:(.*)/; $user = $1; $date = $2; $time = $3; #($year,$month,$day) = split('-', $date); # yyyy-mm-dd date format #($month,$day,$year) = split('/', $date); # US date format ($day,$month,$year) = split('/', $date); # UK date format ($hour,$minute) = split(':', $time); $year = ($year < 80) ? 2000 + $year : 1900 + $year; $datetime = sprintf("%04d-%02d-%02d %02d:%02d",$year,$month,$day,$hour,$minute); $timestamp = POSIX::mktime(0, $minute, $hour, $day, $month - 1, $year - 1900, -1, -1, -1); if (!defined($timestamp)) { print STDERR "$file:\n"; print STDERR "$line => $year-$month-$day $hour:$minute => $timestamp\n"; print "\$timestamp is undef!!!\n"; exit; } $state = STATE_ACTION; } elsif ($state == STATE_ACTION) { if ($line =~ /^Checked in /) { if ($' =~ /^$projre/) { $path = $'; $action = 'checkedin'; $state = STATE_COMMENT; } else { $projre = $'; $projre =~ s/([\$\/\(\)])/\\$1/g; $action = 'checkedin'; $state = STATE_COMMENT; } } elsif ($line =~ /^Created/) { $action = 'created'; $state = STATE_COMMENT; } elsif ($line =~ /^Branched/) { $action = 'branched'; $state = STATE_COMMENT; } elsif ($line =~ /^Labeled/) { $action = 'labeled'; $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 = trim($'); } elsif (length($comment) > 0 && length($line) > 0) { $comment = $comment . '__NL__' . trim($line); } elsif (length($line) == 0) { $state = STATE_FINAL; } } $i++; if ($state == STATE_FINAL || $readhist && $i == $line_count) { # Ignore history before cuttoff unless no history has been found if (0 != $CUTOFFDATE && $history_count > 0 && $timestamp < $CUTOFFDATE) { print STDERR "History too old: $history_count, version: $version - " . POSIX::ctime($timestamp); # DEBUG ONLY last; } $hist = join(',', $file, $version, $datetime, $timestamp, $user, $action, $comment); $comment = ''; if ($DEBUG) { print STDERR "$hist\n"; } push(@histories, $hist); $readhist = 0; $state = STATE_VERSION; # Only 'created' and 'checkedin' count as history if ($action eq 'checkedin' || $action eq 'created') { $history_count++; # Ignore history before cuttoff if (0 != $CUTOFFDATE && $timestamp < $CUTOFFDATE) { print STDERR "History too old: $history_count, version: $version - " . POSIX::ctime($timestamp); # DEBUG ONLY last; } } } } } ############################################################## # Remove white space from the beginning and end of a string # sub trim { my ($a) = @_; $a =~ s/^\s+//; # remove whitespace at beginning $a =~ s/\s+$//; # remove whitespace at end #$a =~ s/\s\s+/ /g; # replace multiple whitespace by a single space return $a; } ############################################################## # Dump the users from the repository into users.txt # sub dump_users { my %USERHASH = (); my $count = 0; print "Building user list:"; foreach my $hist (@histories) { my ($file, $version, $datetime, $timestamp, $user, $action, $comment) = split(',', $hist, 7); $USERHASH{$user} = 1; } open(USERS, "> users.txt"); foreach my $user (keys %USERHASH) { print USERS "$user\n"; $count++; } close(USERS); print "\t\tdone ($count users)\n"; } ############################################################## # Dump the users from the repository into users.txt and exit # sub dump_users_and_exit { &dump_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 resume\n"; print "this migration, picking up from this point, by using the --resume\n"; print "option on the command line.\n\n"; exit 0; } ############################################################## # Group files together that can be commited as an atomic unit, # i.e. were checked in at the same time by the same user, and # with the same comment. # sub build_atoms { if ($DEBUG) { print STDERR "\n#############################################################\n"; print STDERR "# Subroutine: build_atoms #\n"; print STDERR "#############################################################\n"; } print "Building atoms: 0%"; %atomlist = (); my @userhist = sort sort_hist_by_user_timestamp @histories; my ($prevtime,$prevuser,$prevcomment) = (0,'',''); my ($atom_user,$atom_datetime,$atom_timestamp,$atom_comment) = ('','',0,''); my $histcount = @userhist; my $i = 0; my $atom_files = {}; foreach my $hist (@userhist) { # display sugar $i++; printf("\b\b\b\b%3d\%", (($i / $histcount) * 100)); # real work my ($file,$version,$datetime,$timestamp,$user,$action,$comment) = split(/,/,$hist,7); # ignore actions which are not really new versions of the file next unless ($action eq 'checkedin' || $action eq 'created' || $action eq 'branched'); if ($user ne $prevuser || $comment ne $prevcomment || $timestamp - $prevtime >= 120 || exists $$atom_files{$file}) { if ($prevtime != 0) { #print STDERR "New atom ($prevuser/$user, $prevcomment/$comment, " . ($timestamp - $prevtime) . ")\n"; # DEBUG ONLY my $newatom = join(',',$atom_user,$atom_datetime,$atom_timestamp,$prevtime,$atom_comment); while (exists $atomlist{$newatom}) { $newatom .= "+"; } $atomlist{$newatom} = $atom_files; #print STDERR "$newatom\n"; # DEBUG ONLY #for my $f (values %$atom_files) { # print STDERR " $f\n"; #} } $atom_files = {}; } if (scalar %$atom_files == 0) { $atom_user = $user; $atom_timestamp = $timestamp; $atom_datetime = $datetime; $atom_comment = $comment; if ($atom_comment ne '') { $atom_comment .= '__NL__'; } $atom_comment .= "[VSS $datetime]"; } $$atom_files{$file} = join(',',$file,$version,$action); $prevtime = $timestamp; $prevuser = $user; $prevcomment = $comment; } my $newatom = join(',',$atom_user,$atom_datetime,$atom_timestamp,$prevtime,$atom_comment); while (exists $atomlist{$newatom}) { $newatom .= "+"; } $atomlist{$newatom} = $atom_files; # check for conflicting atoms @atoms = sort sort_atoms_by_timestamp (keys %atomlist); my %fileversions = (); my $error = 0; $i = 0; while ($i < $#atoms) { my ($atoma,$atomb) = ($atoms[$i],$atoms[$i+1]); my ($usera,$datetimea,$timestamp1a,$timestampna,$commenta) = split(/,/,$atoma,5); my ($userb,$datetimeb,$timestamp1b,$timestampnb,$commentb) = split(/,/,$atomb,5); # check if atomb overlaps atoma in time if ($timestamp1a <= $timestamp1b && $timestamp1b <= $timestampna) { my $reversed = 0; # check if the atoms are updating the same file in the wrong order CHECK: for my $filea (values %{$atomlist{$atoma}}) { my ($fna,$vera,$resta) = split(/,/,$filea,3); for my $fileb (values %{$atomlist{$atomb}}) { my ($fnb,$verb,$restb) = split(/,/,$fileb,3); if ($fna eq $fnb && $vera > $verb) { if ($reversed) { print STDERR "ERROR: Conflicting atoms, reversing order didn't help:\n$atoma:\n $filea\n$atomb:\n $fileb\n"; print "ERROR: Conflicting atoms\n"; $error = 1; goto DUMP; } else { # Two atoms where checked in at the same time print STDERR "Conflicting atoms, trying to reverse order:\n$atoma:\n $filea\n$atomb:\n $fileb\n"; ($atoms[$i],$atoms[$i+1]) = ($atomb,$atoma); ($atoma,$atomb) = ($atoms[$i],$atoms[$i+1]); $reversed = 1; goto CHECK; } } } } if ($reversed) { print STDERR "Conflict resolved!\n"; } } for my $filea (values %{$atomlist{$atoma}}) { my ($fna,$vera,$resta) = split(/,/,$filea,3); if (exists $fileversions{$fna}) { if ($fileversions{$fna} >= $vera) { print STDERR "ERROR: Files would be checked in in an unexpected order:\nAtom $i,$atoma\n File: $fna\n cur: $fileversions{$fna}\n new: $vera\n"; print "ERROR: Files would be checked in in an unexpected order\n"; $error = 1; goto DUMP; } } $fileversions{$fna} = $vera; } $i++; } if ($DEBUG) { print STDERR "Atom and file order verified correctly.\n"; } DUMP: open(ATOMLIST,">atoms.txt"); for ($i = 0; $i <= $#atoms; $i++) { print ATOMLIST "$i,$atoms[$i]\n"; for my $file (values %{$atomlist{$atoms[$i]}}) { print ATOMLIST " $file\n"; } } close(ATOMLIST); if ($error) { exit; } printf("\b\b\b\b\t\tdone (%d atoms)\n", $#atoms + 1); } ####################################################################### # Sort the history by user and timestamp. # sub sort_hist_by_user_timestamp { my ($patha,$versiona,$datetimea,$timestampa,$usera,$actiona,$commenta) = split(/,/,$a,7); my ($pathb,$versionb,$datetimeb,$timestampb,$userb,$actionb,$commentb) = split(/,/,$b,7); if ($usera ne $userb) { return $usera cmp $userb; } elsif ($timestampa != $timestampb) { return $timestampa <=> $timestampb; } elsif ($commenta ne $commentb) { return $commenta cmp $commentb; } elsif ($patha ne $pathb) { return $patha cmp $pathb; } return $versiona <=> $versionb; } ####################################################################### # Sort the atoms by timestamp(s). Sub sort by user and comment. # sub sort_atoms_by_timestamp { my ($usera,$datetimea,$timestamp1a,$timestampna,$commenta) = split(/,/,$a,5); my ($userb,$datetimeb,$timestamp1b,$timestampnb,$commentb) = split(/,/,$b,5); if ($timestamp1a != $timestamp1b) { return $timestamp1a <=> $timestamp1b; } elsif ($timestampna != $timestampnb) { return $timestampna <=> $timestampnb; } elsif ($usera ne $userb) { return $usera cmp $userb; } return $commenta cmp $commentb; } ####################################################################### # Get the latest checkpoint so allow resuming after refreshing history. # sub get_latest_checkpoint { my ($line); my $i = 0; backup("directories.txt",10); backup("files.txt",10); backup("histories.txt",10); backup("atoms.txt",10); if (-f "extract_progress.txt") { my $lastatom = ''; print "Calculating extract progress:"; open(EXTRACT, "< extract_progress.txt"); while () { chop($_); $lastatom = $_; } close(EXTRACT); $RESUMEAFTERATOM = $lastatom; if ($DEBUG) { print STDERR "Resume after atom: $RESUMEAFTERATOM\n"; } print "\tresume after atom $RESUMEAFTERATOM\n"; } } sub backup { my ($fn,$maxbups) = @_; my $lastfn = $fn . "." . $maxbups; if (-f $lastfn) { unlink($lastfn); } for (my $i=$maxbups-1; $i>=1; $i--) { my $file = $fn . "." . $i; my $pfile = $fn . "." . ($i+1); if (-f $file) { link($file,$pfile); unlink($file); } } if (-f $fn) { link($fn,$fn . ".1"); unlink($fn); } } ############################################################## # Resume from previously generated parsed project data # sub resume { my ($line); my $i = 0; if (-f "directories.txt") { print "Loading directories: "; $i = 0; open(DIRS, "< directories.txt"); while () { $line = $_; chop($line); push(@directorylist, $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 "files.txt") { print "Loading files: "; $i = 0; open(FILES, "< files.txt"); while () { $line = $_; chop($line); push(@filelist, $line); $i++; printf("\b\b\b\b\b\b%6d", $i); } close(FILES); print "\b\b\b\b\b\b\t\t\tdone ($i files)\n"; $PHASE = 2; } if (-f "histories.txt") { print "Loading file histories: "; $i = 0; open(HIST, "< histories.txt"); while () { $line = $_; chop($line); push(@histories, $line); $i++; printf("\b\b\b\b\b\b%6d", $i); } close(HIST); print "\b\b\b\b\b\b\tdone ($i versions)\n"; $PHASE = 3; } if (-f "extract_progress.txt") { my $lastatom = ''; print "Calculating extract progress:"; open(EXTRACT, "< extract_progress.txt"); while () { chop($_); $lastatom = $_; } close(EXTRACT); $RESUMEAFTERATOM = $lastatom; if ($DEBUG) { print STDERR "Resume after atom: $RESUMEAFTERATOM\n"; } print "\tresume after atom $RESUMEAFTERATOM\n"; $PHASE = 6; } } ############################################################## # 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"; } print "Creating local directories: "; &recursive_delete('./work'); mkdir('./work'); foreach my $dir (@directorylist) { if ($dir =~ /^\$\//) { my $rawdir = "./work/$'"; recursive_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) { rmdir($parent); } elsif (-f $parent) { unlink($parent); } } ############################################################## # Make a directory tree and all of its sub dirs recursively # sub recursive_mkdir { my($tpath) = @_; my($dir, $accum); foreach $dir (split(/\//, $tpath)) { $accum = "$accum$dir/"; if ($dir ne "") { if (! -d "$accum") { mkdir $accum; } } } } ############################################################## # 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 $cmd = "svn import --username \"$USERNAME\" --password \"$PASSWORD\" --message \"Initial import from VSS\" . \"$REPOS\""; if ($DEBUG) { print STDERR "$cmd\n"; } chdir('./work'); `$cmd`; chdir('..'); print "\t\tdone\n"; $PHASE = 5; } ############################################################## # 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 $cmd = "svn checkout --username \"$USERNAME\" --password \"$PASSWORD\" --non-interactive \"$REPOS\" \"./work\""; if ($DEBUG) { print STDERR "$cmd\n"; } &recursive_delete('./work'); mkdir('./work'); `$cmd`; if ($? != 0) { print STDERR "FAILED: $cmd => " . $? >> 8 . "\n"; exit; } print "\tdone\n"; $PHASE = 6; } ############################################################## # 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 $padding = " "; my ($cmd, $tmpname, $localpath, $localdir, $out); my ($pyear,$pmon,$pmday,$phour,$pmin,$num) = (0,0,0,0,0,0); my $count = @atoms; my $startatom = 0; print "Extracting and creating:\n"; open(EXTRACT, ">>extract_progress.txt"); if ($RESUMEAFTERATOM ne '') { my ($atomnr,$atom) = split(/,/,$RESUMEAFTERATOM,2); if ($atoms[$atomnr] eq $atom) { $startatom = $atomnr + 1; } else { print STDERR "ERROR! Resume inconsistency: atom $atomnr has changed:\nexp: $atom\ncur: $atoms[$atomnr]\n"; print "ERROR! Resume inconsistency!\n"; exit; } } chdir('./work/'); for (my $i = $startatom; $i <= $#atoms; $i++) { my $atom = $atoms[$i]; my $targets = ''; foreach my $atomfile (values %{$atomlist{$atom}}) { my ($file,$version,$action) = split(',',$atomfile,3); # display sugar $tmpname = substr($file, rindex($file,'/') + 1, 50) . ' (v.' . $version . ')'; printf("\r$padding\r (%3d\%): %s", ((($i+1) / $count) * 100), substr("$tmpname", 0, 60)); # extract the file from VSS if ($file =~ /^\$\//) # strip $/ from the start { # extract to the proper directory $localpath = $'; $localpath =~ s/\//\\/g; $localdir = $localpath; my $slash = rindex($localdir, '\\'); if ($slash == -1) { $localdir = '.'; } else { $localdir = substr($localdir,0,$slash); } #print STDERR "file = $file\n"; # DEBUG ONLY #print STDERR "localpath = $localpath\n"; # DEBUG ONLY #print STDERR "localdir = $localdir\n"; # DEBUG ONLY my $fileexists = -f $localpath; $cmd = $SSCMD . " get -GTM -W -I-Y -GL\"$localdir\" -V$version \"$file\" 2>&1"; $out = `$cmd`; # get rid of stupid VSS warning messages $* = 1; $out =~ s/\n?Project.*rebuilt\.//g; $out =~ s/\n?File.*rebuilt\.//g; $out =~ s/\n.*was moved out of this project.*rebuilt\.//g; $out =~ s/\nContinue anyway.*Y//g; $* = 0; if ($DEBUG) { print STDERR "$cmd\n"; print STDERR "$out"; } if ($? != 0) { print STDERR "FAILED: $cmd => " . $? >> 8 . "\n"; exit; } if ($out =~ /does not retain old versions of itself/) { print STDERR "WARNING: Binary file without history: $file\n"; } elsif (! -f $localpath) { print STDERR "ERROR: File not checked out: $file (v.$version)\n"; } else { # create list of targets to commit in this atom $targets .= "$localpath\n"; if (! $fileexists ) { $cmd = "svn add \"$localpath\" 2>&1"; $out = `$cmd`; if ($DEBUG) { print STDERR "$cmd\n"; print STDERR "$out"; } if ($? != 0) { print STDERR "FAILED: $cmd => " . $? >> 8 . "\n"; exit; } } } } } if ($targets ne '') { my ($user,$datetime,$timestamp1,$timestampn,$comment) = split(/,/,$atom,5); # display sugar $tmpname = "Commit atom $i [$user $datetime]"; printf("\r$padding\r (%3d\%): %s", ((($i+1) / $count) * 100), substr("$tmpname", 0, 60)); if ($DEBUG) { print STDERR "$tmpname\n"; } if ($FORCEUSER ne '') { $user = $FORCEUSER; } # Translate character codes from CP437/CP850 to UTF-8 (åäöÅÄÖ) #$comment =~ tr/\206\204\224\217\216\231/\254\253\271\197\196\214/; #$comment =~ tr/\206\204\224\217\216\231/aaoAAO/; #$comment =~ s//[gt]/g; #$comment =~ s/"/\\"/g; # quote quotes $comment =~ s/__NL__/\n/g; open(TARGETS,">___targets"); print TARGETS $targets; close(TARGETS); open(MESSAGE,">___message"); print MESSAGE "$comment\n"; close(MESSAGE); # commit changes as the VSS user (with a blank password) $cmd = "svn commit --username \"$user\" --password \"\" --non-interactive --non-recursive --file ___message --targets ___targets 2>&1"; $out = `$cmd`; if ($DEBUG) { print STDERR "$cmd\n"; print STDERR "$out"; } if ($? != 0) { print STDERR "FAILED: $cmd => " . $? >> 8 . "\n"; exit; } # Clean up unlink("___targets"); unlink("___message"); # set the SVN commit date to the original VSS check-in date my ($gsec,$gmin,$ghour,$gmday,$gmon,$gyear,$gwday,$gyday,$gisdst) = gmtime($timestamp1); $gyear += 1900; $gmon++; if ($gyear==$pyear && $gmon==$pmon && $gmday==$pmday && $ghour==$phour && $gmin==$pmin) { $num += 100; } else { $num = 0; } my $svntime = sprintf("%04d-%02d-%02dT%02d:%02d:00.%06dZ",$gyear,$gmon,$gmday,$ghour,$gmin,$num); $cmd = "svn propset --username \"$USERNAME\" --password \"$PASSWORD\" --revprop -r HEAD svn:date $svntime \"$REPOS\" 2>&1"; $out = `$cmd`; if ($DEBUG) { print STDERR "$cmd\n"; print STDERR "$out"; } if ($? != 0) { print STDERR "FAILED: $cmd => " . $? >> 8 . "\n"; exit; } ($pyear,$pmon,$pmday,$phour,$pmin) = ($gyear,$gmon,$gmday,$ghour,$gmin); } print EXTRACT "$i,$atom\n"; } close(EXTRACT); printf("\r$padding\r done (%d atoms)\n", $#atoms + 1); } ############################################################## # Get a formatted date time # sub prettydate { my ($sec, $min, $hrs, $day, $month, $year) = (localtime)[0,1,2,3,4,5]; return(sprintf("%04d-%02d-%02d %02d:%02d:%02d\n", $year+1900, $month+1, $day, $hrs, $min, $sec)); }