Automated commit: create /trunk/www/.
[vss2svn.git] / vss2svn.pl
blobaf45f19b94cfa2e5637605e38a7b07e11243e119
1 #!perl
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
16 BEGIN {
17 # ensure script can find lib directory, regardless where it's run from
18 my $path = $0;
19 $path =~ s:^(.*)[/\\].*:$1: or $path = '.';
20 eval "use lib '$path/lib'";
22 if ($@) {
23 die "Could not load Vss2Svn libraries: $@";
27 use warnings;
28 use strict;
30 use Getopt::Long;
31 use Cwd;
32 use File::Path;
33 use Text::Wrap;
34 use Pod::Usage;
36 use Vss2Svn::Subversion;
37 use Vss2Svn::VSS;
39 use DBD::SQLite;
40 use DBI;
42 use Win32::TieRegistry (Delimiter => '/');
44 our(%gCfg, $VSS, $SVN, $TREE, %USERS,);
46 eval "use Encode";
47 $gCfg{allowUtf8} = !$@;
49 # http://www.perl.com/tchrist/defop/defconfaq.html#What_is_the_proposed_operat
50 sub first(&@);
51 sub PrintMsg; # defined later
53 &Vss2Svn::Subversion::Initialize;
54 &Vss2Svn::VSS::Initialize;
56 &Regionalize;
57 &Initialize;
58 &GiveStartupMessage;
59 &SetupLogfile;
61 &CreateDatabase;
63 &GetProjectTree;
64 &PruneVssExcludes;
65 &BuildHistory;
66 &GiveHttpdAuthMessage unless $gCfg{noprompt};
68 $gCfg{dbh}->commit;
70 &SetupSvnProject;
71 &ImportSvnHistory;
73 &CloseDatabase;
74 PrintMsg "\n\n**** VSS MIGRATION COMPLETED SUCCESSFULLY!! ****\n";
76 close STDERR;
77 open STDERR, ">&THE_REAL_STDERR"; # yes, we're about to exit, but leaving
78 # STDERR dangling always makes me nervous!
80 $gCfg{hooray} = 1; # to suppress Win32::TieRegistry global destruction errors
81 exit(0);
84 ###############################################################################
85 # GiveStartupMessage
86 ###############################################################################
87 sub GiveStartupMessage {
89 my $setdates;
90 my $datemsg = '';
92 if ($gCfg{setdates}) {
93 $setdates = 'yes';
94 $datemsg = <<"EOMSG";
97 WARNING: Commit dates can be migrated to a NEW SUBVERSION REPOSITORY only.
98 You WILL CORRUPT your data if you migrate dates to an existing repository
99 which is at any other Revision than 0!
100 EOMSG
101 } else {
102 $setdates = 'no';
105 print <<"EOMSG";
107 ss.exe Found: $gCfg{ssbin}
108 svn.exe Found: $gCfg{svnbin}
110 VSS Project: $gCfg{vssproject}
111 Subversion URL: $gCfg{svnrepo}
113 Local Date Format: $Vss2Svn::VSS::gCfg{dateString}
114 Local Time Format: $Vss2Svn::VSS::gCfg{timeString}
115 Time Bias To Get GMT: $gCfg{timebias} minutes
117 Set SVN Commit Dates: $setdates$datemsg
118 EOMSG
120 return if $gCfg{noprompt};
122 print "Continue with these settings? [Y/n]";
123 my $reply = <STDIN>;
124 exit(1) if ($reply =~ m/\S/ && $reply !~ m/^y/i);
127 ###############################################################################
128 # SetupLogfile
129 ###############################################################################
130 sub SetupLogfile {
131 # redirect STDERR to logfile
132 open THE_REAL_STDERR, ">&STDERR";
133 $gCfg{logfile} = "$gCfg{workbase}/logfile.txt";
134 open STDERR, ">$gCfg{logfile}"
135 or die "Couldn't open logfile $gCfg{workbase}/logfile.txt";
137 # the svn client program outputs to STDOUT; redirect to STDERR instead
138 open STDOUT, ">&STDERR";
140 select THE_REAL_STDERR;
141 $| = 1;
142 select STDOUT;
144 # since we redirected STDERR, make sure user sees die() messages!
145 $SIG{__DIE__} = \&MyDie;
146 $SIG{__WARN__} = \&PrintMsg if $gCfg{debug};
149 ###############################################################################
150 # GetProjectTree
151 ###############################################################################
152 sub GetProjectTree {
153 PrintMsg "\n\n**** BUILDING INITIAL STRUCTURES; PLEASE WAIT... ****\n\n";
155 &SetStatus(0,"Building initial structures");
157 $TREE = $VSS->project_tree($gCfg{vssproject},1,1,1)
158 or die "Couldn't create project tree for $gCfg{vssproject}";
161 ###############################################################################
162 # PruneVssExcludes
163 ###############################################################################
164 sub PruneVssExcludes {
166 return unless defined $gCfg{vssexclude};
168 # By this point, we already have the entire "naked" directory structure
169 # in $TREE, and we prune off any branches that match exclude. It may seem
170 # wasteful to go to the trouble of building $TREE if we're just gonna
171 # cut large chunks off now, but since we had to parse the entire output of
172 # "ss DIR" on "vssproject" anyway, we wouldn't have saved much time by
173 # using these at that stage.
175 my($ref, $parent, $subdir, $last);
177 EXCLUDE:
178 foreach my $exclude ( sort @{ $gCfg{vssexclude} }) {
179 # by sorting, we get parents before their subdirectories, to give more
180 # meaningful warning messages
182 $exclude =~ s/^\s*(.*?)\s*$/$1/;
183 $exclude =~ s:^$gCfg{vssprojmatch}/?::;
185 if ($exclude =~ m:^\$/:) {
186 PrintMsg "**WARNING: Exclude path \"$exclude\" is not underneath "
187 . "$gCfg{vssproject}; ignoring...\n";
188 next EXCLUDE;
191 # Perl doesn't allow us to delete() a hash ref, so we must also keep
192 # track of the parent to fully get rid of the entry
193 $ref = $parent = $TREE;
195 foreach $subdir (split '\/', $exclude) {
196 if (!exists $ref->{$subdir}) {
197 PrintMsg "**WARNING: Exclude path \"$exclude\" not found in "
198 . "$gCfg{vssproject} (or a parent directory was already "
199 . "excluded); ignoring...\n";
200 next EXCLUDE;
203 # can't use foreach() iterator outside of loop, so keep track of it
204 $last = $subdir;
205 $parent = $ref;
206 $ref = $ref->{$subdir};
209 delete $parent->{$last};
214 } # End PruneVssExcludes
216 ###############################################################################
217 # BuildHistory
218 ###############################################################################
219 sub BuildHistory {
220 chdir "$gCfg{importdir}"
221 or die "Couldn't create working directory $gCfg{importdir}";
223 PrintMsg "\n\n**** BUILDING VSS HISTORY ****\n\n";
225 &WalkTreeBranch($TREE, $gCfg{vssproject});
228 ###############################################################################
229 # WalkTreeBranch
230 ###############################################################################
231 sub WalkTreeBranch {
232 my($branch, $project) = @_;
233 PrintMsg "ENTERING PROJECT $project...\n";
235 my($key, $val, $newproj);
236 my @branches = ();
238 foreach $key (sort keys %$branch) {
239 $val = $branch->{$key};
241 if (ref($val) eq 'HASH') {
242 # subproject; create a new branch of the tree
244 push @branches, {branch => $val, project => "$key"};
246 } elsif (!ref $val) {
247 # a scalar, i.e. regular file
249 &AddFileHistory($project, $key);
254 foreach my $subbranch (@branches) {
255 mkdir $subbranch->{project};
256 chdir $subbranch->{project}
257 or die "Could not change to working directory $subbranch->{project}";
259 ($newproj = "$project/$subbranch->{project}") =~ s://:/:;
261 &WalkTreeBranch($subbranch->{branch}, $newproj);
263 chdir '..';
267 ###############################################################################
268 # AddFileHistory
269 ###############################################################################
270 sub AddFileHistory {
271 my($project, $file) = @_;
273 # build the revision history for this file
275 (my $filepath = "$project/$file") =~ s://:/:;
277 # SS.exe uses a semicolon to indicate a "pinned" file
278 $filepath =~ s/;(.*)//;
280 my $filehist = $VSS->file_history("$filepath");
281 die "Internal error while reading VSS file history for $filepath"
282 if !defined $filehist;
284 PrintMsg " $filepath\n";
286 REV:
287 foreach my $rev (@$filehist) {
288 $gCfg{globalCount}++;
290 $rev->{user} = lc( $rev->{user} ); # normalize usernames to lowercase
291 $rev->{comment} .= "\n\n$gCfg{comment}" if defined $gCfg{comment};
293 $rev->{date} =~ s/-//g;
294 $rev->{time} =~ s/://;
296 &InsertDatabaseRevision($filepath, $rev);
298 $USERS{ $rev->{user} } = 1;
303 ###############################################################################
304 # InsertDatabaseRevision
305 ###############################################################################
306 sub InsertDatabaseRevision {
307 my($filepath, $rev) = @_;
309 my %data = %$rev; # don't pollute $rev
311 #quote the text fields
312 map { $data{$_} = $gCfg{dbh}->quote( $rev->{$_} ) }
313 qw(user comment);
315 $filepath = $gCfg{dbh}->quote($filepath);
317 my $cmd = <<"EOSQL";
318 INSERT INTO
319 vss2svn_history (
320 date,
321 time,
322 file,
323 version,
324 user,
325 comment,
326 imported,
327 global_count
329 VALUES (
330 $data{date},
331 $data{time},
332 $filepath,
333 $data{version},
334 $data{user},
335 $data{comment},
337 $gCfg{globalCount}
339 EOSQL
341 warn $cmd;
343 $gCfg{dbh}->do($cmd)
344 or die "Could not execute DBD::SQLite command";
346 } #End InsertDatabaseRevision
348 ###############################################################################
349 # GiveHttpdAuthMessage
350 ###############################################################################
351 sub GiveHttpdAuthMessage {
352 print THE_REAL_STDERR <<"EOTXT";
354 ATTENTION REQUIRED:
355 Following is a list of all VSS users who have made updates at any time in the
356 specified project. In order to preserve the user history during migration to
357 Subversion, these users must exist in the Subversion authentication file.
359 Usually, this is done with an Apache "Basic" HTTP authorization file, where
360 each username is followed by a colon and the hashed password for that user.
361 A blank password is permissible. Copy and paste the following lines into this
362 authorization file in order to allow this user history to be migrated.
364 EOTXT
366 print THE_REAL_STDERR join("\n", map {"$_:"} sort keys %USERS),
367 "\n\nPRESS ENTER TO CONTINUE (or enter [q] to quit and start over)...";
369 my $rep = <STDIN>;
371 if ($rep =~ /^q/i) {
372 print THE_REAL_STDERR "\n\nQuitting...\n";
373 exit(0);
377 ###############################################################################
378 # SetupSvnProject
379 ###############################################################################
380 sub SetupSvnProject {
381 PrintMsg "\n\n**** SETTING UP SUBVERSION DIRECTORIES ****\n\n";
383 &SetStatus(1,"Setting up Subversion directories");
385 chdir $gCfg{importdir}
386 or die "Could not change to directory $gCfg{importdir}";
388 PrintMsg " Importing directory structure from Subversion...\n";
389 $SVN->do('import', '.', '--message "Initial Import"', 0)
390 or die "Could not perform SVN import of $gCfg{importdir}. Have you "
391 . "set your httpd authorization file correctly?";
393 chdir $gCfg{workdir}
394 or die "Could not change to directory $gCfg{workdir}";
396 PrintMsg " Checking out working copy...\n";
397 $SVN->do('checkout', '', '"."')
398 or die "Could not perform SVN checkout of $gCfg{importdir}";
401 ###############################################################################
402 # ImportSvnHistory
403 ###############################################################################
404 sub ImportSvnHistory {
405 # we will walk the history table in date/time order, GETting from VSS
406 # as we go. VSS doesn't allow atomic multi-item commits, so we'll detect
407 # these assuming if the user and comment are the same from one item to the
408 # next, they were part of the "same" action.
410 my($row, $upd, $commitinfo);
412 my %prev = (user => '', comment => '', grain => 0);
413 my %all = (); # hash of all files ever added
414 my %thistime = (); # hash of files added on this commit
416 my $multiple = 0;
417 my $grain = 0.000001;
419 PrintMsg "\n\n**** MIGRATING VSS HISTORY TO SUBVERSION ****\n\n";
421 &SetStatus(2,"Migrating VSS history to Subversion");
423 # date, time, and file fields are formatted to enable sorting numerically
424 my $cmd = "SELECT * FROM vss2svn_history WHERE imported = 0 "
425 . "ORDER BY date, time, file";
427 my $sth = $gCfg{dbh}->prepare($cmd)
428 or die "Could not prepare DBD::SQLite command";
429 $sth->execute
430 or die "Could not execute DBD::SQLite command";
432 ROW:
433 while ($row = $sth->fetchrow_hashref) {
434 $row->{date} =~ s/(....)(..)(..)/$1-$2-$3/;
435 $row->{time} =~ s/(..)(..)/$1:$2/;
436 $row->{comment} = ''
437 if (!exists $row->{comment} || !defined $row->{comment});
439 PrintMsg " ($gCfg{commitNumber})File $row->{file}, "
440 . "$row->{date} $row->{time}...\n";
442 if (defined $prev{date} &&
443 ($row->{date} eq $prev{date}) &&
444 ($row->{user} eq $prev{user}) &&
445 ($row->{comment} eq $prev{comment}) &&
446 (!defined $thistime{ $row->{file} })) {
448 # user and comment are same; this will be multi-item commit
449 $multiple = 1;
451 } elsif ($multiple) {
452 # we're in a multi-item commit but user or comment changed;
453 # commit previous action
454 $multiple = 0;
455 &CommitSvn(1, $prev{comment}, $commitinfo);
456 undef $commitinfo;
457 &SetSvnDates(\%prev) if $gCfg{setdates};
458 %thistime = ();
460 } elsif (defined $commitinfo) {
461 # we're not in a multi-item commit and user or comment
462 # changed; commit the single previous file
463 $multiple = 0;
465 &CommitSvn(0, $prev{comment}, $commitinfo);
466 undef $commitinfo;
467 &SetSvnDates(\%prev) if $gCfg{setdates};
468 %thistime = ();
471 if (defined $prev{date} && ($row->{date} ne $prev{date})) {
472 $grain = 0.000001;
474 if (defined $commitinfo) {
475 # done with this date, so commit what we have so far
476 &CommitSvn($multiple, $prev{comment}, $commitinfo);
477 undef $commitinfo;
479 &SetSvnDates(\%prev) if $gCfg{setdates};
480 %thistime = ();
482 undef $commitinfo;
483 $multiple = 0;
487 $upd = $all{ $row->{file} }++;
488 $commitinfo = &GetVssRevision($row, $upd, \%thistime,);
490 %prev = (%$row, (grain => $grain));
491 $grain += 0.000001;
495 if (defined $commitinfo) {
496 &CommitSvn($multiple, $prev{comment}, $commitinfo);
498 &SetSvnDates(\%prev) if $gCfg{setdates};
499 %thistime = ();
502 $sth->finish;
506 ###############################################################################
507 # GetVssRevision
508 ###############################################################################
509 sub GetVssRevision {
510 my($row, $upd, $thisRef) = @_;
511 # Gets a version of a file from VSS and adds it to SVN
512 # $row is the row hash ref from the history SQLite table
513 # $upd is true if this is an update rather than add
515 my $vsspath = $row->{file};
517 $row->{file} =~ m/^(.*\/)(.*)/
518 or die "Mangled VSS file path information", join("\n", %$row);
519 my($path, $file) = ($1, $2);
521 $path =~ s/$gCfg{vssprojmatch}//
522 or die "Mangled VSS file path information", join("\n", %$row);
523 $path =~ s/\/$//; # remove trailing slash
525 (my $dospath = "$gCfg{workdir}/$path") =~ s/\//\\/g; # use backslashes
526 $dospath =~ s/\\$//; # remove trailing backslash if $path was empty
527 $dospath =~ s/\\\\/\\/g; # replace double backslashes with single
529 my $cmd = "GET -GTM -W -GL\"$dospath\" -V$row->{version} \"$vsspath\"";
530 $VSS->ss($cmd)
531 or die "Could not issue ss.exe command";
533 chdir $dospath
534 or die "Could not switch to directory $dospath";
536 if (!$upd) {
537 $SVN->svn("add", $file)
538 or die "Could not perform SVN add of $file";
541 my $commitinfo =
542 { file => $file,
543 user => $row->{user},
544 dospath => $dospath,};
546 $thisRef->{ $row->{file} } = 1;
548 return $commitinfo;
551 ###############################################################################
552 # CommitSvn
553 ###############################################################################
554 sub CommitSvn {
555 my($multiple, $comment, $commitinfo) = @_;
557 $comment = Encode::encode('utf8', $comment) if $gCfg{utf8};
559 open COMMENTFILE, ">$gCfg{tmpfiledir}/comment.txt"
560 or die "Could not open $gCfg{tmpfiledir}/comment.txt for writing";
561 print COMMENTFILE $comment;
562 close COMMENTFILE;
564 PrintMsg " (COMMITTING SVN...)\n";
566 $multiple? &CommitMultipleItems($commitinfo)
567 : &CommitSingleItem($commitinfo);
569 $gCfg{commitNumber}++;
571 } #End CommitSvn
573 ###############################################################################
574 # CommitSingleItem
575 ###############################################################################
576 sub CommitSingleItem {
577 my($commitinfo) = @_;
579 warn "SINGLE COMMIT\n";
580 chdir $commitinfo->{dospath}
581 or die "Could not change to directory $commitinfo->{dospath}";
583 my $enc = $gCfg{utf8}? ' --encoding UTF-8' : '';
585 $SVN->{user} = $commitinfo->{user};
586 $SVN->svn("commit$enc --file \"$gCfg{tmpfiledir}/comment.txt\" "
587 . "--non-recursive", $commitinfo->{file})
588 or die "Could not perform SVN commit on \"$commitinfo->{file}\". "
589 . "Have you set your httpd authorization file correctly?";
592 ###############################################################################
593 # CommitMultipleItems
594 ###############################################################################
595 sub CommitMultipleItems {
596 my($commitinfo) = @_;
598 warn "MULTIPLE COMMIT\n";
599 chdir $gCfg{workdir}
600 or die "Could not change to directory $gCfg{workdir}";
602 my $enc = $gCfg{utf8}? ' --encoding UTF-8' : '';
604 $SVN->{user} = $commitinfo->{user};
605 $SVN->svn("commit$enc --file \"$gCfg{tmpfiledir}/comment.txt\" \".\"")
606 or die "Could not perform SVN commit. "
607 . "Have you set your httpd authorization file correctly?";
610 ###############################################################################
611 # SetSvnDates
612 ###############################################################################
613 sub SetSvnDates {
614 my($info) = @_;
616 my $grain = sprintf '%0.6f', $info->{grain};
617 my $svn_date = "$info->{date}T$info->{time}:${grain}Z";
619 my $cmd = "propset --revprop -rHEAD svn:date $svn_date $gCfg{svnrepo}";
620 $SVN->svn($cmd)
621 or die "Could not perform SVN propset of $svn_date on $gCfg{svnrepo}";
623 } #End SetSvnDates
625 ###############################################################################
626 # RecursiveDelete
627 ###############################################################################
628 sub RecursiveDelete {
629 my($parent) = @_;
630 my(@dirs, $dir);
632 opendir(DIR, $parent);
633 @dirs = readdir(DIR);
634 closedir(DIR);
636 foreach $dir (@dirs) {
637 if ($dir ne '.' && $dir ne '..') {
638 &RecursiveDelete("$parent/$dir");
642 if (-d $parent) {
643 rmdir($parent);
645 elsif (-f $parent) {
646 unlink($parent);
651 ###############################################################################
652 # PrintMsg
653 ###############################################################################
654 sub PrintMsg {
655 # print to logfile (redirected STDERR) and screen (STDOUT)
656 print STDERR @_;
657 print THE_REAL_STDERR @_;
658 } #End PrintMsg
660 ###############################################################################
661 # MyDie
662 ###############################################################################
663 sub MyDie {
664 # any die() is trapped by $SIG{__DIE__} to ensure user sees fatal errors
665 exit(255) if $gCfg{died}; # don't die 2x if fatal error in global cleanup
666 exit(0) if $gCfg{hooray};
668 warn @_;
669 print THE_REAL_STDERR "\n", @_;
671 (my $logfile = $gCfg{logfile}) =~ s:/:\\:g;
673 my ($vsserr, $svnerr) = ('') x 2;
675 if ((defined $VSS) && (defined $VSS->{ss_error})) {
676 $vsserr = "\nLAST VSS COMMAND:\n$VSS->{ss_error}\n\n(You may find "
677 . "more info on this error at the following website:\n"
678 . "http://msdn.microsoft.com/library/default.asp?url=/library/"
679 . "en-us/guides/html/vsorierrormessages.asp )";
682 if ((defined $SVN) && (defined $SVN->{svn_error})) {
683 $svnerr = "\nLAST SVN COMMAND:\n$SVN->{svn_error}\n";
686 print THE_REAL_STDERR <<"EOERR";
688 ******************************FATAL ERROR********************************
689 *************************************************************************
691 A fatal error has occured. The output from the last VSS or SVN command is
692 below, if available.
694 See $logfile for more information.
695 $vsserr$svnerr
696 EOERR
697 $gCfg{died} = 1;
698 exit(255);
699 } #End MyDie
701 ###############################################################################
702 # Initialize
703 ###############################################################################
704 sub Initialize {
705 GetOptions(\%gCfg,'vssproject=s','vssexclude=s@','svnrepo=s','comment=s',
706 'vsslogin=s','setdates','noprompt','timebias=i','restart',
707 'utf8','debug','help',);
709 &GiveHelp(undef, 1) if defined $gCfg{help};
711 defined $gCfg{vssproject} or GiveHelp("must specify --vssproject\n");
712 defined $gCfg{svnrepo} or GiveHelp("must specify --svnrepo\n");
713 defined $ENV{SSDIR} or GiveHelp("\$SSDIR not defined\n");
715 if ($gCfg{utf8} && ! $gCfg{allowUtf8}) {
716 my $msg = <<"EOMSG";
717 ERROR: UTF-8 support is only available with the "Encoding" module, which
718 requires Perl 5.7.3 or higher. You must either install a newer version of Perl
719 or use the statically-compiled version of vss2svn to get UTF-8 support.
720 EOMSG
721 $msg = fill('', '', $msg);
722 die "\n$msg\n";
725 $gCfg{vssproject} =~ s:/$:: unless $gCfg{vssproject} eq '$/';
726 $gCfg{vssprojmatch} = quotemeta( $gCfg{vssproject} );
728 @{ $gCfg{vssexclude} } = split(',', join(',' ,@{ $gCfg{vssexclude} } ))
729 if defined $gCfg{vssexclude};
731 $gCfg{ssbin} = &CheckForExe
732 ("ss.exe", "the Microsoft Visual SourceSafe client");
734 $gCfg{svnbin} = &CheckForExe("svn.exe", "the Subversion client");
736 my $vss_args = {
737 interactive => 'Y',
738 timebias => $gCfg{timebias},
741 if (defined $gCfg{vsslogin}) {
742 @{ $vss_args }{'user', 'passwd'} = split(':', $gCfg{vsslogin});
743 warn "\nATTENTION: about to issue VSS login command; if program\n"
744 . "hangs here, you have specified an invalid VSS username\n"
745 . "or password. (Press CTRL+Break to kill hung script)\n\n";
748 $VSS = Vss2Svn::VSS->new($ENV{SSDIR}, $gCfg{vssproject}, $vss_args);
749 $VSS->{_debug} = 1;
751 $SVN = Vss2Svn::Subversion->new( $gCfg{svnrepo} );
752 $SVN->{interactive} = 0;
753 $SVN->{user} = 'vss_migration';
754 $SVN->{passwd} = ''; # all passwords are blank
755 $SVN->{_debug} = 1;
757 %USERS = ( vss_migration => 1, );
759 $gCfg{globalCount} = 1;
760 $gCfg{commitNumber} = 1;
762 $gCfg{workbase} = cwd() . "/_vss2svn";
764 print "\nCleaning up any previous vss2svn runs...\n\n";
765 &RecursiveDelete( $gCfg{workbase} );
766 mkdir $gCfg{workbase} or die "Couldn't create $gCfg{workbase} (does "
767 . "another program have a lock on this directory or its files?)";
769 $gCfg{workdir} = "$gCfg{workbase}/work";
770 mkdir $gCfg{workdir} or die "Couldn't create $gCfg{workdir}";
772 $gCfg{importdir} = "$gCfg{workbase}/import";
773 mkdir $gCfg{importdir} or die "Couldn't create $gCfg{importdir}";
775 $gCfg{tmpfiledir} = "$gCfg{workbase}/tmpfile";
776 mkdir $gCfg{tmpfiledir} or die "Couldn't create $gCfg{tmpfiledir}";
778 $gCfg{dbdir} = "$gCfg{workbase}/db";
779 mkdir $gCfg{dbdir} or die "Couldn't create $gCfg{dbdir}";
781 $VSS->{use_tempfiles} = "$gCfg{tmpfiledir}";
785 ###############################################################################
786 # Regionalize
787 ###############################################################################
788 sub Regionalize {
789 my $bias = $Registry->{'HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/'
790 .'Control/TimeZoneInformation/ActiveTimeBias'} || 0;
792 use integer; # forces Perl to interpret two's-complement correctly
793 $gCfg{timebias} = hex($bias) + 0;
798 ###############################################################################
799 # CheckForExe
800 ###############################################################################
801 sub CheckForExe {
802 my($exe, $desc) = @_;
804 foreach my $dir (split ';', ".;$ENV{PATH}") {
805 $dir =~ s/"//g;
806 if (-f "$dir\\$exe") {
807 return "$dir\\$exe";
811 my $msg = fill('', '', <<"EOMSG");
812 Could not find executable '$exe' in your \%PATH\%. Ensure $desc is properly
813 installed on this computer, and manually add the directory in which '$exe' is
814 located to your path if necessary.
816 \%PATH\% currently contains:
817 EOMSG
819 die "$msg\n$ENV{PATH}\n";
822 ###############################################################################
823 # CreateDatabase
824 ###############################################################################
825 sub CreateDatabase {
826 $gCfg{dbh} = DBI->connect("dbi:SQLite(RaiseError=>1,AutoCommit=>0)"
827 . ":dbname=$gCfg{dbdir}/vss2svn.db","","");
828 my $cmd;
830 $cmd = <<"EOSQL";
831 CREATE TABLE vss2svn_history
833 date long NOT NULL,
834 time long NOT NULL,
835 file varchar(1024) NOT NULL,
836 version long NOT NULL,
837 user varchar(256) NOT NULL,
838 comment blob NOT NULL,
839 imported integer NOT NULL,
840 global_count long NOT NULL
842 EOSQL
844 $gCfg{dbh}->do($cmd) or die;
846 $cmd = <<"EOSQL";
847 CREATE TABLE vss2svn_status
849 code long NOT NULL,
850 desc varchar(1024) NOT NULL,
851 datestamp long NOT NULL
853 EOSQL
855 $gCfg{dbh}->do($cmd) or die;
856 } #End CreateDatabase
858 ###############################################################################
859 # CloseDatabase
860 ###############################################################################
861 sub CloseDatabase {
862 $gCfg{dbh}->commit;
863 $gCfg{dbh}->disconnect;
864 } #End CloseDatabase
866 ###############################################################################
867 # SetStatus
868 ###############################################################################
869 sub SetStatus {
870 my($status, $desc) = @_;
871 $desc = $gCfg{dbh}->quote($desc);
872 my $now = time;
874 my $cmd = <<"EOSQL";
875 INSERT INTO
876 vss2svn_status (
877 code,
878 desc,
879 datestamp
881 VALUES (
882 $status,
883 $desc,
884 $now
886 EOSQL
888 $gCfg{dbh}->do($cmd) or die;
889 $gCfg{dbh}->commit;
890 } # End SetStatus
892 ###############################################################################
893 # GiveHelp
894 ###############################################################################
895 sub GiveHelp {
896 my($msg, $verbose) = @_;
897 $msg .= "\n" if defined $msg;
899 $msg .= "USE --help TO VIEW FULL HELP INFORMATION\n" unless $verbose;
901 if ($0 =~ /exe$/) {
902 &GiveExeHelp($msg, $verbose); # will be created by .exe build script
905 pod2usage(
907 -message => $msg,
908 -verbose => $verbose,
909 -exitval => $verbose, # if user requested --help, go to STDOUT
913 } #End GiveHelp
917 sub first(&@) {
918 my $code = shift;
919 &$code && return $_ for @_;
920 return undef;
924 ## EXE PRECOMPILE HERE
928 __END__
929 =pod
931 =head1 LICENSE
933 vss2svn.pl, Copyright (C) 2004 by Toby Johnson.
935 This program is free software; you can redistribute it and/or
936 modify it under the terms of the GNU General Public License
937 as published by the Free Software Foundation; either version 2
938 of the License, or (at your option) any later version.
940 This program is distributed in the hope that it will be useful,
941 but WITHOUT ANY WARRANTY; without even the implied warranty of
942 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
943 GNU General Public License for more details.
944 L<http://www.gnu.org/copyleft/gpl.html>
946 =head1 SYNOPSIS
948 vss2svn.pl S<--vssproject $/vss/project> S<--svnrepo http://svn/repo/url>
950 =over 4
952 =item --vssproject:
954 full path to VSS project you want to migrate
956 =item --svnrepo:
958 URL to target Subversion repository
960 =back
962 =head1 OPTIONS
964 =over 4
966 =item --exclude <EXCLUDE_PROJECTS>:
968 Exclude the given projects from the migration. To list multiple projects,
969 separate with commas or use multiple --exclude commands.
971 Each project can be given as an absolute path (beginning with $/) or
972 relative to --vssproject.
974 =item --comment "MESSAGE":
976 add MESSAGE to end of every migrated comment
978 =item --setdates:
980 Sets the "svn:date" property off all commits to reflect the
981 original VSS commit date, so that the original commit dates
982 (and not today's date) show up in your new SVN logs. This is
983 not the default, since setting svn:date could lead to
984 problems if not done correctly. Using this also requires the
985 "pre-revprop-change" Hook Script to be set; see
986 L<http://svnbook.red-bean.com/svnbook/ch05s02.html#svn-ch-5-sect-2.1>
988 =item --vsslogin "USER:PASSWD":
990 Set VSS username and password, separated by a colon.
991 B<WARNING --> if the username/password combo you provide is
992 incorrect, this program will hang as ss.exe prompts you for
993 a username! (This is an unavoidable Microsoft bug).
995 =item --timebias <OFFSET_MINUTES>:
997 Override the script's guess as to the number of minutes it should
998 add to your local time to get to GMT (for example, if you are
999 in Eastern Daylight Time [-0400], this should be 240).
1001 =item --utf8:
1003 Some users with non-English locales may find that the svn client
1004 causes errors when importing comments containing non-English
1005 characters. If this is the case with you, use this switch to
1006 explicitly convert all comment messages to UTF-8 before importing
1007 to Subversion.
1009 =item --noprompt:
1011 Don't prompt to confirm settings or to create usernames after
1012 the first stage.
1014 =item --debug:
1016 Print all program output to screen as well as logfile.
1018 =back