auto_props: forgot to specify the config file from the commandline
[vss2svn.git] / script / vss2svn.pl
blob614b9833c639f7c7fb4a617ce2431a8da6f1b3fb
1 #!/usr/bin/perl
3 use warnings;
4 use strict;
6 use Getopt::Long;
7 use DBI;
8 use DBD::SQLite2;
9 use XML::Simple;
10 use File::Find;
11 use File::Path;
12 use Time::CTime;
13 use Data::Dumper;
14 use Benchmark ':hireswallclock';
16 use lib '.';
17 use Vss2Svn::ActionHandler;
18 use Vss2Svn::DataCache;
19 use Vss2Svn::SvnRevHandler;
20 use Vss2Svn::Dumpfile;
22 require Encode;
24 our(%gCfg, %gSth, %gErr, %gFh, $gSysOut, %gActionType, %gNameLookup, %gId);
26 our $VERSION = '0.10';
28 &Initialize;
29 &ConnectDatabase;
31 &SetupGlobals;
32 &ShowHeader;
34 &RunConversion;
36 &ShowSummary;
37 &DisconnectDatabase;
39 ###############################################################################
40 # RunConversion
41 ###############################################################################
42 sub RunConversion {
44 # store a hash of actions to take; allows restarting in case of failed
45 # migration
46 my %joblist =
48 INIT => {handler => sub{ 1; },
49 next => 'LOADVSSNAMES'},
51 # Load the "real" names associated with the stored "short" names
52 LOADVSSNAMES => {handler => \&LoadVssNames,
53 next => 'FINDDBFILES'},
55 # Add a stub entry into the Physical table for each physical
56 # file in the VSS DB
57 FINDDBFILES => {handler => \&FindPhysDbFiles,
58 next => 'GETPHYSHIST'},
60 # Load the history of what happened to the physical files. This
61 # only gets us halfway there because we don't know what the real
62 # filenames are yet
63 GETPHYSHIST => {handler => \&GetPhysVssHistory,
64 next => 'MERGEPARENTDATA'},
66 # Merge data from parent records into child records where possible
67 MERGEPARENTDATA => {handler => \&MergeParentData,
68 next => 'MERGEMOVEDATA'},
70 # Merge data from move actions
71 MERGEMOVEDATA => {handler => \&MergeMoveData,
72 next => 'BUILDACTIONHIST'},
74 # Take the history of physical actions and convert them to VSS
75 # file actions
76 BUILDACTIONHIST => {handler => \&BuildVssActionHistory,
77 next => 'IMPORTSVN'},
79 # Create a dumpfile or import to repository
80 IMPORTSVN => {handler => \&ImportToSvn,
81 next => 'DONE'},
84 my $info;
86 while ($gCfg{task} ne 'DONE') {
87 $info = $joblist{ $gCfg{task} }
88 or die "FATAL ERROR: Unknown task '$gCfg{task}'\n";
90 print "TASK: $gCfg{task}\n";
91 push @{ $gCfg{tasks} }, $gCfg{task};
93 if ($gCfg{prompt}) {
94 print "Press ENTER to continue...\n";
95 my $temp = <STDIN>;
96 die if $temp =~ m/^quit/i;
99 &{ $info->{handler} };
100 &SetSystemTask( $info->{next} );
103 } # End RunConversion
105 ###############################################################################
106 # LoadVssNames
107 ###############################################################################
108 sub LoadVssNames {
109 &DoSsCmd("info -e$gCfg{encoding} \"$gCfg{vssdatadir}/names.dat\"");
111 my $xs = XML::Simple->new(KeyAttr => [],
112 ForceArray => [qw(NameCacheEntry Entry)],);
114 my $xml = $xs->XMLin($gSysOut);
116 my $namesref = $xml->{NameCacheEntry} || return 1;
118 my($entry, $count, $offset, $name);
120 my $cache = Vss2Svn::DataCache->new('NameLookup')
121 || &ThrowError("Could not create cache 'NameLookup'");
123 ENTRY:
124 foreach $entry (@$namesref) {
125 $count = $entry->{NrOfEntries};
126 $offset = $entry->{offset};
128 # The cache can contain 4 different entries:
129 # id=1: abbreviated DOS 8.3 name for file items
130 # id=2: full name for file items
131 # id=3: abbreviated 27.3 name for file items
132 # id=10: full name for project items
133 # Both ids 1 and 3 are not of any interest for us, since they only
134 # provide abbreviated names for different szenarios. We are only
135 # interested if we have id=2 for file items, or id=10 for project
136 # items.
137 foreach $name (@{$entry->{Entry}}) {
138 if ($name->{id} == 10 || $name->{id} == 2) {
139 $cache->add($offset, $name->{content});
144 $cache->commit();
145 } # End LoadVssNames
147 ###############################################################################
148 # FindPhysDbFiles
149 ###############################################################################
150 sub FindPhysDbFiles {
152 my $cache = Vss2Svn::DataCache->new('Physical')
153 || &ThrowError("Could not create cache 'Physical'");
155 find(sub{ &FoundSsFile($cache) }, $gCfg{vssdatadir});
157 $cache->commit();
159 } # End FindPhysDbFiles
161 ###############################################################################
162 # FoundSsFile
163 ###############################################################################
164 sub FoundSsFile {
165 my($cache) = @_;
167 my $path = $File::Find::name;
168 return if (-d $path);
170 my $vssdatadir = quotemeta($gCfg{vssdatadir});
172 if ($path =~ m:^$vssdatadir/./([a-z]{8})$:i) {
173 $cache->add(uc($1));
176 } # End FoundSsFile
178 ###############################################################################
179 # GetPhysVssHistory
180 ###############################################################################
181 sub GetPhysVssHistory {
182 my($sql, $sth, $row, $physname, $physdir);
184 &LoadNameLookup;
185 my $cache = Vss2Svn::DataCache->new('PhysicalAction', 1)
186 || &ThrowError("Could not create cache 'PhysicalAction'");
188 $sql = "SELECT * FROM Physical";
189 $sth = $gCfg{dbh}->prepare($sql);
190 $sth->execute();
192 my $xs = XML::Simple->new(ForceArray => [qw(Version)]);
194 while (defined($row = $sth->fetchrow_hashref() )) {
195 $physname = $row->{physname};
197 $physdir = "$gCfg{vssdir}/data";
198 my $physfolder = substr($physname, 0, 1);
200 &GetVssPhysInfo($cache, $physdir, $physfolder, $physname, $xs);
203 $cache->commit();
205 } # End GetPhysVssHistory
207 ###############################################################################
208 # FindPhysnameFile
209 ###############################################################################
210 sub FindPhysnameFile {
211 my($physdir, $physfolder, $physname) = @_;
213 # return it if we can find it without any alteration
214 return ($physdir, $physfolder, $physname) if -f "$physdir/$physfolder/$physname";
215 my $lcphysname = lc($physname);
216 my $lcphysfolder = lc($physfolder);
218 # try finding lowercase folder/filename
219 return ($physdir, $lcphysfolder, $lcphysname) if -f "$physdir/$lcphysfolder/$lcphysname";
221 # try finding lowercase folder/uppercase filename
222 return ($physdir, $lcphysfolder, $physname) if -f "$physdir/$lcphysfolder/$physname";
224 # haven't seen this one, but try it...
225 return ($physdir, $physfolder, $lcphysname) if -f "$physdir/$physfolder/$lcphysname";
227 # no idea what to return...
228 return (undef, undef, undef);
231 ###############################################################################
232 # GetVssPhysInfo
233 ###############################################################################
234 sub GetVssPhysInfo {
235 my($cache, $physdir, $physfolder, $physname, $xs) = @_;
237 my @filesegment = &FindPhysnameFile($physdir, $physfolder, $physname);
239 print "physdir: \"$filesegment[0]\", physfolder: \"$filesegment[1]\" physname: \"$filesegment[2]\"\n" if $gCfg{debug};
241 if (!defined $filesegment[0] || !defined $filesegment[1]
242 || !defined $filesegment[2]) {
243 # physical file doesn't exist; it must have been destroyed later
244 &ThrowWarning("Can't retrieve info from physical file "
245 . "'$physname'; it was either destroyed or corrupted");
246 return;
249 &DoSsCmd("info -e$gCfg{encoding} \"$filesegment[0]/$filesegment[1]/$filesegment[2]\"");
251 my $xml = $xs->XMLin($gSysOut);
252 my $parentphys;
254 my $iteminfo = $xml->{ItemInfo};
256 if (!defined($iteminfo) || !defined($iteminfo->{Type}) ||
257 ref($iteminfo->{Type})) {
259 &ThrowWarning("Can't handle file '$physname'; not a project or file\n");
260 return;
263 if ($iteminfo->{Type} == 1) {
264 $parentphys = (uc($physname) eq 'AAAAAAAA')?
265 '' : &GetProjectParent($xml);
266 } elsif ($iteminfo->{Type} == 2) {
267 $parentphys = undef;
268 } else {
269 &ThrowWarning("Can't handle file '$physname'; not a project or file\n");
270 return;
273 &GetVssItemVersions($cache, $physname, $parentphys, $xml);
275 } # End GetVssPhysInfo
277 ###############################################################################
278 # GetProjectParent
279 ###############################################################################
280 sub GetProjectParent {
281 my($xml) = @_;
283 no warnings 'uninitialized';
284 return $xml->{ItemInfo}->{ParentPhys} || undef;
286 } # End GetProjectParent
288 ###############################################################################
289 # GetVssItemVersions
290 ###############################################################################
291 sub GetVssItemVersions {
292 my($cache, $physname, $parentphys, $xml) = @_;
294 return 0 unless defined $xml->{Version};
296 my($parentdata, $version, $vernum, $action, $name, $actionid, $actiontype,
297 $tphysname, $itemname, $itemtype, $parent, $user, $timestamp, $comment,
298 $is_binary, $info, $priority, $sortkey, $label, $cachename);
300 VERSION:
301 foreach $version (@{ $xml->{Version} }) {
302 $action = $version->{Action};
303 $name = $action->{SSName};
304 $tphysname = $action->{Physical} || $physname;
305 $user = $version->{UserName};
306 $timestamp = $version->{Date};
308 $itemname = &GetItemName($name);
310 $actionid = $action->{ActionId};
311 $info = $gActionType{$actionid};
313 if (!$info) {
314 warn "\nWARNING: Unknown action '$actionid'\n";
315 next VERSION;
318 $itemtype = $info->{type};
319 $actiontype = $info->{action};
321 if ($actiontype eq 'IGNORE') {
322 next VERSION;
325 $comment = undef;
326 $is_binary = 0;
327 $info = undef;
328 $parentdata = 0;
329 $priority = 5;
330 $label = undef;
332 if ($version->{Comment} && !ref($version->{Comment})) {
333 $comment = $version->{Comment} || undef;
336 # In case of Label the itemtype is the type of the item currently
337 # under investigation
338 if ($actiontype eq 'LABEL') {
339 my $iteminfo = $xml->{ItemInfo};
340 $itemtype = $iteminfo->{Type};
344 # we can have label actions and labes attached to versions
345 if (defined $action->{Label} && !ref($action->{Label})) {
346 $label = $action->{Label};
348 # append the label comment to a possible version comment
349 if ($action->{LabelComment} && !ref($action->{LabelComment})) {
350 if (defined $comment) {
351 print "Merging LabelComment and Comment for "
352 . "'$tphysname;$version->{VersionNumber}'\n"; # if $gCfg{verbose};
353 $comment .= "\n";
356 $comment .= $action->{LabelComment} || undef;
360 if (defined($comment)) {
361 $comment =~ s/^\s+//s;
362 $comment =~ s/\s+$//s;
365 if ($itemtype == 1 && uc($physname) eq 'AAAAAAAA'
366 && ref($tphysname)) {
368 $tphysname = $physname;
369 $itemname = '';
370 } elsif ($physname ne $tphysname) {
371 # If version's physical name and file's physical name are different,
372 # this is a project describing an action on a child item. Most of
373 # the time, this very same data will be in the child's physical
374 # file and with more detail (such as check-in comment).
376 # However, in some cases (such as renames, or when the child's
377 # physical file was later purged), this is the only place we'll
378 # have the data; also, sometimes the child record doesn't even
379 # have enough information about itself (such as which project it
380 # was created in and which project(s) it's shared in).
382 # So, for a parent record describing a child action, we'll set a
383 # flag, then combine them in the next phase.
385 $parentdata = 1;
387 # OK, since we're describing an action in the child, the parent is
388 # actually this (project) item
390 $parentphys = $physname;
391 } else {
392 $parentphys = undef;
395 if ($itemtype == 1) {
396 $itemname .= '/';
397 } elsif (defined($xml->{ItemInfo}) &&
398 defined($xml->{ItemInfo}->{Binary}) &&
399 $xml->{ItemInfo}->{Binary}) {
401 $is_binary = 1;
404 if ($actiontype eq 'RENAME') {
405 # if a rename, we store the new name in the action's 'info' field
407 $info = &GetItemName($action->{NewSSName});
409 if ($itemtype == 1) {
410 $info .= '/';
412 } elsif ($actiontype eq 'BRANCH') {
413 $info = $action->{Parent};
416 $vernum = ($parentdata)? undef : $version->{VersionNumber};
418 # since there is no corresponding client action for PIN, we need to
419 # enter the concrete version number here manually
420 # In a share action the pinnedToVersion attribute can also be set
421 # if ($actiontype eq 'PIN') {
422 $vernum = $action->{PinnedToVersion} if (defined $action->{PinnedToVersion});
425 $priority -= 4 if $actiontype eq 'ADD'; # Adds are always first
426 $priority -= 3 if $actiontype eq 'SHARE';
427 $priority -= 3 if $actiontype eq 'PIN';
428 $priority -= 2 if $actiontype eq 'BRANCH';
430 # store the reversed physname as a sortkey; a bit wasteful but makes
431 # debugging easier for the time being...
432 $sortkey = reverse($tphysname);
434 $cache->add($tphysname, $vernum, $parentphys, $actiontype, $itemname,
435 $itemtype, $timestamp, $user, $is_binary, $info, $priority,
436 $sortkey, $parentdata, $label, $comment);
440 } # End GetVssItemVersions
442 ###############################################################################
443 # GetItemName
444 ###############################################################################
445 sub GetItemName {
446 my($nameelem) = @_;
448 my $itemname = $nameelem->{content};
450 if (defined($nameelem->{offset})) {
451 # see if we have a better name in the cache
452 my $cachename = $gNameLookup{ $nameelem->{offset} };
454 if (defined($cachename)) {
455 print "Changing name of '$itemname' to '$cachename' from "
456 . "name cache\n" if $gCfg{debug};
457 $itemname = $cachename;
461 return $itemname;
463 } # End GetItemName
465 ###############################################################################
466 # LoadNameLookup
467 ###############################################################################
468 sub LoadNameLookup {
469 my($sth, $row);
471 $sth = $gCfg{dbh}->prepare('SELECT offset, name FROM NameLookup');
472 $sth->execute();
474 while(defined($row = $sth->fetchrow_hashref() )) {
475 $gNameLookup{ $row->{offset} } = Encode::decode_utf8( $row->{name} );
477 } # End LoadNameLookup
479 ###############################################################################
480 # MergeParentData
481 ###############################################################################
482 sub MergeParentData {
483 # VSS has a funny way of not placing enough information to rebuild history
484 # in one data file; for example, renames are stored in the parent project
485 # rather than in that item's data file. Also, it's sometimes impossible to
486 # tell from a child record which was eventually shared to multiple folders,
487 # which folder it was originally created in.
489 # So, at this stage we look for any parent records which described child
490 # actions, then update those records with data from the child objects. We
491 # then delete the separate child objects to avoid duplication.
493 my($sth, $rows, $row);
494 $sth = $gCfg{dbh}->prepare('SELECT * FROM PhysicalAction '
495 . 'WHERE parentdata = 1');
496 $sth->execute();
498 # need to pull in all recs at once, since we'll be updating/deleting data
499 $rows = $sth->fetchall_arrayref( {} );
501 my($childrecs, $child, $id);
502 my @delchild = ();
504 foreach $row (@$rows) {
505 $childrecs = &GetChildRecs($row);
507 if (scalar @$childrecs > 1) {
508 &ThrowWarning("Multiple child recs for parent rec "
509 . "'$row->{action_id}'");
512 foreach $child (@$childrecs) {
513 &UpdateParentRec($row, $child);
514 push(@delchild, $child->{action_id});
518 foreach $id (@delchild) {
519 &DeleteChildRec($id);
524 } # End MergeParentData
526 ###############################################################################
527 # GetChildRecs
528 ###############################################################################
529 sub GetChildRecs {
530 my($parentrec, $parentdata) = @_;
532 # Here we need to find any child rows which give us additional info on the
533 # parent rows. There's no definitive way to find matching rows, but joining
534 # on physname, actiontype, timestamp, and author gets us close. The problem
535 # is that the "two" actions may not have happened in the exact same second,
536 # so we need to also look for any that are some time apart and hope
537 # we don't get the wrong row.
539 $parentdata = 0 unless defined $parentdata;
541 my $sql = <<"EOSQL";
542 SELECT
544 FROM
545 PhysicalAction
546 WHERE
547 parentdata = ?
548 AND physname = ?
549 AND actiontype = ?
550 AND author = ?
551 ORDER BY
552 ABS(? - timestamp)
553 EOSQL
555 my $sth = $gCfg{dbh}->prepare($sql);
556 $sth->execute( $parentdata, @{ $parentrec }{qw(physname actiontype author timestamp)} );
558 return $sth->fetchall_arrayref( {} );
559 } # End GetChildRecs
561 ###############################################################################
562 # UpdateParentRec
563 ###############################################################################
564 sub UpdateParentRec {
565 my($row, $child) = @_;
567 # The child record has the "correct" version number (relative to the child
568 # and not the parent), as well as the comment info and whether the file is
569 # binary
571 my $comment;
574 no warnings 'uninitialized';
575 $comment = "$row->{comment}\n$child->{comment}";
576 $comment =~ s/\n$//;
579 my $sql = <<"EOSQL";
580 UPDATE
581 PhysicalAction
583 version = ?,
584 is_binary = ?,
585 comment = ?
586 WHERE
587 action_id = ?
588 EOSQL
590 my $sth = $gCfg{dbh}->prepare($sql);
591 $sth->execute( $child->{version}, $child->{is_binary}, $comment,
592 $row->{action_id} );
594 } # End UpdateParentRec
596 ###############################################################################
597 # MergeMoveData
598 ###############################################################################
599 sub MergeMoveData {
600 # Similar to the MergeParentData, the MergeMove Data combines two the src
601 # and target move actions into one move action. Since both items are parents
602 # the MergeParentData function can not deal with this specific problem
604 my($sth, $rows, $row);
605 $sth = $gCfg{dbh}->prepare('SELECT * FROM PhysicalAction '
606 . 'WHERE actiontype = "MOVE_FROM"');
607 $sth->execute();
609 # need to pull in all recs at once, since we'll be updating/deleting data
610 $rows = $sth->fetchall_arrayref( {} );
612 my($childrecs, $child, $id);
613 my @delchild = ();
615 foreach $row (@$rows) {
616 $row->{actiontype} = 'MOVE';
617 $childrecs = &GetChildRecs($row, 1);
619 if (scalar @$childrecs > 1) {
620 &ThrowWarning("Multiple chidl recs for parent MOVE rec "
621 . "'$row->{action_id}'");
624 foreach $child (@$childrecs) {
625 my $update;
626 $update = $gCfg{dbh}->prepare('UPDATE PhysicalAction SET info = ?'
627 . 'WHERE action_id = ?');
629 $update->execute( $row->{parentphys}, $child->{action_id} );
632 if (scalar @$childrecs == 0) {
633 my $sql = <<"EOSQL";
634 UPDATE
635 PhysicalAction
637 parentphys = ?,
638 actiontype = 'MOVE',
639 info = ?
640 WHERE
641 action_id = ?
642 EOSQL
643 my $update;
644 $update = $gCfg{dbh}->prepare($sql);
645 $update->execute( undef, $row->{parentphys},
646 $row->{action_id});
647 } else {
648 push(@delchild, $row->{action_id});
652 foreach $id (@delchild) {
653 &DeleteChildRec($id);
658 } # End MergeMoveData
660 ###############################################################################
661 # DeleteChildRec
662 ###############################################################################
663 sub DeleteChildRec {
664 my($id) = @_;
666 my $sql = "DELETE FROM PhysicalAction WHERE action_id = ?";
668 my $sth = $gCfg{dbh}->prepare($sql);
669 $sth->execute($id);
670 } # End DeleteChildRec
672 ###############################################################################
673 # BuildVssActionHistory
674 ###############################################################################
675 sub BuildVssActionHistory {
676 my $vsscache = Vss2Svn::DataCache->new('VssAction', 1)
677 || &ThrowError("Could not create cache 'VssAction'");
679 my $joincache = Vss2Svn::DataCache->new('SvnRevisionVssAction')
680 || &ThrowError("Could not create cache 'SvnRevisionVssAction'");
682 my $labelcache = Vss2Svn::DataCache->new('Label')
683 || &ThrowError("Could not create cache 'Label'");
685 # This will keep track of the current SVN revision, and increment it when
686 # the author or comment changes, the timestamps span more than an hour
687 # (by default), or the same physical file is affected twice
689 my $svnrevs = Vss2Svn::SvnRevHandler->new()
690 || &ThrowError("Could not create SVN revision handler");
691 $svnrevs->{verbose} = $gCfg{verbose};
693 my($sth, $row, $action, $handler, $physinfo, $itempaths, $allitempaths);
695 my $sql = 'SELECT * FROM PhysicalAction ORDER BY timestamp ASC, '
696 . 'itemtype ASC, priority ASC, sortkey ASC, action_id ASC';
698 $sth = $gCfg{dbh}->prepare($sql);
699 $sth->execute();
701 ROW:
702 while(defined($row = $sth->fetchrow_hashref() )) {
703 $action = $row->{actiontype};
705 $handler = Vss2Svn::ActionHandler->new($row);
706 $handler->{verbose} = $gCfg{verbose};
707 $handler->{trunkdir} = $gCfg{trunkdir};
708 $physinfo = $handler->physinfo();
710 if (defined($physinfo) && $physinfo->{type} != $row->{itemtype} ) {
711 &ThrowError("Inconsistent item type for '$row->{physname}'; "
712 . "'$row->{itemtype}' unexpected");
715 $row->{itemname} = Encode::decode_utf8( $row->{itemname} );
716 $row->{info} = Encode::decode_utf8( $row->{info} );
717 $row->{comment} = Encode::decode_utf8( $row->{comment} );
718 $row->{author} = Encode::decode_utf8( $row->{author} );
719 $row->{label} = Encode::decode_utf8( $row->{label} );
721 # The handler's job is to keep track of physical-to-real name mappings
722 # and return the full item paths corresponding to the physical item. In
723 # case of a rename, it will return the old name, so we then do another
724 # lookup on the new name.
726 # Commits and renames can apply to multiple items if that item is
727 # shared; since SVN has no notion of such shares, we keep track of
728 # those ourself and replicate the functionality using multiple actions.
730 if (!$handler->handle($action)) {
731 &ThrowWarning($handler->{errmsg})
732 if $handler->{errmsg};
733 next ROW;
736 $itempaths = $handler->{itempaths};
738 # In cases of a corrupted share source, the handler may change the
739 # action from 'SHARE' to 'ADD'
740 $row->{actiontype} = $handler->{action};
742 if (!defined $itempaths) {
743 # Couldn't determine name of item
744 &ThrowWarning($handler->{errmsg})
745 if $handler->{errmsg};
747 # If we were adding or modifying a file, commit it to lost+found;
748 # otherwise give up on it
749 if ($row->{itemtype} == 2 && ($row->{actiontype} eq 'ADD' ||
750 $row->{actiontype} eq 'COMMIT')) {
752 $itempaths = [undef];
753 } else {
754 next ROW;
758 # we need to check for the next rev number, after all pathes that can
759 # prematurally call the next row. Otherwise, we get an empty revision.
760 $svnrevs->check($row);
762 # May contain add'l info for the action depending on type:
763 # RENAME: the new name (without path)
764 # SHARE: the source path which was shared
765 # MOVE: the new path
766 # PIN: the path of the version that was pinned
767 # LABEL: the name of the label
768 $row->{info} = $handler->{info};
770 # The version may have changed
771 if (defined $handler->{version}) {
772 $row->{version} = $handler->{version};
775 $allitempaths = join("\t", @$itempaths);
776 $row->{itempaths} = $allitempaths;
778 $vsscache->add(@$row{ qw(parentphys physname version actiontype itempaths
779 itemtype is_binary info) });
780 $joincache->add( $svnrevs->{revnum}, $vsscache->{pkey} );
782 if (defined $row->{label}) {
783 $labelcache->add(@$row{ qw(physname version label itempaths) });
788 $vsscache->commit();
789 $svnrevs->commit();
790 $joincache->commit();
791 $labelcache->commit();
793 } # End BuildVssActionHistory
795 ###############################################################################
796 # ImportToSvn
797 ###############################################################################
798 sub ImportToSvn {
799 # For the time being, we support only creating a dumpfile and not directly
800 # importing to SVN. We could perhaps add this functionality by making the
801 # CreateSvnDumpfile logic more generic and using polymorphism to switch out
802 # the Vss2Svn::Dumpfile object with one that handles imports.
804 &CreateSvnDumpfile;
805 } # End ImportToSvn
807 ###############################################################################
808 # CreateSvnDumpfile
809 ###############################################################################
810 sub CreateSvnDumpfile {
811 my $fh;
813 my $file = $gCfg{dumpfile};
814 open $fh, ">$file"
815 or &ThrowError("Could not create dumpfile '$file'");
817 my($sql, $sth, $action_sth, $row, $revision, $actions, $action, $physname, $itemtype);
819 my %exported = ();
821 $sql = 'SELECT * FROM SvnRevision ORDER BY revision_id ASC';
823 $sth = $gCfg{dbh}->prepare($sql);
824 $sth->execute();
826 $sql = <<"EOSQL";
827 SELECT * FROM
828 VssAction
829 WHERE action_id IN
830 (SELECT action_id FROM SvnRevisionVssAction WHERE revision_id = ?)
831 ORDER BY action_id
832 EOSQL
834 $action_sth = $gCfg{dbh}->prepare($sql);
836 my $autoprops = Vss2Svn::Dumpfile::AutoProps->new($gCfg{auto_props}) if $gCfg{auto_props};
837 my $dumpfile = Vss2Svn::Dumpfile->new($fh, $autoprops);
839 REVISION:
840 while(defined($row = $sth->fetchrow_hashref() )) {
842 my $t0 = new Benchmark;
844 $revision = $row->{revision_id};
845 $dumpfile->begin_revision($row);
847 # next REVISION if $revision == 0;
849 $action_sth->execute($revision);
850 $actions = $action_sth->fetchall_arrayref( {} );
852 ACTION:
853 foreach $action(@$actions) {
854 $physname = $action->{physname};
855 $itemtype = $action->{itemtype};
857 if (!exists $exported{$physname}) {
858 if ($itemtype == 2) {
859 $exported{$physname} = &ExportVssPhysFile($physname, $action->{version});
860 } else {
861 $exported{$physname} = undef;
865 # do_action needs to know the revision_id, so paste it on
866 $action->{revision_id} = $revision;
868 $dumpfile->do_action($action, $exported{$physname});
870 print "revision $revision: ", timestr(timediff(new Benchmark, $t0)),"\n"
871 if $gCfg{timing};
874 my @err = @{ $dumpfile->{errors} };
876 if (scalar @err > 0) {
877 map { &ThrowWarning($_) } @err;
880 $dumpfile->finish();
881 close $fh;
883 } # End CreateSvnDumpfile
885 ###############################################################################
886 # ExportVssPhysFile
887 ###############################################################################
888 sub ExportVssPhysFile {
889 my($physname, $version) = @_;
891 $physname =~ m/^((.).)/;
893 my $exportdir = "$gCfg{vssdata}/$1";
894 my @filesegment = &FindPhysnameFile("$gCfg{vssdir}/data", $2, $physname);
896 if (!defined $filesegment[0] || !defined $filesegment[1] || !defined $filesegment[2]) {
897 # physical file doesn't exist; it must have been destroyed later
898 &ThrowWarning("Can't retrieve revisions from physical file "
899 . "'$physname'; it was either destroyed or corrupted");
900 return undef;
902 my $physpath = "$filesegment[0]/$filesegment[1]/$filesegment[2]";
904 if (! -f $physpath) {
905 # physical file doesn't exist; it must have been destroyed later
906 &ThrowWarning("Can't retrieve revisions from physical file "
907 . "'$physname'; it was either destroyed or corrupted");
908 return undef;
911 mkpath($exportdir) if ! -e $exportdir;
913 # MergeParentData normally will merge two corresponding item and parent
914 # actions. But if the actions are more appart than the maximum allowed
915 # timespan, we will end up with an undefined version in an ADD action here
916 # As a hot fix, we define the version to 1, which will also revert to the
917 # alpha 1 version behavoir.
918 if (! defined $version) {
919 &ThrowWarning("'$physname': no version specified for retrieval");
921 # fall through and try with version 1.
922 $version = 1;
925 if (! -e "$exportdir/$physname.$version" ) {
926 &DoSsCmd("get -b -v$version --force-overwrite -e$gCfg{encoding} \"$physpath\" $exportdir/$physname");
929 return $exportdir;
930 } # End ExportVssPhysFile
932 ###############################################################################
933 # ShowHeader
934 ###############################################################################
935 sub ShowHeader {
936 my $info = $gCfg{task} eq 'INIT'? 'BEGINNING CONVERSION...' :
937 "RESUMING CONVERSION FROM TASK '$gCfg{task}' AT STEP $gCfg{step}...";
938 my $starttime = ctime($^T);
940 my $ssversion = &GetSsVersion();
942 print <<"EOTXT";
943 ======== VSS2SVN ========
944 $info
945 Start Time : $starttime
947 VSS Dir : $gCfg{vssdir}
948 Temp Dir : $gCfg{tempdir}
949 Dumpfile : $gCfg{dumpfile}
950 VSS Encoding : $gCfg{encoding}
952 SSPHYS exe : $gCfg{ssphys}
953 SSPHYS ver : $ssversion
954 XML Parser : $gCfg{xmlParser}
956 EOTXT
958 my @version = split '\.', $ssversion;
959 # we need at least ssphys 0.22
960 if ($version[0] == 0 && $version[1] < 22) {
961 &ThrowError("The conversion needs at least ssphys version 0.22");
964 } # End ShowHeader
966 ###############################################################################
967 # ShowSummary
968 ###############################################################################
969 sub ShowSummary {
971 if (keys(%gErr) || $gCfg{resume}) {
972 print <<"EOTXT";
973 =============================================================================
974 ERROR SUMMARY
976 EOTXT
978 if($gCfg{resume}) {
979 print <<"EOTXT";
980 **NOTICE** Because this run was resumed from a previous run, this may be only
981 a partial list; other errors may have been reported during previous run.
983 EOTXT
986 foreach my $task (@{ $gCfg{errortasks} }) {
987 print "\n$task:\n ";
988 print join("\n ", @{ $gErr{$task} }),"\n";
992 print <<"EOTXT";
993 =============================================================================
994 END OF CONVERSION
996 The VSS to SVN conversion is complete. You should now use the "svnadmin load"
997 command to load the generated dumpfile '$gCfg{dumpfile}'. The "svnadmin"
998 utility is provided as part of the Subversion command-line toolset; use a
999 command such as the following:
1000 svnadmin load <repodir> < "$gCfg{dumpfile}"
1002 You may need to precede this with "svnadmin create <repodir>" if you have not
1003 yet created a repository. Type "svnadmin help <cmd>" for more information on
1004 "create" and/or "load".
1006 If any errors occurred during the conversion, they are summarized above.
1008 For more information on the vss2svn project, see:
1009 http://www.pumacode.org/projects/vss2svn/
1011 EOTXT
1013 my $starttime = ctime($^T);
1014 chomp $starttime;
1015 my $endtime = ctime(time);
1016 chomp $endtime;
1017 my $elapsed;
1020 use integer;
1021 my $secs = time - $^T;
1023 my $hours = $secs / 3600;
1024 $secs -= ($hours * 3600);
1026 my $mins = $secs / 60;
1027 $secs -= ($mins * 60);
1029 $elapsed = sprintf("%2.2i:%2.2i:%2.2i", $hours, $mins, $secs);
1032 my($actions, $revisions, $mintime, $maxtime) = &GetStats();
1034 print <<"EOTXT";
1035 Started at : $starttime
1036 Ended at : $endtime
1037 Elapsed time : $elapsed (H:M:S)
1039 VSS Actions read : $actions
1040 SVN Revisions converted : $revisions
1041 Date range (YYYY/MM/DD) : $mintime to $maxtime
1043 EOTXT
1045 } # End ShowSummary
1047 ###############################################################################
1048 # GetStats
1049 ###############################################################################
1050 sub GetStats {
1051 my($sql, $actions, $revisions, $mintime, $maxtime);
1053 $sql = <<"EOSQL";
1054 SELECT
1055 COUNT(*)
1056 FROM
1057 VssAction
1058 EOSQL
1060 ($actions) = $gCfg{dbh}->selectrow_array($sql);
1062 $sql = <<"EOSQL";
1063 SELECT
1064 COUNT(*)
1065 FROM
1066 SvnRevision
1067 EOSQL
1069 ($revisions) = $gCfg{dbh}->selectrow_array($sql);
1071 $sql = <<"EOSQL";
1072 SELECT
1073 MIN(timestamp), MAX(timestamp)
1074 FROM
1075 PhysicalAction
1076 EOSQL
1078 ($mintime, $maxtime) = $gCfg{dbh}->selectrow_array($sql);
1080 foreach($mintime, $maxtime) {
1081 $_ = &Vss2Svn::Dumpfile::SvnTimestamp($_);
1082 s:T.*::;
1083 s:-:/:g;
1086 # initial creation of the repo wasn't considered an action or revision
1087 return($actions - 1, $revisions - 1, $mintime, $maxtime);
1089 } # End GetStats
1091 ###############################################################################
1092 # DoSsCmd
1093 ###############################################################################
1094 sub DoSsCmd {
1095 my($cmd) = @_;
1097 my $ok = &DoSysCmd("\"$gCfg{ssphys}\" $cmd", 1);
1099 $gSysOut =~ s/\x00//g; # remove null bytes
1100 $gSysOut =~ s/.\x08//g; # yes, I've seen VSS store backspaces in names!
1101 # allow all characters in the windows-1252 codepage: see http://de.wikipedia.org/wiki/Windows-1252
1102 $gSysOut =~ s/[\x00-\x09\x11\x12\x14-\x1F\x81\x8D\x8F\x90\x9D]/_/g; # just to be sure
1104 } # End DoSsCmd
1106 ###############################################################################
1107 # DoSysCmd
1108 ###############################################################################
1109 sub DoSysCmd {
1110 my($cmd, $allowfail) = @_;
1112 print "$cmd\n" if $gCfg{verbose};
1113 $gSysOut = `$cmd`;
1115 print $gSysOut if $gCfg{debug};
1117 my $rv = 1;
1119 if ($? == -1) {
1120 &ThrowWarning("FAILED to execute: $!");
1121 die unless $allowfail;
1123 $rv = 0;
1124 } elsif ($?) {
1125 &ThrowWarning(sprintf "FAILED with non-zero exit status %d (cmd: %s)", $? >> 8, $cmd);
1126 die unless $allowfail;
1128 $rv = 0;
1131 return $rv;
1133 } # End DoSysCmd
1135 ###############################################################################
1136 # GetSsVersion
1137 ###############################################################################
1138 sub GetSsVersion {
1139 my $out = `\"$gCfg{ssphys}\" --version 2>&1`;
1140 # Build numbers look like:
1141 # a.) ssphys 0.20.0, Build 123
1142 # b.) ssphys 0.20.0, Build 123:150
1143 # c.) ssphys 0.20.0, Build 123:150 (locally modified)
1144 $out =~ m/^ssphys (.*?), Build (.*?)[ \n]/m;
1146 # turn it into
1147 # a.) 0.20.0.123
1148 # b.) 0.20.0.123:150
1149 # c.) 0.20.0.123:150
1150 return $1 . "." . $2 || 'unknown';
1151 } # End GetSsVersion
1153 ###############################################################################
1154 # ThrowWarning
1155 ###############################################################################
1156 sub ThrowWarning {
1157 my($msg, $callinfo) = @_;
1159 $callinfo ||= [caller()];
1161 $msg .= "\nat $callinfo->[1] line $callinfo->[2]";
1163 warn "ERROR -- $msg\n";
1165 my $task = $gCfg{task};
1167 if(!defined $gErr{$task}) {
1168 $gErr{$task} = [];
1169 push @{ $gCfg{errortasks} }, $task;
1172 push @{ $gErr{$task} }, $msg;
1174 } # End ThrowWarning
1176 ###############################################################################
1177 # ThrowError
1178 ###############################################################################
1179 sub ThrowError {
1180 &ThrowWarning(@_, [caller()]);
1181 &StopConversion;
1182 } # End ThrowError
1184 ###############################################################################
1185 # StopConversion
1186 ###############################################################################
1187 sub StopConversion {
1188 &DisconnectDatabase;
1189 &CloseAllFiles;
1191 exit(1);
1192 } # End StopConversion
1194 ###############################################################################
1195 # CloseAllFiles
1196 ###############################################################################
1197 sub CloseAllFiles {
1199 } # End CloseAllFiles
1201 ###############################################################################
1202 # SetSystemTask
1203 ###############################################################################
1204 sub SetSystemTask {
1205 my($task, $leavestep) = @_;
1207 print "\nSETTING TASK $task\n" if $gCfg{verbose};
1209 my($sql, $sth);
1211 $sth = $gSth{'SYSTEMTASK'};
1213 if (!defined $sth) {
1214 $sql = <<"EOSQL";
1215 UPDATE
1216 SystemInfo
1218 task = ?
1219 EOSQL
1221 $sth = $gSth{'SYSTEMTASK'} = $gCfg{dbh}->prepare($sql);
1224 $sth->execute($task);
1226 $gCfg{task} = $task;
1228 &SetSystemStep(0) unless $leavestep;
1230 } # End SetSystemTask
1232 ###############################################################################
1233 # SetSystemStep
1234 ###############################################################################
1235 sub SetSystemStep {
1236 my($step) = @_;
1238 print "\nSETTING STEP $step\n" if $gCfg{verbose};
1240 my($sql, $sth);
1242 $sth = $gSth{'SYSTEMSTEP'};
1244 if (!defined $sth) {
1245 $sql = <<"EOSQL";
1246 UPDATE
1247 SystemInfo
1249 step = ?
1250 EOSQL
1252 $sth = $gCfg{'SYSTEMSTEP'} = $gCfg{dbh}->prepare($sql);
1255 $sth->execute($step);
1257 $gCfg{step} = $step;
1259 } # End SetSystemStep
1261 ###############################################################################
1262 # ConnectDatabase
1263 ###############################################################################
1264 sub ConnectDatabase {
1265 my $db = $gCfg{sqlitedb};
1267 if (-e $db && (!$gCfg{resume} ||
1268 (defined($gCfg{task}) && $gCfg{task} eq 'INIT'))) {
1270 unlink $db or &ThrowError("Could not delete existing database "
1271 .$gCfg{sqlitedb});
1274 print "Connecting to database $db\n\n";
1276 $gCfg{dbh} = DBI->connect("dbi:SQLite2:dbname=$db", '', '',
1277 {RaiseError => 1, AutoCommit => 1})
1278 or die "Couldn't connect database $db: $DBI::errstr";
1280 } # End ConnectDatabase
1282 ###############################################################################
1283 # DisconnectDatabase
1284 ###############################################################################
1285 sub DisconnectDatabase {
1286 $gCfg{dbh}->disconnect if defined $gCfg{dbh};
1287 } # End DisconnectDatabase
1289 ###############################################################################
1290 # SetupGlobals
1291 ###############################################################################
1292 sub SetupGlobals {
1293 if (defined($gCfg{task}) && $gCfg{task} eq 'INIT') {
1294 &InitSysTables;
1295 } else {
1296 &ReloadSysTables;
1299 $gCfg{ssphys} = 'ssphys' if !defined($gCfg{ssphys});
1300 $gCfg{vssdatadir} = "$gCfg{vssdir}/data";
1302 (-d "$gCfg{vssdatadir}") or &ThrowError("$gCfg{vssdir} does not appear "
1303 . "to be a valid VSS database");
1305 &SetupActionTypes;
1307 Vss2Svn::DataCache->SetCacheDir($gCfg{tempdir});
1308 Vss2Svn::DataCache->SetDbHandle($gCfg{dbh});
1309 Vss2Svn::DataCache->SetVerbose($gCfg{verbose});
1311 Vss2Svn::SvnRevHandler->SetRevTimeRange($gCfg{revtimerange})
1312 if defined $gCfg{revtimerange};
1314 } # End SetupGlobals
1316 ###############################################################################
1317 # SetupActionTypes
1318 ###############################################################################
1319 sub SetupActionTypes {
1320 # RollBack is only seen in combiation with a BranchFile activity, so actually
1321 # RollBack is the item view on the activity and BranchFile is the parent side
1322 # ==> map RollBack to BRANCH, so that we can join the two actions in the
1323 # MergeParentData step
1324 # RestoredProject seems to act like CreatedProject, except that the
1325 # project was recreated from an archive file, and its timestamp is
1326 # the time of restoration. Timestamps of the child files retain
1327 # their original values.
1328 %gActionType = (
1329 CreatedProject => {type => 1, action => 'ADD'},
1330 AddedProject => {type => 1, action => 'ADD'},
1331 RestoredProject => {type => 1, action => 'RESTOREDPROJECT'},
1332 RenamedProject => {type => 1, action => 'RENAME'},
1333 MovedProjectTo => {type => 1, action => 'MOVE'},
1334 MovedProjectFrom => {type => 1, action => 'MOVE_FROM'},
1335 DeletedProject => {type => 1, action => 'DELETE'},
1336 DestroyedProject => {type => 1, action => 'DELETE'},
1337 RecoveredProject => {type => 1, action => 'RECOVER'},
1338 ArchiveProject => {type => 1, action => 'DELETE'},
1339 RestoredProject => {type => 1, action => 'RESTORE'},
1340 CheckedIn => {type => 2, action => 'COMMIT'},
1341 CreatedFile => {type => 2, action => 'ADD'},
1342 AddedFile => {type => 2, action => 'ADD'},
1343 RenamedFile => {type => 2, action => 'RENAME'},
1344 DeletedFile => {type => 2, action => 'DELETE'},
1345 DestroyedFile => {type => 2, action => 'DELETE'},
1346 RecoveredFile => {type => 2, action => 'RECOVER'},
1347 ArchiveVersionsofFile => {type => 2, action => 'RESTORE'},
1348 ArchiveFile => {type => 2, action => 'DELETE'},
1349 RestoredFile => {type => 2, action => 'RESTORE'},
1350 SharedFile => {type => 2, action => 'SHARE'},
1351 BranchFile => {type => 2, action => 'BRANCH'},
1352 PinnedFile => {type => 2, action => 'PIN'},
1353 RollBack => {type => 2, action => 'BRANCH'},
1354 UnpinnedFile => {type => 2, action => 'PIN'},
1355 Labeled => {type => 2, action => 'LABEL'},
1358 } # End SetupActionTypes
1360 ###############################################################################
1361 # InitSysTables
1362 ###############################################################################
1363 sub InitSysTables {
1364 my($sql, $sth);
1366 $sql = <<"EOSQL";
1367 CREATE TABLE
1368 Physical (
1369 physname VARCHAR
1371 EOSQL
1373 $sth = $gCfg{dbh}->prepare($sql);
1374 $sth->execute;
1376 $sql = <<"EOSQL";
1377 CREATE TABLE
1378 NameLookup (
1379 offset INTEGER,
1380 name VARCHAR
1382 EOSQL
1384 $sth = $gCfg{dbh}->prepare($sql);
1385 $sth->execute;
1387 $sql = <<"EOSQL";
1388 CREATE TABLE
1389 PhysicalAction (
1390 action_id INTEGER PRIMARY KEY,
1391 physname VARCHAR,
1392 version INTEGER,
1393 parentphys VARCHAR,
1394 actiontype VARCHAR,
1395 itemname VARCHAR,
1396 itemtype INTEGER,
1397 timestamp INTEGER,
1398 author VARCHAR,
1399 is_binary INTEGER,
1400 info VARCHAR,
1401 priority INTEGER,
1402 sortkey VARCHAR,
1403 parentdata INTEGER,
1404 label VARCHAR,
1405 comment TEXT
1407 EOSQL
1409 $sth = $gCfg{dbh}->prepare($sql);
1410 $sth->execute;
1412 $sql = <<"EOSQL";
1413 CREATE INDEX
1414 PhysicalAction_IDX1 ON PhysicalAction (
1415 timestamp ASC,
1416 priority ASC,
1417 sortkey ASC
1419 EOSQL
1421 $sth = $gCfg{dbh}->prepare($sql);
1422 $sth->execute;
1424 $sql = <<"EOSQL";
1425 CREATE INDEX
1426 PhysicalAction_IDX2 ON PhysicalAction (
1427 physname ASC,
1428 parentphys ASC,
1429 actiontype ASC,
1430 timestamp ASC,
1431 author ASC
1433 EOSQL
1435 $sth = $gCfg{dbh}->prepare($sql);
1436 $sth->execute;
1438 $sql = <<"EOSQL";
1439 CREATE TABLE
1440 VssAction (
1441 action_id INTEGER PRIMARY KEY,
1442 parentphys VARCHAR,
1443 physname VARCHAR,
1444 version INTEGER,
1445 action VARCHAR,
1446 itempaths VARCHAR,
1447 itemtype INTEGER,
1448 is_binary INTEGER,
1449 info VARCHAR
1451 EOSQL
1453 $sth = $gCfg{dbh}->prepare($sql);
1454 $sth->execute;
1456 $sql = <<"EOSQL";
1457 CREATE INDEX
1458 VssAction_IDX1 ON VssAction (
1459 action_id ASC
1461 EOSQL
1463 $sth = $gCfg{dbh}->prepare($sql);
1464 $sth->execute;
1466 $sql = <<"EOSQL";
1467 CREATE TABLE
1468 SvnRevision (
1469 revision_id INTEGER PRIMARY KEY,
1470 timestamp INTEGER,
1471 author VARCHAR,
1472 comment TEXT
1474 EOSQL
1476 $sth = $gCfg{dbh}->prepare($sql);
1477 $sth->execute;
1479 $sql = <<"EOSQL";
1480 CREATE TABLE
1481 SvnRevisionVssAction (
1482 revision_id INTEGER,
1483 action_id INTEGER
1485 EOSQL
1487 $sth = $gCfg{dbh}->prepare($sql);
1488 $sth->execute;
1490 $sql = <<"EOSQL";
1491 CREATE INDEX
1492 SvnRevisionVssAction_IDX1 ON SvnRevisionVssAction (
1493 revision_id ASC,
1494 action_id ASC
1496 EOSQL
1498 $sth = $gCfg{dbh}->prepare($sql);
1499 $sth->execute;
1501 $sql = <<"EOSQL";
1502 CREATE TABLE
1503 Label (
1504 physical VARCHAR,
1505 version INTEGER,
1506 label VARCHAR,
1507 imtempaths VARCHAR
1509 EOSQL
1511 $sth = $gCfg{dbh}->prepare($sql);
1512 $sth->execute;
1514 my @cfgitems = qw(task step vssdir svnurl svnuser svnpwd ssphys tempdir
1515 setsvndate starttime);
1517 my $fielddef = join(",\n ",
1518 map {sprintf('%-12.12s VARCHAR', $_)} @cfgitems);
1520 $sql = <<"EOSQL";
1521 CREATE TABLE
1522 SystemInfo (
1523 $fielddef
1525 EOSQL
1527 $sth = $gCfg{dbh}->prepare($sql);
1528 $sth->execute;
1530 my $fields = join(', ', @cfgitems);
1531 my $args = join(', ', map {'?'} @cfgitems);
1533 $sql = <<"EOSQL";
1534 INSERT INTO
1535 SystemInfo ($fields)
1536 VALUES
1537 ($args)
1538 EOSQL
1540 $sth = $gCfg{dbh}->prepare($sql);
1541 $sth->execute(map {$gCfg{$_}} @cfgitems);
1542 $sth->finish();
1544 } # End InitSysTables
1546 ###############################################################################
1547 # ReloadSysTables
1548 ###############################################################################
1549 sub ReloadSysTables {
1550 my($sql, $sth, $sthup, $row, $field, $val);
1552 $sql = "SELECT * FROM SystemInfo";
1554 $sth = $gCfg{dbh}->prepare($sql);
1555 $sth->execute();
1557 $row = $sth->fetchrow_hashref();
1559 FIELD:
1560 while (($field, $val) = each %$row) {
1561 if (defined($gCfg{$field})) { # allow user to override saved vals
1562 $sql = "UPDATE SystemInfo SET $field = ?";
1563 $sthup = $gCfg{dbh}->prepare($sql);
1564 $sthup->execute($gCfg{$field});
1565 } else {
1566 $gCfg{$field} = $val;
1570 $sth->finish();
1571 &SetSystemTask($gCfg{task});
1573 } # End ReloadSysTables
1575 ###############################################################################
1576 # Initialize
1577 ###############################################################################
1578 sub Initialize {
1579 GetOptions(\%gCfg,'vssdir=s','tempdir=s','dumpfile=s','resume','verbose',
1580 'debug','timing+','task=s','revtimerange=i','ssphys=s','encoding=s',
1581 'trunkdir=s', 'auto_props=s');
1583 &GiveHelp("Must specify --vssdir") if !defined($gCfg{vssdir});
1584 $gCfg{tempdir} = './_vss2svn' if !defined($gCfg{tempdir});
1585 $gCfg{dumpfile} = 'vss2svn-dumpfile.txt' if !defined($gCfg{dumpfile});
1587 $gCfg{sqlitedb} = "$gCfg{tempdir}/vss_data.db";
1589 # XML output from ssphysout placed here.
1590 $gCfg{ssphysout} = "$gCfg{tempdir}/ssphysout";
1591 $gCfg{encoding} = 'windows-1252' if !defined($gCfg{encoding});
1593 # Commit messages for SVN placed here.
1594 $gCfg{svncomment} = "$gCfg{tempdir}/svncomment.tmp.txt";
1595 mkdir $gCfg{tempdir} unless (-d $gCfg{tempdir});
1597 # Directories for holding VSS revisions
1598 $gCfg{vssdata} = "$gCfg{tempdir}/vssdata";
1600 if ($gCfg{resume} && !-e $gCfg{sqlitedb}) {
1601 warn "WARNING: --resume set but no database exists; starting new "
1602 . "conversion...";
1603 $gCfg{resume} = 0;
1606 if ($gCfg{debug}) {
1607 $gCfg{verbose} = 1;
1609 $gCfg{timing} = 0 unless defined $gCfg{timing};
1611 $gCfg{starttime} = scalar localtime($^T);
1613 # trunkdir should (must?) be without leading slash
1614 $gCfg{trunkdir} = '' unless defined $gCfg{trunkdir};
1615 $gCfg{trunkdir} =~ s:\\:/:g;
1616 $gCfg{trunkdir} =~ s:/$::;
1618 $gCfg{junkdir} = '/lost+found';
1620 $gCfg{labeldir} = '/labels';
1622 $gCfg{errortasks} = [];
1624 &ConfigureXmlParser();
1626 ### Don't go past here if resuming a previous run ###
1627 if ($gCfg{resume}) {
1628 return 1;
1631 rmtree($gCfg{vssdata}) if (-e $gCfg{vssdata});
1632 mkdir $gCfg{vssdata};
1634 $gCfg{ssphys} ||= 'ssphys';
1635 $gCfg{svn} ||= 'SVN.exe';
1637 $gCfg{task} = 'INIT';
1638 $gCfg{step} = 0;
1639 } # End Initialize
1641 ###############################################################################
1642 # ConfigureXmlParser
1643 ###############################################################################
1644 sub ConfigureXmlParser {
1646 if(defined($ENV{XML_SIMPLE_PREFERRED_PARSER})) {
1647 # user has defined a preferred parser; don't mess with it
1648 $gCfg{xmlParser} = $ENV{XML_SIMPLE_PREFERRED_PARSER};
1649 return 1;
1652 $gCfg{xmlParser} = 'XML::Simple';
1654 eval { require XML::SAX; };
1655 if($@) {
1656 # no XML::SAX; let XML::Simple use its own parser
1657 return 1;
1660 $gCfg{xmlParser} = 'XML::SAX::Expat';
1661 $XML::SAX::ParserPackage = $gCfg{xmlParser};
1663 my $p;
1665 eval { $p = XML::SAX::ParserFactory->parser(); };
1667 if(!$@) {
1668 # XML::SAX::Expat installed; use it
1670 # for exe version, XML::Parser::Expat needs help finding its encmaps
1671 no warnings 'once';
1672 push(@XML::Parser::Expat::Encoding_Path, @INC);
1673 return 1;
1676 undef $XML::SAX::ParserPackage;
1677 eval { $p = XML::SAX::ParserFactory->parser(); };
1679 if(!$@) {
1680 $gCfg{xmlParser} = ref $p;
1681 return 1;
1684 # couldn't find a better package; go back to XML::Simple
1685 $gCfg{'xmlParser'} = 'XML::Simple';
1686 return 1;
1688 } # End ConfigureXmlParser
1690 ###############################################################################
1691 # GiveHelp
1692 ###############################################################################
1693 sub GiveHelp {
1694 my($msg) = @_;
1696 $msg ||= 'Online Help';
1698 print <<"EOTXT";
1700 $msg
1702 USAGE: perl vss2svn.pl --vssdir <dir> [options]
1704 REQUIRED PARAMETERS:
1705 --vssdir <dir> : Directory where VSS database is located. This should be
1706 the directory in which the "srcsafe.ini" file is located.
1708 OPTIONAL PARAMETERS:
1709 --ssphys <path> : Full path to ssphys.exe program; uses PATH otherwise
1710 --tempdir <dir> : Temp directory to use during conversion;
1711 default is ./_vss2svn
1712 --dumpfile <file> : specify the subversion dumpfile to be created;
1713 default is ./vss2svn-dumpfile.txt
1714 --revtimerange <sec> : specify the difference between two ss actions
1715 that are treated as one subversion revision;
1716 default is 3600 seconds (== 1hour)
1718 --resume : Resume a failed or aborted previous run
1719 --task <task> : specify the task to resume; task is one of the following
1720 INIT, LOADVSSNAMES, FINDDBFILES, GETPHYSHIST,
1721 MERGEPARENTDATA, BUILDACTIONHIST, IMPORTSVN
1723 --verbose : Print more info about the items being processed
1724 --debug : Print lots of debugging info.
1725 --timing : Show timing information during various steps
1726 --encoding : Specify the encoding used in VSS;
1727 Default is windows-1252
1728 --trunkdir : Specify where to map the VSS Project Root in the
1729 converted repository (default = "/")
1730 --auto_props : Specify an autoprops ini file to use, e.g.
1731 --auto_props="c:/Dokumente und Einstellungen/user/Anwendungsdaten/Subversion/config"
1732 EOTXT
1734 exit(1);
1735 } # End GiveHelp