[Top][All Lists]
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: vss2cvs for Migrating from VSS to CVS
From: |
Mark D. Baushke |
Subject: |
Re: vss2cvs for Migrating from VSS to CVS |
Date: |
Wed, 14 Jan 2004 15:17:09 -0800 |
Frank Fabbrocino <address@hidden> writes:
> I've read the archives on migrating from VSS to CVS and virtually all
> posts recommend the perl script "vss2cvs". However, I can't seem to find
> this script anywhere. The URLs from the posts are bad (for example,
> http://www.laine.org/cvs/vss2cvs).
>
> Does anyone know where I can get this script from? Or if there are any
> other equivalent or better alternatives?
Use the wayback machine to get what you needed...
http://web.archive.org/web/20010604040504/www.laine.org/cvs/vss2cvs/
There are other files in that directory such as a ReadMe.txt which
you may want to fetch as well.
I have included a copy of the vss2cvs.pl script after my .signature
so that archives of this e-mail list will have a copy for future
reference in case it goes missing from the web.archive.org someday.
btw: I have never used this script, so good luck.
-- Mark
--------------- vss2cvs.pl 2001-02-26 18:48 ---------------
#!/usr/bin/perl
# Version: $Id: vss2cvs.pl,v 1.12 2001/02/24 17:18:27 laine Exp $
#
print ("vss2cvs - SourceSafe to CVS converter.\n");
# no buffer
$| = 1;
# Options:
#
# SSROOT=xxx - VSS repository location
# - default: nothing (uses whatever is already in %ENV{'SSDIR'})
# CVSROOT=xxx - CVS repository location
# - default: nothing (uses whatever is already in %ENV{'CVSROOT'})
# SSPROJ=xxx - VSS project name
# - default: none - THIS IS A REQUIRED argument
# - notes: if this string doesn't start with "$/", one will be appended
# CVSPROJ=xxx - CVS project name
# - default: VSSPROJ with the leading "$/" removed
# CVSBRANCH=xxx - branch tag to use for CVS commits
# - default: none - commits to the trunk
# SSUSERPASS=xxx - username,pass for VSS
# - default: none - uses the current Windows login
# cycle through the commandline and process options
while ($opt = shift)
{
($field, $value) = split(/=/, $opt, 2);
$ENV{uc($field)} = "$value";
}
$workdir = $ENV{'WORKDIR'};
$ssroot = $ENV{'SSROOT'};
$cvsroot = $ENV{'CVSROOT'};
$ssproj = $ENV{'SSPROJ'};
$cvsproj = $ENV{'CVSPROJ'};
$cvsbranch = $ENV{'CVSBRANCH'};
$ssuserpass = $ENV{'SSUSERPASS'};
# if $ssroot isn't empty set %ENV{'SSDIR'} to its value
$ENV{'SSDIR'} = backslashes($ssroot) if ($ssroot);
# if $cvsroot isn't empty, prepend "-d " to it
die "You *must* specify CVSROOT in environment or on commandline!\n"
unless ($cvsroot);
$cvsroot = slashes($cvsroot);
$ENV{'CVSROOT'} = $cvsroot;
# if $ssproj is empty, print a usage message and quit
die "You *must* specify an SSPROJ!\n"
unless ($ssproj);
# if $ssproj doesn't start with "$/", prepend it
$ssproj = slashes($ssproj);
$ssproj =~ s/^/\$\// unless ($ssproj =~ /\$\//);
# if $cvsproj is empty, copy in $vssproj, but without "$/"
$cvsproj = $ssproj unless ($cvsproj);
$cvsproj =~ s/^\$\///;
# replace all spaces and '.' in $cvsbranch with _, and if the
# first char is numeric, prepend a "b"
$cvsbranch =~ s/[\.\s]/_/g;
$cvsbranch =~ s/(^[\d])/b\1/;
# if $ssuserpass isn't empty, prepend "-y" to it
$ssuserpass =~ s/^/-y/ if ($ssuserpass);
if (not defined $workdir) {
use Cwd;
$workdir = cwd();
}
print("**********************************************************************\n");
print("WORKDIR=|$workdir|\n");
print("SSDIR=|$ENV{'SSDIR'}|\n");
print("SSPROJ=|$ssproj|\n");
print("CVSROOT=|$cvsroot|\n");
print("CVSPROJ=|$cvsproj|\n");
print("CVSBRANCH=|$cvsbranch|\n");
print("SSUSERPASS=|$ssuserpass|\n");
print("**********************************************************************\n\n");
$subdir = "convert"; # subdirectory within $workdir that we'll use
#
# Make an empty tree matching the vss project
#
chdir $workdir;
rmdirp("$subdir");
mkdirp("$subdir");
chdir $subdir;
# cvs import to create the toplevel of the tree in one step
exec_cmd("cvs -f import -m \"Directory structure from VSS\" \"$cvsproj\"
fromVSS transfer");
# remove the directory we created ourselves, and check it out from CVS
# (so we have the CVS version info).
# As per Ephraim Ofir - If $cvsbranch is set, do the initial checkout
# onto the branch, and only checkout the toplevel directory, as the
# lower levels aren't really necessary (they'll automatically be
# filled in during the cvs update of the individual files). By doing
# the initial checkout onto the branch, we avoid having to switch the
# working directory back and forth between trunk and branch, as was
# previously done. Note that if the branch doesn't already exist, CVS
# will return an error; however, as of cvs 1.11, it does go ahead and
# create enough of a work directory to get us by (contains a CVS
# subdirectory, with Entries, Root, Repository, and Tag files)
chdir "$workdir";
rmdirp("$subdir");
$branchopt = "-r $cvsbranch" if $cvsbranch;
exec_cmd("cvs -f -r co $branchopt -l -d $subdir \"$cvsproj\"");
# Set the VSS project and working directory, and get a listing of all
# directories and files in the project
chdir "$workdir";
exec_cmd("ss cd \"$ssproj\" $ssuserpass");
exec_cmd("ss workfold \"$ssproj\" \"$workdir\\$subdir\" $ssuserpass");
# this makes this pattern more useable for matching
$ssprojpat = $ssproj;
$ssprojpat =~ s%\$%\\\$%;
$ssprojpat =~ s%\/%\\/%g;
# switch down to here, to make the mkdir and cvs add commands easier
chdir $subdir;
# create a CVS working directory, while also adding each directory
# into the CVS module. Build a list of files & their type.
exec_cmd("ss dir -R \"$ssproj\" -Odirlist $ssuserpass");
open(PROJLIST, "< dirlist");
open(FILELIST,"> ssfiledump") or die "Couldn't open ssfiledump for writing!\n";
$incvsdir = 0;
foreach $dirline (<PROJLIST>) {
if ($dirline =~ /\/CVS:/) {
# skip any CVS directories in VSS
$incvsdir = 1;
next;
} elsif ($dirline =~ /^$ssprojpat/) {
$incvsdir = 0;
$currdir = $dirline;
$dirline =~ s/^$ssprojpat//;
chomp $dirline;
chomp $currdir;
$currdir =~ s/\:$/\//;
next if (!$dirline);
$dirline =~ s/^([^\:]*)\:[\s]*$/\1/;
$dirline =~ s/^\///;
# $dirline = lc($dirline);
mkdirp("$dirline");
cvsadddirp("$dirline");
} else {
chomp $dirline;
next if ($incvsdir
||(!$dirline)
|| ($dirline =~ /^\$/)
|| ($dirline =~ /^No\ items\ found\ under\ /)
|| ($dirline =~ /item\(s\)/));
$dirline =~ s/\;[0-9]*$//; # pinned files
$currfile = "$currdir$dirline";
open(FILETYPE,"ss filetype \"$currfile\" $ssuserpass |");
$type = lc(<FILETYPE>);
close(FILETYPE);
chomp $type;
$type =~ s/^.*\ //;
print FILELIST ("No $type $currfile\n");
}
}
close PROJLIST;
close FILELIST;
print "\n";
# We have an empty directory tree in CVS, as well as an empty CVS
# working directory tree. Now we simply cycle through the list of all
# files, calling resync_file for each.
open(FILELIST,"< ssfiledump") or die "Couldn't open ssfiledump for reading!\n";
foreach $fileline (<FILELIST>) {
($nohistory, $type, $file) = split(/\s/,$fileline,3);
chomp $file; # NOTE: contains complete vss project path!
resync_file($file,$type,$nohistory,$cvsbranch);
}
close FILELIST;
chdir "$workdir";
rmdirp("$subdir");
# end of main()
my (%label_comments, %label_warned);
my @linebuffer;
#
# resync-file() - reads through ss history of given file, compares to
# cvs history ("log") of the file, and adds anything
# new from ss to cvs. If ss history hasn't changed since
# last time it was run, it should be a NOP.
#
# Note: the *actual* timestamp and user are stored in the
# comments, and moved into proper position later by
# massagecomments.pl. This is because we are probably running
# vss2cvs.pl on a remote machine, where we don't have the direct
# access to the cvs *,v file that we need in order to manipulate
# timestamps.
#
# Note2:if a branch has been requested, this function will first
# attempt to match revisions on the trunk with SS revisions,
# then create the branch at the point the two diverge. If the
# named branch already exists, this will be skipped - the
# timestamp of the last existing revision on the branch will be
# compared to the timestamps of all the vss revisions, and those
# that are later than that will be committed to the cvs branch.
sub resync_file
{
my $file = shift; # NOTE: contains *complete* vss project path
my $type = shift; # "binary", "text"
my $nohistory = shift;
my $cvsbranch = shift;
my $savedrevs = ($nohistory eq "No");
my $dir;
$file = slashes($file);
print "******** Syncing $file *************\n";
# construct commandlines
my $sscmd = "ss history \"$file\" $ssuserpass";
$file =~ s/^$ssprojpat\///i;
my $cvscmd = "cvs -f log \"$file\"";
my $file_bs = backslashes($file);
my $backslashpos = rindex($file_bs, "\\");
if ($backslashpos eq -1) {
$dir = ".";
} else {
# $dir is everything up to last '/' in filename
$dir = substr($file_bs, 0, $backslashpos);
}
# construct associative arrays of labels and revisions already in
# CVS. the labels will be indexed by label name, and the revisions
# will be indexed by the *actual* date/time of the change
my %cvslabels; # array holding revs of all known cvs labels
my %cvsrevs; # all known cvs revisions, indexed by date
my $alreadyincvs; # set to non-0 if file is found in cvs
my $line;
open(CVS,"$cvscmd |");
# skip up to beginning of labels
while ($line = <CVS>) {
last if ($line =~ /^symbolic names:$/);
}
# now read all labels
while ($line = <CVS>) {
# each label line is started with a tab character
last if (!($line =~ /^\t/));
my $cvsrev;
my $cvslabel;
# strip tab
$line =~ s/^\t//;
# break "Label_Name: rev" into separate items
($cvslabel, $cvsrev) = split(/[\s\:]+/,$line);
# add to %cvslabels
$cvslabels{$cvslabel} = $cvsrev;
}
# now look for groups of:
#
# ----------------------
# revision <x>
# date: <x>
# [comments]
# possibly another "date <x>" (note lack of :)
# ----------------------
# followed by =================, which is EOF
undef $cvsrev; undef $cvsdate;
while ($line = <CVS>) {
chomp($line);
# NOTE: If any comments have lines of 20 - or =, this will
# produce erroneous results! I can't think of a foolproof
# way to get around that, though... :-(
if ($line =~ /^[-=]{20,}$/) {
if ($cvsrev && $cvsdate) {
print "Existing rev $cvsrev on $cvsdate\n";
$cvsrevs{$cvsdate} = $cvsrev;
$alreadyincvs = 1;
undef $cvsrev; undef $cvsdate;
}
} elsif ((!$cvsrev) && $line =~ /^revision /) {
# revision <x> line
$line =~ s/^revision //;
$cvsrev = $line;
} elsif ((!$cvsdate) && $line =~ /^date: /) {
# CVS's "date: <x>" line
$line =~ s/^date: //; # chop off beginning of line
$line =~ s/;.*$//; # chop off everything past date
$line =~ s/[\/ \:]/\./g; # replace all separators with "."
$cvsdate = $line;
} elsif (($cvsdate) && $line =~ /^date[\s]+/) {
# a "date <x>" line previously added to comments by us
$line =~ s/^date[\s]+//; # chop off beginning of line
$line =~ s/;.*$//; # chop off everything past date
# now see if we need to add 1900 to it (if yr is 2 digits)
($temp) = split(/\./, $line);
$line =~ s/^/19/ if (length($temp) le 2);
$cvsdate = $line;
}
} # foreach $line
close(CVS);
# Now start through the SS history, saving a command to execute for every
# change or label
open(SS,"$sscmd |");
read_comment(); # ignore everything up to the first data block
# (previously I verified that it started with "History of $file ..."
# except that MS word wraps long filenames with spaces so just hang it)
my $highestssver; # the highest (first) version we found in VSS for this
file
my $matchrev; # the CVS rev of the highest VSS rev we found in CVS.
my $somethingtocommit = 0; # set non-0 if we find any rev we need to commit
my (@pending_commands);
ITEM:
while($_=myread()) {
if(!(/^\*{17}/)) {
die "parsing messed up on '$_'\n";
}
my ($version,$user,$timestamp,$label,$comment);
if(/^\*{17}( +Version (\d+) +\*{17})?/) {
$version = $2;
$highestssver = $version unless ($highestssver > $version);
}
$_=myread();
if(/^Label: "(.+)"$/) {
$label = $1;
$_=myread();
}
($user,$timestamp) = parse_user_and_timestamp($_);
$_=myread();
if(/Labeled/) {
# this revision isn't really a revision - it's just a label
if( $label =~ s%[^\w\-]+%_%g && !$label_warned{$label}) {
# print "@, #, /, ', spaces or unprintable characters in label
";
# print "\"$label\" were mapped to _\n";
$label_warned{$label} = 1;
}
if( $label =~ /^([^\w]).*$/ ) {
$label = "A_$label";
# print "\"A_\" prepended to label starting with $1
\"$label\"\n";
$label_warned{$label} = 1;
}
if( $label =~ /^([\_\-\d]).*$/ ) {
$label = "A$label";
# print "\"A\" prepended to label starting with $1
\"$label\"\n";
$label_warned{$label} = 1;
}
if (defined($cvslabels{$label})) {
# print "******* SKIPPING LABEL '$label', ALREADY THERE!.\n";
} else {
# print "will tag '$label' in cvs.\n";
$somethingtocommit = 1;
push(@pending_commands,
"cvs -f tag -l $label \"$file\"");
}
# assume all the comments for a particular label are the same...
$comment = read_comment();
$label_comments{$label} = $comment
if ! defined($label_comments{$label});
next ITEM;
} # if label
# this is a "real" revision - either "Checked in
# projectname" or "Branched"
$comment = read_comment();
undef $kflags;
$kflags = "-kb " if ($type eq "binary");
# make a "normalized" timestamp so we can search for it in
# %cvsdates
my $normaltime = $timestamp;
($temp) = split(/\./, $normaltime); # get year only
$normaltime =~ s/^/19/ if (length($temp) le 2);
if (defined ($cvsrevs{$normaltime})) {
print("Skip commit rev dated $normaltime (and prior) - ",
"already in cvs as $cvsrevs{$normaltime}.\n");
$matchrev = $cvsrevs{$normaltime};
last; # skip all prev. revs, assume they're there
(should be)
} else {
print "Will commit rev dated $normaltime to cvs.\n";
push(@pending_commands,
"$comment\ndate\t$timestamp\;\tauthor $user\;\tstate Exp\;\n");
# NOTE: -f is because, unfortunately, we must force all commits,
# even if there isn't really any change. This is because CVS
# won't notice that a file has changed if its timestamp is the same
# (which can easily happen in a script that does many operations
# in the space of a single second)
push(@pending_commands,
"cvs -f -r commit -f -F commentfile \"$file\"");
if ($savedrevs || !$somethingtocommit) {
# do at least one get per file, even if there's no
# history
my $v = "-v$version" if ($savedrevs);
push(@pending_commands,
"ss get \"$file_bs\" -GL\"$dir\" -I-Y $v $ssuserpass");
}
$somethingtocommit = 1;
} # if this is a revision not already in CVS
if ($version == 1) {
# any labels beyond version 1 happened before the file
# was created
last;
}
next ITEM;
}
print "========\n";
@trash = <SS>; # trying to flush out the rest of SS
undef @linebuffer;
close SS;
if ($alreadyincvs) {
# If the file is in cvs already, we need to do the following:
#
# If we're on a branch and there's no $cvsbranch label
# create branch label at last rev in common with trunk
#
# update the *file* tag to the end of the branch
my $branchopt = "-r $cvsbranch" if $cvsbranch;
if ($somethingtocommit) {
# the rm is so that we can verify the upcoming "ss get"
# was successful by checking for existence of the file
push(@pending_commands, "rm -f \"$file\"");
push(@pending_commands, "cvs -f -r update $branchopt \"$file\"");
}
if ($cvsbranch) {
if (!$matchrev) {
# if there was no match, it's just as if we're creating from scratch
$alreadyincvs = 0;
} elsif (!defined($cvslabels{$cvsbranch})) {
# if branch isn't already there, create it.
push(@pending_commands, "cvs -f tag -b $cvsbranch \"$file\"");
push(@pending_commands, "cvs -f tag $cvsbranch"."-initial
\"$file\"");
push(@pending_commands, "cvs -f -r update -r $matchrev \"$file\"");
}
} # if $cvsbranch
} # if $alreadyincvs
my ($cmd, $comment);
while ($cmd = pop @pending_commands) {
if ( $cmd =~ /.* commit .*/ ) {
$comment = pop @pending_commands;
open (COMMENT, "> commentfile");
print COMMENT "$comment\n";
close COMMENT;
}
exec_cmd("$cmd");
if ((!$alreadyincvs) && ($cmd =~ /^ss get/)) {
# if the file doesn't exist in the work directory, that
# means the ss get failed, so we need to try getting the
# next higher revision. Iteratively do this until we get
# one or until there are no more versions available in
# VSS. This way we'll end up with the next version newer
# than the desired version.
# Note that, in the case a version somewhere in the middle
# is missing (and earlier versions exist), you'll end up with
# the next *older* version instead. In order to fix this, you'd need
# to add an rm of the workfile after each cvs commit, which would
# could have a noticeable impact on the time required to to
# the conversion.
if (! (-e $file)) {
# first get the ver number from the failed ss commandline
# it will be the number just past "-I-Y -v"
$cmd =~ /-I-Y -v(\d+) /;
my $ver = $1;
print "****** no copy of $file;$ver found!\n";
while ($ver && ($ver < $highestssver) && !(-e $file)) {
$ver++;
$cmd =~ s/-I-Y -v(\d+) /-I-Y -v$ver /;
exec_cmd($cmd);
}
}
# if the file isn't already in cvs, we need to do a cvs
# add right after the 1st ss get (we couldn't put this
# into pending_commands to begin with because we could
# never know for sure which ss get was the first until we
# were done - apparently the 1st revision of some VSS
# files *isn't* revision 1 (which was previously used as
# the cue to do the cvs add))
$cmd = "cvs -f -r add $kflags -m\"Imported from VSS\" \"$file\"";
$alreadyincvs = 1;
exec_cmd("$cmd");
} # if cvs add needed
} # while more commands to process
} # end of sub resync_file()
sub parse_user_and_timestamp
{
$_=shift;
if (address@hidden@) {
read_comment();
}
# *** DATE FORMAT CHANGE HERE ***
#
# For U.S. format dates: mm/dd/yy, time as hh:mm[am or pm indicator]
#
die "can't parse timestamp $_"
unless(address@hidden:[\s]*([^\s]+)\s+Date:\s+(\d+)/(\d+)/(\d+)\s+Time:\s+(\d+):(\d+)([ap])@);
my ($user, $mo, $day, $yr, $hr, $min, $sec) = ($1, $2, $3, $4, $5, $6, 0);
#
# For U.K (and other) format dates: dd.mm.yy hh:mm (no am or PM):
#
#
unless(address@hidden:[\s]*([^\s]+)\s+Date:\s+(\d+)/(\d+)/(\d+)\s+Time:\s+(\d+):(\d+)([ap]*)@);
# my ($user, $day, $mo, $yr, $hr, $min, $sec) = ($1, $2, $3, $4, $5, $6, 0);
#
# gmtime returns and
# timelocal takes second, minute, hour, day, month, year
# in the range 0..59, 0..59, 0..23, 1..31, 0..11, 0..137
# The two digit year has assumptions made about it such that
# any time before 2037 (when the 32-bit seconds-since-1970 time
# will run out) is handled correctly. i.e. 97 -> 1997, 101 -> 2001
$hr = $hr % 12;
if($7 eq 'p') { $hr += 12; }
$mo = $mo - 1;
if ( $yr < 38 ) { $yr += 100; }
use Time::Local;
$totalsec = timelocal($sec, $min, $hr, $day, $mo, $yr);
($sec, $min, $hr, $day, $mo, $yr, $unused) = gmtime($totalsec);
$mo += 1;
if ($yr > 99) { $yr -= 100; }
for $timething ($sec, $min, $hr, $day, $mo, $yr) {
if ($timething !~ /../) {
$timething = "0" . $timething;
}
}
if ($yr < 34) { $yr += 2000; }
my $timestamp="$yr.$mo.$day.$hr.$min.$sec";
#******later: how do I handle the local date and time formatting settings?
## Is there a way to figure out what they are, so that I can parse 'em?
## output when "English, Australia" selected: dd/mm/yy hh:mm
$user=lc($user);
return ($user,$timestamp);
} # parse_user_and_timestamp
sub read_comment
{
my $comment="";
while($_=myread()) {
# SAB 980503 - Comment can be terminated either by a new version
# banner or by a Label separator...
## ***************** Version 28 *****************
## User: User1 Date: 3/19/98 Time: 2:16p
## Checked in $/Projects/Common/AppUtils
## Comment: setup CoffFormat build configuration
##
## **********************
## Label: "demo #1"
## User: User2 Date: 3/16/98 Time: 4:23a
## Labeled
## Label comment: Proposed demo.
##
## **********************
## Label: "demo."
## User: User2 Date: 3/13/98 Time: 2:35a
## Labeled
## Label comment: Project ready for demo.
##
## ***************** Version 27 *****************
## User: User2 Date: 2/17/98 Time: 10:27p
## Checked in $/Projects/Common/AppUtils
## Comment: Fixed location of output .lib/.bsc files.
##
if(/^\*{17}/) {
$comment =~ s/^(Label comment|Comment): //;
# strip trailing blank lines & final newline
$comment =~ s/[\s\r\n]*$//;
pushback($_);
return $comment;
}
$comment .= $_;
}
$comment =~ s/^Comment: //;
return $comment;
} # read_comment
# functions to read from SS with pushback
# my @linebuffer;
sub myread
{
my $line;
$line = pop @linebuffer if(defined(@linebuffer));
if (! defined($line)) {
$line = <SS>;
}
return $line;
}
sub pushback
{
push @linebuffer,shift;
}
sub exec_cmd
{
my $cmd = shift;
system("$cmd");
$cmd =~ s/$ssuserpass/-y**SECRET**/;
print("++++ $cmd ++++\n");
} # exec_cmd
sub slashes
{
my $ret = shift;
$ret =~ s%\\%\/%g;
return $ret;
} # slashes
sub backslashes
{
my $ret = shift;
$ret =~ s%\/%\\%g;
return $ret;
} # backslashes
# mkdirp - make a directory, including all parents if needed,
# like "mkdir -p"
sub mkdirp
{
my $fulldir = shift;
$fulldir = slashes($fulldir);
my $curdir = "";
for my $thisdir (split(/\//,$fulldir)) {
$curdir = "$curdir$thisdir/";
mkdir "$curdir", 0700;
}
} # mkdirp
# iteratively do a cvs add of each component of a directory.
sub cvsadddirp
{
my $fulldir = shift;
$fulldir = slashes($fulldir);
my $curdir = "";
# print ("*** Adding directory $fulldir to CVS\n");
for my $thisdir (split(/\//,$fulldir)) {
$curdir = "$curdir$thisdir/";
exec_cmd("cvs -f -r add \"$curdir\"") unless -d "$curdir/CVS";
}
} # cvsadddirp
# rmdirp - like "rm -rf", but does a chmod +w of everything first, so
# that it works on braindead WinNT.
sub rmdirp
{
my $subdir = shift;
system("chmod -R +w $subdir");
system("rm -rf $subdir");
} # rmdirp