From 68ad85ea7476de4e37bec747a6a2f81b4587c6f2 Mon Sep 17 00:00:00 2001 From: toby Date: Mon, 31 Jul 2006 15:17:47 +0000 Subject: [PATCH] move branches/Dirk/pin_handler back to trunk git-svn-id: http://vss2svn.googlecode.com/svn/trunk@248 2cfd5912-9055-84bd-9a12-e3c18a4b6e42 --- script/Vss2Svn/ActionHandler.pm | 918 ++++++++++++++++++++++++++----- script/Vss2Svn/DataCache.pm | 2 +- script/Vss2Svn/Dumpfile.pm | 679 +++++++++++------------ script/Vss2Svn/Dumpfile/SanityChecker.pm | 522 ++++++++++++++++++ script/Vss2Svn/SvnRevHandler.pm | 7 +- script/vss2svn.pl | 167 +++++- 6 files changed, 1774 insertions(+), 521 deletions(-) create mode 100644 script/Vss2Svn/Dumpfile/SanityChecker.pm diff --git a/script/Vss2Svn/ActionHandler.pm b/script/Vss2Svn/ActionHandler.pm index 63c6f1f..90928f8 100644 --- a/script/Vss2Svn/ActionHandler.pm +++ b/script/Vss2Svn/ActionHandler.pm @@ -11,11 +11,15 @@ our %gHandlers = SHARE => \&_share_handler, BRANCH => \&_branch_handler, MOVE => \&_move_handler, + RESTORE => \&_restore_handler, DELETE => \&_delete_handler, RECOVER => \&_recover_handler, + PIN => \&_pin_handler, + LABEL => \&_label_handler, ); our(%gPhysInfo); +our(%gOrphanedInfo); ############################################################################### # new @@ -28,6 +32,7 @@ sub new { row => $row, action => undef, info => undef, + version => undef, errmsg => '', itempaths => undef, recursed => 0, @@ -88,26 +93,50 @@ sub _add_handler { # the 'sharedphys' array. my $parentphys = $row->{parentphys}; - + my ($orphaned); + if (!defined $parentphys) { + # '_' is used as a magic marker for orphaned files + $row->{parentphys} = '_' . $row->{physname}; +# $row->{itemname} = $row->{physname} . '_' . $row->{itemname}; + $orphaned = 1; + } + + # the version number could have been changed by the share handler + # or in the branch handler, this is the version we branch. + my $version = defined $row->{version} ? $row->{version} + : $self->{version}; + + # if the item to be added was destroyed, then we don't have a version + # number here. So we don't need to add the item anyway. + if (!defined $version ) { $self->{errmsg} .= "Attempt to add entry '$row->{physname}' with " - . "unknown parent\n"; + . "unknown version number (probably destroyed)\n"; + + $gOrphanedInfo {$row->{physname} } = 1; return 0; } - + $gPhysInfo{ $row->{physname} } = { type => $row->{itemtype}, name => $row->{itemname}, - parentphys => $row->{parentphys}, - sharedphys => [], +# parentphys => $row->{parentphys}, +# sharedphys => [], + parents => {}, + last_version => $version, + orphaned => $orphaned, }; + $self->_add_parent ($row->{physname}, $row->{parentphys}); + $self->_track_item_paths ($version); + # File was just created so no need to look for shares - $self->{itempaths} = $self->_get_current_item_paths(1); + $self->{itempaths} = [$self->_get_current_item_path()]; + # don't convert orphaned items +# return $orphaned ? 0 : 1; return 1; - } # End _add_handler ############################################################################### @@ -117,8 +146,44 @@ sub _commit_handler { my($self) = @_; my $row = $self->{row}; - $self->{itempaths} = $self->_get_current_item_paths(); + my $physname = $row->{physname}; + my $physinfo = $gPhysInfo{$physname}; + + if (!defined $physinfo) { + $self->{errmsg} .= "Attempt to commit unknown item '$physname':\n" + . "$self->{physname_seen}\n"; + + return 0; + } + # We need to track at least the version number, even if there is no + # active parent. This is necessary, if we later share this item, we need + # to share from the latest seen version. + + # remember the last version, in which the file was modified + $physinfo->{last_version} = $row->{version}; + + # and track all itempaths for the new version + $self->_track_item_paths ($row->{version}); + + my $itempaths = $self->_get_active_item_paths(); + if (!defined $itempaths && defined $physinfo->{orphaned}) { + $self->{errmsg} .= "No more active itempath to commit to orphaned item '$physname':\n" + . "$self->{physname_seen}\n"; + + return 0; + } + + $self->{itempaths} = $itempaths; + + if (!defined $self->{itempaths}) { + $self->{errmsg} .= "No more active itempath to commit to '$physname':\n" + . "$self->{physname_seen}\n"; + + return 0; + } + + return 1; } # End _commit_handler ############################################################################### @@ -128,19 +193,22 @@ sub _rename_handler { my($self) = @_; my $row = $self->{row}; - # Get the existing paths before the rename; info will contain the new name my $physname = $row->{physname}; - my $itempaths = $self->_get_current_item_paths(); - my $physinfo = $gPhysInfo{$physname}; if (!defined $physinfo) { - $self->{errmsg} .= "Attempt to rename unknown item '$physname':\n" - . "$self->{nameResolveSeen}\n"; - + # only report an error, if the file wasn't detected as orphaned. + if (!defined $gOrphanedInfo {$physname}) { + $self->{errmsg} .= "Attempt to rename unknown item '$physname':\n" + . "$self->{physname_seen}\n"; + } + return 0; } + # Get the existing paths before the rename; info will contain the new name + my $itempaths = $self->_get_vivid_item_paths(); + # Renames on shares may show up once for each share, which we don't want # since one rename takes care of all locations. If the "new" name is # already the same as the old, just ignore it. @@ -151,6 +219,9 @@ sub _rename_handler { # A rename of an item renames it in all its shares $physinfo->{name} = $row->{info}; + # no need to track the itempathes, since a rename doesn't create a new + # item version + $self->{itempaths} = $itempaths; $self->{info} = $row->{info}; @@ -174,27 +245,52 @@ sub _share_handler { return 0; } - push @{ $physinfo->{sharedphys} }, $row->{parentphys}; - - # 'itempaths' is the path for this new location (the share target); - # 'info' contains the source path - my $parentpaths = $self->_get_item_paths($row->{parentphys}, 1); - - $self->{itempaths} = [$parentpaths->[0] . $physinfo->{name}]; - my $sourceinfo = $self->_get_current_item_paths(1); - - if (!defined($sourceinfo) || scalar(@$sourceinfo) == 0) { +# # if this is not a share+pin action, then add this item to the sharedphys +# # list. Otherwise, this item is pinned to a specific version and does not +# # participate in shared actions +# if (!defined $row->{version}) { +# push @{ $physinfo->{sharedphys} }, $row->{parentphys}; +# } + + my $version = $row->{version}; + $version = $physinfo->{last_version} if (!defined $version); + + # 'itempath' is the path for this new location (the share target); + # note: since we can share from a orphaned item, we use the itemname that + # is provided in the row information for the share target and not the + # current name of the item. The orphaned name is mangeled to make it unique + my $parentpath = $self->_get_current_parent_path (); + my $itempath = $parentpath . $row->{itemname}; + + # 'sourceinfo' contains the source path + my $sourceinfo = $self->_get_valid_path ($physname, $row->{parentphys}, $version); + + if (!defined($sourceinfo)) { # We can't figure out the path for the parent that this share came from, # so it was either destroyed or corrupted. That means that this isn't # a share anymore; it's a new add. $self->{action} = 'ADD'; - return $self->_add_handler(); +# $self->{version} = $version; +# return $self->_add_handler(); } - $self->{info} = $sourceinfo->[0]; - return 1; + # track the addition of the new parent + $self->_add_parent ($physname, $row->{parentphys}); + + # if this is a share+pin action, then remember the pin version + if (defined $row->{version}) { + $physinfo->{parents}->{$row->{parentphys}}->{pinned} = $row->{version}; + } + $self->{itempaths} = [$itempath]; + $self->{info} = $sourceinfo; + $self->{version} = $version; + + # the share target is now also a valid "copy from" itempath + $self->_track_item_path ($physname, $row->{parentphys}, $version, $itempath); + + return 1; } # End _share_handler ############################################################################### @@ -215,33 +311,62 @@ sub _branch_handler { my $oldphysinfo = $gPhysInfo{$oldphysname}; - # First delete this parentphys from the old shared object; see - # _delete_handler for details - if ($oldphysinfo->{parentphys} eq $row->{parentphys}) { - $oldphysinfo->{parentphys} = shift( @{ $oldphysinfo->{sharedphys} } ); - } else { - my $sharedphys = []; - - foreach my $oldparent (@{ $oldphysinfo->{sharedphys} }) { - push @$sharedphys, $oldparent - unless $oldparent eq $row->{parentphys}; - } + if (!defined $oldphysinfo) { + $self->{errmsg} .= "Attempt to branch unknown item '$oldphysname':\n" + . "$self->{physname_seen}\n"; - $oldphysinfo->{sharedphys} = $sharedphys; + return 0; + } + +# # First delete this parentphys from the old shared object; see +# # _delete_handler for details +# if ($oldphysinfo->{parentphys} eq $row->{parentphys}) { +# $oldphysinfo->{parentphys} = shift( @{ $oldphysinfo->{sharedphys} } ); +# } else { +# my $sharedphys = []; +# +# foreach my $oldparent (@{ $oldphysinfo->{sharedphys} }) { +# push @$sharedphys, $oldparent +# unless $oldparent eq $row->{parentphys}; +# } +# +# $oldphysinfo->{sharedphys} = $sharedphys; +# } + + # treat the old path as deleted + # we can't branch an item, that doesn't have a parent. This happens when the + # parent was destroyed. + if (defined $row->{parentphys}) { + $oldphysinfo->{parents}->{$row->{parentphys}}->{deleted} = 1; + } + else { + # since we have the "orphaned" handling, we can map this action to an + # addition, so that this item will show up in the orphaned cache. + # TODO: To keep the history of the item we can try to ShareBranch + # from original item if it is also somewhere accessible. + # something like: +# my $copypath = $self->_get_valid_path ($oldphysinfo, $row->{parentphys}, $row->{version}); + + $self->{action} = 'ADD'; } - # Now create a new entry for this branched item - $gPhysInfo{$physname} = - { - type => $row->{itemtype}, - name => $row->{itemname}, - parentphys => $row->{parentphys}, - sharedphys => [], - }; + # Now treat the new entry as a new addition + return $self->_add_handler(); - $self->{itempaths} = $self->_get_current_item_paths(1); +# # Now create a new entry for this branched item +# $gPhysInfo{$physname} = +# { +# type => $row->{itemtype}, +# name => $row->{itemname}, +## parentphys => $row->{parentphys}, +## sharedphys => [], +# parents => {}, +# }; - return 1; +# $self->_add_parent ($physname, $row->{parentphys}); +# $self->{itempaths} = $self->_get_current_item_paths(1); + +# return 1; } # End _branch_handler @@ -255,80 +380,117 @@ sub _move_handler { # Get the existing paths before the move; parent sub will get the new # name my $physname = $row->{physname}; - my $itempaths = $self->_get_current_item_paths(); - my $physinfo = $gPhysInfo{$physname}; - + if (!defined $physinfo) { - $self->{errmsg} .= "Attempt to rename unknown item '$physname':\n" + $self->{errmsg} .= "Attempt to move unknown item '$physname':\n" . "$self->{physname_seen}\n"; return 0; } - # Only projects can have true "moves", and projects don't have shares, so - # we don't need to worry about any shared paths - $physinfo->{parentphys} = $row->{parentphys}; + # '$sourceinfo' is the path for the old location (the move source); + my $parentpath = $self->_get_current_parent_path (); + my $sourceinfo = $parentpath . $row->{itemname}; - # 'itempaths' has the original path; 'info' has the new - $self->{itempaths} = $itempaths; - $self->{info} = $self->_get_current_item_paths(1)->[0]; + # '$itempath' contains the move target path + my $itempath = $self->_get_parent_path ($row->{info}) . $row->{itemname}; - return 1; + if (!defined($sourceinfo)) { + # We can't figure out the path for the parent that this move came from, + # so it was either destroyed or corrupted. That means that this isn't + # a move anymore; it's a new add. + + $self->{action} = 'ADD'; +# $self->{version} = $version; +# return $self->_add_handler(); + + # we need to swap the source and the target path + $sourceinfo = $itempath; + undef $itempath; + } + else { + # set the old parent inactive + $physinfo->{parents}->{$row->{parentphys}}->{deleted} = 1; + } + + # track the addition of the new parent + $self->_add_parent ($physname, $row->{info}); + + $self->{itempaths} = [$sourceinfo]; + $self->{info} = $itempath; + + # the move target is now also a valid "copy from" itempath + $self->_track_item_path ($physname, $row->{parentphys}, $physinfo->{last_version}, $itempath); + return 1; } # End _move_handler ############################################################################### +# _restore_handler +############################################################################### +sub _restore_handler { + my($self) = @_; + my $row = $self->{row}; + + $self->{action} = 'MOVE'; + $row->{actiontype} = 'MOVE'; + $row->{info} = $row->{parentphys}; + $row->{parentphys} = '_' . $row->{physname}; + return $self->_move_handler (); +} + +############################################################################### # _delete_handler ############################################################################### sub _delete_handler { my($self) = @_; my $row = $self->{row}; - # For a delete operation we return only the "main" path, since any deletion - # of shared paths will have their own entry: - # WRONG: we need to return the path of the item to be deleted, and not the - # parent path + # For a delete operation we return the path of the item to be deleted my $physname = $row->{physname}; - my $physinfo = $gPhysInfo{$physname}; if (!defined $physinfo) { - $self->{errmsg} .= "Attempt to delete unknown item '$physname':\n" - . "$self->{physname_seen}\n"; + # only report an error, if the file wasn't detected as orphaned. + if (!defined $gOrphanedInfo {$physname}) { + $self->{errmsg} .= "Attempt to delete unknown item '$physname':\n" + . "$self->{physname_seen}\n"; + } return 0; } + my $parentpath = $self->_get_current_parent_path (); + my $itempaths = [$parentpath . $physinfo->{name}]; + +# if ($physinfo->{parentphys} eq $row->{parentphys}) { +# # Deleting from the "main" parent; find a new one by shifting off the +# # first shared path, if any; if none exists this will leave a null +# # parent entry. We could probably just delete the whole node at this +# # point. +# +# $physinfo->{parentphys} = shift( @{ $physinfo->{sharedphys} } ); +# +# } else { +# my $sharedphys = []; +# +# foreach my $parent (@{ $physinfo->{sharedphys} }) { +# push @$sharedphys, $parent +# unless $parent eq $row->{parentphys}; +# } +# +# $physinfo->{sharedphys} = $sharedphys; +# } + # protect for delete/purge cycles: if the parentphys isn't in the shares # anymore, the file was already deleted from the parent and is now purged - my $parentFound = defined $physinfo->{parentphys}; - foreach my $parent (@{ $physinfo->{sharedphys} }) { - $parentFound = 1 if ($physinfo->{parentphys} eq $parent); + if (defined $physinfo->{parents}->{$row->{parentphys}}->{deleted}) { + return 0; } - return 0 unless $parentFound; - - my $parentpaths = $self->_get_item_paths($row->{parentphys}, 1); - my $itempaths = [$parentpaths->[0] . $physinfo->{name}]; - - if ($physinfo->{parentphys} eq $row->{parentphys}) { - # Deleting from the "main" parent; find a new one by shifting off the - # first shared path, if any; if none exists this will leave a null - # parent entry. We could probably just delete the whole node at this - # point. - - $physinfo->{parentphys} = shift( @{ $physinfo->{sharedphys} } ); - - } else { - my $sharedphys = []; - - foreach my $parent (@{ $physinfo->{sharedphys} }) { - push @$sharedphys, $parent - unless $parent eq $row->{parentphys}; - } - $physinfo->{sharedphys} = $sharedphys; - } + # set the parent inactive + $physinfo->{parents}->{$row->{parentphys}}->{deleted} = 1; $self->{itempaths} = $itempaths; @@ -344,51 +506,137 @@ sub _recover_handler { my $row = $self->{row}; my $physname = $row->{physname}; - my $physinfo = $gPhysInfo{$physname}; if (!defined $physinfo) { - $self->{errmsg} .= "Attempt to recover unknown item '$physname':\n" + # only report an error, if the file wasn't detected as orphaned. + if (!defined $gOrphanedInfo {$physname}) { + $self->{errmsg} .= "Attempt to recover unknown item '$physname':\n" + . "$self->{physname_seen}\n"; + } + + return 0; + } + +# if (defined $physinfo->{parentphys}) { +# # Item still has other shares, so recover it by pushing this parent +# # onto its shared list +# +# push( @{ $physinfo->{sharedphys} }, $row->{parentphys} ); +# +# } else { +# # Recovering its only location; set the main parent back to this +# $physinfo->{parentphys} = $row->{parentphys}; +# } + + # recover this item within the current parent + my $parentinfo = $physinfo->{parents}->{$row->{parentphys}}; + if (undef $parentinfo->{deleted}) { + $self->{errmsg} .= "Attempt to recover an active item '$physname':\n" . "$self->{physname_seen}\n"; return 0; } + undef $parentinfo->{deleted}; + + # We only recover the path explicitly set in this row, so build the path + # ourself by taking the path of this parent and appending the name + my $parentpath = $self->_get_current_parent_path(); + my $itempath = $parentpath . $physinfo->{name}; + + # Since the item could be modified between the delete and the recovery, + # we need to find a valid source for the recover + $self->{info} = + $self->_get_valid_path ($physname, $row->{parentphys}, $row->{version}); + $self->{itempaths} = [$itempath]; + + # We only set the version number, if this item is a file item. If it is a + # project item, we must recover from the last known revision, which is + # determined in the dumpfile handler + if ($row->{itemtype} == 2) { + $self->{version} = $physinfo->{last_version}; + } - if (defined $physinfo->{parentphys}) { - # Item still has other shares, so recover it by pushing this parent - # onto its shared list + return 1; +} # End _recover_handler - push( @{ $physinfo->{sharedphys} }, $row->{parentphys} ); +############################################################################### +# _pin_handler +############################################################################### +sub _pin_handler { + my($self) = @_; + my $row = $self->{row}; + + my $physname = $row->{physname}; + my $physinfo = $gPhysInfo{$physname}; - } else { - # Recovering its only location; set the main parent back to this - $physinfo->{parentphys} = $row->{parentphys}; + if (!defined $physinfo) { + $self->{errmsg} .= "Attempt to pin unknown item '$physname':\n" + . "$self->{physname_seen}\n"; + + return 0; } - # We only recover the path explicitly set in this row, so build the path - # ourself by taking the path of this parent and appending the name - my $parentpaths = $self->_get_item_paths($row->{parentphys}, 1); - $self->{itempaths} = [$parentpaths->[0] . $physinfo->{name}]; + my $parentpath = $self->_get_current_parent_path(); + my $itempath = $parentpath . $physinfo->{name}; + + my $parentinfo = \%{$physinfo->{parents}->{$row->{parentphys}}}; + + my $version = $row->{version}; + if (!defined $row->{version}) { + # this is the unpin handler + undef $parentinfo->{pinned}; + $version = $physinfo->{last_version}; + } + else { + $parentinfo->{pinned} = $row->{version}; + } + + $self->{itempaths} = [$itempath]; + $self->{info} = + $self->_get_valid_path ($physname, $row->{parentphys}, $row->{version}); + $self->{version} = $version; + + # the unpinned target is now also a valid "copy from" itempath + $self->_track_item_path ($physname, $row->{parentphys}, $version, $itempath); return 1; +} # End _pin_handler -} # End _recover_handler +############################################################################### +# _label_handler +############################################################################### +sub _label_handler { + # currently the handler only tracks labels that where assigned to files + # we need this for the item name tracking + my($self) = @_; + my $row = $self->{row}; + my $itempaths = $self->_get_active_item_paths(); + + $self->_track_item_paths ($row->{version}); + + $self->{itempaths} = $itempaths; + $self->{info} = $row->{label}; + + return 1; +} # End _label_handler ############################################################################### -# _get_current_item_paths +# _get_current_parent_path ############################################################################### -sub _get_current_item_paths { - my($self, $mainonly) = @_; +sub _get_current_parent_path { + my($self) = @_; + + return $self->_get_parent_path($self->{row}->{parentphys}); +} # End _get_current_parent_path - return $self->_get_item_paths($self->{row}->{physname}, $mainonly); -} # End _get_current_item_paths ############################################################################### -# _get_item_paths +# _get_parent_path ############################################################################### -sub _get_item_paths { - my($self, $physname, $mainonly) = @_; +sub _get_parent_path { + my($self, $physname) = @_; # Uses recursion to determine the current full paths for an item based on # the name of its physical file. We can't cache this information because @@ -416,10 +664,21 @@ sub _get_item_paths { return undef; } + if ($physname eq '') { + return ''; + } + if ($physname eq 'AAAAAAAA') { # End of recursion; all items must go back to 'AAAAAAAA', which was so # named because that's what most VSS users yell after using it much. :-) - return ['/']; + return '/'; + } + + if ($physname =~ m/^_.*/) { + # End of recursion; this is the orphaned node + # return the name of the orphaned directory + the name of the orphaned + # file in order to make the path unique + return '/orphaned/' . $physname . '/'; } my $physinfo = $gPhysInfo{$physname}; @@ -431,33 +690,169 @@ sub _get_item_paths { return undef; } + #todo: make the behavoir of orphaned file tracking configurable +# if ($physinfo->{orphaned}) { +# return undef; +# } + $self->{physname_seen} .= "$physname, "; - my @pathstoget = - ($physinfo->{parentphys}, @{ $physinfo->{sharedphys} } ); + # In a move szenario, we can have one deleted and one active parent. We + # are only interested in the active ones here. + my @pathstoget = $self->_get_active_parents ($physname); + + # TODO: For projects there should be only one active parent + my $parent = $pathstoget[0]; + + # if we don't have any active parents, the item path itself is deleted + if (!defined ($parent)) { + return undef; + } + + my $result; + + $result = $self->_get_parent_path($pathstoget[0], 1); + + if(!defined($result)) { + return undef; + } + + return $result . $physinfo->{name}; + +} # End _get_parent_path + +############################################################################### +# _get_current_item_paths +############################################################################### +sub _get_current_item_paths { + my($self, $mainonly) = @_; + + my @parents = $self->_get_parents ($self->{row}->{physname}); + return $self->_get_item_paths($self->{row}->{physname}, @parents); +} # End _get_current_item_paths + +############################################################################### +# _get_vivid_item_paths +############################################################################### +sub _get_vivid_item_paths { + my($self, $mainonly) = @_; + + my @parents = $self->_get_vivid_parents ($self->{row}->{physname}); + return $self->_get_item_paths($self->{row}->{physname}, @parents); +} # End _get_vivid_item_paths + +############################################################################### +# _get_active_item_paths +############################################################################### +sub _get_active_item_paths { + my($self, $mainonly) = @_; + + my @parents = $self->_get_active_parents ($self->{row}->{physname}); + return $self->_get_item_paths($self->{row}->{physname}, @parents); +} # End _get_active_item_paths + +############################################################################### +# _get_current_item_path +############################################################################### +sub _get_current_item_path { + my($self) = @_; + + my @parents = $self->_get_parents ($self->{row}->{physname}); + + if (scalar @parents == 0) { + return undef; + } + + my $physname = $self->{row}->{physname}; + my $paths = $self->_get_item_paths($physname, $parents[0]); + + if (!defined $paths) { + $self->{errmsg} .= "Could not retrieve item path for '$physname': " + . "(probably bogous timestamp in parent and child action)\n"; + return undef; + } + + return $paths->[0]; +} # End _get_current_item_path + +############################################################################### +# _get_item_paths +############################################################################### +sub _get_item_paths { + my($self, $physname, @parents) = @_; + + # Uses recursion to determine the current full paths for an item based on + # the name of its physical file. We can't cache this information because + # a rename in a parent folder would not immediately trigger a rename in + # all of the child items. + + # By default, we return an anonymous array of all paths in which the item + # is shared, unless $mainonly is true. Luckily, only files can be shared, + # not projects, so once we start recursing we can set $mainonly to true. + + if ($self->{verbose}) { + my $physprint = (defined $physname)? $physname : '!UNDEF'; + my $space = ($self->{recursed})? ' ' : ''; + print "${space}_get_item_paths($physprint)\n"; + } + - my $paths = []; + if (!defined($physname)) { + return undef; + } + + if ($physname eq 'AAAAAAAA') { + # End of recursion; all items must go back to 'AAAAAAAA', which was so + # named because that's what most VSS users yell after using it much. :-) + return ['/']; + } + + if ($physname =~ m/^_.*/) { + # End of recursion; this is the orphaned node + # return the name of the orphaned directory + the name of the orphaned + # file in order to make the path unique + return '/orphaned/' . $physname . '/'; + } + + my $physinfo = $gPhysInfo{$physname}; + + if (!defined $physinfo) { + $self->{errmsg} .= "Could not determine real path for '$physname':\n" + . "$self->{physname_seen}\n"; + + return undef; + } + + #todo: make the behavoir of orphaned file tracking configurable +# if ($physinfo->{orphaned}) +# { +# return undef; +# } + + $self->{physname_seen} .= "$physname, "; + +# my @pathstoget = +# ($physinfo->{parentphys}, @{ $physinfo->{sharedphys} } ); + my @pathstoget = @parents; + + my $paths; my $result; +# if (defined $physinfo->{parents}->{$row->{parentphys}}->{deleted}) { +# return 0; +# } PARENT: foreach my $parent (@pathstoget) { if (!defined $parent) { next PARENT; } - $result = $self->_get_item_paths($parent, 1); + $result = $self->_get_parent_path($parent); - if(!defined($result) || scalar(@$result) == 0) { + if(!defined($result)) { next PARENT; } - push @$paths, $result->[0] . $physinfo->{name}; - } - - # It may seem unnecessary to get all the paths if we knew we were going to - # return just one, but sometimes one of the paths becomes corrupted so we - # take the first one we can get. - if ($mainonly && @$paths) { - return [ $paths->[0] ]; + push @$paths, $result . $physinfo->{name}; } return $paths; @@ -466,4 +861,271 @@ PARENT: +############################################################################### +# _track_item_paths: +# This function maintains a map that records the itempath that was valid for +# each version of the physical file in the context of the different parents. +# This map is needed, e.g. during pinning when a file is pinned to a previous +# version. Since the file, could have renamed in between, we need to know the +# previous itempath that was valid in the previous version. +# +# This map does not replace the recursive lookup of the itempath in teh function +# _get_item_paths. The itempathes stored here are "historic" item pathes. +# A rename e.g. is not reflectected in the version history of the physical file +# and therefor does not have a distinct version as in subversion. +############################################################################### +sub _track_item_paths { + my($self, $version) = @_; + + my $row = $self->{row}; + + # we only need to track the path for actions that deal with a specific + # version + if (defined $version) { + + my $physinfo = $gPhysInfo{ $row->{physname} }; + + my @parents = $self->_get_active_parents ($row->{physname}); + my $result; + +PARENT: + foreach my $parent (@parents) { + + my $parentpath = $self->_get_parent_path ($parent); + if (!defined $parentpath) { + next PARENT; + } + $result = $parentpath . $physinfo->{name}; + + $self->_track_item_path ($row->{physname}, $parent, $row->{version}, $result); + +# my $versions = \@{$physinfo->{parents}->{$parent}->{versions}}; +# +# # in the case of pinning and sharing with pinning, the version number +# # denotes a version in the past. So if there is already an entry for +# # this version number skip this parent. +# if (exists $versions->[$row->{version}]) { +# next PARENT; +# } + +# # remember the last version, in which the file was modified +# $physinfo->{last_version} = $row->{version}; + +# $result = $self->_get_parent_path ($parent) . $physinfo->{name}; + +# if(!defined($result)) { +# next PARENT; +# } + +# $versions->[$row->{version}] = $result; + } + } +} # End _track_item_paths + + +############################################################################### +# _track_item_path: +############################################################################### +sub _track_item_path { + my($self, $physname, $parent, $version, $itempath) = @_; + + if (defined $version && defined $itempath) { + + my $physinfo = $gPhysInfo{ $physname }; + + my $versions = \@{$physinfo->{parents}->{$parent}->{versions}}; + + # in the case of pinning and sharing with pinning, the version number + # denotes a version in the past. So if there is already an entry for + # this version number skip this parent. + if (exists $versions->[$version]) { + return; + } + + $versions->[$version] = $itempath; + } +} # End _track_item_path + + +############################################################################### +# _get_vivid_parents +# This function returns all parents where the physical file is not deleted, +# r all active projects. If a file is deleted, the file +# does nor take place in any further rename activity, so it is +# inactive. +############################################################################### +sub _get_vivid_parents { + my($self, $physname) = @_; + + my $physinfo = $gPhysInfo{$physname}; + + my @parents; + if (defined $physinfo) { + +PARENT: + foreach my $parentphys (@{$physinfo->{order}}) { + + # skip orphaned parents +# if ($parentphys eq '99999999' ) { +# next PARENT; +# } + + my $parent = $physinfo->{parents}->{$parentphys}; + if (!defined $parent) + { + next PARENT; + } + + # skip deleted parents, since these parents do not + # participate in specific vss action + if (defined $parent->{deleted} ) { + next PARENT; + } + + push @parents, $parentphys; + } + } + + return @parents +} # End _get_vivid_parents + +############################################################################### +# _get_active_parents +# This function returns all parents where the physical file is not deleted +# or pinned, or all active projects. If a file is pinned or deleted, the file +# does nor take place in any further checkin or rename activity, so it is +# inactive. +############################################################################### +sub _get_active_parents { + my($self, $physname) = @_; + + my $physinfo = $gPhysInfo{$physname}; + + my @parents; + if (defined $physinfo) { + +PARENT: + foreach my $parentphys (@{$physinfo->{order}}) { + + # skip orphaned parents +# if ($parentphys eq '99999999' ) { +# next PARENT; +# } + + my $parent = $physinfo->{parents}->{$parentphys}; + if (!defined $parent) + { + next PARENT; + } + + # skip deleted or pinned parents, since these parents do not + # participate in any vss action + if (defined $parent->{deleted} || defined $parent->{pinned} ) { + next PARENT; + } + + push @parents, $parentphys; + } + } + + return @parents +} # End _get_active_parents + +############################################################################### +# _get_parents +# This function returns all parents for the physical file +############################################################################### +sub _get_parents { + my($self, $physname) = @_; + + my $physinfo = $gPhysInfo{$physname}; + + my @parents; + if (defined $physinfo) { + +PARENT: + foreach my $parentphys (@{$physinfo->{order}}) { + + # skip orphaned parents +# if ($parentphys eq '99999999' ) { +# next PARENT; +# } + + my $parent = $physinfo->{parents}->{$parentphys}; + if (!defined $parent) + { + next PARENT; + } + + push @parents, $parentphys; + } + } + + return @parents +} # End _get_active_parents + +############################################################################### +# _get_valid_path +# This function returns an itempath for the physical file, that was valid in +# the previous version. Since all activities that create a new version of a file +# must be done on at least one active path, there should be at least one valid +# item path for the version. +# If we can't find any valid itempath, we can not perform a "copy from" revision +# In this case, we need to recheckin the current content of the item +############################################################################### +sub _get_valid_path { + my($self, $physname, $parentphys, $version) = @_; + + my $physinfo = $gPhysInfo{$physname}; + if (!defined $physinfo) { + return undef; + } + + if (!defined $version) { + $version = $physinfo->{last_version}; + } + + # 1. check the parent requested, if there was an item name for this version + # we can use this item name, since it was valid in that time + my $parent = $physinfo->{parents}->{$parentphys}; + if (defined $parent && +# $parentphys ne '99999999' && + $parent->{versions}->[$version]) { + return $parent->{versions}->[$version]; + } + + # 2. check all other parents in the order, the where added + my @parents; + +PARENT: + foreach $parentphys (@{$physinfo->{order}}) { + + $parent = $physinfo->{parents}->{$parentphys}; + if (defined $parent && +# $parentphys ne '99999999' && + $parent->{versions}->[$version]) { + return $parent->{versions}->[$version]; + } + } + + return undef; +} # End _get_valid_path + +############################################################################### +# _add_parent +# Track the addition of a new parent to this itempath. This will also track the +# order, in which the parents where added to the physical file. The valid +# itempath lookup will search for valid pathes in the order the parents where +# added to the project. +############################################################################### +sub _add_parent { + my($self, $physname, $parentphys) = @_; + + my $physinfo = $gPhysInfo{$physname}; + if (defined $physinfo) { + $physinfo->{parents}->{$parentphys} = {}; + push @{ $physinfo->{order} }, $parentphys; + } +} # End _add_parent + 1; diff --git a/script/Vss2Svn/DataCache.pm b/script/Vss2Svn/DataCache.pm index 0b92bf3..65aba66 100644 --- a/script/Vss2Svn/DataCache.pm +++ b/script/Vss2Svn/DataCache.pm @@ -24,7 +24,7 @@ sub new { pkey => -1, verbose => $gCfg{verbose}, fh => undef, - file => "$gCfg{cachedir}\\datachache.$table.tmp.txt", + file => "$gCfg{cachedir}/datachache.$table.tmp.txt", }; $self = bless($self, $class); diff --git a/script/Vss2Svn/Dumpfile.pm b/script/Vss2Svn/Dumpfile.pm index 29aadd4..d32ef25 100644 --- a/script/Vss2Svn/Dumpfile.pm +++ b/script/Vss2Svn/Dumpfile.pm @@ -1,6 +1,7 @@ package Vss2Svn::Dumpfile; use Vss2Svn::Dumpfile::Node; +use Vss2Svn::Dumpfile::SanityChecker; use Encode qw(from_to); use warnings; @@ -16,13 +17,16 @@ our %gHandlers = MOVE => \&_move_handler, DELETE => \&_delete_handler, RECOVER => \&_recover_handler, + PIN => \&_pin_handler, + LABEL => \&_label_handler, ); # Keep track of when paths were modified or deleted, for subsequent copies # or recovers. -our %gModified = (); +#our %gModified = (); our %gDeleted = (); +our %gVersion = (); ############################################################################### # new @@ -35,12 +39,9 @@ sub new { fh => $fh, revision => 0, errors => [], - modified_cache => {}, deleted_cache => {}, - svn_items => {}, - junk_itempaths => {}, - need_junkdir => 0, - need_missing_dirs => [], + version_cache => [], + repository => Vss2Svn::Dumpfile::SanityChecker->new(), }; # prevent perl from doing line-ending conversions @@ -117,52 +118,32 @@ sub do_action { # if we need to copy or recover one. $self->{is_primary} = 1; - $self->{modified_cache} = {}; $self->{deleted_cache} = {}; + $self->{version_cache} = []; my($handler, $this_action); foreach my $itempath (split "\t", $data->{itempaths}) { $this_action = $action; - if(defined($itempath)) { - ($this_action, $itempath) = - $self->_action_path_sanity_check($this_action, $itempath, $data); - - return 0 unless defined($itempath); - - } else { - # if the item's path isn't defined, its real name was corrupted in - # vss, so we'll check it in to the junk drawer as an add - if (defined $main::gCfg{junkdir}) { - $itempath = $self->_get_junk_itempath($main::gCfg{junkdir}, - join('.', @$data{ qw(physname version revision_id) })); - - $self->add_error("Using filename '$itempath' for item with " - . "unrecoverable name at revision $data->{revision_id}"); - - $this_action = 'ADD'; - } else { - return 0; - } - } +# $this_action = $self->sanity_checker->check ($data, $itempath, $nodes); +# if (!defined ($this_action)) { +# return 0; +# } + + $handler = $gHandlers{$this_action}; - # if need_junkdir = 1, the first item is just about to be added to the - # junk drawer, so create the dumpfile node to add this directory - if ($self->{need_junkdir} == 1) { - $self->_add_svn_dir($nodes, $main::gCfg{junkdir}); - $self->{need_junkdir} = -1; - } + my $thisnodes = []; + $self->$handler($itempath, $thisnodes, $data, $expdir); - foreach my $dir (@{ $self->{need_missing_dirs} }) { - $self->_add_svn_dir($nodes, $dir); - $self->add_error("Creating missing directory '$dir' for item " - . "'$itempath' at revision $data->{revision_id}"); + # we need to apply all local changes to our repository directly: if we + # have an action that operates on multiple items, e.g labeling, the + # necessary missing directories are created for the first item + foreach my $node (@$thisnodes) { + $self->{repository}->load($node); + push @$nodes, $node; } - - $handler = $gHandlers{$this_action}; - - $self->$handler($itempath, $nodes, $data, $expdir); + $self->{is_primary} = 0; } @@ -171,299 +152,57 @@ sub do_action { } my($physname, $cache); - while(($physname, $cache) = each %{ $self->{modified_cache} }) { - $gModified{$physname} = $cache; + + my ($parentphys, $physnames); + while(($parentphys, $physnames) = each %{ $self->{deleted_cache} }) { + while(($physname, $cache) = each %{ $physnames }) { + $gDeleted{$parentphys}->{$physname} = $cache; + } } - while(($physname, $cache) = each %{ $self->{deleted_cache} }) { - $gDeleted{$physname} = $cache; + # track the version -> revision mapping for the file + foreach my $record (@{$self->{version_cache}}) { + my $version = \%{$gVersion{$record->{physname}}->[$record->{version}]}; + $version->{$record->{itempath}} = $record->{revision}; } } # End do_action -############################################################################### -# _get_junk_itempath -############################################################################### -sub _get_junk_itempath { - my($self, $dir, $base) = @_; - - $base =~ s:.*/::; - my $itempath = "$dir/$base"; - my $count = 1; - - if($self->{need_junkdir} == 0) { - $self->{need_junkdir} = 1; - } - - if(!defined($self->{junk_itempaths}->{$itempath})) { - $self->{junk_itempaths}->{$itempath} = 1; - return $itempath; - } - - my($file, $ext); - - if($base =~ m/^(.*)\.(.*)/) { - ($file, $ext) = ($1, ".$2"); - } else { - ($file, $ext) = ($base, ''); - } - - while(defined($self->{junk_itempaths}->{$itempath})) { - $itempath = "$dir/$file.$count$ext"; - $count++; - } - - return $itempath; -} # End _get_junk_itempath - -############################################################################### -# _action_path_sanity_check -############################################################################### -sub _action_path_sanity_check { - my($self, $action, $itempath, $data) = @_; - - my($itemtype, $revision_id) = @{ $data }{qw(itemtype revision_id)}; - - return($action, $itempath) if ($itempath eq '' || $itempath eq '/'); - - my($newaction, $newpath) = ($action, $itempath); - my $success; - - $self->{need_missing_dirs} = []; - - if($action eq 'ADD' || $action eq 'SHARE' || $action eq 'RECOVER') { - $success = $self->_add_svn_struct_item($itempath, $itemtype); - - if(!defined($success)) { - $newpath = undef; - $self->add_error("Path consistency failure while trying to add " - . "item '$itempath' at revision $revision_id; skipping"); - - } elsif($success == 0) { - # trying to re-add existing item; if file, change it to a commit - if ($itemtype == 1) { - - $newpath = undef; - $self->add_error("Attempt to re-add directory '$itempath' at " - . "revision $revision_id; possibly missing delete"); - - } else { - - $newaction = 'COMMIT'; - $self->add_error("Attempt to re-add file '$itempath' at " - . "revision $revision_id, changing to modify; possibly " - . "missing delete"); - - } - } - - } elsif ($action eq 'DELETE') { - $success = $self->_delete_svn_struct_item($itempath, $itemtype); - - if(!$success) { - $newpath = undef; - $self->add_error("Attempt to delete non-existent item '$itempath' " - . "at revision $revision_id; skipping..."); - } - - } elsif ($action eq 'RENAME') { - $success = $self->_rename_svn_struct_item($itempath, $itemtype, - $data->{info}); - - if(!$success) { - $newpath = undef; - $self->add_error("Attempt to rename non-existent item '$itempath' " - . "at revision $revision_id; skipping..."); - } - } elsif ($action eq 'MOVE') { - my ($ref, $item) = $self->_get_svn_struct_ref_for_move($itempath); - - if(!$ref) { - $newpath = undef; - $self->add_error("Attempt to move non-existent directory '$itempath' " - . "at revision $revision_id; skipping..."); - } - - $success = $self->_add_svn_struct_item($data->{info}, 1, $ref->{$item}); - - if(!$success) { - $newpath = undef; - $self->add_error("Error while attempting to move directory '$itempath' " - . "at revision $revision_id; skipping..."); - } - - delete $ref->{$item}; - } - - return($newaction, $newpath); - -} # End _action_path_sanity_check ############################################################################### -# _add_svn_struct_item +# _add_handler ############################################################################### -sub _add_svn_struct_item { - my($self, $itempath, $itemtype, $newref) = @_; - - $itempath =~ s:^/::; - my @subdirs = split '/', $itempath; - - my $item = pop(@subdirs); - my $ref = $self->{svn_items}; - - my $thispath = ''; - - foreach my $subdir (@subdirs) { - $thispath .= "$subdir/"; - - if(ref($ref) ne 'HASH') { - return undef; - } - if(!defined($ref->{$subdir})) { - # parent directory doesn't exist; add it to list of missing dirs - # to build up - push @{ $self->{need_missing_dirs} }, $thispath; +sub _add_handler { + my($self, $itempath, $nodes, $data, $expdir) = @_; - $ref->{$subdir} = {}; + if ($self->{repository}->exists ($itempath)) { + if ($data->{itemtype} == 2) { + $self->add_error("Attempt to re-add file '$itempath' at " + . "revision $data->{revision_id}, changing to modify; possibly " + . "missing delete"); + return $self->_commit_handler ($itempath, $nodes, $data, $expdir); } - - $ref = $ref->{$subdir}; - } - - if(ref($ref) ne 'HASH') { - # parent "directory" is actually a file - return undef; - } - - if(defined($ref->{$item})) { - # item already exists; can't add it - return 0; - } - - if(defined($newref)) { - $ref->{$item} = $newref; - } else { - $ref->{$item} = ($itemtype == 1)? {} : 1; - } - - return 1; - -} # End _add_svn_struct_item - -############################################################################### -# _delete_svn_struct_item -############################################################################### -sub _delete_svn_struct_item { - my($self, $itempath, $itemtype) = @_; - - return $self->_delete_rename_svn_struct_item($itempath, $itemtype); -} # End _delete_svn_struct_item - -############################################################################### -# _rename_svn_struct_item -############################################################################### -sub _rename_svn_struct_item { - my($self, $itempath, $itemtype, $newname) = @_; - - return $self->_delete_rename_svn_struct_item($itempath, $itemtype, $newname); -} # End _rename_svn_struct_item - -############################################################################### -# _delete_rename_svn_struct_item -############################################################################### -sub _delete_rename_svn_struct_item { - my($self, $itempath, $itemtype, $newname, $movedref) = @_; - - $itempath =~ s:^/::; - $newname =~ s:/$:: if defined($newname); - my @subdirs = split '/', $itempath; - - my $item = pop(@subdirs); - my $ref = $self->{svn_items}; - - foreach my $subdir (@subdirs) { - if(!(ref($ref) eq 'HASH') || !defined($ref->{$subdir})) { - # can't get to item because a parent directory doesn't exist; give up - return undef; + else { + $self->add_error("Attempt to re-add directory '$itempath' at " + . "revision $data->{revision_id}, skipping action: possibly " + . "missing delete"); + return 0; } - - $ref = $ref->{$subdir}; } - if((ref($ref) ne 'HASH') || !defined($ref->{$item})) { - # item doesn't exist; can't delete/rename it + my $success = $self->{repository}->exists_parent ($itempath); + if(!defined($success)) { + $self->add_error("Path consistency failure while trying to add " + . "item '$itempath' at revision $data->{revision_id}; skipping"); return 0; } - - if(defined $newname) { - $ref->{$newname} = $ref->{$item}; - } - - delete $ref->{$item}; - - return 1; - -} # End _delete_rename_svn_struct_item - -############################################################################### -# _get_svn_struct_ref_for_move -############################################################################### -sub _get_svn_struct_ref_for_move { - my($self, $itempath) = @_; - - $itempath =~ s:^/::; - my @subdirs = split '/', $itempath; - - my $item = pop(@subdirs); - my $ref = $self->{svn_items}; - - my $thispath = ''; - - foreach my $subdir (@subdirs) { - $thispath .= "$subdir/"; - - if(ref($ref) ne 'HASH') { - return undef; - } - if(!defined($ref->{$subdir})) { - return undef; - } - - $ref = $ref->{$subdir}; - } - - if((ref($ref) ne 'HASH') || !defined($ref->{$item}) || - (ref($ref->{$item} ne 'HASH'))) { - return undef; + elsif ($success == 0) { + $self->add_error("Parent path missing while trying to add " + . "item '$itempath' at revision $data->{revision_id}: adding missing " + . "parents"); + $self->_create_svn_path ($nodes, $itempath); } - - return ($ref, $item); - -} # End _get_svn_struct_ref_for_move - -############################################################################### -# _add_svn_dir -############################################################################### -sub _add_svn_dir { - my($self, $nodes, $dir) = @_; - - my $node = Vss2Svn::Dumpfile::Node->new(); - my $data = { itemtype => 1, is_binary => 0 }; - - $node->set_initial_props($dir, $data); - $node->{action} = 'add'; - - push @$nodes, $node; - $self->_add_svn_struct_item($dir, 1); - -} # End _add_svn_dir - -############################################################################### -# _add_handler -############################################################################### -sub _add_handler { - my($self, $itempath, $nodes, $data, $expdir) = @_; - + my $node = Vss2Svn::Dumpfile::Node->new(); $node->set_initial_props($itempath, $data); $node->{action} = 'add'; @@ -472,7 +211,8 @@ sub _add_handler { $self->get_export_contents($node, $data, $expdir); } - $self->track_modified($data->{physname}, $data->{revision_id}, $itempath); +# $self->track_modified($data->{physname}, $data->{revision_id}, $itempath); + $self->track_version ($data->{physname}, $data->{version}, $itempath); push @$nodes, $node; @@ -484,6 +224,13 @@ sub _add_handler { sub _commit_handler { my($self, $itempath, $nodes, $data, $expdir) = @_; + if (!$self->{repository}->exists ($itempath)) { + $self->add_error("Attempt to commit to non-existant file '$itempath' at " + . "revision $data->{revision_id}, changing to add; possibly " + . "missing recover"); + return $self->_add_handler ($itempath, $nodes, $data, $expdir); + } + my $node = Vss2Svn::Dumpfile::Node->new(); $node->set_initial_props($itempath, $data); $node->{action} = 'change'; @@ -492,7 +239,8 @@ sub _commit_handler { $self->get_export_contents($node, $data, $expdir); } - $self->track_modified($data->{physname}, $data->{revision_id}, $itempath); +# $self->track_modified($data->{physname}, $data->{revision_id}, $itempath); + $self->track_version ($data->{physname}, $data->{version}, $itempath); push @$nodes, $node; @@ -507,7 +255,6 @@ sub _rename_handler { # to rename a file in SVN, we must add "with history" then delete the orig. my $newname = $data->{info}; - my $newpath = $itempath; if ($data->{itemtype} == 1) { @@ -516,6 +263,20 @@ sub _rename_handler { $newpath =~ s:(.*/)?.*:$1$newname:; } + if ($self->{repository}->exists ($newpath)) { + $self->add_error("Attempt to rename item '$itempath' to '$newpath' at " + . "revision $data->{revision_id}, but destination already exists: possibly " + . "missing delete; skipping"); + return 0; + } + + if (!$self->{repository}->exists ($itempath)) { + $self->add_error("Attempt to rename item '$itempath' to '$newpath' at " + . "revision $data->{revision_id}, but source doesn't exists: possibly " + . "missing recover; skipping"); + return 0; + } + my $node = Vss2Svn::Dumpfile::Node->new(); $node->set_initial_props($newpath, $data); $node->{action} = 'add'; @@ -532,10 +293,11 @@ sub _rename_handler { push @$nodes, $node; - $self->track_modified($data->{physname}, $data->{revision_id}, $newpath); +# $self->track_modified($data->{physname}, $data->{revision_id}, $newpath); +# $self->track_version ($data->{physname}, $data->{version}, $newpath); $node = Vss2Svn::Dumpfile::Node->new(); - $node->{path} = $itempath; + $node->set_initial_props($itempath, $data); $node->{action} = 'delete'; $node->{hideprops} = 1; @@ -552,14 +314,36 @@ sub _rename_handler { sub _share_handler { my($self, $itempath, $nodes, $data, $expdir) = @_; + if ($self->{repository}->exists ($itempath)) { + $self->add_error("Attempt to share item '$data->{info}' to '$itempath' at " + . "revision $data->{revision_id}, but destination already exists: possibly " + . "missing delete; skipping"); + return 0; + } + +# It could be possible that we share from a historically renamed item, so we don't check the source +# if ($self->{repository}->exists ($data->{info})) { +# $self->add_error("Attempt to share item '$itempath' to '$newpath' at " +# . "revision $data->{revision_id}, but destination already exists: possibly " +# . "missing delete; skipping"); +# return 0; +# } + my $node = Vss2Svn::Dumpfile::Node->new(); $node->set_initial_props($itempath, $data); $node->{action} = 'add'; - @{ $node }{ qw(copyrev copypath) } - = $self->last_modified_rev_path($data->{physname}); +# @{ $node }{ qw(copyrev copypath) } +# = $self->last_modified_rev_path($data->{physname}); + $node->{copyrev} = + $self->get_revision ($data->{physname}, $data->{version}, $data->{info}); + $node->{copypath} = $data->{info}; + + if (!defined $node->{copyrev} || !defined $node->{copypath}) { + return $self->_commit_handler ($itempath, $nodes, $data, $expdir); + } - return unless defined($node->{copyrev}); + $self->track_version ($data->{physname}, $data->{version}, $itempath); push @$nodes, $node; @@ -573,9 +357,10 @@ sub _branch_handler { # branching is a no-op in SVN - # if the file is copied later, we need to track, the revision of this branch - # see the shareBranchShareModify Test - $self->track_modified($data->{physname}, $data->{revision_id}, $itempath); +# # if the file is copied later, we need to track, the revision of this branch +# # see the shareBranchShareModify Test +# $self->track_modified($data->{physname}, $data->{revision_id}, $itempath); + $self->track_version ($data->{physname}, $data->{version}, $itempath); } # End _branch_handler @@ -589,6 +374,20 @@ sub _move_handler { my $newpath = $data->{info}; + if ($self->{repository}->exists ($newpath)) { + $self->add_error("Attempt to move item '$itempath' to '$newpath' at " + . "revision $data->{revision_id}, but destination already exists: possibly " + . "missing delete; skipping"); + return 0; + } + + if (!$self->{repository}->exists ($itempath)) { + $self->add_error("Attempt to move item '$itempath' to '$newpath' at " + . "revision $data->{revision_id}, but source doesn't exists: possibly " + . "missing recover; skipping"); + return 0; + } + my $node = Vss2Svn::Dumpfile::Node->new(); $node->set_initial_props($newpath, $data); $node->{action} = 'add'; @@ -603,10 +402,11 @@ sub _move_handler { push @$nodes, $node; - $self->track_modified($data->{physname}, $data->{revision_id}, $newpath); +# $self->track_modified($data->{physname}, $data->{revision_id}, $newpath); +# $self->track_version ($data->{physname}, $data->{version}, $newpath); $node = Vss2Svn::Dumpfile::Node->new(); - $node->{path} = $itempath; + $node->set_initial_props($itempath, $data); $node->{action} = 'delete'; $node->{hideprops} = 1; @@ -620,15 +420,22 @@ sub _move_handler { sub _delete_handler { my($self, $itempath, $nodes, $data, $expdir) = @_; + if (!$self->{repository}->exists ($itempath)) { + $self->add_error("Attempt to delete non-existent item '$itempath' at " + . "revision $data->{revision_id}: possibly " + . "missing recover/add/share; skipping"); + return 0; + } + my $node = Vss2Svn::Dumpfile::Node->new(); - $node->{path} = $itempath; + $node->set_initial_props($itempath, $data); $node->{action} = 'delete'; $node->{hideprops} = 1; push @$nodes, $node; - $self->track_deleted($data->{physname}, $data->{revision_id}, - $itempath); + $self->track_deleted($data->{parentphys}, $data->{physname}, + $data->{revision_id}, $itempath); } # End _delete_handler @@ -638,49 +445,200 @@ sub _delete_handler { sub _recover_handler { my($self, $itempath, $nodes, $data, $expdir) = @_; + if ($self->{repository}->exists ($itempath)) { + $self->add_error("Attempt to recover existing item '$itempath' at " + . "revision $data->{revision_id}: possibly " + . "missing delete; change to commit"); + return $self->_commit_handler ($itempath, $nodes, $data, $expdir); + } + my $node = Vss2Svn::Dumpfile::Node->new(); $node->set_initial_props($itempath, $data); $node->{action} = 'add'; - my($copyrev, $copypath) = $self->last_deleted_rev_path($data->{physname}); - - if (!defined $copyrev) { + # for projects we want to go back to the revision just one before the deleted + # revision. For files, we need to go back to the specified revision, since + # the file could have been modified via a share. + my($copyrev, $copypath); + if (!defined ($data->{version})) { + ($copyrev, $copypath)= $self->last_deleted_rev_path($data->{parentphys}, + $data->{physname}); + $copyrev -= 1; + } + else { + $copyrev = + $self->get_revision ($data->{physname}, $data->{version}, $data->{info}); + $copypath = $data->{info}; + } + + if (!defined $copyrev || !defined $copypath) { $self->add_error( "Could not recover path $itempath at revision $data->{revision_id};" - . " unable to determine deleted revision"); + . " unable to determine deleted revision or path"); return 0; } - $node->{copyrev} = $copyrev - 1; + $node->{copyrev} = $copyrev; $node->{copypath} = $copypath; + if (defined ($data->{version})) { + $self->track_version ($data->{physname}, $data->{version}, $itempath); + } + push @$nodes, $node; } # End _recover_handler ############################################################################### -# track_modified +# _pin_handler ############################################################################### -sub track_modified { - my($self, $physname, $revision, $path) = @_; +sub _pin_handler { + my($self, $itempath, $nodes, $data, $expdir) = @_; - return unless $self->{is_primary}; + if (!$self->{repository}->exists ($itempath)) { + $self->add_error("Attempt to pin non-existing item '$itempath' at " + . "revision $data->{revision_id}: possibly " + . "missing recover; skipping"); + return 0; + } - $self->{modified_cache}->{$physname} = + my $copyrev = + $self->get_revision ($data->{physname}, $data->{version}, $data->{info}); + my $copypath = $data->{info}; + + # if one of the necessary copy from attributes are unavailable we fall back + # to a complete checkin + if (!defined $copyrev || !defined $copypath) { + return $self->_commit_handler ($itempath, $nodes, $data, $expdir); + } + + my $node = Vss2Svn::Dumpfile::Node->new(); + $node->set_initial_props($itempath, $data); + $node->{action} = 'add'; + + $node->{copyrev} = $copyrev; + $node->{copypath} = $copypath; + + $self->track_version ($data->{physname}, $data->{version}, $itempath); + + push @$nodes, $node; + +} # End _pin_handler + +############################################################################### +# _label_handler +############################################################################### +sub _label_handler { + my($self, $itempath, $nodes, $data, $expdir) = @_; + + if (!$self->{repository}->exists ($itempath)) { + $self->add_error("Attempt to label non-existing item '$itempath' at " + . "revision $data->{revision_id}: possibly " + . "missing recover; skipping"); + return 0; + } + + my $label = $data->{info}; + + # It is possible that the label was deleted later, so we see here a label + # action, but no label was assigned. In this case, we only need to track + # the version->revision mapping, since the version could have been used + # as a valid share source. + if (defined ($label)) { + my $uniquepath = join('.', @$data{ qw(physname version) }); + my $labelpath = "$main::gCfg{labeldir}/$data->{info}$itempath"; + + $self->_create_svn_path ($nodes, $labelpath); + + my $node = Vss2Svn::Dumpfile::Node->new(); + $node->set_initial_props($labelpath, $data); + $node->{action} = 'add'; + + my $copyrev = $data->{revision_id} - 1; + my $copypath = $itempath; + + $node->{copyrev} = $copyrev; + $node->{copypath} = $copypath; + + push @$nodes, $node; + + } + + $self->track_version ($data->{physname}, $data->{version}, $itempath); +} # End _label_handler + +############################################################################### +# _add_svn_dir +############################################################################### +sub _add_svn_dir { + my($self, $nodes, $dir) = @_; + + my $node = Vss2Svn::Dumpfile::Node->new(); + my $data = { itemtype => 1, is_binary => 0 }; + + $node->set_initial_props($dir, $data); + $node->{action} = 'add'; + + push @$nodes, $node; +} # End _add_svn_dir + + +############################################################################### +# _create_svn_path +############################################################################### +sub _create_svn_path { + my($self, $nodes, $itempath) = @_; + + my $missing_dirs = $self->{repository}->get_missing_dirs($itempath); + + foreach my $dir (@$missing_dirs) { + $self->_add_svn_dir($nodes, $dir); + } +} # End _create_svn_path + +############################################################################### +# track_version +############################################################################### +sub track_version { + my($self, $physname, $version, $itempath) = @_; + + my $record = { - revision => $revision, - path => $path, + physname => $physname, + version => $version, + revision => $self->{revision}, + itempath => $itempath, }; + push @{$self->{version_cache}}, $record; -} # End track_modified +} # End track_version + + +############################################################################### +# get_revision +############################################################################### +sub get_revision { + my($self, $physname, $version, $itempath) = @_; + + if (!defined($gVersion{$physname})) { + return (undef); + } + + if (!exists($gVersion{$physname}->[$version])) { + return (undef); + } + + return $gVersion{$physname}->[$version]->{$itempath}; + +} # End get_revision ############################################################################### # track_deleted ############################################################################### sub track_deleted { - my($self, $physname, $revision, $path) = @_; + my($self, $parentphys, $physname, $revision, $path) = @_; - $self->{deleted_cache}->{$physname} = + $self->{deleted_cache}->{$parentphys}->{$physname} = { revision => $revision, path => $path, @@ -689,29 +647,20 @@ sub track_deleted { } # End track_deleted ############################################################################### -# last_modified_rev_path +# last_deleted_rev_path ############################################################################### -sub last_modified_rev_path { - my($self, $physname) = @_; +sub last_deleted_rev_path { + my($self, $parentphys, $physname) = @_; - if (!defined($gModified{$physname})) { + if (!defined($gDeleted{$parentphys})) { return (undef, undef); } - return @{ $gModified{$physname} }{ qw(revision path) }; -} # End last_modified_rev_path - -############################################################################### -# last_deleted_rev_path -############################################################################### -sub last_deleted_rev_path { - my($self, $physname) = @_; - - if (!defined($gDeleted{$physname})) { + if (!defined($gDeleted{$parentphys}->{$physname})) { return (undef, undef); } - return @{ $gDeleted{$physname} }{ qw(revision path) }; + return @{ $gDeleted{$parentphys}->{$physname} }{ qw(revision path) }; } # End last_deleted_rev_path ############################################################################### @@ -728,7 +677,7 @@ sub get_export_contents { return 0; } - my $file = "$expdir\\$data->{physname}.$data->{version}"; + my $file = "$expdir/$data->{physname}.$data->{version}"; if (!open EXP, "$file") { $self->add_error("Could not open export file '$file'"); diff --git a/script/Vss2Svn/Dumpfile/SanityChecker.pm b/script/Vss2Svn/Dumpfile/SanityChecker.pm new file mode 100644 index 0000000..6c8ca7f --- /dev/null +++ b/script/Vss2Svn/Dumpfile/SanityChecker.pm @@ -0,0 +1,522 @@ +package Vss2Svn::Dumpfile::SanityChecker; + +use warnings; +use strict; + +############################################################################### +# new +############################################################################### +sub new { + my($class) = @_; + + my $self = + { + svn_items => {}, + junk_itempaths => {}, + need_junkdir => 0, + need_missing_dirs => [], + deleted => {}, + }; + + $self = bless($self, $class); + return $self; + +} # End new + + +############################################################################### +# sanity_check +############################################################################### +sub sanity_check { + my($self, $data, $itempath, $nodes) = @_; + my ($this_action); + if (defined ($itempath)) { +# ($this_action, $itempath) = +# $self->_action_path_sanity_check($this_action, $itempath, $data); + + return 0 unless defined($itempath); + + } else { + # if the item's path isn't defined, its real name was corrupted in + # vss, so we'll check it in to the junk drawer as an add + if (defined $main::gCfg{junkdir}) { + $itempath = $self->_get_junk_itempath($main::gCfg{junkdir}, + join('.', @$data{ qw(physname version revision_id) })); + + $self->add_error("Using filename '$itempath' for item with " + . "unrecoverable name at revision $data->{revision_id}"); + + $this_action = 'ADD'; + } else { + return undef; + } + } + + # if need_junkdir = 1, the first item is just about to be added to the + # junk drawer, so create the dumpfile node to add this directory + if ($self->{need_junkdir} == 1) { + $self->_add_svn_dir($nodes, $main::gCfg{junkdir}); + $self->{need_junkdir} = -1; + } + + foreach my $dir (@{ $self->{need_missing_dirs} }) { + $self->_add_svn_dir($nodes, $dir); + $self->add_error("Creating missing directory '$dir' for item " + . "'$itempath' at revision $data->{revision_id}"); + } +} + +############################################################################### +# _get_junk_itempath +############################################################################### +sub _get_junk_itempath { + my($self, $dir, $base) = @_; + + $base =~ s:.*/::; + my $itempath = "$dir/$base"; + my $count = 1; + + if($self->{need_junkdir} == 0) { + $self->{need_junkdir} = 1; + } + + if(!defined($self->{junk_itempaths}->{$itempath})) { + $self->{junk_itempaths}->{$itempath} = 1; + return $itempath; + } + + my($file, $ext); + + if($base =~ m/^(.*)\.(.*)/) { + ($file, $ext) = ($1, ".$2"); + } else { + ($file, $ext) = ($base, ''); + } + + while(defined($self->{junk_itempaths}->{$itempath})) { + $itempath = "$dir/$file.$count$ext"; + $count++; + } + + return $itempath; +} # End _get_junk_itempath + + +############################################################################### +# exists +############################################################################### +sub exists { + my($self, $itempath) = @_; + + my ($ref, $missing, $item) = $self->_get_svn_struct_ref_for_move ($itempath); + + if(!defined ($ref)) { + # some strange problem + return undef; + } + + if(ref($ref) ne 'HASH') { + # parent isn't a directory + return 0; + } + + if (!defined ($item)) { + # Are we looking for the root node? + if ($itempath eq '/') { + return 1; + } + + return 0; # or undef?? + } + + if(!defined ($missing) || (@$missing == 0)) { + # all parent dirs exists + return defined ($ref->{$item}); + } + + return 0; + +} # End exists + +############################################################################### +# exists_parent +############################################################################### +sub exists_parent { + my($self, $itempath) = @_; + + my ($ref, $missing, $item) = $self->_get_svn_struct_ref_for_move ($itempath); + + if(!defined ($ref)) { + # some strange problem + return undef; + } + + if(ref($ref) ne 'HASH') { + # parent isn't a directory + return 0; + } + + if(!defined ($missing) || (@$missing == 0)) { + # all parent dirs exists + return 1; + } + + return 0; +} # End exists_parent + +############################################################################### +# get_missing_dirs +############################################################################### +sub get_missing_dirs { + my($self, $itempath) = @_; + + my ($ref, $missing, $item) = $self->_get_svn_struct_ref_for_move ($itempath); + + if(!defined ($ref)) { + # some strange problem + return undef; + } + + if(ref($ref) ne 'HASH') { + # parent isn't a directory + return 0; + } + + return $missing; +} # End get_missing_dirs + +############################################################################### +# _get_svn_struct_ref_for_copy +############################################################################### +sub _get_svn_struct_ref_for_copy { + my($self, $itempath) = @_; + + my ($ref, $missing, $item) = $self->_get_svn_struct_ref_for_move ($itempath); + + if(!defined ($ref)) { + # some strange problem + return undef; + } + + if(ref($ref) ne 'HASH') { + # parent isn't a directory + return undef; + } + + if (!defined ($item)) { + # Are we looking for the root node? + if ($itempath eq '/') { + return $ref; + } + + return undef; + } + + if(!defined ($missing) || (@$missing == 0)) { + # all parent dirs exists + if (defined ($ref->{$item})) { + return $ref->{$item}; + } + elsif (defined ($self->{deleted}->{$itempath})) { + $ref = $self->{deleted}->{$itempath}; + delete $self->{deleted}->{$itempath}; + return $ref; + } + } + + return undef; +} # End _get_svn_struct_ref_for_copy + +############################################################################### +# load +############################################################################### +sub load { + my($self, $node) = @_; + + if($node->{action} eq 'add'){ + my $ref; + if ($node->{kind} eq 'dir' && defined $node->{copypath}) { + $ref = $self->_get_svn_struct_ref_for_copy ($node->{copypath}); + } + $self->_add_svn_struct_item ($node->{path}, ($node->{kind} eq 'dir') ? 1 : 2, $ref); + } + elsif($node->{action} eq 'change'){ + # nothing to do + } + elsif($node->{action} eq 'delete'){ + $self->_delete_svn_struct_item ($node->{path}, ($node->{kind} eq 'dir') ? 1 : 2); + } +} + +############################################################################### +# _action_path_sanity_check +############################################################################### +sub _action_path_sanity_check { + my($self, $action, $itempath, $data) = @_; + + my($itemtype, $revision_id) = @{ $data }{qw(itemtype revision_id)}; + + return($action, $itempath) if ($itempath eq '' || $itempath eq '/'); + + my($newaction, $newpath) = ($action, $itempath); + my $success; + + $self->{need_missing_dirs} = []; + + if($action eq 'ADD' || $action eq 'SHARE' || $action eq 'RECOVER') { + $success = $self->_add_svn_struct_item($itempath, $itemtype); + + if(!defined($success)) { + $newpath = undef; + $self->add_error("Path consistency failure while trying to add " + . "item '$itempath' at revision $revision_id; skipping"); + + } elsif($success == 0) { + # trying to re-add existing item; if file, change it to a commit + if ($itemtype == 1) { + + $newpath = undef; + $self->add_error("Attempt to re-add directory '$itempath' at " + . "revision $revision_id; possibly missing delete"); + + } else { + + $newaction = 'COMMIT'; + $self->add_error("Attempt to re-add file '$itempath' at " + . "revision $revision_id, changing to modify; possibly " + . "missing delete"); + + } + } + + } elsif ($action eq 'DELETE') { + $success = $self->_delete_svn_struct_item($itempath, $itemtype); + + if(!$success) { + $newpath = undef; + $self->add_error("Attempt to delete non-existent item '$itempath' " + . "at revision $revision_id; skipping..."); + } + + } elsif ($action eq 'RENAME') { + $success = $self->_rename_svn_struct_item($itempath, $itemtype, + $data->{info}); + + if(!$success) { + $newpath = undef; + $self->add_error("Attempt to rename non-existent item '$itempath' " + . "at revision $revision_id; skipping..."); + } + } elsif ($action eq 'MOVE') { + my ($ref, $item) = $self->_get_svn_struct_ref_for_move($itempath); + + if(!$ref) { + $newpath = undef; + $self->add_error("Attempt to move non-existent directory '$itempath' " + . "at revision $revision_id; skipping..."); + } + + $success = $self->_add_svn_struct_item($data->{info}, 1, $ref->{$item}); + + if(!$success) { + $newpath = undef; + $self->add_error("Error while attempting to move directory '$itempath' " + . "at revision $revision_id; skipping..."); + } + + delete $ref->{$item}; + } + + return($newaction, $newpath); + +} # End _action_path_sanity_check + +############################################################################### +# _add_svn_struct_item +############################################################################### +sub _add_svn_struct_item { + my($self, $itempath, $itemtype, $newref) = @_; + + $itempath =~ s:^/::; + my @subdirs = split '/', $itempath; + + my $item = pop(@subdirs); + my $ref = $self->{svn_items}; + + my $thispath = ''; + + foreach my $subdir (@subdirs) { + $thispath .= "$subdir/"; + + if(ref($ref) ne 'HASH') { + return undef; + } + if(!defined($ref->{$subdir})) { + # parent directory doesn't exist; add it to list of missing dirs + # to build up + push @{ $self->{need_missing_dirs} }, $thispath; + + $ref->{$subdir} = {}; + } + + $ref = $ref->{$subdir}; + } + + if(ref($ref) ne 'HASH') { + # parent "directory" is actually a file + return undef; + } + + if(defined($ref->{$item})) { + # item already exists; can't add it + return 0; + } + + if(defined($newref)) { + $ref->{$item} = $newref; + } else { + $ref->{$item} = ($itemtype == 1)? {} : 1; + } + + return 1; + +} # End _add_svn_struct_item + +############################################################################### +# _delete_svn_struct_item +############################################################################### +sub _delete_svn_struct_item { + my($self, $itempath, $itemtype) = @_; + + return $self->_delete_rename_svn_struct_item($itempath, $itemtype); +} # End _delete_svn_struct_item + +############################################################################### +# _rename_svn_struct_item +############################################################################### +sub _rename_svn_struct_item { + my($self, $itempath, $itemtype, $newname) = @_; + + return $self->_delete_rename_svn_struct_item($itempath, $itemtype, $newname); +} # End _rename_svn_struct_item + +############################################################################### +# _delete_rename_svn_struct_item +############################################################################### +sub _delete_rename_svn_struct_item { + my($self, $itempath, $itemtype, $newname, $movedref) = @_; + + my ($path); + $path = $itempath; + $path =~ s:^/::; + $newname =~ s:/$:: if defined($newname); + my @subdirs = split '/', $path; + + my $item = pop(@subdirs); + my $ref = $self->{svn_items}; + + foreach my $subdir (@subdirs) { + if(!(ref($ref) eq 'HASH') || !defined($ref->{$subdir})) { + # can't get to item because a parent directory doesn't exist; give up + return undef; + } + + $ref = $ref->{$subdir}; + } + + if((ref($ref) ne 'HASH') || !defined($ref->{$item})) { + # item doesn't exist; can't delete/rename it + return 0; + } + + if(defined $newname) { + $ref->{$newname} = $ref->{$item}; + } + + if (defined ($ref->{$item}) && ref($ref->{$item}) eq 'HASH') { + $self->{deleted}->{$itempath} = $ref->{$item}; + } + + delete $ref->{$item}; + + return 1; + +} # End _delete_rename_svn_struct_item + +############################################################################### +# _get_svn_struct_ref_for_move +############################################################################### +sub _get_svn_struct_ref_for_move { + my($self, $itempath) = @_; + + $itempath =~ s:^/::; + my @subdirs = split '/', $itempath; + + my $item = pop(@subdirs); + my $ref = $self->{svn_items}; + + my $thispath = ''; + + while (@subdirs) { +# foreach my $subdir (@subdirs) { + my $subdir = shift @subdirs; + $thispath .= "$subdir/"; + + if(ref($ref) ne 'HASH') { + return ($ref, undef, $item); + } + + if(!defined($ref->{$subdir})) { + my @missing_dirs; + push @missing_dirs, $thispath; + + foreach $subdir (@subdirs) { + $thispath .= "$subdir/"; + push @missing_dirs, $thispath; + } + return ($ref, \@missing_dirs, $item); + + } + + $ref = $ref->{$subdir}; + } + + return ($ref, undef, $item); + +} # End _get_svn_struct_ref_for_move + +############################################################################### +# _add_svn_dir +############################################################################### +sub _add_svn_dir { + my($self, $nodes, $dir) = @_; + + my $node = Vss2Svn::Dumpfile::Node->new(); + my $data = { itemtype => 1, is_binary => 0 }; + + $node->set_initial_props($dir, $data); + $node->{action} = 'add'; + + push @$nodes, $node; + $self->_add_svn_struct_item($dir, 1); + +} # End _add_svn_dir + +############################################################################### +# _create_svn_path +############################################################################### +sub _create_svn_path { + my($self, $nodes, $itempath) = @_; + + $self->{need_missing_dirs} = []; + + $self->_add_svn_struct_item($itempath, 1); + + foreach my $dir (@{ $self->{need_missing_dirs} }) { + $self->_add_svn_dir($nodes, $dir); + } + + $self->{need_missing_dirs} = []; +} # End _create_svn_path + +1; diff --git a/script/Vss2Svn/SvnRevHandler.pm b/script/Vss2Svn/SvnRevHandler.pm index 0416acb..b4d2a1e 100644 --- a/script/Vss2Svn/SvnRevHandler.pm +++ b/script/Vss2Svn/SvnRevHandler.pm @@ -1,4 +1,4 @@ -package Vss2Svn::SvnRevHandler; + package Vss2Svn::SvnRevHandler; use warnings; use strict; @@ -14,7 +14,10 @@ sub new { my($class) = @_; my $svncache = Vss2Svn::DataCache->new('SvnRevision', 1); - + + # we need to start at revision 1 and not 0 + ++$svncache->{pkey}; + if (!defined($svncache)) { print "\nERROR: Could not create cache 'SvnRevision'\n"; return undef; diff --git a/script/vss2svn.pl b/script/vss2svn.pl index eee4b06..45c4884 100755 --- a/script/vss2svn.pl +++ b/script/vss2svn.pl @@ -63,6 +63,10 @@ sub RunConversion { # Merge data from parent records into child records where possible MERGEPARENTDATA => {handler => \&MergeParentData, + next => 'MERGEMOVEDATA'}, + + # Merge data from move actions + MERGEMOVEDATA => {handler => \&MergeMoveData, next => 'BUILDACTIONHIST'}, # Take the history of physical actions and convert them to VSS @@ -136,10 +140,8 @@ ENTRY: } $cache->commit(); - } # End LoadVssNames - ############################################################################### # FindPhysDbFiles ############################################################################### @@ -291,7 +293,7 @@ sub GetVssItemVersions { my($parentdata, $version, $vernum, $action, $name, $actionid, $actiontype, $tphysname, $itemname, $itemtype, $parent, $user, $timestamp, $comment, - $is_binary, $info, $priority, $sortkey, $cachename); + $is_binary, $info, $priority, $sortkey, $label, $cachename); VERSION: foreach $version (@{ $xml->{Version} }) { @@ -323,11 +325,36 @@ VERSION: $info = undef; $parentdata = 0; $priority = 5; - + $label = undef; + if ($version->{Comment} && !ref($version->{Comment})) { $comment = $version->{Comment} || undef; } + # In case of Label the itemtype is the type of the item currently + # under investigation + if ($actiontype eq 'LABEL') { + my $iteminfo = $xml->{ItemInfo}; + $itemtype = $iteminfo->{Type}; + + } + + # we can have label actions and labes attached to versions + if (defined $action->{Label} && !ref($action->{Label})) { + $label = $action->{Label}; + + # append the label comment to a possible version comment + if ($action->{LabelComment} && !ref($action->{LabelComment})) { + if (defined $comment) { + print "Merging LabelComment and Comment for " + . "'$tphysname;$version->{VersionNumber}'"; # if $gCfg{verbose}; + $comment .= "\n"; + } + + $comment .= $action->{LabelComment} || undef; + } + } + if (defined($comment)) { $comment =~ s/^\s+//s; $comment =~ s/\s+$//s; @@ -382,14 +409,20 @@ VERSION: } } elsif ($actiontype eq 'BRANCH') { $info = $action->{Parent}; - } elsif ($actiontype eq 'PIN') { - $info = $action->{PinnedToVersion}; - } + } $vernum = ($parentdata)? undef : $version->{VersionNumber}; + # since there is no corresponding client action for PIN, we need to + # enter the concrete version number here manually + # In a share action the pinnedToVersion attribute can also be set +# if ($actiontype eq 'PIN') { + $vernum = $action->{PinnedToVersion} if (defined $action->{PinnedToVersion}); +# } + $priority -= 4 if $actiontype eq 'ADD'; # Adds are always first $priority -= 3 if $actiontype eq 'SHARE'; + $priority -= 3 if $actiontype eq 'PIN'; $priority -= 2 if $actiontype eq 'BRANCH'; # store the reversed physname as a sortkey; a bit wasteful but makes @@ -398,7 +431,7 @@ VERSION: $cache->add($tphysname, $vernum, $parentphys, $actiontype, $itemname, $itemtype, $timestamp, $user, $is_binary, $info, $priority, - $sortkey, $parentdata, $comment); + $sortkey, $parentdata, $label, $comment); } @@ -492,7 +525,7 @@ sub MergeParentData { # GetChildRecs ############################################################################### sub GetChildRecs { - my($parentrec) = @_; + my($parentrec, $parentdata) = @_; # Here we need to find any child rows which give us additional info on the # parent rows. There's no definitive way to find matching rows, but joining @@ -501,23 +534,25 @@ sub GetChildRecs { # so we need to also look for any that are up to two seconds apart and hope # we don't get the wrong row. + $parentdata = 0 unless defined $parentdata; + my $sql = <<"EOSQL"; SELECT * FROM PhysicalAction WHERE - parentdata = 0 + parentdata = ? AND physname = ? AND actiontype = ? - AND (? - timestamp IN (0, 1, 2, 3, 4)) + AND (? - timestamp IN (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25)) AND author = ? ORDER BY timestamp EOSQL my $sth = $gCfg{dbh}->prepare($sql); - $sth->execute( @{ $parentrec }{qw(physname actiontype timestamp author)} ); + $sth->execute( $parentdata, @{ $parentrec }{qw(physname actiontype timestamp author)} ); return $sth->fetchall_arrayref( {} ); } # End GetChildRecs @@ -558,6 +593,53 @@ EOSQL } # End UpdateParentRec ############################################################################### +# MergeMoveData +############################################################################### +sub MergeMoveData { + # Similar to the MergeParentData, the MergeMove Data combines two the src + # and target move actions into one move action. Since both items are parents + # the MergeParentData function can not deal with this specific problem + + my($sth, $rows, $row); + $sth = $gCfg{dbh}->prepare('SELECT * FROM PhysicalAction ' + . 'WHERE actiontype = "MOVE_FROM"'); + $sth->execute(); + + # need to pull in all recs at once, since we'll be updating/deleting data + $rows = $sth->fetchall_arrayref( {} ); + + my($childrecs, $child, $id); + my @delchild = (); + + foreach $row (@$rows) { + $row->{actiontype} = 'MOVE'; + $childrecs = &GetChildRecs($row, 1); + + if (scalar @$childrecs > 1) { + &ThrowWarning("Multiple chidl recs for parent MOVE rec " + . "'$row->{action_id}'"); + } + + foreach $child (@$childrecs) { + my $update; + $update = $gCfg{dbh}->prepare('UPDATE PhysicalAction SET info = ?' + . 'WHERE action_id = ?'); + + $update->execute( $row->{parentphys}, $child->{action_id} ); + } + + push(@delchild, $row->{action_id}); + } + + foreach $id (@delchild) { + &DeleteChildRec($id); + } + + 1; + +} # End MergeMoveData + +############################################################################### # DeleteChildRec ############################################################################### sub DeleteChildRec { @@ -579,6 +661,9 @@ sub BuildVssActionHistory { my $joincache = Vss2Svn::DataCache->new('SvnRevisionVssAction') || &ThrowError("Could not create cache 'SvnRevisionVssAction'"); + my $labelcache = Vss2Svn::DataCache->new('Label') + || &ThrowError("Could not create cache 'Label'"); + # This will keep track of the current SVN revision, and increment it when # the author or comment changes, the timestamps span more than an hour # (by default), or the same physical file is affected twice @@ -590,14 +675,13 @@ sub BuildVssActionHistory { my($sth, $row, $action, $handler, $physinfo, $itempaths, $allitempaths); my $sql = 'SELECT * FROM PhysicalAction ORDER BY timestamp ASC, ' - . 'priority ASC, sortkey ASC'; + . 'itemtype ASC, priority ASC, sortkey ASC'; $sth = $gCfg{dbh}->prepare($sql); $sth->execute(); ROW: while(defined($row = $sth->fetchrow_hashref() )) { - $svnrevs->check($row); $action = $row->{actiontype}; $handler = Vss2Svn::ActionHandler->new($row); @@ -646,26 +730,41 @@ ROW: } } + # we need to check for the next rev number, after all pathes that can + # prematurally call the next row. Otherwise, we get an empty revision. + $svnrevs->check($row); + # May contain add'l info for the action depending on type: # RENAME: the new name (without path) # SHARE: the source path which was shared # MOVE: the new path - # PIN: the version that was pinned + # PIN: the path of the version that was pinned + # LABEL: the name of the label $row->{info} = $handler->{info}; + # The version may have changed + if (defined $handler->{version}) { + $row->{version} = $handler->{version}; + } + $allitempaths = join("\t", @$itempaths); $row->{itempaths} = $allitempaths; - $vsscache->add(@$row{ qw(physname version actiontype itempaths + $vsscache->add(@$row{ qw(parentphys physname version actiontype itempaths itemtype is_binary info) }); $joincache->add( $svnrevs->{revnum}, $vsscache->{pkey} ); + + if (defined $row->{label}) { + $labelcache->add(@$row{ qw(physname version label itempaths) }); + } } $vsscache->commit(); $svnrevs->commit(); $joincache->commit(); - + $labelcache->commit(); + } # End BuildVssActionHistory ############################################################################### @@ -719,7 +818,7 @@ REVISION: $revision = $row->{revision_id}; $dumpfile->begin_revision($row); - next REVISION if $revision == 0; +# next REVISION if $revision == 0; $action_sth->execute($revision); $actions = $action_sth->fetchall_arrayref( {} ); @@ -1156,7 +1255,7 @@ sub SetupGlobals { &ReloadSysTables; } - $gCfg{ssphys} = 'SSPHYS.exe' if !defined($gCfg{ssphys}); + $gCfg{ssphys} = 'ssphys' if !defined($gCfg{ssphys}); $gCfg{vssdatadir} = "$gCfg{vssdir}/data"; (-d "$gCfg{vssdatadir}") or &ThrowError("$gCfg{vssdir} does not appear " @@ -1186,11 +1285,12 @@ sub SetupActionTypes { CreatedProject => {type => 1, action => 'ADD'}, AddedProject => {type => 1, action => 'ADD'}, RenamedProject => {type => 1, action => 'RENAME'}, - MovedProjectTo => {type => 1, action => 'IGNORE'}, - MovedProjectFrom => {type => 1, action => 'MOVE'}, + MovedProjectTo => {type => 1, action => 'MOVE'}, + MovedProjectFrom => {type => 1, action => 'MOVE_FROM'}, DeletedProject => {type => 1, action => 'DELETE'}, DestroyedProject => {type => 1, action => 'DELETE'}, RecoveredProject => {type => 1, action => 'RECOVER'}, + Restore => {type => 1, action => 'RESTORE'}, CheckedIn => {type => 2, action => 'COMMIT'}, CreatedFile => {type => 2, action => 'ADD'}, AddedFile => {type => 2, action => 'ADD'}, @@ -1200,10 +1300,10 @@ sub SetupActionTypes { RecoveredFile => {type => 2, action => 'RECOVER'}, SharedFile => {type => 2, action => 'SHARE'}, BranchFile => {type => 2, action => 'BRANCH'}, - PinnedFile => {type => 2, action => 'IGNORE'}, + PinnedFile => {type => 2, action => 'PIN'}, RollBack => {type => 2, action => 'BRANCH'}, - UnpinnedFile => {type => 2, action => 'IGNORE'}, - Labeled => {type => 2, action => 'IGNORE'}, + UnpinnedFile => {type => 2, action => 'PIN'}, + Labeled => {type => 2, action => 'LABEL'}, ); } # End SetupActionTypes @@ -1252,6 +1352,7 @@ CREATE TABLE priority INTEGER, sortkey VARCHAR, parentdata INTEGER, + label VARCHAR, comment TEXT ) EOSQL @@ -1289,6 +1390,7 @@ EOSQL CREATE TABLE VssAction ( action_id INTEGER PRIMARY KEY, + parentphys VARCHAR, physname VARCHAR, version INTEGER, action VARCHAR, @@ -1347,6 +1449,19 @@ EOSQL $sth = $gCfg{dbh}->prepare($sql); $sth->execute; + $sql = <<"EOSQL"; +CREATE TABLE + Label ( + physical VARCHAR, + version INTEGER, + label VARCHAR, + imtempaths VARCHAR + ) +EOSQL + + $sth = $gCfg{dbh}->prepare($sql); + $sth->execute; + my @cfgitems = qw(task step vssdir svnurl svnuser svnpwd ssphys tempdir setsvndate starttime); @@ -1446,6 +1561,8 @@ sub Initialize { $gCfg{junkdir} = '/lost+found'; + $gCfg{labeldir} = '/labels'; + $gCfg{errortasks} = []; &ConfigureXmlParser(); @@ -1458,7 +1575,7 @@ sub Initialize { rmtree($gCfg{vssdata}) if (-e $gCfg{vssdata}); mkdir $gCfg{vssdata}; - $gCfg{ssphys} ||= 'SSPHYS.exe'; + $gCfg{ssphys} ||= 'ssphys'; $gCfg{svn} ||= 'SVN.exe'; $gCfg{task} = 'INIT'; -- 2.11.4.GIT