3 # vss2svn.pl, Copyright (C) 2004 by Toby Johnson.
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 # http://www.gnu.org/copyleft/gpl.html
29 use Win32
::TieRegistry
(Delimiter
=> '/');
31 our(%gCfg, $VSS, $SVN, $TREE, %USERS,);
33 # http://www.perl.com/tchrist/defop/defconfaq.html#What_is_the_proposed_operat
35 sub PrintMsg
; # defined later
37 &Vss2Svn
::Subversion
::Initialize
;
38 &Vss2Svn
::VSS
::Initialize
;
50 &GiveHttpdAuthMessage
unless $gCfg{noprompt
};
58 PrintMsg
"\n\n**** VSS MIGRATION COMPLETED SUCCESSFULLY!! ****\n";
61 open STDERR
, ">&THE_REAL_STDERR"; # yes, we're about to exit, but leaving
62 # STDERR dangling always makes me nervous!
64 $gCfg{hooray
} = 1; # to suppress Win32::TieRegistry global destruction errors
68 ###############################################################################
70 ###############################################################################
71 sub GiveStartupMessage
{
76 if ($gCfg{setdates
}) {
81 WARNING: Commit dates can be migrated to a NEW SUBVERSION REPOSITORY only.
82 You WILL CORRUPT your data if you migrate dates to an existing repository
83 which is at any other Revision than 0!
91 ss.exe Found: $gCfg{ssbin}
92 svn.exe Found: $gCfg{svnbin}
94 VSS Project: $gCfg{vssproject}
95 Subversion URL: $gCfg{svnrepo}
97 Local Date Format: $Vss2Svn::VSS::gCfg{dateString}
98 Local Time Format: $Vss2Svn::VSS::gCfg{timeString}
99 Time Bias To Get GMT: $gCfg{timebias} minutes
101 Set SVN Commit Dates: $setdates$datemsg
104 return if $gCfg{noprompt
};
106 print "Continue with these settings? [Y/n]";
108 exit(1) if ($reply =~ m/\S/ && $reply !~ m/^y/i);
111 ###############################################################################
113 ###############################################################################
115 # redirect STDERR to logfile
116 open THE_REAL_STDERR
, ">&STDERR";
117 $gCfg{logfile
} = "$gCfg{workbase}/logfile.txt";
118 open STDERR
, ">$gCfg{logfile}"
119 or die "Couldn't open logfile $gCfg{workbase}/logfile.txt";
121 # the svn client program outputs to STDOUT; redirect to STDERR instead
122 open STDOUT
, ">&STDERR";
124 select THE_REAL_STDERR
;
128 # since we redirected STDERR, make sure user sees die() messages!
129 $SIG{__DIE__
} = \
&MyDie
;
130 $SIG{__WARN__
} = \
&PrintMsg
if $gCfg{debug
};
133 ###############################################################################
135 ###############################################################################
137 PrintMsg
"\n\n**** BUILDING INITIAL STRUCTURES; PLEASE WAIT... ****\n\n";
139 $TREE = $VSS->project_tree($gCfg{vssproject
},1,1,1)
140 or die "Couldn't create project tree for $gCfg{vssproject}";
143 ###############################################################################
145 ###############################################################################
146 sub PruneVssExcludes
{
148 return unless defined $gCfg{vssexclude
};
150 # By this point, we already have the entire "naked" directory structure
151 # in $TREE, and we prune off any branches that match exclude. It may seem
152 # wasteful to go to the trouble of building $TREE if we're just gonna
153 # cut large chunks off now, but since we had to parse the entire output of
154 # "ss DIR" on "vssproject" anyway, we wouldn't have saved much time by
155 # using these at that stage.
157 my($ref, $parent, $subdir, $last);
160 foreach my $exclude ( sort @
{ $gCfg{vssexclude
} }) {
161 # by sorting, we get parents before their subdirectories, to give more
162 # meaningful warning messages
164 $exclude =~ s/^\s*(.*?)\s*$/$1/;
165 $exclude =~ s
:^$gCfg{vssprojmatch
}/?
::;
167 if ($exclude =~ m
:^\
$/:) {
168 PrintMsg
"**WARNING: Exclude path \"$exclude\" is not underneath "
169 . "$gCfg{vssproject}; ignoring...\n";
173 # Perl doesn't allow us to delete() a hash ref, so we must also keep
174 # track of the parent to fully get rid of the entry
175 $ref = $parent = $TREE;
177 foreach $subdir (split '\/', $exclude) {
178 if (!exists $ref->{$subdir}) {
179 PrintMsg
"**WARNING: Exclude path \"$exclude\" not found in "
180 . "$gCfg{vssproject} (or a parent directory was already "
181 . "excluded); ignoring...\n";
185 # can't use foreach() iterator outside of loop, so keep track of it
188 $ref = $ref->{$subdir};
191 delete $parent->{$last};
196 } # End PruneVssExcludes
198 ###############################################################################
200 ###############################################################################
202 chdir "$gCfg{importdir}"
203 or die "Couldn't create working directory $gCfg{importdir}";
205 PrintMsg
"\n\n**** BUILDING VSS HISTORY ****\n\n";
207 &WalkTreeBranch
($TREE, $gCfg{vssproject
});
210 ###############################################################################
212 ###############################################################################
214 my($branch, $project) = @_;
215 PrintMsg
"ENTERING PROJECT $project...\n";
217 my($key, $val, $newproj);
220 foreach $key (sort keys %$branch) {
221 $val = $branch->{$key};
223 if (ref($val) eq 'HASH') {
224 # subproject; create a new branch of the tree
226 push @branches, {branch
=> $val, project
=> "$key"};
228 } elsif (!ref $val) {
229 # a scalar, i.e. regular file
231 &AddFileHistory
($project, $key);
236 foreach my $subbranch (@branches) {
237 mkdir $subbranch->{project
};
238 chdir $subbranch->{project
}
239 or die "Could not change to working directory $subbranch->{project}";
241 ($newproj = "$project/$subbranch->{project}") =~ s
://:/:;
243 &WalkTreeBranch
($subbranch->{branch
}, $newproj);
249 ###############################################################################
251 ###############################################################################
253 my($project, $file) = @_;
255 # build the revision history for this file
257 (my $filepath = "$project/$file") =~ s
://:/:;
259 # SS.exe uses a semicolon to indicate a "pinned" file
260 $filepath =~ s/;(.*)//;
262 my $filehist = $VSS->file_history("$filepath");
263 die "Internal error while reading VSS file history for $filepath"
264 if !defined $filehist;
266 PrintMsg
" $filepath\n";
269 foreach my $rev (@
$filehist) {
270 $gCfg{globalCount
}++;
272 $rev->{user
} = lc( $rev->{user
} ); # normalize usernames to lowercase
273 $rev->{comment
} .= "\n\n$gCfg{comment}" if defined $gCfg{comment
};
275 $rev->{date
} =~ s/-//g;
276 $rev->{time} =~ s/://;
278 &InsertDatabaseRevision
($filepath, $rev);
280 $USERS{ $rev->{user
} } = 1;
285 ###############################################################################
286 # InsertDatabaseRevision
287 ###############################################################################
288 sub InsertDatabaseRevision
{
289 my($filepath, $rev) = @_;
291 my %data = %$rev; # don't pollute $rev
293 #quote the text fields
294 map { $data{$_} = $gCfg{dbh
}->quote( $rev->{$_} ) }
295 qw(date time user comment);
297 $filepath = $gCfg{dbh
}->quote($filepath);
324 or die "Could not execute DBD::SQLite command";
326 } #End InsertDatabaseRevision
328 ###############################################################################
329 # GiveHttpdAuthMessage
330 ###############################################################################
331 sub GiveHttpdAuthMessage
{
332 print THE_REAL_STDERR
<<"EOTXT";
335 Following is a list of all VSS users who have made updates at any time in the
336 specified project. In order to preserve the user history during migration to
337 Subversion, these users must exist in the Subversion authentication file.
339 Usually, this is done with an Apache "Basic" HTTP authorization file, where
340 each username is followed by a colon and the hashed password for that user.
341 A blank password is permissible. Copy and paste the following lines into this
342 authorization file in order to allow this user history to be migrated.
346 print THE_REAL_STDERR
join("\n", map {"$_:"} sort keys %USERS),
347 "\n\nPRESS ENTER TO CONTINUE (or enter [q] to quit and start over)...";
352 print THE_REAL_STDERR
"\n\nQuitting...\n";
357 ###############################################################################
359 ###############################################################################
360 sub SetupSvnProject
{
361 PrintMsg
"\n\n**** SETTING UP SUBVERSION DIRECTORIES ****\n\n";
363 chdir $gCfg{importdir
}
364 or die "Could not change to directory $gCfg{importdir}";
366 PrintMsg
" Importing directory structure from Subversion...\n";
367 $SVN->do('import', '.', '--message "Initial Import"', 0)
368 or die "Could not perform SVN import of $gCfg{importdir}. Have you "
369 . "set your httpd authorization file correctly?";
372 or die "Could not change to directory $gCfg{workdir}";
374 PrintMsg
" Checking out working copy...\n";
375 $SVN->do('checkout', '', '"."')
376 or die "Could not perform SVN checkout of $gCfg{importdir}";
379 ###############################################################################
381 ###############################################################################
382 sub ImportSvnHistory
{
383 # we will walk the history table in date/time order, GETting from VSS
384 # as we go. VSS doesn't allow atomic multi-item commits, so we'll detect
385 # these assuming if the user and comment are the same from one item to the
386 # next, they were part of the "same" action.
388 my($row, $upd, $commitinfo);
390 my %prev = (user
=> '', comment
=> '', grain
=> 0);
391 my %all = (); # hash of all files ever added
392 my %thistime = (); # hash of files added on this commit
395 my $grain = 0.000001;
397 PrintMsg
"\n\n**** MIGRATING VSS HISTORY TO SUBVERSION ****\n\n";
399 # date, time, and file fields are formatted to enable sorting numerically
400 my $cmd = "SELECT * FROM history ORDER BY date, time, file";
401 my $sth = $gCfg{dbh
}->prepare($cmd)
402 or die "Could not execute DBD::SQLite command";
404 or die "Could not execute DBD::SQLite command";
407 while ($row = $sth->fetchrow_hashref) {
408 $row->{date
} =~ s/(....)(..)(..)/$1-$2-$3/;
409 $row->{time} =~ s/(..)(..)/$1:$2/;
411 if (!exists $row->{comment
} || !defined $row->{comment
});
413 PrintMsg
" ($gCfg{commitNumber})File $row->{file}, "
414 . "$row->{date} $row->{time}...\n";
416 if (defined $prev{date
} &&
417 ($row->{date
} eq $prev{date
}) &&
418 ($row->{user
} eq $prev{user
}) &&
419 ($row->{comment
} eq $prev{comment
}) &&
420 (!defined $thistime{ $row->{file
} })) {
422 # user and comment are same; this will be multi-item commit
425 } elsif ($multiple) {
426 # we're in a multi-item commit but user or comment changed;
427 # commit previous action
429 &CommitSvn
(1, $prev{comment
}, $commitinfo);
431 &SetSvnDates
(\
%prev) if $gCfg{setdates
};
434 } elsif (defined $commitinfo) {
435 # we're not in a multi-item commit and user or comment
436 # changed; commit the single previous file
439 &CommitSvn
(0, $prev{comment
}, $commitinfo);
441 &SetSvnDates
(\
%prev) if $gCfg{setdates
};
445 if (defined $prev{date
} && ($row->{date
} ne $prev{date
})) {
448 if (defined $commitinfo) {
449 # done with this date, so commit what we have so far
450 &CommitSvn
($multiple, $prev{comment
}, $commitinfo);
453 &SetSvnDates
(\
%prev) if $gCfg{setdates
};
461 $upd = $all{ $row->{file
} }++;
462 $commitinfo = &GetVssRevision
($row, $upd, \
%thistime,);
464 %prev = (%$row, (grain
=> $grain));
469 if (defined $commitinfo) {
470 &CommitSvn
($multiple, $prev{comment
}, $commitinfo);
472 &SetSvnDates
(\
%prev) if $gCfg{setdates
};
480 ###############################################################################
482 ###############################################################################
484 my($row, $upd, $thisRef) = @_;
485 # Gets a version of a file from VSS and adds it to SVN
486 # $row is the row hash ref from the history SQLite table
487 # $upd is true if this is an update rather than add
489 my $vsspath = $row->{file
};
491 $row->{file
} =~ m/^(.*\/)(.*)/
492 or die "Mangled VSS file path information", join("\n", %$row);
493 my($path, $file) = ($1, $2);
495 $path =~ s/$gCfg{vssprojmatch}//
496 or die "Mangled VSS file path information", join("\n", %$row);
497 $path =~ s/\/$//; # remove trailing slash
499 (my $dospath = "$gCfg{workdir}/$path") =~ s
/\
//\\/g
; # use backslashes
500 $dospath =~ s/\\$//; # remove trailing backslash if $path was empty
501 $dospath =~ s/\\\\/\\/g; # replace double backslashes with single
503 my $cmd = "GET -GTM -W -GL\"$dospath\" -V$row->{version} \"$vsspath\"";
505 or die "Could not issue ss.exe command";
508 or die "Could not switch to directory $dospath";
511 $SVN->svn("add \"$file\"")
512 or die "Could not perform SVN add of $file";
517 user
=> $row->{user
},
518 dospath
=> $dospath,};
520 $thisRef->{ $row->{file
} } = 1;
525 ###############################################################################
527 ###############################################################################
529 my($multiple, $comment, $commitinfo) = @_;
531 open COMMENTFILE
, ">$gCfg{tmpfiledir}/comment.txt"
532 or die "Could not open $gCfg{tmpfiledir}/comment.txt for writing";
533 print COMMENTFILE
$comment;
536 PrintMsg
" (COMMITTING SVN...)\n";
538 $multiple?
&CommitMultipleItems
($commitinfo)
539 : &CommitSingleItem
($commitinfo);
541 $gCfg{commitNumber
}++;
545 ###############################################################################
547 ###############################################################################
548 sub CommitSingleItem
{
549 my($commitinfo) = @_;
551 warn "SINGLE COMMIT\n";
552 chdir $commitinfo->{dospath
}
553 or die "Could not change to directory $commitinfo->{dospath}";
555 $SVN->{user
} = $commitinfo->{user
};
556 $SVN->svn("commit --file \"$gCfg{tmpfiledir}/comment.txt\" "
557 . "--non-recursive \"$commitinfo->{file}\"")
558 or die "Could not perform SVN commit on \"$commitinfo->{file}\". "
559 . "Have you set your httpd authorization file correctly?";
562 ###############################################################################
563 # CommitMultipleItems
564 ###############################################################################
565 sub CommitMultipleItems
{
566 my($commitinfo) = @_;
568 warn "MULTIPLE COMMIT\n";
570 or die "Could not change to directory $gCfg{workdir}";
572 $SVN->{user
} = $commitinfo->{user
};
573 $SVN->svn("commit --file \"$gCfg{tmpfiledir}/comment.txt\" \".\"")
574 or die "Could not perform SVN commit. "
575 . "Have you set your httpd authorization file correctly?";
578 ###############################################################################
580 ###############################################################################
584 my $grain = sprintf '%0.6f', $info->{grain
};
585 my $svn_date = "$info->{date}T$info->{time}:${grain}Z";
587 my $cmd = "propset --revprop -rHEAD svn:date $svn_date $gCfg{svnrepo}";
589 or die "Could not perform SVN propset of $svn_date on $gCfg{svnrepo}";
593 ###############################################################################
595 ###############################################################################
596 sub RecursiveDelete
{
600 opendir(DIR
, $parent);
601 @dirs = readdir(DIR
);
604 foreach $dir (@dirs) {
605 if ($dir ne '.' && $dir ne '..') {
606 &RecursiveDelete
("$parent/$dir");
619 ###############################################################################
621 ###############################################################################
623 # print to logfile (redirected STDERR) and screen (STDOUT)
625 print THE_REAL_STDERR
@_;
628 ###############################################################################
630 ###############################################################################
632 # any die() is trapped by $SIG{__DIE__} to ensure user sees fatal errors
633 exit(255) if $gCfg{died
}; # don't die 2x if fatal error in global cleanup
634 exit(0) if $gCfg{hooray
};
637 print THE_REAL_STDERR
"\n", @_;
639 (my $logfile = $gCfg{logfile
}) =~ s
:/:\\:g
;
641 my ($vsserr, $svnerr) = ('') x
2;
643 if ((defined $VSS) && (defined $VSS->{ss_error
})) {
644 $vsserr = "\nLAST VSS COMMAND:\n$VSS->{ss_error}\n\n(You may find "
645 . "more info on this error at the following website:\n"
646 . "http://msdn.microsoft.com/library/default.asp?url=/library/"
647 . "en-us/guides/html/vsorierrormessages.asp )";
650 if ((defined $SVN) && (defined $SVN->{svn_error
})) {
651 $svnerr = "\nLAST SVN COMMAND:\n$SVN->{svn_error}\n";
654 print THE_REAL_STDERR
<<"EOERR";
656 ******************************FATAL ERROR********************************
657 *************************************************************************
659 A fatal error has occured. The output from the last VSS or SVN command is
662 See $logfile for more information.
669 ###############################################################################
671 ###############################################################################
673 GetOptions
(\
%gCfg,'vssproject=s','vssexclude=s@','svnrepo=s','comment=s',
674 'vsslogin=s','setdates','noprompt','timebias=i',
677 &GiveHelp
(undef, 1) if defined $gCfg{help
};
679 defined $gCfg{vssproject
} or GiveHelp
("must specify --vssproject\n");
680 defined $gCfg{svnrepo
} or GiveHelp
("must specify --svnrepo\n");
681 defined $ENV{SSDIR
} or GiveHelp
("\$SSDIR not defined");
683 $gCfg{vssproject
} =~ s
:/$:: unless $gCfg{vssproject} eq '$/';
684 $gCfg{vssprojmatch} = quotemeta( $gCfg{vssproject} );
686 @{ $gCfg{vssexclude} } = split(',', join(',' ,@{ $gCfg{vssexclude} } ))
687 if defined $gCfg{vssexclude};
689 $gCfg{ssbin} = &CheckForExe
690 ("ss.exe", "the Microsoft Visual SourceSafe client");
692 $gCfg{svnbin} = &CheckForExe("svn.exe", "the Subversion client");
696 timebias => $gCfg{timebias},
699 if (defined $gCfg{vsslogin}) {
700 @{ $vss_args }{'user
', 'passwd
'} = split(':', $gCfg{vsslogin});
701 warn "\nATTENTION: about to issue VSS login command; if program\n"
702 . "hangs here, you have specified an invalid VSS username\n"
703 . "or password. (Press CTRL+Break to kill hung script)\n\n";
706 $VSS = Vss2Svn::VSS->new($ENV{SSDIR}, $gCfg{vssproject}, $vss_args);
709 $SVN = Vss2Svn::Subversion->new( $gCfg{svnrepo} );
710 $SVN->{interactive} = 0;
711 $SVN->{user} = 'vss_migration
';
712 $SVN->{passwd} = ''; # all passwords are blank
715 %USERS = ( vss_migration => 1, );
717 $gCfg{globalCount} = 1;
718 $gCfg{commitNumber} = 1;
720 $gCfg{workbase} = cwd() . "/_vss2svn";
721 &RecursiveDelete( $gCfg{workbase} );
722 mkdir $gCfg{workbase} or die "Couldn't create
$gCfg{workbase
} (does
"
723 . "another program have a
lock on this directory
or its files?
)";
725 $gCfg{workdir} = "$gCfg{workbase
}/work
";
726 mkdir $gCfg{workdir} or die "Couldn
't create $gCfg{workdir}";
728 $gCfg{importdir} = "$gCfg{workbase}/import";
729 mkdir $gCfg{importdir} or die "Couldn't create
$gCfg{importdir
}";
731 $gCfg{tmpfiledir} = "$gCfg{workbase
}/tmpfile
";
732 mkdir $gCfg{tmpfiledir} or die "Couldn
't create $gCfg{tmpfiledir}";
734 $gCfg{dbdir} = "$gCfg{workbase}/db";
735 mkdir $gCfg{dbdir} or die "Couldn't create
$gCfg{dbdir
}";
737 $VSS->{use_tempfiles} = "$gCfg{tmpfiledir
}";
741 ###############################################################################
743 ###############################################################################
745 my $bias = $Registry->{'HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/'
746 .'Control/TimeZoneInformation/ActiveTimeBias'} || 0;
748 use integer; # forces Perl to interpret two's-complement correctly
749 $gCfg{timebias} = hex($bias) + 0;
754 ###############################################################################
756 ###############################################################################
758 my($exe, $desc) = @_;
760 foreach my $dir (split ';', ".;$ENV{PATH
}") {
761 if (-f "$dir\\$exe") {
766 my $msg = fill('', '', <<"EOMSG");
767 Could not find executable '$exe' in your \%PATH\%. Ensure $desc is properly
768 installed on this computer, and manually add the directory in which '$exe' is
769 located to your path if necessary.
771 \%PATH\% currently contains:
774 die "$msg\n$ENV{PATH}\n";
777 ###############################################################################
779 ###############################################################################
781 $gCfg{dbh
} = DBI
->connect("dbi:SQLite(RaiseError=>1,AutoCommit=>0)"
782 . ":dbname=$gCfg{dbdir}/vss2svn.db","","");
788 date char(8) NOT NULL,
789 time char(5) NOT NULL,
790 file varchar(1024) NOT NULL,
791 version long NOT NULL,
792 user varchar(256) NOT NULL,
793 comment blob NOT NULL,
794 global_count long NOT NULL
798 $gCfg{dbh
}->do($cmd) or die;
799 } #End CreateDatabase
801 ###############################################################################
803 ###############################################################################
806 $gCfg{dbh
}->disconnect;
809 ###############################################################################
811 ###############################################################################
813 my($msg, $verbose) = @_;
814 $msg .= "\n" if defined $msg;
816 $msg .= "USE --help TO VIEW FULL HELP INFORMATION\n" unless $verbose;
821 -verbose
=> $verbose,
822 -exitval
=> $verbose, # if user requested --help, go to STDOUT
832 &$code && return $_ for @_;
839 ###############################################################################
841 ###############################################################################
854 our $VERSION = '1.00';
856 ###############################################################################
858 ###############################################################################
860 my($self, $user, $passwd) = @_;
862 $self->{user
} = $user;
865 no warnings
'uninitialized'; # we want to undef passwd if none passed
866 $self->{passwd
} = $passwd unless $passwd eq '';
876 ###############################################################################
877 # package Vss2Svn::Subversion #
878 ###############################################################################
880 package Vss2Svn
::Subversion
;
890 &$code && return $_ for @_;
902 our(%gInteractiveCmds);
904 ###############################################################################
906 ###############################################################################
908 my($class, $svnrep, $project) = @_;
910 if (!defined $svnrep) {
911 croak
"Must specify Subversion repository URL";
914 $project = '' if ! defined $project;
918 repository
=> $svnrep,
931 implicit_projects
=> undef,
938 # test to ensure 'svn' command is available
939 $self->svn("help", -2) or
940 croak
"Could not run Subversion 'svn' command: "
941 . "ensure it is in your PATH";
943 $self->set_project($project);
949 ###############################################################################
951 ###############################################################################
953 my($self, $project) = @_;
956 $self->{project
} = $project;
960 ###############################################################################
962 ###############################################################################
964 my($self, $cmd, $file, $args, $silent) = @_;
966 # basically a wrapper for "svn" to set current project and repository
968 my $url = "$self->{repository}/";
969 $url .= $self->{project
}
970 if defined $self->{project
} && $self->{project
} ne '';
972 $url .= $file if defined $file;
973 $args = '' unless defined $args;
975 return $self->svn("$cmd $url $args", $silent);
978 ###############################################################################
980 ###############################################################################
982 my($self, $cmd, $silent) = @_;
983 # "raw" svn client access.
986 # 0: print everything
987 # 1: print program output only
988 # 2: print err msgs only
990 # -n: use 'n' only if 'silent' attribute not set
992 if (defined($silent) && $silent < 0) {
993 $silent = first
{defined} $self->{silent
}, $silent;
995 $silent = first
{defined} $silent, $self->{silent
}, 0;
998 $silent = abs($silent);
1000 $cmd =~ s/^\s*(svn)?\s*//; #take off "svn" if present; we'll add back later
1005 my $disp_cmd = $cmd;
1007 if (defined $gInteractiveCmds{$1} && !$self->{interactive
}) {
1008 $cmd = "$cmd --non-interactive";
1010 if (defined $self->{user
} && $cmd !~ /--username/) {
1011 if (defined $self->{passwd
} && $cmd !~ /--password/) {
1012 $disp_cmd = "$cmd --username \"$self->{user}\" --password *****";
1013 $cmd = "$cmd --username \"$self->{user}\" "
1014 . "--password \"$self->{passwd}\"";
1016 $disp_cmd = $cmd = "$cmd --username \"$self->{user}\"";
1026 warn "DEBUG: $disp_cmd\n\n" if $self->{_debug
};
1028 open CMDOUT
, '-|', "$cmd 2>&1";
1035 $output =~ s/\s+$// if defined $output;
1038 if ($self->{paginate
}) {
1041 foreach my $line (split "\n", $output) {
1044 unless ($linecount++ % $self->{paginate
}) {
1045 print "Hit ENTER to continue...\r";
1064 $self->{svn_error
} = undef;
1066 $self->{svn_error
} = "$disp_cmd\n$output";
1069 if (!$success && ($silent == 0 || $silent == 2)) {
1071 carp
"\nERROR in Vss2Svn::Subversion-\>ss\n"
1072 . "Command was: $disp_cmd\n "
1073 . "(Error $ev) $output\n ";
1078 $self->{svn_output
} = $output;
1083 ###############################################################################
1085 ###############################################################################
1088 # commands which allow --non-interactive
1089 %gInteractiveCmds = ( map {$_,1 }
1090 qw(blame cat checkout co commit ci copy cp delete del
1091 remove rm diff di export import list ls log merge
1092 mkdir move rename rn propdel pdel pd propedit pedit pe
1093 propget pget pg proplist plist pl propset pset ps
1094 status stat st switch sw update up))
1098 ###############################################################################
1099 # package Vss2Svn::VSS #
1100 ###############################################################################
1102 package Vss2Svn
::VSS
;
1111 use Win32
::TieRegistry
(Delimiter
=> '/');
1112 use Time
::ParseDate
;
1120 our $VERSION = '1.05';
1122 our(%gCfg, %gErrMatch, %gHistLineMatch, @gDevPatterns);
1124 ###############################################################################
1126 ###############################################################################
1128 my($class, $db, $project, $args) = @_;
1131 croak
"Must specify VSS database path";
1134 $db =~ s/[\/\\]?(srcsafe.ini)?$//i
;
1136 if (defined $project && $project ne ''
1137 && $project ne '$' && $project !~ /^\$\//) {
1138 croak
"Project path must be absolute (begin with $/)";
1141 $project = first
{defined} $project, '$/';
1142 $args = first
{defined} $args, {};
1158 implicit_projects
=> undef,
1167 # test to ensure 'ss' command is available
1168 $self->ss("WHOAMI", -2) or
1169 croak
"Could not run VSS 'ss' command: ensure it is in your PATH";
1171 $self->{_whoami
} = $self->{ss_output
};
1172 $self->{_whoami
} =~ s/\s*$//;
1173 $self->{_whoami
} =~ s/^.*\n//;
1175 if ($self->{ss_output
} =~ /changing project/im ||
1176 !$self->_check_ss_inifile) {
1177 croak
"FATAL ERROR: You must not set the Force_Dir or Force_Prj VSS\n"
1178 . "variables when running SourceSync. These variables can be\n"
1179 . "cleared by unchecking the two \"Assume...\" boxes in SourceSafe\n"
1180 . "Explorer under Tools -> Options -> Command Line Options.\n ";
1183 if ($project eq '') {
1184 $self->ss('PROJECT', -2);
1186 $project = $self->{ss_output
};
1187 $project =~ s/^Current project is *//i;
1188 $project .= '/' unless $project =~ m
/\
/$/;
1190 $self->{project
} = $project;
1192 $self->set_project($project);
1199 ###############################################################################
1201 ###############################################################################
1202 sub _check_ss_inifile
{
1205 my $user = lc($self->{_whoami
});
1206 my $path = "$self->{database}/users/$user/ss.ini";
1208 open SSINI
, $path or croak
"Could not open user init file $path";
1222 } # End _check_ss_inifile
1224 ###############################################################################
1226 ###############################################################################
1228 my($self, $project) = @_;
1230 $project .= '/' unless $project =~ m
/\
/$/;
1232 $self->ss("CP \"$project\"", -2) or
1233 croak
"Could not set current project to $project:\n"
1234 . " $self->{ss_output}\n ";
1236 $self->{project
} = $project;
1240 ###############################################################################
1242 ###############################################################################
1244 my($self, $project, $recursive, $remove_dev) = @_;
1246 # returns a nested-hash "tree" of all subprojects and files below the given
1247 # project; the "leaves" of regular files are the value "1".
1249 $project = $self->full_path($project);
1250 $recursive = 1 unless defined $recursive;
1251 $remove_dev = 0 unless defined $remove_dev;
1253 if ($self->filetype($project) ) { # projects are type 0
1254 carp
"project_tree(): '$project' is not a valid project";
1258 my $cmd = "DIR \"$project\"";
1259 $cmd .= ($recursive)?
' -R' : ' -R-';
1261 $self->ss($cmd, -2) or return undef;
1263 # It would be nice if Microsoft made it easy for scripts to pick useful
1264 # information out of the project 'DIR' listings, but unfortunately that's
1265 # not the case. It appears that project listings always follow blank
1266 # lines, and begin with the full project path with a colon appended.
1267 # Within a listing, subprojects come first and begin with a dollar sign,
1268 # then files are listed alphabetically. If there are no items in a project,
1269 # it prints out a message saying so. And at the end of it all, you get
1270 # a statement like "7 item(s)".
1273 my $branch_ref = \
%tree;
1275 my $seen_blank_line = 0;
1276 my($current_project);
1277 my $match_project = quotemeta($project);
1280 foreach my $line (split "\n", $self->{ss_output
}) {
1284 if ($seen_blank_line) {
1285 carp
"project_tree(): an internal error has occured -- 1";
1289 $seen_blank_line = 1;
1293 $seen_blank_line = 0;
1295 if ($line =~ m/^\d+\s+item\(s\)$/i) {
1296 # this is a count of # of items found; ignore
1299 } elsif ($line =~ m/^No items found under/i) {
1303 } elsif ($line =~ m/^(\$\/.*):$/) {
1304 # this is the beginning of a project's listing
1305 $current_project = $1;
1306 # make current project relative to initial
1307 $current_project =~ s/^$match_project\/?//i
;
1308 $current_project =~ s/^\$\///; # take off initial $/ if still there
1310 $branch_ref = \
%tree;
1312 if ($current_project ne '') {
1313 # get a reference to the end branch of subprojects
1314 ($branch_ref) = reverse(map {$branch_ref = $branch_ref->{$_}}
1315 split('/', $current_project));
1318 if (!defined $branch_ref) {
1319 carp
"project_tree(): an internal error has occured -- 2";
1324 } elsif ($line =~ m/^\$(.*)/) {
1325 # this is a subproject; create empty hash if not already there
1326 if (!defined $current_project) {
1327 carp
"project_tree(): an internal error has occured -- 3";
1331 $branch_ref->{$1} = {} unless defined($branch_ref->{$1});
1333 # just a regular file
1334 if (!defined $current_project) {
1335 carp
"project_tree(): an internal error has occured -- 4";
1340 foreach my $pattern (@gDevPatterns) {
1341 next LINE
if $line =~ m/$pattern/i;
1345 $branch_ref->{$line} = 1;
1352 } # End project_tree
1354 ###############################################################################
1356 ###############################################################################
1358 my($self, $file) = @_;
1359 # returns an array ref of hash refs from earliest to most recent;
1360 # each hash has the following items:
1361 # version: version (revision) number
1362 # user : name of user who committed change
1363 # date : date in YYYYMMDD format
1364 # time : time in HH:MM (24h) format
1365 # comment: checkin comment
1367 $file = $self->full_path($file);
1369 my $cmd = "HISTORY \"$file\"";
1372 $self->ss($cmd, -2) or return undef;
1376 my $last = 0; # what type was the last line read?
1377 # 0=start;1=version line;2=user/date/time;3="Checked In";
1380 my $last_version = -1;
1382 my$rev = {}; # hash of info for the lastent revision
1383 my($year,$month,$day,$hour,$min,$ampm,$comment,$version);
1386 foreach my $line (split "\n", $self->{ss_output
}) {
1387 if ($self->{_debug
}) {
1388 warn "\nDEBUG:($last)<$line>\n";
1392 if ($line =~ m/$gHistLineMatch{version}/) {
1394 if ($last_version == 0 ||
1395 (($last_version != -1) && ($1 != ($last_version - 1)))) {
1397 # each version should be one less than the last
1398 print "file_history(): internal consistency failure";
1403 $rev->{version
} = $1;
1410 if ($line =~ m/$gHistLineMatch{userdttm}/) {
1414 if ($gCfg{dateFormat
} == 1) {
1416 ($rev->{user
}, $day, $month, $year, $hour, $min, $ampm)
1417 = ($1, $2, $3, $4, $5, $6, $7);
1418 } elsif ($gCfg{dateFormat
} == 2) {
1420 ($rev->{user
}, $year, $month, $day, $hour, $min, $ampm)
1421 = ($1, $2, $3, $4, $5, $6, $7);
1424 ($rev->{user
}, $month, $day, $year, $hour, $min, $ampm)
1425 = ($1, $2, $3, $4, $5, $6, $7);
1428 $year = ($year > 79)?
"19$year" : "20$year";
1429 $hour += 12 if $ampm =~ /p/i;
1431 if ($self->{timebias
} != 0) {
1432 my $basis = parsedate
("$year/$month/$day $hour:$min");
1433 (my $bias = $self->{timebias
}) =~ s/^(\d+)/+ $1/;
1434 my $epoch_secs = parsedate
("$bias minutes",
1437 (undef,$min,$hour,$day,$month,$year)
1438 = localtime($epoch_secs);
1441 $year += 1900; #no, not a Y2K bug; $year = 100 in 2000
1444 $rev->{date
} = sprintf("%4.4i-%2.2i-%2.2i",
1445 $year, $month, $day);
1446 $rev->{time} = sprintf("%2.2i:%2.2i", $hour, $min);
1447 } elsif ($line =~ m/$gHistLineMatch{label}/) {
1448 # this is an inherited Label; ignore it
1451 # user, date, and time should always come after header line
1452 print "file_history(): internal consistency failure";
1460 if ($line =~ s/$gHistLineMatch{comment}//) {
1469 if ($line =~ m/$gHistLineMatch{version}/) {
1473 $comment =~ s/\s+$//;
1474 $comment =~ s/^\s+//;
1475 $rev->{comment
} = $comment;
1477 unshift @
$hist, $rev;
1480 $rev->{version
} = $version;
1482 $comment .= "\n$line";
1490 $comment =~ s/\n/ /g;
1491 $comment =~ s/\s+$//;
1492 $comment =~ s/^\s+//;
1493 $rev->{comment
} = $comment;
1495 # last line of history should always be part of a comment, but
1496 # sometimes VSS doesn't include the final comment line
1497 $rev->{comment
} = '(no comment)';
1500 unshift @
$hist, $rev;
1504 ###############################################################################
1506 ###############################################################################
1513 my($self, $file) = @_;
1514 return -1 unless defined $file;
1519 return 0 if $file eq '$/';
1520 return -1 if $file eq '$';
1522 # VSS has no decent way of determining whether an item is a project or
1523 # a file, so we do this in a somewhat roundabout way
1525 $file =~ s/[\/\\]$//;
1528 $bare =~ s/.*[\/\\]//;
1529 $bare = quotemeta($bare);
1531 $self->ss("PROPERTIES \"$file\" -R-", -3) or return -1;
1533 my $match_isproject = "^Project:.*$bare\\s*\$";
1534 my $match_notfound = "$bare\\s*is not an existing filename or project";
1536 if ($self->{ss_output
} =~ m/$match_isproject/mi) {
1538 } elsif ($self->{ss_output
} =~ m/$match_notfound/mi) {
1541 $self->ss("FILETYPE \"$file\"", -3) or return -1;
1543 if ($self->{ss_output
} =~ m/^$bare\s*Text/mi) {
1553 ###############################################################################
1555 ###############################################################################
1557 # returns the full VSS path to a given project file.
1559 my($self, $file) = @_;
1563 $file =~ s/\/$// unless $file eq '$/';
1565 return $file if $self->{implicit_projects};
1567 $file = "$self->{project}$file" unless $file =~ m/^\$/;
1568 $file =~ s/\/$// unless $file eq '$/'; # in case empty string was passed
1573 ###############################################################################
1575 ###############################################################################
1577 my($self, $cmd, $silent) = @_;
1579 # SS command-line tool access.
1582 # 0: print everything
1583 # 1: print program output only
1584 # 2: print err msgs only
1586 # -n: use 'n
' only if 'silent
' attribute not set
1588 if (defined($silent) && $silent < 0) {
1589 $silent = first {defined} $self->{silent}, $silent;
1591 $silent = first {defined} $silent, $self->{silent}, 0;
1594 $silent = abs($silent);
1599 (my $cmd_word = lc($cmd)) =~ s/^(ss(\.exe)?\s+)?(\S+).*/$3/i;
1601 $cmd = "ss $cmd" unless ($cmd =~ m/^ss(\.exe)?\s/i);
1603 if ($self->{interactive} =~ m/^y/i) {
1605 } elsif ($self->{interactive} =~ m/^n/i) {
1607 } elsif (!$self->{interactive}) {
1611 my $disp_cmd = $cmd;
1613 if (defined $self->{user} && $cmd !~ /\s-Y/i) {
1614 if (defined $self->{passwd}) {
1615 $disp_cmd = "$cmd -Y$self->{user},******";
1616 $cmd = "$cmd -Y$self->{user},$self->{passwd}";
1618 $disp_cmd = $cmd = "$cmd -Y$self->{user}";
1624 warn "DEBUG: $disp_cmd\n\n" if $self->{_debug};
1626 $ENV{SSDIR} = $self->{database};
1628 if ($self->{use_tempfiles} &&
1629 $cmd_word =~ /^(dir|filetype|history|properties)$/) {
1630 my $tmpfile = "$self->{use_tempfiles}/${cmd_word}_cmd.txt";
1632 $cmd = "$cmd \"-O\&$tmpfile\"";
1635 if (open SS_OUTPUT, "$tmpfile") {
1637 $output = scalar <SS_OUTPUT>;
1641 warn "Can't
open '$cmd_word' tempfile
$tmpfile";
1646 open SS_OUTPUT, '-|', "$cmd 2>&1";
1648 while (<SS_OUTPUT>) {
1653 $output =~ s/\s+$// if defined $output;
1657 if ($self->{paginate}) {
1660 foreach my $line (split "\n", $output) {
1663 unless ($linecount++ % $self->{paginate}) {
1664 print "Hit ENTER to
continue...\r";
1681 # SourceSafe returns 1 to indicate warnings, such as no results returned
1682 # from a 'DIR'. We don't want to consider these an error.
1683 my $success = !($ev > 1);
1686 # This is interesting. If a command only partially fails (such as GET-ing
1687 # multiple files), that's apparently considered a success. So we have to
1689 my $base_cmd = uc($cmd);
1690 $base_cmd =~ s/^(ss\s*)?(\w+).*/$2/i;
1694 if (defined($err_match = $gErrMatch{$base_cmd}) &&
1695 $output =~ m/$err_match/m) {
1702 $self->{ss_error} = undef;
1704 $self->{ss_error} = "$disp_cmd\n$output";
1707 if (!$success && ($silent == 0 || $silent == 2)) {
1709 carp "\nERROR
in Vss2Svn
::VSS
-\
>ss
\n"
1710 . "Command was
: $disp_cmd\n "
1711 . "(Error
$ev) $output\n ";
1716 $self->{ss_output} = $output;
1721 ###############################################################################
1723 ###############################################################################
1726 print @_ unless $self->{silent};
1729 ###############################################################################
1730 # _vm -- "verbose message
"
1731 ###############################################################################
1734 print @_ if $self->{verbose};
1737 ###############################################################################
1739 ###############################################################################
1741 my $dateFormat = $Registry->{'HKEY_CURRENT_USER/Control Panel/'
1742 . 'International/iDate'} || 0;
1743 my $dateSep = $Registry->{'HKEY_CURRENT_USER/Control Panel/'
1744 . 'International/sDate'} || '/';
1745 my $timeSep = $Registry->{'HKEY_CURRENT_USER/Control Panel/'
1746 . 'International/sTime'} || ':';
1747 $gCfg{dateFormat} = $dateFormat;
1749 if ($dateFormat == 1) {
1750 $gCfg{dateString} = "DD
${dateSep
}MM
${dateSep
}YY
";
1751 } elsif ($dateFormat == 2) {
1752 $gCfg{dateString} = "YY
${dateSep
}MM
${dateSep
}DD
";
1754 $gCfg{dateString} = "MM
${dateSep
}DD
${dateSep
}YY
";
1757 $gCfg{timeString} = "HH
${timeSep
}MM
";
1759 # see ss method for explanation of this
1761 GET => 'is not an existing filename or project',
1762 CREATE => 'Cannot change project to',
1763 CP => 'Cannot change project to',
1767 version => qr/^\*+\s*Version\s+(\d+)\s*\*+\s*$/,
1768 userdttm => qr/^User:\s+(.*?)\s+
1769 Date:\s+(\d+)$dateSep(\d+)$dateSep(\d+)\s+
1770 Time:\s+(\d+)$timeSep(\d+)([ap]*)\s*$/x,
1771 comment => qr/^Comment:\s*/,
1772 label => qr/^Label:/,
1775 # patterns to match development files that project_tree will ignore
1786 &$code && return $_ for @_;
1798 vss2svn.pl, Copyright (C) 2004 by Toby Johnson.
1800 This program is free software; you can redistribute it and/or
1801 modify it under the terms of the GNU General Public License
1802 as published by the Free Software Foundation; either version 2
1803 of the License, or (at your option) any later version.
1805 This program is distributed in the hope that it will be useful,
1806 but WITHOUT ANY WARRANTY; without even the implied warranty of
1807 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1808 GNU General Public License for more details.
1809 L<http://www.gnu.org/copyleft/gpl.html>
1813 vss2svn.pl S<--vssproject $/vss/project> S<--svnrepo http://svn/repo/url>
1819 full path to VSS project you want to migrate
1823 URL to target Subversion repository
1831 =item --exclude <EXCLUDE_PROJECTS>:
1833 Exclude the given projects from the migration. To list multiple projects,
1834 separate with commas or use multiple --exclude commands.
1836 Each project can be given as an absolute path (beginning with $/) or
1837 relative to --vssproject.
1839 =item --comment "MESSAGE
":
1841 add MESSAGE to end of every migrated comment
1845 Sets the "svn
:date
" property off all commits to reflect the
1846 original VSS commit date, so that the original commit dates
1847 (and not today's date) show up in your new SVN logs. This is
1848 not the default, since setting svn:date could lead to
1849 problems if not done correctly. Using this also requires the
1850 "pre
-revprop
-change
" Hook Script to be set; see
1851 L<http://svnbook.red-bean.com/svnbook/ch05s02.html#svn-ch-5-sect-2.1>
1853 =item --vsslogin "USER
:PASSWD
":
1855 Set VSS username and password, separated by a colon.
1856 B<WARNING --> if the username/password combo you provide is
1857 incorrect, this program will hang as ss.exe prompts you for
1858 a username! (This is an unavoidable Microsoft bug).
1860 =item --timebias <OFFSET_MINUTES>:
1862 Override the script's guess as to the number of minutes it should
1863 add to your local time to get to GMT (for example, if you are
1864 in Eastern Daylight Time [-0400], this should be 240).
1868 Don't prompt to confirm settings or to create usernames after
1873 Print all program output to screen as well as logfile.