14 use Benchmark
':hireswallclock';
17 use Vss2Svn
::ActionHandler
;
18 use Vss2Svn
::DataCache
;
19 use Vss2Svn
::SvnRevHandler
;
20 use Vss2Svn
::Dumpfile
;
24 our(%gCfg, %gSth, %gErr, %gFh, $gSysOut, %gActionType, %gNameLookup, %gId);
26 our $VERSION = '0.10';
39 ###############################################################################
41 ###############################################################################
44 # store a hash of actions to take; allows restarting in case of failed
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
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
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
76 BUILDACTIONHIST
=> {handler
=> \
&BuildVssActionHistory
,
79 # Create a dumpfile or import to repository
80 IMPORTSVN
=> {handler
=> \
&ImportToSvn
,
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
};
94 print "Press ENTER to continue...\n";
96 die if $temp =~ m/^quit/i;
99 &{ $info->{handler
} };
100 &SetSystemTask
( $info->{next} );
103 } # End RunConversion
105 ###############################################################################
107 ###############################################################################
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'");
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
137 foreach $name (@
{$entry->{Entry
}}) {
138 if ($name->{id
} == 10 || $name->{id
} == 2) {
139 $cache->add($offset, $name->{content
});
147 ###############################################################################
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
});
159 } # End FindPhysDbFiles
161 ###############################################################################
163 ###############################################################################
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
) {
178 ###############################################################################
180 ###############################################################################
181 sub GetPhysVssHistory
{
182 my($sql, $sth, $row, $physname, $physdir);
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);
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);
205 } # End GetPhysVssHistory
207 ###############################################################################
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 ###############################################################################
233 ###############################################################################
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");
249 &DoSsCmd
("info -e$gCfg{encoding} \"$filesegment[0]/$filesegment[1]/$filesegment[2]\"");
251 my $xml = $xs->XMLin($gSysOut);
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");
263 if ($iteminfo->{Type
} == 1) {
264 $parentphys = (uc($physname) eq 'AAAAAAAA')?
265 '' : &GetProjectParent
($xml);
266 } elsif ($iteminfo->{Type
} == 2) {
269 &ThrowWarning
("Can't handle file '$physname'; not a project or file\n");
273 &GetVssItemVersions
($cache, $physname, $parentphys, $xml);
275 } # End GetVssPhysInfo
277 ###############################################################################
279 ###############################################################################
280 sub GetProjectParent
{
283 no warnings
'uninitialized';
284 return $xml->{ItemInfo
}->{ParentPhys
} || undef;
286 } # End GetProjectParent
288 ###############################################################################
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);
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};
314 warn "\nWARNING: Unknown action '$actionid'\n";
318 $itemtype = $info->{type
};
319 $actiontype = $info->{action
};
321 if ($actiontype eq 'IGNORE') {
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};
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;
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.
387 # OK, since we're describing an action in the child, the parent is
388 # actually this (project) item
390 $parentphys = $physname;
395 if ($itemtype == 1) {
397 } elsif (defined($xml->{ItemInfo
}) &&
398 defined($xml->{ItemInfo
}->{Binary
}) &&
399 $xml->{ItemInfo
}->{Binary
}) {
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) {
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 ###############################################################################
444 ###############################################################################
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;
465 ###############################################################################
467 ###############################################################################
471 $sth = $gCfg{dbh
}->prepare('SELECT offset, name FROM NameLookup');
474 while(defined($row = $sth->fetchrow_hashref() )) {
475 $gNameLookup{ $row->{offset
} } = Encode
::decode_utf8
( $row->{name
} );
477 } # End LoadNameLookup
479 ###############################################################################
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');
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);
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 ###############################################################################
528 ###############################################################################
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;
555 my $sth = $gCfg{dbh
}->prepare($sql);
556 $sth->execute( $parentdata, @
{ $parentrec }{qw(physname actiontype author timestamp)} );
558 return $sth->fetchall_arrayref( {} );
561 ###############################################################################
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
574 no warnings
'uninitialized';
575 $comment = "$row->{comment}\n$child->{comment}";
590 my $sth = $gCfg{dbh
}->prepare($sql);
591 $sth->execute( $child->{version
}, $child->{is_binary
}, $comment,
594 } # End UpdateParentRec
596 ###############################################################################
598 ###############################################################################
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"');
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);
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) {
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) {
644 $update = $gCfg{dbh
}->prepare($sql);
645 $update->execute( undef, $row->{parentphys
},
648 push(@delchild, $row->{action_id
});
652 foreach $id (@delchild) {
653 &DeleteChildRec
($id);
658 } # End MergeMoveData
660 ###############################################################################
662 ###############################################################################
666 my $sql = "DELETE FROM PhysicalAction WHERE action_id = ?";
668 my $sth = $gCfg{dbh
}->prepare($sql);
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);
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
};
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];
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
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) });
790 $joincache->commit();
791 $labelcache->commit();
793 } # End BuildVssActionHistory
795 ###############################################################################
797 ###############################################################################
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.
807 ###############################################################################
809 ###############################################################################
810 sub CreateSvnDumpfile
{
813 my $file = $gCfg{dumpfile
};
815 or &ThrowError
("Could not create dumpfile '$file'");
817 my($sql, $sth, $action_sth, $row, $revision, $actions, $action, $physname, $itemtype);
821 $sql = 'SELECT * FROM SvnRevision ORDER BY revision_id ASC';
823 $sth = $gCfg{dbh
}->prepare($sql);
830 (SELECT action_id FROM SvnRevisionVssAction WHERE revision_id = ?)
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);
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( {} );
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
});
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"
874 my @err = @
{ $dumpfile->{errors
} };
876 if (scalar @err > 0) {
877 map { &ThrowWarning
($_) } @err;
883 } # End CreateSvnDumpfile
885 ###############################################################################
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");
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");
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.
925 if (! -e
"$exportdir/$physname.$version" ) {
926 &DoSsCmd
("get -b -v$version --force-overwrite -e$gCfg{encoding} \"$physpath\" $exportdir/$physname");
930 } # End ExportVssPhysFile
932 ###############################################################################
934 ###############################################################################
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
();
943 ======== VSS2SVN ========
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}
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");
966 ###############################################################################
968 ###############################################################################
971 if (keys(%gErr) || $gCfg{resume
}) {
973 =============================================================================
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.
986 foreach my $task (@
{ $gCfg{errortasks
} }) {
988 print join("\n ", @
{ $gErr{$task} }),"\n";
993 =============================================================================
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/
1013 my $starttime = ctime
($^T
);
1015 my $endtime = ctime
(time);
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
();
1035 Started at : $starttime
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
1047 ###############################################################################
1049 ###############################################################################
1051 my($sql, $actions, $revisions, $mintime, $maxtime);
1060 ($actions) = $gCfg{dbh
}->selectrow_array($sql);
1069 ($revisions) = $gCfg{dbh
}->selectrow_array($sql);
1073 MIN(timestamp), MAX(timestamp)
1078 ($mintime, $maxtime) = $gCfg{dbh
}->selectrow_array($sql);
1080 foreach($mintime, $maxtime) {
1081 $_ = &Vss2Svn
::Dumpfile
::SvnTimestamp
($_);
1086 # initial creation of the repo wasn't considered an action or revision
1087 return($actions - 1, $revisions - 1, $mintime, $maxtime);
1091 ###############################################################################
1093 ###############################################################################
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
1106 ###############################################################################
1108 ###############################################################################
1110 my($cmd, $allowfail) = @_;
1112 print "$cmd\n" if $gCfg{verbose
};
1115 print $gSysOut if $gCfg{debug
};
1120 &ThrowWarning
("FAILED to execute: $!");
1121 die unless $allowfail;
1125 &ThrowWarning
(sprintf "FAILED with non-zero exit status %d (cmd: %s)", $?
>> 8, $cmd);
1126 die unless $allowfail;
1135 ###############################################################################
1137 ###############################################################################
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;
1148 # b.) 0.20.0.123:150
1149 # c.) 0.20.0.123:150
1150 return $1 . "." . $2 || 'unknown';
1151 } # End GetSsVersion
1153 ###############################################################################
1155 ###############################################################################
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}) {
1169 push @
{ $gCfg{errortasks
} }, $task;
1172 push @
{ $gErr{$task} }, $msg;
1174 } # End ThrowWarning
1176 ###############################################################################
1178 ###############################################################################
1180 &ThrowWarning
(@_, [caller()]);
1184 ###############################################################################
1186 ###############################################################################
1187 sub StopConversion
{
1188 &DisconnectDatabase
;
1192 } # End StopConversion
1194 ###############################################################################
1196 ###############################################################################
1199 } # End CloseAllFiles
1201 ###############################################################################
1203 ###############################################################################
1205 my($task, $leavestep) = @_;
1207 print "\nSETTING TASK $task\n" if $gCfg{verbose
};
1211 $sth = $gSth{'SYSTEMTASK'};
1213 if (!defined $sth) {
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 ###############################################################################
1234 ###############################################################################
1238 print "\nSETTING STEP $step\n" if $gCfg{verbose
};
1242 $sth = $gSth{'SYSTEMSTEP'};
1244 if (!defined $sth) {
1252 $sth = $gCfg{'SYSTEMSTEP'} = $gCfg{dbh
}->prepare($sql);
1255 $sth->execute($step);
1257 $gCfg{step
} = $step;
1259 } # End SetSystemStep
1261 ###############################################################################
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 "
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 ###############################################################################
1291 ###############################################################################
1293 if (defined($gCfg{task
}) && $gCfg{task
} eq 'INIT') {
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");
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 ###############################################################################
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.
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 ###############################################################################
1362 ###############################################################################
1373 $sth = $gCfg{dbh
}->prepare($sql);
1384 $sth = $gCfg{dbh
}->prepare($sql);
1390 action_id INTEGER PRIMARY KEY,
1409 $sth = $gCfg{dbh
}->prepare($sql);
1414 PhysicalAction_IDX1 ON PhysicalAction (
1421 $sth = $gCfg{dbh
}->prepare($sql);
1426 PhysicalAction_IDX2 ON PhysicalAction (
1435 $sth = $gCfg{dbh
}->prepare($sql);
1441 action_id INTEGER PRIMARY KEY,
1453 $sth = $gCfg{dbh
}->prepare($sql);
1458 VssAction_IDX1 ON VssAction (
1463 $sth = $gCfg{dbh
}->prepare($sql);
1469 revision_id INTEGER PRIMARY KEY,
1476 $sth = $gCfg{dbh
}->prepare($sql);
1481 SvnRevisionVssAction (
1482 revision_id INTEGER,
1487 $sth = $gCfg{dbh
}->prepare($sql);
1492 SvnRevisionVssAction_IDX1 ON SvnRevisionVssAction (
1498 $sth = $gCfg{dbh
}->prepare($sql);
1511 $sth = $gCfg{dbh
}->prepare($sql);
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);
1527 $sth = $gCfg{dbh
}->prepare($sql);
1530 my $fields = join(', ', @cfgitems);
1531 my $args = join(', ', map {'?'} @cfgitems);
1535 SystemInfo ($fields)
1540 $sth = $gCfg{dbh
}->prepare($sql);
1541 $sth->execute(map {$gCfg{$_}} @cfgitems);
1544 } # End InitSysTables
1546 ###############################################################################
1548 ###############################################################################
1549 sub ReloadSysTables
{
1550 my($sql, $sth, $sthup, $row, $field, $val);
1552 $sql = "SELECT * FROM SystemInfo";
1554 $sth = $gCfg{dbh
}->prepare($sql);
1557 $row = $sth->fetchrow_hashref();
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});
1566 $gCfg{$field} = $val;
1571 &SetSystemTask
($gCfg{task
});
1573 } # End ReloadSysTables
1575 ###############################################################################
1577 ###############################################################################
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 "
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
}) {
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';
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
};
1652 $gCfg{xmlParser
} = 'XML::Simple';
1654 eval { require XML
::SAX
; };
1656 # no XML::SAX; let XML::Simple use its own parser
1660 $gCfg{xmlParser
} = 'XML::SAX::Expat';
1661 $XML::SAX
::ParserPackage
= $gCfg{xmlParser
};
1665 eval { $p = XML
::SAX
::ParserFactory
->parser(); };
1668 # XML::SAX::Expat installed; use it
1670 # for exe version, XML::Parser::Expat needs help finding its encmaps
1672 push(@XML::Parser
::Expat
::Encoding_Path
, @INC);
1676 undef $XML::SAX
::ParserPackage
;
1677 eval { $p = XML
::SAX
::ParserFactory
->parser(); };
1680 $gCfg{xmlParser
} = ref $p;
1684 # couldn't find a better package; go back to XML::Simple
1685 $gCfg{'xmlParser'} = 'XML::Simple';
1688 } # End ConfigureXmlParser
1690 ###############################################################################
1692 ###############################################################################
1696 $msg ||= 'Online Help';
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"