* Track shared files to better handle renames, deletes, commits on shared items
[vss2svn.git] / script / vss2svn.pl
blob896f6bd121297c83eff0a0016836163ae4ccc7b6
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 Time::CTime;
12 use Data::Dumper;
14 our(%gCfg, %gSth, @gErr, %gFh, $gSysOut, %gActionType, %gNameLookup, $gId,
15 %gPhysInfo,);
17 our $VERSION = '0.10';
19 &Initialize;
20 &ConnectDatabase;
22 &SetupGlobals;
23 &ShowHeader;
25 &RunConversion;
27 &DisconnectDatabase;
28 &ShowSummary;
30 ###############################################################################
31 # RunConversion
32 ###############################################################################
33 sub RunConversion {
35 # store a hash of actions to take; allows restarting in case of failed
36 # migration
37 my %joblist =
39 INIT => {handler => sub{ 1; },
40 next => 'LOADVSSNAMES'},
42 # Load the "real" names associated with the stored "short" names
43 LOADVSSNAMES => {handler => \&LoadVssNames,
44 next => 'FINDDBFILES'},
46 # Add a stub entry into the Physical table for each physical
47 # file in the VSS DB
48 FINDDBFILES => {handler => \&FindPhysDbFiles,
49 next => 'GETPHYSHIST'},
51 # Load the history of what happened to the physical files. This
52 # only gets us halfway there because we don't know what the real
53 # filenames are yet
54 GETPHYSHIST => {handler => \&GetPhysVssHistory,
55 next => 'MERGEPARENTDATA'},
57 # Merge data from parent records into child records where possible
58 MERGEPARENTDATA => {handler => \&MergeParentData,
59 next => 'BUILDACTIONHIST'},
61 # Take the history of physical actions and convert them to VSS
62 # file actions
63 BUILDACTIONHIST => {handler => \&BuildVssActionHistory,
64 next => 'BUILDREVS'},
66 # Combine these individual actions into atomic actions a' la SVN
67 BUILDREVS => {handler => \&BuildRevs,
68 next => 'IMPORTSVN'},
70 # Create a dumpfile or import to repository
71 IMPORTSVN => {handler => \&ImportToSvn,
72 next => 'DONE'},
75 my $info;
77 while ($gCfg{task} ne 'DONE') {
78 $info = $joblist{ $gCfg{task} }
79 or die "FATAL ERROR: Unknown task '$gCfg{task}'\n";
81 print "TASK: $gCfg{task}\n";
83 if ($gCfg{prompt}) {
84 print "Press ENTER to continue...\n";
85 my $temp = <STDIN>;
86 die if $temp =~ m/^quit/i;
89 &{ $info->{handler} };
90 &SetSystemTask( $info->{next} );
94 } # End RunConversion
96 ###############################################################################
97 # LoadVssNames
98 ###############################################################################
99 sub LoadVssNames {
100 &DoSsCmd("info -a \"$gCfg{vssdatadir}\\names.dat\" -s xml");
102 my $xs = XML::Simple->new(KeyAttr => [],
103 ForceArray => [qw(Entry)],);
105 my $xml = $xs->XMLin($gSysOut);
107 my $namesref = $xml->{NameCacheEntry} || return 1;
109 my($entry, $count, $offset, $name);
111 &StartDataCache('NameLookup', 1);
113 ENTRY:
114 foreach $entry (@$namesref) {
115 $count = $entry->{NrOfEntries};
116 next ENTRY unless $count > 1;
118 $offset = $entry->{offset};
120 if ($count == 2) {
121 $name = $entry->{Entry}->[1]->{content};
122 } else {
123 $name = $entry->{Entry}->[$count - 2]->{content};
126 &AddDataCache($offset, $name);
129 &CommitDataCache();
131 } # End LoadVssNames
133 ###############################################################################
134 # FindPhysDbFiles
135 ###############################################################################
136 sub FindPhysDbFiles {
138 &StartDataCache('Physical', 1);
140 find(\&FoundSsFile, $gCfg{vssdatadir});
142 &CommitDataCache();
144 } # End FindPhysDbFiles
146 ###############################################################################
147 # FoundSsFile
148 ###############################################################################
149 sub FoundSsFile {
151 my $path = $File::Find::name;
152 return if (-d $path);
154 my $vssdatadir = quotemeta($gCfg{vssdatadir});
156 if ($path =~ m:^$vssdatadir/./([a-z]{8})$:i) {
157 &AddDataCache(uc($1));
160 } # End FoundSsFile
162 ###############################################################################
163 # GetPhysVssHistory
164 ###############################################################################
165 sub GetPhysVssHistory {
166 my($sql, $sth, $row, $physname, $physdir);
168 &LoadNameLookup;
169 &StartDataCache('PhysicalAction', 1, 1);
171 $sql = "SELECT * FROM Physical";
172 $sth = &PrepSql($sql);
173 $sth->execute();
175 my $xs = XML::Simple->new(ForceArray => [qw(Version)]);
177 while (defined($row = $sth->fetchrow_hashref() )) {
178 $physname = $row->{physname};
180 $physdir = "$gCfg{vssdir}\\data\\" . substr($physname, 0, 1);
182 &GetVssPhysInfo($physdir, $physname, $xs);
185 &CommitDataCache();
187 } # End GetPhysVssHistory
189 ###############################################################################
190 # GetVssPhysInfo
191 ###############################################################################
192 sub GetVssPhysInfo {
193 my($physdir, $physname, $xs) = @_;
195 &DoSsCmd("info -a \"$physdir\\$physname\" -s xml");
197 my $xml = $xs->XMLin($gSysOut);
198 my $parentphys;
200 if (defined($xml->{ProjectItem})) {
201 $parentphys = ($physname eq 'AAAAAAAA')?
202 '' : &GetProjectParent($xml);
203 } elsif (defined($xml->{FileItem})) {
204 $parentphys = &GetFileParent($xml);
205 } else {
206 &ThrowWarning("Can't handle file '$physname'; not a project or file\n");
207 return;
210 &GetVssItemInfo($physname, $parentphys, $xml);
212 } # End GetVssPhysInfo
214 ###############################################################################
215 # GetProjectParent
216 ###############################################################################
217 sub GetProjectParent {
218 my($xml) = @_;
220 no warnings 'uninitialized';
221 return $xml->{ProjectItem}->{ParentPhys} || undef;
223 } # End GetProjectParent
225 ###############################################################################
226 # GetFileParent
227 ###############################################################################
228 sub GetFileParent {
229 my($xml) = @_;
231 # TODO: determine whether we ever really need to get the parent for a child
232 # item at this phase. For commits, we'll apply the change to all existing
233 # shares at that time, and for renames, deletes, shares, etc., we'll have
234 # that info from the parent already.
236 return undef;
238 no warnings 'uninitialized';
240 my $parents = $xml->{ParentFolder};
241 my $parentphys;
243 if (ref $parents eq 'ARRAY') {
244 # If there is more than one parent folder, this is a shared or branched
245 # item. Since the child item has no way of knowing who its original
246 # parent is, we'll leave it blank and expect it to be filled in by the
247 # parent.
248 $parentphys = undef;
249 } else {
250 $parentphys = $parents->{ParentPhys} || undef;
253 return $parentphys;
255 } # End GetFileParent
257 ###############################################################################
258 # GetVssItemInfo
259 ###############################################################################
260 sub GetVssItemInfo {
261 my($physname, $parentphys, $xml) = @_;
263 return 0 unless defined $xml->{Version};
265 my($parentdata, $version, $number, $action, $name, $actionid, $actiontype,
266 $tphysname, $itemname, $itemtype, $parent, $user, $timestamp, $comment,
267 $info, $priority, $cachename);
269 VERSION:
270 foreach $version (@{ $xml->{Version} }) {
271 $action = $version->{Action};
272 $name = $action->{SSName};
273 $tphysname = $action->{Physical} || $physname;
274 $user = $version->{UserName};
275 $timestamp = $version->{Date};
277 $itemname = $name->{content};
279 if (defined($name->{offset})) {
280 # Might have a "better" name in the name cache, but sometimes the
281 # original name is best.
282 if ($name->{offset} == 39080) {
286 $cachename = $gNameLookup{ $name->{offset} };
288 if (!defined($itemname) || ($itemname =~ m/~/ &&
289 length($cachename) > length($itemname))) {
291 print "Changing name of '$itemname' to '$cachename' from "
292 . "name cache\n" if $gCfg{debug};
294 $itemname = $cachename;
295 } else {
296 print "Found name '$cachename' in namecache, but kept original "
297 . "'$itemname'\n" if $gCfg{debug};
303 $actionid = $action->{ActionId};
304 $info = $gActionType{$actionid}; # || next VERSION; # unknown action
305 if (!$info) {
306 next VERSION;
308 $itemtype = $info->{type};
309 $actiontype = $info->{action};
311 $comment = undef;
312 $info = undef;
313 $parentdata = 0;
314 $priority = 5;
316 if ($version->{Comment} && !ref($version->{Comment})) {
317 $comment = $version->{Comment} || undef;
320 if ($itemtype == 1 && $physname eq 'AAAAAAAA' && ref($tphysname)) {
321 $tphysname = $physname;
322 $itemname = '';
323 } elsif ($physname ne $tphysname) {
324 # If version's physical name and file physical name are different,
325 # this is a project describing an action on a child item. Most of
326 # the time, this very same data will be in the child's physical
327 # file and with more detail (such as check-in comment).
329 # However, in some cases (such as renames, or when the child's
330 # physical file was later purged), this is the only place we'll
331 # have the data; also, sometimes the child record doesn't even
332 # have enough information about itself (such as which project it
333 # was created in and which project(s) it's shared in).
335 # So, for a parent record describing a child action, we'll set a
336 # flag, then combine them in the next phase.
338 $parentdata = 1;
340 # OK, since we're describing an action in the child, the parent is
341 # actually this (project) item
343 $parentphys = $physname;
346 if ($itemtype == 1) {
347 $itemname .= '/';
350 if ($actiontype eq 'RENAME') {
351 # if a rename, we store the new name in the action's 'info' field
352 no warnings 'uninitialized';
354 $name = $action->{NewSSName};
355 if (defined($name->{offset})) {
356 $info = $gNameLookup{ $name->{offset} } || $name->{content};
357 } else {
358 $info = $name->{content} || undef;
361 if ($itemtype == 1) {
362 $info .= '/';
366 $number = ($parentdata)? undef : $version->{VersionNumber};
368 $priority -= 4 if $actiontype eq 'ADD'; # Adds are always first
369 $priority -= 3 if $actiontype eq 'COPY';
371 &AddDataCache($tphysname, $number, $parentphys, $actiontype, $itemname,
372 $itemtype, $timestamp, $user, $info, $priority,
373 $parentdata, $comment);
377 } # End GetVssItemInfo
379 ###############################################################################
380 # LoadNameLookup
381 ###############################################################################
382 sub LoadNameLookup {
383 my($sth, $row);
385 $sth = &PrepSql('SELECT offset, name FROM NameLookup');
386 $sth->execute();
388 while(defined($row = $sth->fetchrow_hashref() )) {
389 $gNameLookup{ $row->{offset} } = $row->{name};
391 } # End LoadNameLookup
393 ###############################################################################
394 # MergeParentData
395 ###############################################################################
396 sub MergeParentData {
397 # VSS has a funny way of not placing enough information to rebuild history
398 # in one data file; for example, renames are stored in the parent project
399 # rather than in that item's data file. Also, it's sometimes impossible to
400 # tell from a child record which was eventually shared to multiple folders,
401 # which folder it was originally created in.
403 # So, at this stage we look for any parent records which described child
404 # actions, then update those records with data from the child objects. We
405 # then delete the separate child objects to avoid duplication.
407 my($sth, $rows, $row);
408 $sth = &PrepSql('SELECT * FROM PhysicalAction WHERE parentdata = 1');
409 $sth->execute();
411 # need to pull in all recs at once, since we'll be updating/deleting data
412 $rows = $sth->fetchall_arrayref( {} );
414 my($childrecs, $child, $id);
415 my @delchild = ();
417 foreach $row (@$rows) {
418 $childrecs = &GetChildRecs($row);
420 if (scalar @$childrecs > 1) {
421 &ThrowWarning("Multiple child recs for parent rec "
422 . "'$row->{action_id}'");
425 foreach $child (@$childrecs) {
426 &UpdateParentRec($row, $child);
427 push(@delchild, $child->{action_id});
431 foreach $id (@delchild) {
432 &DeleteChildRec($id);
437 } # End MergeParentData
439 ###############################################################################
440 # GetChildRecs
441 ###############################################################################
442 sub GetChildRecs {
443 my($parentrec) = @_;
445 my $sql = <<"EOSQL";
446 SELECT
448 FROM
449 PhysicalAction
450 WHERE
451 parentdata = 0
452 AND physname = ?
453 AND actiontype = ?
454 AND timestamp = ?
455 AND author = ?
456 EOSQL
458 my $sth = &PrepSql($sql);
459 $sth->execute( @{ $parentrec }{qw(physname actiontype timestamp author)} );
461 return $sth->fetchall_arrayref( {} );
462 } # End GetChildRecs
464 ###############################################################################
465 # UpdateParentRec
466 ###############################################################################
467 sub UpdateParentRec {
468 my($row, $child) = @_;
470 # The child record has the "correct" version number (relative to the child
471 # and not the parent), as well as the comment info
473 my $sql = <<"EOSQL";
474 UPDATE
475 PhysicalAction
477 version = ?,
478 comment = ?
479 WHERE
480 action_id = ?
481 EOSQL
483 my $sth = &PrepSql($sql);
484 $sth->execute( $child->{version}, $child->{comment}, $row->{action_id} );
486 } # End UpdateParentRec
488 ###############################################################################
489 # DeleteChildRec
490 ###############################################################################
491 sub DeleteChildRec {
492 my($id) = @_;
494 my $sql = "DELETE FROM PhysicalAction WHERE action_id = ?";
496 my $sth = &PrepSql($sql);
497 $sth->execute($id);
498 } # End DeleteChildRec
500 ###############################################################################
501 # BuildVssActionHistory
502 ###############################################################################
503 sub BuildVssActionHistory {
504 &StartDataCache('VssAction', 1, 1);
506 my($sth, $row, $action, $handler, $itempaths, $itempath);
508 $sth = &PrepSql('SELECT * FROM PhysicalAction ORDER BY timestamp ASC, '
509 . 'priority ASC');
510 $sth->execute();
512 my %handlers =
514 ADD => \&VssAddHandler,
515 RENAME => \&VssRenameHandler,
516 COPY => \&VssCopyHandler,
517 DELETE => \&VssDeleteHandler,
518 RECOVER => \&VssRecoverHandler,
521 while(defined($row = $sth->fetchrow_hashref() )) {
522 $action = $row->{actiontype};
524 $handler = $handlers{$action};
526 if (defined($gPhysInfo{ $row->{physname}} ) &&
527 $gPhysInfo{ $row->{physname} }->{type} != $row->{itemtype} ) {
529 &ThrowError("Inconsistent item type for '$row->{physname}'; "
530 . "'$row->{itemtype}' unexpected");
533 if ($row->{physname} eq 'YAAAAAAA') {
537 # The handler's job is to keep %gPhysInfo up to date with physical-to-
538 # real item name mappings and return the full item paths of the physical
539 # item. In case of a rename, it will return the old name, so we then do
540 # another lookup on the new name.
542 # Most actions can actually be done on multiple items, if that item is
543 # shared; since SVN has no equivalent of shares, we replicate this by
544 # applying commit actions to all shares.
546 if (defined($handler)) {
547 $itempaths = &$handler($row);
548 } else {
549 $itempaths = &GetCurrentItemPaths($row->{physname});
552 if ($row->{actiontype} eq 'RENAME') {
553 $row->{info} = &GetCurrentItemName($row->{physname});
554 } elsif ($row->{actiontype} eq 'COPY') {
555 $row->{info} = &GetCurrentItemPaths($row->{physname}, 1)->[0];
558 foreach $itempath (@$itempaths) {
559 $row->{itempath} = $itempath;
561 &AddDataCache(@$row{ qw(physname version actiontype itempath itemtype
562 timestamp author info comment) });
567 &CommitDataCache();
569 } # End BuildVssActionHistory
571 ###############################################################################
572 # VssAddHandler
573 ###############################################################################
574 sub VssAddHandler {
575 my($row) = @_;
577 # For each physical item, we store its "real" physical parent in the
578 # 'parentphys' property, then keep a list of additional shared parents in
579 # the 'sharedphys' array.
581 $gPhysInfo{ $row->{physname} } =
583 type => $row->{itemtype},
584 name => $row->{itemname},
585 parentphys => $row->{parentphys},
586 sharedphys => [],
589 # File was just created so no need to look for shares
590 return &GetCurrentItemPaths($row->{physname}, 1);
591 } # End VssAddHandler
593 ###############################################################################
594 # VssRenameHandler
595 ###############################################################################
596 sub VssRenameHandler {
597 my($row) = @_;
599 # Get the existing paths before the rename; parent sub will get the new
600 # name and apply it to all existing paths
601 my $physname = $row->{physname};
602 my $itempaths = &GetCurrentItemPaths($physname);
604 my $physinfo = $gPhysInfo{$physname};
606 if (!defined $physinfo) {
607 &ThrowError("Attempt to rename unknown item '$physname':\n"
608 . $gCfg{nameResolveSeen});
611 # A rename of an item renames it in all its shares, so we can just change
612 # the name in one place
613 $physinfo->{name} = $row->{info};
615 return $itempaths;
616 } # End VssRenameHandler
618 ###############################################################################
619 # VssCopyHandler
620 ###############################################################################
621 sub VssCopyHandler {
622 my($row) = @_;
624 my $physname = $row->{physname};
625 my $physinfo = $gPhysInfo{$physname};
627 if (!defined $physinfo) {
628 &ThrowError("Attempt to rename unknown item '$physname':\n"
629 . $gCfg{nameResolveSeen});
632 push @{ $physinfo->{sharedphys} }, $row->{parentphys};
634 # We only return only the path for this new location (the copy target);
635 # the source path will be added to the "info" field by caller
636 my $parentpaths = &GetCurrentItemPaths($row->{parentphys}, 1);
637 return [$parentpaths->[0] . $physinfo->{name}];
639 } # End VssCopyHandler
641 ###############################################################################
642 # VssDeleteHandler
643 ###############################################################################
644 sub VssDeleteHandler {
645 my($row) = @_;
647 # For a delete operation we return only the "main" path, since any deletion
648 # of shared paths will have their own entry
649 my $physname = $row->{physname};
650 my $itempaths = &GetCurrentItemPaths($physname, 1);
652 my $physinfo = $gPhysInfo{$physname};
654 if (!defined $physinfo) {
655 &ThrowError("Attempt to delete unknown item '$physname':\n"
656 . $gCfg{nameResolveSeen});
659 if ($physinfo->{parentphys} eq $row->{parentphys}) {
660 # Deleting from the "main" parent; find a new one by shifting off the
661 # first shared path, if any; if none exists this will leave a null
662 # parent entry. We could probably just delete the whole node at this
663 # point.
665 $physinfo->{parentphys} = shift( @{ $physinfo->{sharedphys} } );
667 } else {
668 my $sharedphys = [];
670 foreach my $parent (@{ $physinfo->{sharedphys} }) {
671 push @$sharedphys, $parent
672 unless $parent eq $row->{parentphys};
675 $physinfo->{sharedphys} = $sharedphys;
678 return $itempaths;
680 } # End VssDeleteHandler
682 ###############################################################################
683 # VssRecoverHandler
684 ###############################################################################
685 sub VssRecoverHandler {
686 my($row) = @_;
688 my $physname = $row->{physname};
690 my $physinfo = $gPhysInfo{$physname};
692 if (!defined $physinfo) {
693 &ThrowError("Attempt to recover unknown item '$physname':\n"
694 . $gCfg{nameResolveSeen});
697 if (defined $physinfo->{parentphys}) {
698 # Item still has other shares, so recover it by pushing this parent
699 # onto its shared list
701 push( @{ $physinfo->{sharedphys} }, $row->{parentphys} );
703 } else {
704 # Recovering its only location; set the main parent back to this
705 $physinfo->{parentphys} = $row->{parentphys};
708 # We only recover the path explicitly set in this row, so build the path
709 # ourself by taking the path of this parent and appending the name
710 my $parentpaths = &GetCurrentItemPaths($row->{parentphys}, 1);
711 return [$parentpaths->[0] . $physinfo->{name}];
713 } # End VssRecoverHandler
715 ###############################################################################
716 # GetCurrentItemPaths
717 ###############################################################################
718 sub GetCurrentItemPaths {
719 my($physname, $mainonly, $recursed) = @_;
721 # Uses recursion to determine the current full paths for an item based on
722 # the name of its physical file. We can't cache this information because
723 # a rename in a parent folder would not immediately trigger a rename in
724 # all of the child items.
726 # By default, we return an anonymous array of all paths in which the item
727 # is shared, unless $mainonly is true. Luckily, only files can be shared,
728 # not projects, so once we start recursing we can set $mainonly to true.
730 if (!$recursed) {
731 $gCfg{nameResolveRecurse} = 0;
732 $gCfg{nameResolveSeen} = '';
733 } elsif (++$gCfg{nameResolveRecurse} >= 1000) {
734 &ThrowError("Infinite recursion detected while looking up parent for "
735 . "'$physname'");
738 if ($physname eq 'AAAAAAAA') {
739 # End of recursion; all items must go back to 'AAAAAAAA', which was so
740 # named because that's what most VSS users yell after using it much. :-)
741 return ['/'];
744 my $physinfo = $gPhysInfo{$physname};
746 if (!defined $physinfo) {
747 &ThrowError("Could not determine real path for '$physname':\n"
748 . $gCfg{nameResolveSeen});
751 $gCfg{nameResolveSeen} .= "$physname, ";
753 my @pathstoget = $mainonly? ($physinfo->{parentphys}) :
754 ($physinfo->{parentphys}, @{ $physinfo->{sharedphys} } );
756 my $paths = [];
757 my $result;
759 foreach my $parent (@pathstoget) {
760 if (!defined $parent) {
763 $result = &GetCurrentItemPaths($parent, 1, 1);
765 push @$paths, $result->[0] . $physinfo->{name};
768 return $paths;
770 } # End GetCurrentItemPaths
772 ###############################################################################
773 # GetCurrentItemName
774 ###############################################################################
775 sub GetCurrentItemName {
776 my($physname) = @_;
778 my $physinfo = $gPhysInfo{$physname};
780 if (!defined $physinfo) {
781 &ThrowError("Could not determine real name for '$physname':\n"
782 . $gCfg{nameResolveSeen});
785 return $physinfo->{name};
786 } # End GetCurrentItemName
788 ###############################################################################
789 # ImportToSvn
790 ###############################################################################
791 sub ImportToSvn {
792 defined($gCfg{svnurl})? &CheckinToSvn : &CreateSvnDumpfile;
793 } # End ImportToSvn
795 ###############################################################################
796 # CheckinToSvn
797 ###############################################################################
798 sub CheckinToSvn {
800 } # End CheckinToSvn
802 ###############################################################################
803 # CreateSvnDumpfile
804 ###############################################################################
805 sub CreateSvnDumpfile {
807 } # End CreateSvnDumpfile
809 ###############################################################################
810 # ShowHeader
811 ###############################################################################
812 sub ShowHeader {
813 if ($gCfg{log}) {
814 my $prefix = $gCfg{pvcsproj} || $gCfg{svnurl} || "log-$$";
815 $prefix =~ s:.*[\\/]::;
816 $gCfg{logfile} = "./logs/$prefix.txt";
817 print "All output will be logged to $gCfg{logfile}...\n";
818 open LOG, ">>$gCfg{logfile}"
819 or die "Couldn't append to logfile $gCfg{logfile}";
820 open STDERR, ">&LOG";
821 select STDERR; $| = 1;
822 select LOG; $| = 1;
825 my $info = $gCfg{task} eq 'INIT'? 'BEGINNING CONVERSION...' :
826 "RESUMING CONVERSION FROM TASK '$gCfg{task}' AT STEP $gCfg{step}...";
827 my $starttime = ctime($^T);
829 my $ssversion = &GetSsVersion();
831 print <<"EOTXT";
832 ======== VSS2SVN ========
833 $info
834 Start Time : $starttime
836 VSS Dir : $gCfg{vssdir}
837 Temp Dir : $gCfg{tempdir}
839 SSPHYS exe : $gCfg{ssphys}
840 SSPHYS ver : $ssversion
842 EOTXT
844 } # End ShowHeader
846 ###############################################################################
847 # ShowSummary
848 ###############################################################################
849 sub ShowSummary {
850 if (@gErr) {
851 print "\n\n\n====ERROR SUMMARY====\n\n";
852 foreach (@gErr) {
853 print "$_\n";
855 } else {
856 print "\n\n\n====NO ERRORS ENCOUNTERED THIS RUN====\n\n";
859 my $starttime = ctime($^T);
860 chomp $starttime;
861 my $endtime = ctime(time);
862 chomp $endtime;
863 my $elapsed;
866 use integer;
867 my $secs = time - $^T;
869 my $hours = $secs / 3600;
870 $secs -= ($hours * 3600);
872 my $mins = $secs / 60;
873 $secs -= ($mins * 60);
875 $elapsed = sprintf("%2.2i:%2.2i:%2.2i", $hours, $mins, $secs);
878 print <<"EOTXT";
879 SVN rev range : $gCfg{firstrev} - $gCfg{lastrev}
880 Started at : $starttime
881 Ended at : $endtime
882 Elapsed time : $elapsed (H:M:S)
884 EOTXT
886 &CloseFile('LOG');
888 } # End ShowSummary
890 ###############################################################################
891 # DoSsCmd
892 ###############################################################################
893 sub DoSsCmd {
894 my($cmd) = @_;
896 my $ok = &DoSysCmd("\"$gCfg{ssphys}\" $cmd", 1);
898 $gSysOut =~ s/.\x08//g; # yes, I've seen VSS store backspaces in names!
899 $gSysOut =~ s/[\x00-\x09\x11\x12\x14-\x1F\x7F-\xFF]/_/g; # just to be sure
901 if (!$ok) {
902 # ssphys.exe has bailed on us; hope we were between items and add
903 # a closing element!
904 $gSysOut =~ s/^ssphys v0\.16:.*name as the source name//ms;
905 $gSysOut .= "\n</File>\n";
908 } # End DoSsCmd
910 ###############################################################################
911 # DoSysCmd
912 ###############################################################################
913 sub DoSysCmd {
914 my($cmd, $allowfail) = @_;
916 print "$cmd\n" if $gCfg{verbose};
917 $gSysOut = `$cmd`;
919 print $gSysOut if $gCfg{debug};
921 my $rv = 1;
923 if ($? == -1) {
924 &ThrowWarning("FAILED to execute: $!");
925 die unless $allowfail;
927 $rv = 0;
928 } elsif ($?) {
929 &ThrowWarning(sprintf "FAILED with non-zero exit status %d", $? >> 8);
930 die unless $allowfail;
932 $rv = 0;
935 return $rv;
937 } # End DoSysCmd
939 ###############################################################################
940 # GetSsVersion
941 ###############################################################################
942 sub GetSsVersion {
943 my $out = `\"$gCfg{ssphys}\" -v 2>&1`;
944 $out =~ m/^(ssphys v.*?)[:\n]/m;
946 return $1 || 'unknown';
947 } # End GetSsVersion
949 ###############################################################################
950 # ThrowWarning
951 ###############################################################################
952 sub ThrowWarning {
953 my($msg, $callinfo) = @_;
955 $callinfo ||= [caller()];
957 $msg .= "\nat $callinfo->[1] line $callinfo->[2]";
959 warn "ERROR -- $msg\n";
960 print "ERROR -- $msg\n" if $gCfg{log};
962 push @gErr, $msg;
964 } # End ThrowWarning
966 ###############################################################################
967 # ThrowError
968 ###############################################################################
969 sub ThrowError {
970 &ThrowWarning(@_, [caller()]);
971 &StopConversion;
972 } # End ThrowError
974 ###############################################################################
975 # StopConversion
976 ###############################################################################
977 sub StopConversion {
978 &DisconnectDatabase;
979 &CloseAllFiles;
981 exit(1);
982 } # End StopConversion
984 ###############################################################################
985 # OpenFile
986 ###############################################################################
987 sub OpenFile {
988 my($fhname, $target) = @_;
990 (my $name = $target) =~ s/^>//;
992 print "\nOPENING FILE $name\n" if $gCfg{verbose};
994 open $gFh{$fhname}, $target
995 or &ThrowError("Could not open file $name");
997 } # End OpenFile
999 ###############################################################################
1000 # CloseFile
1001 ###############################################################################
1002 sub CloseFile {
1003 my($fhname) = @_;
1005 close $gFh{$fhname};
1006 delete $gFh{$fhname};
1008 } # End CloseFile
1010 ###############################################################################
1011 # CloseAllFiles
1012 ###############################################################################
1013 sub CloseAllFiles {
1014 map { &CloseFile($_) } values %gFh;
1015 close LOG;
1016 } # End CloseAllFiles
1018 ###############################################################################
1019 # SetSystemTask
1020 ###############################################################################
1021 sub SetSystemTask {
1022 my($task, $leavestep) = @_;
1024 print "\nSETTING TASK $task\n" if $gCfg{verbose};
1026 my($sql, $sth);
1028 $sth = $gSth{'SYSTEMTASK'};
1030 if (!defined $sth) {
1031 $sql = <<"EOSQL";
1032 UPDATE
1033 SystemInfo
1035 task = ?
1036 EOSQL
1038 $sth = $gSth{'SYSTEMTASK'} = &PrepSql($sql);
1041 $sth->execute($task);
1043 $gCfg{task} = $task;
1045 &SetSystemStep(0) unless $leavestep;
1047 } # End SetSystemTask
1049 ###############################################################################
1050 # SetSystemStep
1051 ###############################################################################
1052 sub SetSystemStep {
1053 my($step) = @_;
1055 print "\nSETTING STEP $step\n" if $gCfg{verbose};
1057 my($sql, $sth);
1059 $sth = $gSth{'SYSTEMSTEP'};
1061 if (!defined $sth) {
1062 $sql = <<"EOSQL";
1063 UPDATE
1064 SystemInfo
1066 step = ?
1067 EOSQL
1069 $sth = $gCfg{'SYSTEMSTEP'} = &PrepSql($sql);
1072 $sth->execute($step);
1074 $gCfg{step} = $step;
1076 } # End SetSystemStep
1078 ###############################################################################
1079 # DeleteTable
1080 ###############################################################################
1081 sub DeleteTable {
1082 my($table) = @_;
1084 my $sth = &PrepSql("DELETE FROM $table");
1085 return $sth->execute;
1086 } # End DeleteTable
1088 ###############################################################################
1089 # StartDataCache
1090 ###############################################################################
1091 sub StartDataCache {
1092 my($table, $delete, $autoinc) = @_;
1094 if ($delete) {
1095 &DeleteTable($table);
1098 if ($autoinc) {
1099 $gId = 0;
1100 } else {
1101 undef $gId;
1104 $gCfg{cachetarget} = $table;
1105 unlink $gCfg{datacache};
1107 &OpenFile('DATACACHE', ">$gCfg{datacache}");
1109 } # End StartDataCache
1111 ###############################################################################
1112 # AddDataCache
1113 ###############################################################################
1114 sub AddDataCache {
1115 my(@data) = @_;
1117 if (ref($data[0]) eq 'ARRAY') {
1118 @data = @{ $data[0] };
1121 if (defined $gId) {
1122 unshift(@data, $gId++);
1125 my $fh = $gFh{DATACACHE};
1126 print $fh join("\t", map {&FormatCacheData($_)} @data), "\n";
1128 } # End AddDataCache
1130 ###############################################################################
1131 # FormatCacheData
1132 ###############################################################################
1133 sub FormatCacheData {
1134 my($data) = @_;
1135 return '\\N' if !defined($data);
1137 $data =~ s/([\t\n\\])/\\$1/g;
1139 return $data;
1140 } # End FormatCacheData
1142 ###############################################################################
1143 # CommitDataCache
1144 ###############################################################################
1145 sub CommitDataCache {
1146 my($sql, $sth);
1148 &CloseFile('DATACACHE');
1150 print "\n\nCOMMITTING $gCfg{cachetarget} CACHE TO DATABASE\n"
1151 if $gCfg{verbose};
1152 $sql = "COPY $gCfg{cachetarget} FROM '$gCfg{datacache}'";
1154 $sth = &PrepSql($sql);
1155 $sth->execute();
1157 unlink $gCfg{datacache};
1159 } # End CommitDataCache
1161 ###############################################################################
1162 # PrepSql
1163 ###############################################################################
1164 sub PrepSql {
1165 my($sql) = @_;
1167 print "\nSQL:\n$sql\n" if $gCfg{debug};
1168 return $gCfg{dbh}->prepare($sql);
1170 } # End PrepSql
1172 ###############################################################################
1173 # ConnectDatabase
1174 ###############################################################################
1175 sub ConnectDatabase {
1176 my $db = $gCfg{sqlitedb};
1178 if (-e $db && (!$gCfg{resume} ||
1179 (defined($gCfg{task}) && $gCfg{task} eq 'INIT'))) {
1181 unlink $db or &ThrowError("Could not delete existing database "
1182 .$gCfg{sqlitedb});
1185 print "Connecting to database $db\n\n";
1187 $gCfg{dbh} = DBI->connect("dbi:SQLite2:dbname=$db", '', '',
1188 {RaiseError => 1, AutoCommit => 1})
1189 or die "Couldn't connect database $db: $DBI::errstr";
1191 } # End ConnectDatabase
1193 ###############################################################################
1194 # DisconnectDatabase
1195 ###############################################################################
1196 sub DisconnectDatabase {
1197 $gCfg{dbh}->disconnect if defined $gCfg{dbh};
1198 } # End DisconnectDatabase
1200 ###############################################################################
1201 # SetupGlobals
1202 ###############################################################################
1203 sub SetupGlobals {
1204 if (defined($gCfg{task}) && $gCfg{task} eq 'INIT') {
1205 &InitSysTables;
1206 } else {
1207 &ReloadSysTables;
1210 $gCfg{ssphys} = 'SSPHYS.exe' if !defined($gCfg{ssphys});
1211 $gCfg{vssdatadir} = "$gCfg{vssdir}\\data";
1213 (-d "$gCfg{vssdatadir}") or &ThrowError("$gCfg{vssdir} does not appear "
1214 . "to be a valid VSS database");
1216 my($id, $type, $action);
1217 while(<DATA>) {
1218 chomp;
1219 ($id, $type, $action) = split "\t";
1220 $gActionType{$id} = {type => $type, action => $action};
1223 } # End SetupGlobals
1225 ###############################################################################
1226 # InitSysTables
1227 ###############################################################################
1228 sub InitSysTables {
1229 my($sql, $sth);
1231 $sql = <<"EOSQL";
1232 CREATE TABLE
1233 Physical (
1234 physname VARCHAR
1236 EOSQL
1238 $sth = &PrepSql($sql);
1239 $sth->execute;
1241 $sql = <<"EOSQL";
1242 CREATE TABLE
1243 NameLookup (
1244 offset INTEGER,
1245 name VARCHAR
1247 EOSQL
1249 $sth = &PrepSql($sql);
1250 $sth->execute;
1252 $sql = <<"EOSQL";
1253 CREATE TABLE
1254 PhysicalAction (
1255 action_id INTEGER PRIMARY KEY,
1256 physname VARCHAR,
1257 version INTEGER,
1258 parentphys VARCHAR,
1259 actiontype VARCHAR,
1260 itemname VARCHAR,
1261 itemtype INTEGER,
1262 timestamp INTEGER,
1263 author VARCHAR,
1264 info VARCHAR,
1265 priority INTEGER,
1266 parentdata INTEGER,
1267 comment TEXT
1269 EOSQL
1271 $sth = &PrepSql($sql);
1272 $sth->execute;
1274 $sql = <<"EOSQL";
1275 CREATE INDEX
1276 PhysicalAction_IDX1 ON PhysicalAction (
1277 timestamp ASC
1279 EOSQL
1281 $sth = &PrepSql($sql);
1282 $sth->execute;
1284 $sql = <<"EOSQL";
1285 CREATE INDEX
1286 PhysicalAction_IDX2 ON PhysicalAction (
1287 physname ASC,
1288 parentphys ASC,
1289 actiontype ASC,
1290 timestamp ASC,
1291 author ASC
1293 EOSQL
1295 $sth = &PrepSql($sql);
1296 $sth->execute;
1298 $sql = <<"EOSQL";
1299 CREATE TABLE
1300 VssAction (
1301 action_id INTEGER PRIMARY KEY,
1302 physname VARCHAR,
1303 version INTEGER,
1304 action VARCHAR,
1305 itempath VARCHAR,
1306 itemtype INTEGER,
1307 timestamp INTEGER,
1308 author VARCHAR,
1309 info VARCHAR,
1310 comment TEXT
1312 EOSQL
1314 $sth = &PrepSql($sql);
1315 $sth->execute;
1317 $sql = <<"EOSQL";
1318 CREATE TABLE
1319 ItemNameHistory (
1320 physname VARCHAR,
1321 timestamp INTEGER,
1322 itemname VARCHAR
1324 EOSQL
1326 $sth = &PrepSql($sql);
1327 $sth->execute;
1329 $sql = <<"EOSQL";
1330 CREATE TABLE
1331 Revision (
1332 revision_id INTEGER PRIMARY KEY,
1333 svndate VARCHAR,
1334 author VARCHAR,
1335 comment VARCHAR,
1336 status INTEGER
1338 EOSQL
1340 $sth = &PrepSql($sql);
1341 $sth->execute;
1343 $sql = <<"EOSQL";
1344 CREATE TABLE
1345 AtomRevision (
1346 atom_id INTEGER,
1347 rev_id INTEGER
1349 EOSQL
1351 $sth = &PrepSql($sql);
1352 $sth->execute;
1354 my @cfgitems = qw(task step vssdir svnurl svnuser svnpwd ssphys tempdir
1355 setsvndate debug verbose starttime);
1357 my $fielddef = join(",\n ",
1358 map {sprintf('%-12.12s VARCHAR', $_)} @cfgitems);
1360 $sql = <<"EOSQL";
1361 CREATE TABLE
1362 SystemInfo (
1363 $fielddef
1365 EOSQL
1367 $sth = &PrepSql($sql);
1368 $sth->execute;
1370 my $fields = join(', ', @cfgitems);
1371 my $args = join(', ', map {'?'} @cfgitems);
1373 $sql = <<"EOSQL";
1374 INSERT INTO
1375 SystemInfo ($fields)
1376 VALUES
1377 ($args)
1378 EOSQL
1380 $sth = &PrepSql($sql);
1381 $sth->execute(map {$gCfg{$_}} @cfgitems);
1382 $sth->finish();
1384 } # End InitSysTables
1386 ###############################################################################
1387 # ReloadSysTables
1388 ###############################################################################
1389 sub ReloadSysTables {
1390 my($sql, $sth, $sthup, $row, $field, $val);
1392 $sql = "SELECT * FROM SystemInfo";
1394 $sth = &PrepSql($sql);
1395 $sth->execute();
1397 $row = $sth->fetchrow_hashref();
1399 FIELD:
1400 while (($field, $val) = each %$row) {
1401 if (defined($gCfg{$field})) { # allow user to override saved vals
1402 $sql = "UPDATE SystemInfo SET $field = ?";
1403 $sthup = &PrepSql($sql);
1404 $sthup->execute($gCfg{$field});
1405 } else {
1406 $gCfg{$field} = $val;
1410 $sth->finish();
1411 &SetSystemTask($gCfg{task}, 1);
1413 } # End ReloadSysTables
1415 ###############################################################################
1416 # Initialize
1417 ###############################################################################
1418 sub Initialize {
1419 GetOptions(\%gCfg,'vssdir=s','tempdir=s','resume','verbose',
1420 'debug','task=s');
1422 &GiveHelp("Must specify --vssdir") if !defined($gCfg{vssdir});
1423 $gCfg{tempdir} = '.\\_vss2svn' if !defined($gCfg{tempdir});
1425 $gCfg{sqlitedb} = "$gCfg{tempdir}\\vss_data.db";
1427 # XML output from ssphysout placed here.
1428 $gCfg{ssphysout} = "$gCfg{tempdir}\\ssphysout";
1430 # SQLite data cache placed here.
1431 $gCfg{datacache} = "$gCfg{tempdir}\\datacache.tmp.txt";
1433 # Commit messages for SVN placed here.
1434 $gCfg{svncomment} = "$gCfg{tempdir}\\svncomment.tmp.txt";
1435 mkdir $gCfg{tempdir} unless (-d $gCfg{tempdir});
1437 if ($gCfg{resume} && !-e $gCfg{sqlitedb}) {
1438 warn "WARNING: --resume set but no database exists; starting new "
1439 . "conversion...";
1440 $gCfg{resume} = 0;
1443 ### Don't go past here if resuming a previous run ###
1444 if ($gCfg{resume}) {
1445 return 1;
1448 #foreach my $check (qw(svnurl)) {
1449 # &GiveHelp("ERROR: missing required parameter $check")
1450 # unless defined $gCfg{$check};
1453 $gCfg{ssphys} ||= 'SSPHYS.exe';
1454 $gCfg{svn} ||= 'SVN.exe';
1456 $gCfg{task} = 'INIT';
1457 $gCfg{step} = 0;
1458 $gCfg{starttime} = scalar localtime($^T);
1460 if ($gCfg{debug}) {
1461 $gCfg{verbose} = 1;
1464 } # End Initialize
1466 ###############################################################################
1467 # GiveHelp
1468 ###############################################################################
1469 sub GiveHelp {
1470 my($msg) = @_;
1472 $msg ||= 'Online Help';
1474 print <<"EOTXT";
1476 $msg
1478 USAGE: perl vss2svn.pl --vssdir <dir> [options]
1480 REQUIRED PARAMETERS:
1481 --vssdir <dir> : Directory where VSS database is located. This should be
1482 the directory in which the "srcsafe.ini" file is located.
1484 OPTIONAL PARAMETERS:
1485 --ssphys <path> : Full path to ssphys.exe program; uses PATH otherwise
1486 --tempdir <dir> : Temp directory to use during conversion;
1487 default is .\\_vss2svn
1488 --setsvndate : Set svn:date property to original VSS checkin date
1489 (see SVN:DATE WARNING in readme.txt)
1490 --log : Log all output to <tempdir>\\vss2svn.log.txt
1491 --debug : Print lots of debugging info.
1492 EOTXT
1494 exit(1);
1495 } # End GiveHelp
1497 # Following is the data for %gActionType. First field is the node type from
1498 # ssphys; second field is item type (1=project, 2=file); third field is the
1499 # generic action it should be mapped to (loosely mapped to SVN actions)
1501 __DATA__
1502 CreatedProject 1 ADD
1503 AddedProject 1 ADD
1504 RenamedProject 1 RENAME
1505 DeletedProject 1 DELETE
1506 RecoveredProject 1 RECOVER
1507 Checkedin 2 COMMIT
1508 CreatedFile 2 ADD
1509 AddedFile 2 ADD
1510 RenamedFile 2 RENAME
1511 DeletedFile 2 DELETE
1512 RecoveredFile 2 RECOVER
1513 SharedFile 2 COPY
1514 PinnedFile 2 XXX
1515 UnpinnedFile 2 XXX