1 package Vss2Svn
::Dumpfile
;
3 use Vss2Svn
::Dumpfile
::Node
;
4 use Vss2Svn
::Dumpfile
::SanityChecker
;
5 use Vss2Svn
::Dumpfile
::AutoProps
;
6 use Vss2Svn
::Dumpfile
::LabelMapper
;
19 ADD
=> \
&_add_handler
,
20 COMMIT
=> \
&_commit_handler
,
21 RENAME
=> \
&_rename_handler
,
22 SHARE
=> \
&_share_handler
,
23 BRANCH
=> \
&_branch_handler
,
24 MOVE
=> \
&_move_handler
,
25 DELETE
=> \
&_delete_handler
,
26 RECOVER
=> \
&_recover_handler
,
27 PIN
=> \
&_pin_handler
,
28 LABEL
=> \
&_label_handler
,
31 # Keep track of when paths were modified or deleted, for subsequent copies
39 ###############################################################################
41 ###############################################################################
43 my($class, $dir) = @_;
48 ###############################################################################
50 ###############################################################################
52 my($class, $fh, $autoprops, $md5, $labelmapper) = @_;
61 repository
=> Vss2Svn
::Dumpfile
::SanityChecker
->new(),
62 auto_props
=> $autoprops,
64 label_mapper
=> $labelmapper,
67 # prevent perl from doing line-ending conversions
70 my $old = select($fh);
74 print $fh "SVN-fs-dump-format-version: 2\n\n";
76 my $ug = new Data
::UUID
;
77 my $uuid = $ug->to_string( $ug->create() );
79 print $fh "UUID: $uuid\n\n";
81 $self = bless($self, $class);
86 ###############################################################################
88 ###############################################################################
98 ###############################################################################
100 ###############################################################################
102 my($self, $data) = @_;
103 my($revision, $author, $timestamp, $comment) =
104 @
{ $data }{qw(revision_id author timestamp comment)};
107 my $fh = $self->{fh
};
109 print $fh "\nRevision-number: $revision\n";
111 $comment = '' if !defined($comment);
112 $author = '' if !defined($author);
115 $props = { 'svn:log' => $comment,
116 'svn:author' => $author,
120 $props->{'svn:date'} = $self->svn_timestamp($timestamp);
122 $self->output_content($props);
123 $self->{revision
} = $revision;
125 } # End begin_revision
127 ###############################################################################
129 ###############################################################################
131 my($self, $data, $expdir) = @_;
133 my $action = $data->{action
};
137 # Temporary hack to prevent shared files from stepping on the "modified"
138 # flag for other than the first commit. Ideally, we should keep all paths
139 # for a given physical file's last modified flags, and use the best match
140 # if we need to copy or recover one.
142 $self->{is_primary
} = 1;
143 $self->{deleted_cache
} = {};
144 $self->{version_cache
} = [];
146 my($handler, $this_action);
148 foreach my $itempath (split "\t", $data->{itempaths
}) {
149 $this_action = $action;
151 # $this_action = $self->sanity_checker->check ($data, $itempath, $nodes);
152 # if (!defined ($this_action)) {
156 $handler = $gHandlers{$this_action};
159 $self->$handler($itempath, $thisnodes, $data, $expdir);
161 # we need to apply all local changes to our repository directly: if we
162 # have an action that operates on multiple items, e.g labeling, the
163 # necessary missing directories are created for the first item
164 foreach my $node (@
$thisnodes) {
165 $self->{repository
}->load($node);
169 $self->{is_primary
} = 0;
172 foreach my $node (@
$nodes) {
173 $self->output_node($node);
176 my($physname, $cache);
178 my ($parentphys, $physnames);
179 while(($parentphys, $physnames) = each %{ $self->{deleted_cache
} }) {
180 while(($physname, $cache) = each %{ $physnames }) {
181 $gDeleted{$parentphys}->{$physname} = $cache;
185 # track the version -> revision mapping for the file
186 foreach my $record (@
{$self->{version_cache
}}) {
187 my $version = \
%{$gVersion{$record->{physname
}}->[$record->{version
}]};
188 $version->{$record->{itempath
}} = $record->{revision
};
194 ###############################################################################
196 ###############################################################################
198 my($self, $itempath, $nodes, $data, $expdir) = @_;
200 if ($self->{repository
}->exists ($itempath)) {
201 if ($data->{itemtype
} == 2) {
202 $self->add_error("Attempt to re-add file '$itempath' at "
203 . "revision $data->{revision_id}, changing to modify; possibly "
205 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
208 #creating a new VSS database can cause a "ADD" for a "/" item which will fail.
209 if (!($itempath eq "/")) {
210 $self->add_error("Attempt to re-add directory '$itempath' at "
211 . "revision $data->{revision_id}, skipping action: possibly "
219 my $success = $self->{repository
}->exists_parent ($itempath);
220 if(!defined($success)) {
221 $self->add_error("Path consistency failure while trying to add "
222 . "item '$itempath' at revision $data->{revision_id}; skipping");
225 elsif ($success == 0) {
226 if (!($itempath =~ m/^\/orphaned\
/_.*/))
228 $self->add_error("Parent path missing while trying to add "
229 . "item '$itempath' at revision $data->{revision_id}: adding missing "
232 $self->_create_svn_path ($nodes, $itempath);
235 my $node = Vss2Svn
::Dumpfile
::Node
->new();
236 $node->set_initial_props($itempath, $data);
237 if ($data->{is_binary
}) {
238 $node->add_prop('svn:mime-type', 'application/octet-stream');
240 if (defined $self->{auto_props
}) {
241 $node->add_props ($self->{auto_props
}->get_props ($itempath));
244 $node->{action
} = 'add';
246 if ($data->{itemtype
} == 2) {
247 $self->get_export_file($node, $data, $expdir);
250 # $self->track_modified($data->{physname}, $data->{revision_id}, $itempath);
251 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
257 ###############################################################################
259 ###############################################################################
260 sub _commit_handler
{
261 my($self, $itempath, $nodes, $data, $expdir) = @_;
263 if (!$self->{repository
}->exists ($itempath)) {
264 $self->add_error("Attempt to commit to non-existant file '$itempath' at "
265 . "revision $data->{revision_id}, changing to add; possibly "
266 . "missing recover");
267 return $self->_add_handler ($itempath, $nodes, $data, $expdir);
270 my $node = Vss2Svn
::Dumpfile
::Node
->new();
271 $node->set_initial_props($itempath, $data);
272 $node->{action
} = 'change';
274 if ($data->{itemtype
} == 2) {
275 $self->get_export_file($node, $data, $expdir);
278 # $self->track_modified($data->{physname}, $data->{revision_id}, $itempath);
279 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
283 } # End _commit_handler
285 ###############################################################################
287 ###############################################################################
288 sub _rename_handler
{
289 my($self, $itempath, $nodes, $data, $expdir) = @_;
291 # to rename a file in SVN, we must add "with history" then delete the orig.
293 my $newname = $data->{info
};
294 my $newpath = $itempath;
296 if ($data->{itemtype
} == 1) {
297 $newpath =~ s
:(.*/)?
.+$:$1$newname:;
299 $newpath =~ s
:(.*/)?
.*:$1$newname:;
302 if ($self->{repository
}->exists ($newpath)) {
303 $self->add_error("Attempt to rename item '$itempath' to '$newpath' at "
304 . "revision $data->{revision_id}, but destination already exists: possibly "
305 . "missing delete; skipping");
309 if (!$self->{repository
}->exists ($itempath)) {
310 $self->add_error("Attempt to rename item '$itempath' to '$newpath' at "
311 . "revision $data->{revision_id}, but source doesn't exists: possibly "
312 . "missing recover; skipping");
316 my $node = Vss2Svn
::Dumpfile
::Node
->new();
317 $node->set_initial_props($newpath, $data);
318 # change the properties according to the new name
319 if (defined $self->{auto_props
}) {
320 $node->add_props ($self->{auto_props
}->get_props ($newpath));
322 $node->{action
} = 'add';
324 my($copyrev, $copypath);
326 # ideally, we should be finding the last time the file was modified and
327 # copy it from there, but that becomes difficult to track...
328 $copyrev = $data->{revision_id
} - 1;
329 $copypath = $itempath;
331 $node->{copyrev
} = $copyrev;
332 $node->{copypath
} = $copypath;
336 # $self->track_modified($data->{physname}, $data->{revision_id}, $newpath);
337 # $self->track_version ($data->{physname}, $data->{version}, $newpath);
339 $node = Vss2Svn
::Dumpfile
::Node
->new();
340 $node->set_initial_props($itempath, $data);
341 $node->{action
} = 'delete';
342 $node->{hideprops
} = 1;
346 # We don't add this to %gDeleted since VSS doesn't treat a rename as an
347 # add/delete and therefore we wouldn't recover from this point
349 } # End _rename_handler
351 ###############################################################################
353 ###############################################################################
355 my($self, $itempath, $nodes, $data, $expdir) = @_;
357 if ($self->{repository
}->exists ($itempath)) {
358 $self->add_error("Attempt to share item '$data->{info}' to '$itempath' at "
359 . "revision $data->{revision_id}, but destination already exists: possibly "
360 . "missing delete; skipping");
364 # It could be possible that we share from a historically renamed item, so we don't check the source
365 # if ($self->{repository}->exists ($data->{info})) {
366 # $self->add_error("Attempt to share item '$itempath' to '$newpath' at "
367 # . "revision $data->{revision_id}, but destination already exists: possibly "
368 # . "missing delete; skipping");
372 my $node = Vss2Svn
::Dumpfile
::Node
->new();
373 $node->set_initial_props($itempath, $data);
374 $node->{action
} = 'add';
376 # @{ $node }{ qw(copyrev copypath) }
377 # = $self->last_modified_rev_path($data->{physname});
379 $self->get_revision ($data->{physname
}, $data->{version
}, $data->{info
});
380 $node->{copypath
} = $data->{info
};
382 if (!defined $node->{copyrev
} || !defined $node->{copypath
}) {
383 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
386 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
390 } # End _share_handler
392 ###############################################################################
394 ###############################################################################
395 sub _branch_handler
{
396 my($self, $itempath, $nodes, $data, $expdir) = @_;
398 # branching is a no-op in SVN
400 # since it is possible, that we refer to version prior to the branch later, we
401 # need to copy all internal information about the ancestor to the child.
402 if (defined $data->{info
}) {
403 # only copy versions, that are common between the branch source and the branch.
404 my $copy_version=$data->{version
};
405 while(--$copy_version > 0) {
406 if (defined $gVersion{$data->{info
}}->[$copy_version]) {
407 $gVersion{$data->{physname
}}->[$copy_version] =
408 $gVersion{$data->{info
}}->[$copy_version];
413 # # if the file is copied later, we need to track, the revision of this branch
414 # # see the shareBranchShareModify Test
415 # $self->track_modified($data->{physname}, $data->{revision_id}, $itempath);
416 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
418 } # End _branch_handler
420 ###############################################################################
422 ###############################################################################
424 my($self, $itempath, $nodes, $data, $expdir) = @_;
426 # moving in SVN is the same as renaming; add the new and delete the old
428 my $oldpath = $data->{info
};
430 if ($self->{repository
}->exists ($itempath)) {
431 $self->add_error("Attempt to move item '$oldpath' to '$itempath' at "
432 . "revision $data->{revision_id}, but destination already exists: possibly "
433 . "missing delete; skipping");
437 if (!$self->{repository
}->exists ($oldpath)) {
438 $self->add_error("Attempt to move item '$oldpath' to '$itempath' at "
439 . "revision $data->{revision_id}, but source doesn't exists: possibly "
440 . "missing recover; skipping");
444 my $success = $self->{repository
}->exists_parent($itempath);
445 if(!defined($success)) {
446 $self->add_error("Attempt to move item '$oldpath' to '$itempath' at "
447 . "revision $data->{revision_id}, but path consistency failure at dest");
450 elsif ($success == 0) {
451 $self->add_error("Parent path missing while trying to move "
452 . "item '$oldpath' to '$itempath' at "
453 . "revision $data->{revision_id}: adding missing parents");
454 $self->_create_svn_path ($nodes, $itempath);
457 my $node = Vss2Svn
::Dumpfile
::Node
->new();
458 $node->set_initial_props($itempath, $data);
459 $node->{action
} = 'add';
461 my($copyrev, $copypath);
463 $copyrev = $data->{revision_id
} - 1;
464 $copypath = $oldpath;
466 $node->{copyrev
} = $copyrev;
467 $node->{copypath
} = $copypath;
471 # the new move target is a valid path.
472 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
474 $node = Vss2Svn
::Dumpfile
::Node
->new();
475 $node->set_initial_props($oldpath, $data);
476 $node->{action
} = 'delete';
477 $node->{hideprops
} = 1;
479 # Deleted tracking is only necessary to be able to recover the item. But a move
480 # does not set a recover point, so we don't need to track the delete here. Additionally
481 # we do not have enough information for this operation.
482 # $self->track_deleted($data->{oldparentphys}, $data->{physname},
483 # $data->{revision_id}, $oldpath);
487 } # End _move_handler
489 ###############################################################################
491 ###############################################################################
492 sub _delete_handler
{
493 my($self, $itempath, $nodes, $data, $expdir) = @_;
495 if (!$self->{repository
}->exists ($itempath)) {
496 $self->add_error("Attempt to delete non-existent item '$itempath' at "
497 . "revision $data->{revision_id}: possibly "
498 . "missing recover/add/share; skipping");
502 my $node = Vss2Svn
::Dumpfile
::Node
->new();
503 $node->set_initial_props($itempath, $data);
504 $node->{action
} = 'delete';
505 $node->{hideprops
} = 1;
509 $self->track_deleted($data->{parentphys
}, $data->{physname
},
510 $data->{revision_id
}, $itempath);
512 } # End _delete_handler
514 ###############################################################################
516 ###############################################################################
517 sub _recover_handler
{
518 my($self, $itempath, $nodes, $data, $expdir) = @_;
520 if ($self->{repository
}->exists ($itempath)) {
521 $self->add_error("Attempt to recover existing item '$itempath' at "
522 . "revision $data->{revision_id}: possibly "
523 . "missing delete; change to commit");
524 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
527 my $node = Vss2Svn
::Dumpfile
::Node
->new();
528 $node->set_initial_props($itempath, $data);
529 $node->{action
} = 'add';
531 # for projects we want to go back to the revision just one before the deleted
532 # revision. For files, we need to go back to the specified revision, since
533 # the file could have been modified via a share.
534 my($copyrev, $copypath);
535 if (!defined ($data->{version
})) {
536 ($copyrev, $copypath)= $self->last_deleted_rev_path($data->{parentphys
},
542 $self->get_revision ($data->{physname
}, $data->{version
}, $data->{info
});
543 $copypath = $data->{info
};
546 if (!defined $copyrev || !defined $copypath) {
548 "Could not recover path $itempath at revision $data->{revision_id};"
549 . " unable to determine deleted revision or path");
553 $node->{copyrev
} = $copyrev;
554 $node->{copypath
} = $copypath;
556 if (defined ($data->{version
})) {
557 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
562 } # End _recover_handler
564 ###############################################################################
566 ###############################################################################
568 my($self, $itempath, $nodes, $data, $expdir) = @_;
570 if (!$self->{repository
}->exists ($itempath)) {
571 $self->add_error("Attempt to pin non-existing item '$itempath' at "
572 . "revision $data->{revision_id}: possibly "
573 . "missing recover; skipping");
578 $self->get_revision ($data->{physname
}, $data->{version
}, $data->{info
});
579 my $copypath = $data->{info
};
581 # if one of the necessary copy from attributes are unavailable we fall back
582 # to a complete checkin
583 if (defined $copyrev && defined $copypath) {
584 $data->{comment
} = "ported from $copypath r$copyrev";
586 # if (!defined $copyrev || !defined $copypath) {
587 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
590 my $node = Vss2Svn
::Dumpfile
::Node
->new();
591 $node->set_initial_props($itempath, $data);
592 $node->{action
} = 'add';
594 $node->{copyrev
} = $copyrev;
595 $node->{copypath
} = $copypath;
597 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
603 ###############################################################################
605 ###############################################################################
607 my($self, $itempath, $nodes, $data, $expdir) = @_;
609 if (!$self->{repository
}->exists ($itempath)) {
610 $self->add_error("Attempt to label non-existing item '$itempath' at "
611 . "revision $data->{revision_id}: possibly "
612 . "missing recover; skipping");
616 my $label = $data->{info
};
618 # It is possible that the label was deleted later, so we see here a label
619 # action, but no label was assigned. In this case, we only need to track
620 # the version->revision mapping, since the version could have been used
621 # as a valid share source.
622 if (defined ($label)) {
623 my $labeldir = $main::gCfg
{labeldir
};
625 if (defined $self->{label_mapper
}) {
626 $labeldir = $self->{label_mapper
}->remap ($main::gCfg
{labeldir
}, $label);
628 $labeldir =~ s
:\\:/:g
;
631 $label =~ s![\\/:*?"<>|]!_!g;
633 my $vssitempath = $itempath;
634 $vssitempath =~ s/^$main::gCfg{trunkdir}//;
635 my $labelpath = "$labeldir/$label$vssitempath";
637 $self->_create_svn_path ($nodes, $labelpath);
639 my $node = Vss2Svn
::Dumpfile
::Node
->new();
640 $node->set_initial_props($labelpath, $data);
641 $node->{action
} = 'add';
643 my $copyrev = $data->{revision_id
} - 1;
644 my $copypath = $itempath;
646 $node->{copyrev
} = $copyrev;
647 $node->{copypath
} = $copypath;
653 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
654 } # End _label_handler
656 ###############################################################################
658 ###############################################################################
660 my($self, $nodes, $dir) = @_;
662 my $node = Vss2Svn
::Dumpfile
::Node
->new();
663 my $data = { itemtype
=> 1, is_binary
=> 0 };
665 $node->set_initial_props($dir, $data);
666 $node->{action
} = 'add';
672 ###############################################################################
674 ###############################################################################
675 sub _create_svn_path
{
676 my($self, $nodes, $itempath) = @_;
678 my $missing_dirs = $self->{repository
}->get_missing_dirs($itempath);
680 foreach my $dir (@
$missing_dirs) {
681 $self->_add_svn_dir($nodes, $dir);
683 } # End _create_svn_path
685 ###############################################################################
687 ###############################################################################
689 my($self, $physname, $version, $itempath) = @_;
693 physname
=> $physname,
695 revision
=> $self->{revision
},
696 itempath
=> $itempath,
698 push @
{$self->{version_cache
}}, $record;
700 } # End track_version
703 ###############################################################################
705 ###############################################################################
707 my($self, $physname, $version, $itempath) = @_;
709 if (!defined($gVersion{$physname})) {
713 if (!exists($gVersion{$physname}->[$version])) {
717 return $gVersion{$physname}->[$version]->{$itempath};
721 ###############################################################################
723 ###############################################################################
725 my($self, $parentphys, $physname, $revision, $path) = @_;
727 $self->{deleted_cache
}->{$parentphys}->{$physname} =
729 revision
=> $revision,
733 } # End track_deleted
735 ###############################################################################
736 # last_deleted_rev_path
737 ###############################################################################
738 sub last_deleted_rev_path
{
739 my($self, $parentphys, $physname) = @_;
741 if (!defined($gDeleted{$parentphys})) {
742 return (undef, undef);
745 if (!defined($gDeleted{$parentphys}->{$physname})) {
746 return (undef, undef);
749 return @
{ $gDeleted{$parentphys}->{$physname} }{ qw(revision path) };
750 } # End last_deleted_rev_path
752 ###############################################################################
754 ###############################################################################
755 sub get_export_file
{
756 my($self, $node, $data, $expdir) = @_;
758 if (!defined($expdir)) {
760 } elsif (!defined($data->{version
})) {
762 "Attempt to retrieve file contents with unknown version number");
766 $node->{file
} = "$expdir/$data->{physname}.$data->{version}";
769 } # End get_export_file
771 ###############################################################################
773 ###############################################################################
775 my($self, $node) = @_;
776 my $fh = $self->{fh
};
778 # only in an add or rename action the propery array is set. So we have
779 # to lookup the eol-style flag again. The best thing is to query the
780 # property always temporarirly
782 if (defined $self->{auto_props
}) {
783 %tmpProps = $self->{auto_props
}->get_props ($node->{path
});
785 my $eolStyle = $tmpProps{'svn:eol-style'};
786 my $isNative = (defined $eolStyle && $eolStyle eq 'native') ?
1 : 0;
788 my $string = $node->get_headers();
790 $self->output_content($node->{hideprops
}?
undef : $node->{props
},
791 $node->{text
}, $node->{file
}, $isNative);
794 ###############################################################################
796 ###############################################################################
798 my($self, $props, $text, $file, $isNative) = @_;
800 my $fh = $self->{fh
};
802 $text = '' unless defined $text || defined $file;
806 my($propout, $textout) = ('') x
2;
808 if (defined($props)) {
809 foreach my $key (keys %$props) {
810 my $value = $props->{$key};
811 $propout .= 'K ' . length($key) . "\n$key\n";
812 if (defined $value) {
813 $propout .= 'V ' . length($value) . "\n$value\n";
816 $propout .= "V 0\n\n";
820 $propout .= "PROPS-END\n";
821 $proplen = length($propout);
825 $md5 = Digest
::MD5
->new if $self->{do_md5
};
827 # prevent errors due to non existing files
828 if(!defined $text && defined $file && !-e
$file) {
832 # convert CRLF -> LF before calculating the size and compute the md5
833 if(!defined $text && defined $file) {
835 my ($input, $output);
836 if (defined $isNative && $isNative) {
837 open ($input, "<:crlf", $file);
838 my $tmpFile = "$gTmpDir/crlf_to_lf.tmp.txt";
839 open ($output, ">", $tmpFile);
843 $md5->add($_) if $self->{do_md5
};
852 open ($input, "<", $file);
854 $md5->addfile($input) if $self->{do_md5
};
858 $md5->add($text) if $self->{do_md5
};
861 my $digest = $md5->hexdigest if $self->{do_md5
};
862 # print "digest: $digest\n";
864 if(!defined $text && defined $file) {
867 $textlen = length($text);
869 return if ($textlen + $proplen == 0 && !defined $file);
872 print $fh "Prop-content-length: $proplen\n";
875 if (defined $file || $textlen > 0) {
876 print $fh "Text-content-length: $textlen\n";
877 print $fh "Text-content-md5: $digest\n" if $self->{do_md5
};
880 print $fh "Content-length: " . ($proplen + $textlen)
883 if(!defined $text && defined $file) {
890 } # End output_content
892 ###############################################################################
894 ###############################################################################
896 my($self, $vss_timestamp) = @_;
898 return &SvnTimestamp
($vss_timestamp);
900 } # End svn_timestamp
902 ###############################################################################
904 ###############################################################################
906 my($vss_timestamp) = @_;
908 # set the correct time: VSS stores the local time as the timestamp, but subversion
909 # needs a gmtime. So we need to reverse adjust the timestamp in order to turn back
911 my($sec, $min, $hour, $day, $mon, $year) = gmtime($vss_timestamp);
912 my($faketime) = Time
::Local
::timelocal
($sec, $min, $hour, $day, $mon, $year);
913 ($sec, $min, $hour, $day, $mon, $year) = gmtime($faketime);
918 return sprintf("%4.4i-%2.2i-%2.2iT%2.2i:%2.2i:%2.2i.%6.6iZ",
919 $year, $mon, $day, $hour, $min, $sec, 0);
923 ###############################################################################
925 ###############################################################################
927 my($self, $msg) = @_;
929 push @
{ $self->{errors
} }, $msg;