* Dumpfile.pm: Create the parent path of the target path of a move operation if it...
[vss2svn.git] / script / Vss2Svn / Dumpfile.pm
blobf941f27398816868196cf6f7f5a635e6e637ac49
1 package Vss2Svn::Dumpfile;
3 use Vss2Svn::Dumpfile::Node;
4 use Vss2Svn::Dumpfile::SanityChecker;
5 use Vss2Svn::Dumpfile::AutoProps;
7 require Time::Local;
9 use warnings;
10 use strict;
12 our %gHandlers =
14 ADD => \&_add_handler,
15 COMMIT => \&_commit_handler,
16 RENAME => \&_rename_handler,
17 SHARE => \&_share_handler,
18 BRANCH => \&_branch_handler,
19 MOVE => \&_move_handler,
20 DELETE => \&_delete_handler,
21 RECOVER => \&_recover_handler,
22 PIN => \&_pin_handler,
23 LABEL => \&_label_handler,
26 # Keep track of when paths were modified or deleted, for subsequent copies
27 # or recovers.
29 #our %gModified = ();
30 our %gDeleted = ();
31 our %gVersion = ();
33 ###############################################################################
34 # new
35 ###############################################################################
36 sub new {
37 my($class, $fh, $autoprops) = @_;
39 my $self =
41 fh => $fh,
42 revision => 0,
43 errors => [],
44 deleted_cache => {},
45 version_cache => [],
46 repository => Vss2Svn::Dumpfile::SanityChecker->new(),
47 auto_props => $autoprops,
50 # prevent perl from doing line-ending conversions
51 binmode($fh);
53 my $old = select($fh);
54 $| = 1;
55 select($old);
57 print $fh "SVN-fs-dump-format-version: 2\n\n";
59 $self = bless($self, $class);
60 return $self;
62 } # End new
64 ###############################################################################
65 # finish
66 ###############################################################################
67 sub finish {
68 my($self) = @_;
70 my $fh = $self->{fh};
72 print $fh "\n\n";
74 } # End finish
76 ###############################################################################
77 # begin_revision
78 ###############################################################################
79 sub begin_revision {
80 my($self, $data) = @_;
81 my($revision, $author, $timestamp, $comment) =
82 @{ $data }{qw(revision_id author timestamp comment)};
84 my $props = undef;
85 my $fh = $self->{fh};
87 print $fh "\nRevision-number: $revision\n";
89 $comment = '' if !defined($comment);
90 $author = '' if !defined($author);
92 if ($revision > 0) {
93 $props = { 'svn:log' => $comment,
94 'svn:author' => $author,
98 $props->{'svn:date'} = $self->svn_timestamp($timestamp);
100 $self->output_content($props);
101 $self->{revision} = $revision;
103 } # End begin_revision
105 ###############################################################################
106 # do_action
107 ###############################################################################
108 sub do_action {
109 my($self, $data, $expdir) = @_;
111 my $action = $data->{action};
113 my $nodes = [];
115 # Temporary hack to prevent shared files from stepping on the "modified"
116 # flag for other than the first commit. Ideally, we should keep all paths
117 # for a given physical file's last modified flags, and use the best match
118 # if we need to copy or recover one.
120 $self->{is_primary} = 1;
121 $self->{deleted_cache} = {};
122 $self->{version_cache} = [];
124 my($handler, $this_action);
126 foreach my $itempath (split "\t", $data->{itempaths}) {
127 $this_action = $action;
129 # $this_action = $self->sanity_checker->check ($data, $itempath, $nodes);
130 # if (!defined ($this_action)) {
131 # return 0;
134 $handler = $gHandlers{$this_action};
136 my $thisnodes = [];
137 $self->$handler($itempath, $thisnodes, $data, $expdir);
139 # we need to apply all local changes to our repository directly: if we
140 # have an action that operates on multiple items, e.g labeling, the
141 # necessary missing directories are created for the first item
142 foreach my $node (@$thisnodes) {
143 $self->{repository}->load($node);
144 push @$nodes, $node;
147 $self->{is_primary} = 0;
150 foreach my $node (@$nodes) {
151 $self->output_node($node);
154 my($physname, $cache);
156 my ($parentphys, $physnames);
157 while(($parentphys, $physnames) = each %{ $self->{deleted_cache} }) {
158 while(($physname, $cache) = each %{ $physnames }) {
159 $gDeleted{$parentphys}->{$physname} = $cache;
163 # track the version -> revision mapping for the file
164 foreach my $record (@{$self->{version_cache}}) {
165 my $version = \%{$gVersion{$record->{physname}}->[$record->{version}]};
166 $version->{$record->{itempath}} = $record->{revision};
169 } # End do_action
172 ###############################################################################
173 # _add_handler
174 ###############################################################################
175 sub _add_handler {
176 my($self, $itempath, $nodes, $data, $expdir) = @_;
178 if ($self->{repository}->exists ($itempath)) {
179 if ($data->{itemtype} == 2) {
180 $self->add_error("Attempt to re-add file '$itempath' at "
181 . "revision $data->{revision_id}, changing to modify; possibly "
182 . "missing delete");
183 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
185 else {
186 $self->add_error("Attempt to re-add directory '$itempath' at "
187 . "revision $data->{revision_id}, skipping action: possibly "
188 . "missing delete");
189 return 0;
193 my $success = $self->{repository}->exists_parent ($itempath);
194 if(!defined($success)) {
195 $self->add_error("Path consistency failure while trying to add "
196 . "item '$itempath' at revision $data->{revision_id}; skipping");
197 return 0;
199 elsif ($success == 0) {
200 $self->add_error("Parent path missing while trying to add "
201 . "item '$itempath' at revision $data->{revision_id}: adding missing "
202 . "parents");
203 $self->_create_svn_path ($nodes, $itempath);
206 my $node = Vss2Svn::Dumpfile::Node->new();
207 $node->set_initial_props($itempath, $data);
208 if ($data->{is_binary}) {
209 $node->add_prop('svn:mime-type', 'application/octet-stream');
211 if (defined $self->{auto_props}) {
212 $node->add_props ($self->{auto_props}->get_props ($itempath));
215 $node->{action} = 'add';
217 if ($data->{itemtype} == 2) {
218 $self->get_export_contents($node, $data, $expdir);
221 # $self->track_modified($data->{physname}, $data->{revision_id}, $itempath);
222 $self->track_version ($data->{physname}, $data->{version}, $itempath);
224 push @$nodes, $node;
226 } # End _add_handler
228 ###############################################################################
229 # _commit_handler
230 ###############################################################################
231 sub _commit_handler {
232 my($self, $itempath, $nodes, $data, $expdir) = @_;
234 if (!$self->{repository}->exists ($itempath)) {
235 $self->add_error("Attempt to commit to non-existant file '$itempath' at "
236 . "revision $data->{revision_id}, changing to add; possibly "
237 . "missing recover");
238 return $self->_add_handler ($itempath, $nodes, $data, $expdir);
241 my $node = Vss2Svn::Dumpfile::Node->new();
242 $node->set_initial_props($itempath, $data);
243 $node->{action} = 'change';
245 if ($data->{itemtype} == 2) {
246 $self->get_export_contents($node, $data, $expdir);
249 # $self->track_modified($data->{physname}, $data->{revision_id}, $itempath);
250 $self->track_version ($data->{physname}, $data->{version}, $itempath);
252 push @$nodes, $node;
254 } # End _commit_handler
256 ###############################################################################
257 # _rename_handler
258 ###############################################################################
259 sub _rename_handler {
260 my($self, $itempath, $nodes, $data, $expdir) = @_;
262 # to rename a file in SVN, we must add "with history" then delete the orig.
264 my $newname = $data->{info};
265 my $newpath = $itempath;
267 if ($data->{itemtype} == 1) {
268 $newpath =~ s:(.*/)?.+$:$1$newname:;
269 } else {
270 $newpath =~ s:(.*/)?.*:$1$newname:;
273 if ($self->{repository}->exists ($newpath)) {
274 $self->add_error("Attempt to rename item '$itempath' to '$newpath' at "
275 . "revision $data->{revision_id}, but destination already exists: possibly "
276 . "missing delete; skipping");
277 return 0;
280 if (!$self->{repository}->exists ($itempath)) {
281 $self->add_error("Attempt to rename item '$itempath' to '$newpath' at "
282 . "revision $data->{revision_id}, but source doesn't exists: possibly "
283 . "missing recover; skipping");
284 return 0;
287 my $node = Vss2Svn::Dumpfile::Node->new();
288 $node->set_initial_props($newpath, $data);
289 $node->{action} = 'add';
291 my($copyrev, $copypath);
293 # ideally, we should be finding the last time the file was modified and
294 # copy it from there, but that becomes difficult to track...
295 $copyrev = $data->{revision_id} - 1;
296 $copypath = $itempath;
298 $node->{copyrev} = $copyrev;
299 $node->{copypath} = $copypath;
301 push @$nodes, $node;
303 # $self->track_modified($data->{physname}, $data->{revision_id}, $newpath);
304 # $self->track_version ($data->{physname}, $data->{version}, $newpath);
306 $node = Vss2Svn::Dumpfile::Node->new();
307 $node->set_initial_props($itempath, $data);
308 $node->{action} = 'delete';
309 $node->{hideprops} = 1;
311 push @$nodes, $node;
313 # We don't add this to %gDeleted since VSS doesn't treat a rename as an
314 # add/delete and therefore we wouldn't recover from this point
316 } # End _rename_handler
318 ###############################################################################
319 # _share_handler
320 ###############################################################################
321 sub _share_handler {
322 my($self, $itempath, $nodes, $data, $expdir) = @_;
324 if ($self->{repository}->exists ($itempath)) {
325 $self->add_error("Attempt to share item '$data->{info}' to '$itempath' at "
326 . "revision $data->{revision_id}, but destination already exists: possibly "
327 . "missing delete; skipping");
328 return 0;
331 # It could be possible that we share from a historically renamed item, so we don't check the source
332 # if ($self->{repository}->exists ($data->{info})) {
333 # $self->add_error("Attempt to share item '$itempath' to '$newpath' at "
334 # . "revision $data->{revision_id}, but destination already exists: possibly "
335 # . "missing delete; skipping");
336 # return 0;
339 my $node = Vss2Svn::Dumpfile::Node->new();
340 $node->set_initial_props($itempath, $data);
341 $node->{action} = 'add';
343 # @{ $node }{ qw(copyrev copypath) }
344 # = $self->last_modified_rev_path($data->{physname});
345 $node->{copyrev} =
346 $self->get_revision ($data->{physname}, $data->{version}, $data->{info});
347 $node->{copypath} = $data->{info};
349 if (!defined $node->{copyrev} || !defined $node->{copypath}) {
350 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
353 $self->track_version ($data->{physname}, $data->{version}, $itempath);
355 push @$nodes, $node;
357 } # End _share_handler
359 ###############################################################################
360 # _branch_handler
361 ###############################################################################
362 sub _branch_handler {
363 my($self, $itempath, $nodes, $data, $expdir) = @_;
365 # branching is a no-op in SVN
367 # since it is possible, that we refer to version prior to the branch later, we
368 # need to copy all internal information about the ancestor to the child.
369 if (defined $data->{info}) {
370 # only copy versions, that are common between the branch source and the branch.
371 my $copy_version=$data->{version};
372 while(--$copy_version > 0) {
373 if (defined $gVersion{$data->{info}}->[$copy_version]) {
374 $gVersion{$data->{physname}}->[$copy_version] =
375 $gVersion{$data->{info}}->[$copy_version];
380 # # if the file is copied later, we need to track, the revision of this branch
381 # # see the shareBranchShareModify Test
382 # $self->track_modified($data->{physname}, $data->{revision_id}, $itempath);
383 $self->track_version ($data->{physname}, $data->{version}, $itempath);
385 } # End _branch_handler
387 ###############################################################################
388 # _move_handler
389 ###############################################################################
390 sub _move_handler {
391 my($self, $itempath, $nodes, $data, $expdir) = @_;
393 # moving in SVN is the same as renaming; add the new and delete the old
395 my $oldpath = $data->{info};
397 if ($self->{repository}->exists ($itempath)) {
398 $self->add_error("Attempt to move item '$oldpath' to '$itempath' at "
399 . "revision $data->{revision_id}, but destination already exists: possibly "
400 . "missing delete; skipping");
401 return 0;
404 if (!$self->{repository}->exists ($oldpath)) {
405 $self->add_error("Attempt to move item '$oldpath' to '$itempath' at "
406 . "revision $data->{revision_id}, but source doesn't exists: possibly "
407 . "missing recover; skipping");
408 return 0;
411 my $success = $self->{repository}->exists_parent ($newpath);
412 if(!defined($success)) {
413 $self->add_error("Attempt to move item '$itempath' to '$newpath' at "
414 . "revision $data->{revision_id}, but path consistency failure at dest");
415 return 0;
417 elsif ($success == 0) {
418 $self->add_error("Parent path missing while trying to move "
419 . "item '$itempath' to '$newpath' at "
420 . "revision $data->{revision_id}: adding missing parents");
421 $self->_create_svn_path ($nodes, $newpath);
424 my $node = Vss2Svn::Dumpfile::Node->new();
425 $node->set_initial_props($itempath, $data);
426 $node->{action} = 'add';
428 my($copyrev, $copypath);
430 $copyrev = $data->{revision_id} - 1;
431 $copypath = $oldpath;
433 $node->{copyrev} = $copyrev;
434 $node->{copypath} = $copypath;
436 push @$nodes, $node;
438 # the new move target is a valid path.
439 $self->track_version ($data->{physname}, $data->{version}, $itempath);
441 $node = Vss2Svn::Dumpfile::Node->new();
442 $node->set_initial_props($oldpath, $data);
443 $node->{action} = 'delete';
444 $node->{hideprops} = 1;
446 # Deleted tracking is only necessary to be able to recover the item. But a move
447 # does not set a recover point, so we don't need to track the delete here. Additionally
448 # we do not have enough information for this operation.
449 # $self->track_deleted($data->{oldparentphys}, $data->{physname},
450 # $data->{revision_id}, $oldpath);
452 push @$nodes, $node;
454 } # End _move_handler
456 ###############################################################################
457 # _delete_handler
458 ###############################################################################
459 sub _delete_handler {
460 my($self, $itempath, $nodes, $data, $expdir) = @_;
462 if (!$self->{repository}->exists ($itempath)) {
463 $self->add_error("Attempt to delete non-existent item '$itempath' at "
464 . "revision $data->{revision_id}: possibly "
465 . "missing recover/add/share; skipping");
466 return 0;
469 my $node = Vss2Svn::Dumpfile::Node->new();
470 $node->set_initial_props($itempath, $data);
471 $node->{action} = 'delete';
472 $node->{hideprops} = 1;
474 push @$nodes, $node;
476 $self->track_deleted($data->{parentphys}, $data->{physname},
477 $data->{revision_id}, $itempath);
479 } # End _delete_handler
481 ###############################################################################
482 # _recover_handler
483 ###############################################################################
484 sub _recover_handler {
485 my($self, $itempath, $nodes, $data, $expdir) = @_;
487 if ($self->{repository}->exists ($itempath)) {
488 $self->add_error("Attempt to recover existing item '$itempath' at "
489 . "revision $data->{revision_id}: possibly "
490 . "missing delete; change to commit");
491 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
494 my $node = Vss2Svn::Dumpfile::Node->new();
495 $node->set_initial_props($itempath, $data);
496 $node->{action} = 'add';
498 # for projects we want to go back to the revision just one before the deleted
499 # revision. For files, we need to go back to the specified revision, since
500 # the file could have been modified via a share.
501 my($copyrev, $copypath);
502 if (!defined ($data->{version})) {
503 ($copyrev, $copypath)= $self->last_deleted_rev_path($data->{parentphys},
504 $data->{physname});
505 $copyrev -= 1;
507 else {
508 $copyrev =
509 $self->get_revision ($data->{physname}, $data->{version}, $data->{info});
510 $copypath = $data->{info};
513 if (!defined $copyrev || !defined $copypath) {
514 $self->add_error(
515 "Could not recover path $itempath at revision $data->{revision_id};"
516 . " unable to determine deleted revision or path");
517 return 0;
520 $node->{copyrev} = $copyrev;
521 $node->{copypath} = $copypath;
523 if (defined ($data->{version})) {
524 $self->track_version ($data->{physname}, $data->{version}, $itempath);
527 push @$nodes, $node;
529 } # End _recover_handler
531 ###############################################################################
532 # _pin_handler
533 ###############################################################################
534 sub _pin_handler {
535 my($self, $itempath, $nodes, $data, $expdir) = @_;
537 if (!$self->{repository}->exists ($itempath)) {
538 $self->add_error("Attempt to pin non-existing item '$itempath' at "
539 . "revision $data->{revision_id}: possibly "
540 . "missing recover; skipping");
541 return 0;
544 my $copyrev =
545 $self->get_revision ($data->{physname}, $data->{version}, $data->{info});
546 my $copypath = $data->{info};
548 # if one of the necessary copy from attributes are unavailable we fall back
549 # to a complete checkin
550 if (!defined $copyrev || !defined $copypath) {
551 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
554 my $node = Vss2Svn::Dumpfile::Node->new();
555 $node->set_initial_props($itempath, $data);
556 $node->{action} = 'add';
558 $node->{copyrev} = $copyrev;
559 $node->{copypath} = $copypath;
561 $self->track_version ($data->{physname}, $data->{version}, $itempath);
563 push @$nodes, $node;
565 } # End _pin_handler
567 ###############################################################################
568 # _label_handler
569 ###############################################################################
570 sub _label_handler {
571 my($self, $itempath, $nodes, $data, $expdir) = @_;
573 if (!$self->{repository}->exists ($itempath)) {
574 $self->add_error("Attempt to label non-existing item '$itempath' at "
575 . "revision $data->{revision_id}: possibly "
576 . "missing recover; skipping");
577 return 0;
580 my $label = $data->{info};
582 # It is possible that the label was deleted later, so we see here a label
583 # action, but no label was assigned. In this case, we only need to track
584 # the version->revision mapping, since the version could have been used
585 # as a valid share source.
586 if (defined ($label)) {
587 $label =~ s![\\/:*?"<>|]!_!g;
589 my $vssitempath = $itempath;
590 $vssitempath =~ s/^$main::gCfg{trunkdir}//;
591 my $labelpath = "$main::gCfg{labeldir}/$label$vssitempath";
593 $self->_create_svn_path ($nodes, $labelpath);
595 my $node = Vss2Svn::Dumpfile::Node->new();
596 $node->set_initial_props($labelpath, $data);
597 $node->{action} = 'add';
599 my $copyrev = $data->{revision_id} - 1;
600 my $copypath = $itempath;
602 $node->{copyrev} = $copyrev;
603 $node->{copypath} = $copypath;
605 push @$nodes, $node;
609 $self->track_version ($data->{physname}, $data->{version}, $itempath);
610 } # End _label_handler
612 ###############################################################################
613 # _add_svn_dir
614 ###############################################################################
615 sub _add_svn_dir {
616 my($self, $nodes, $dir) = @_;
618 my $node = Vss2Svn::Dumpfile::Node->new();
619 my $data = { itemtype => 1, is_binary => 0 };
621 $node->set_initial_props($dir, $data);
622 $node->{action} = 'add';
624 push @$nodes, $node;
625 } # End _add_svn_dir
628 ###############################################################################
629 # _create_svn_path
630 ###############################################################################
631 sub _create_svn_path {
632 my($self, $nodes, $itempath) = @_;
634 my $missing_dirs = $self->{repository}->get_missing_dirs($itempath);
636 foreach my $dir (@$missing_dirs) {
637 $self->_add_svn_dir($nodes, $dir);
639 } # End _create_svn_path
641 ###############################################################################
642 # track_version
643 ###############################################################################
644 sub track_version {
645 my($self, $physname, $version, $itempath) = @_;
647 my $record =
649 physname => $physname,
650 version => $version,
651 revision => $self->{revision},
652 itempath => $itempath,
654 push @{$self->{version_cache}}, $record;
656 } # End track_version
659 ###############################################################################
660 # get_revision
661 ###############################################################################
662 sub get_revision {
663 my($self, $physname, $version, $itempath) = @_;
665 if (!defined($gVersion{$physname})) {
666 return (undef);
669 if (!exists($gVersion{$physname}->[$version])) {
670 return (undef);
673 return $gVersion{$physname}->[$version]->{$itempath};
675 } # End get_revision
677 ###############################################################################
678 # track_deleted
679 ###############################################################################
680 sub track_deleted {
681 my($self, $parentphys, $physname, $revision, $path) = @_;
683 $self->{deleted_cache}->{$parentphys}->{$physname} =
685 revision => $revision,
686 path => $path,
689 } # End track_deleted
691 ###############################################################################
692 # last_deleted_rev_path
693 ###############################################################################
694 sub last_deleted_rev_path {
695 my($self, $parentphys, $physname) = @_;
697 if (!defined($gDeleted{$parentphys})) {
698 return (undef, undef);
701 if (!defined($gDeleted{$parentphys}->{$physname})) {
702 return (undef, undef);
705 return @{ $gDeleted{$parentphys}->{$physname} }{ qw(revision path) };
706 } # End last_deleted_rev_path
708 ###############################################################################
709 # get_export_contents
710 ###############################################################################
711 sub get_export_contents {
712 my($self, $node, $data, $expdir) = @_;
714 if (!defined($expdir)) {
715 return 0;
716 } elsif (!defined($data->{version})) {
717 $self->add_error(
718 "Attempt to retrieve file contents with unknown version number");
719 return 0;
722 my $file = "$expdir/$data->{physname}.$data->{version}";
724 if (!open EXP, "$file") {
725 $self->add_error("Could not open export file '$file'");
726 return 0;
729 binmode(EXP);
731 # $node->{text} = join('', <EXP>);
732 $node->{text} = do { local( $/ ) ; <EXP> } ;
734 close EXP;
736 return 1;
738 } # End get_export_contents
740 ###############################################################################
741 # output_node
742 ###############################################################################
743 sub output_node {
744 my($self, $node) = @_;
745 my $fh = $self->{fh};
747 my $string = $node->get_headers();
748 print $fh $string;
749 $self->output_content($node->{hideprops}? undef : $node->{props},
750 $node->{text});
751 } # End output_node
753 ###############################################################################
754 # output_content
755 ###############################################################################
756 sub output_content {
757 my($self, $props, $text) = @_;
759 my $fh = $self->{fh};
761 $text = '' unless defined $text;
763 my $proplen = 0;
764 my $textlen = 0;
765 my($propout, $textout) = ('') x 2;
767 if (defined($props)) {
768 foreach my $key (keys %$props) {
769 my $value = $props->{$key};
770 $propout .= 'K ' . length($key) . "\n$key\n";
771 if (defined $value) {
772 $propout .= 'V ' . length($value) . "\n$value\n";
774 else {
775 $propout .= "V 0\n\n";
779 $propout .= "PROPS-END\n";
780 $proplen = length($propout);
783 $textlen = length($text);
784 return if ($textlen + $proplen == 0);
786 if ($proplen > 0) {
787 print $fh "Prop-content-length: $proplen\n";
790 if ($textlen > 0) {
791 print $fh "Text-content-length: $textlen\n";
794 print $fh "Content-length: " . ($proplen + $textlen)
795 . "\n\n$propout$text\n";
797 } # End output_content
799 ###############################################################################
800 # svn_timestamp
801 ###############################################################################
802 sub svn_timestamp {
803 my($self, $vss_timestamp) = @_;
805 return &SvnTimestamp($vss_timestamp);
807 } # End svn_timestamp
809 ###############################################################################
810 # SvnTimestamp
811 ###############################################################################
812 sub SvnTimestamp {
813 my($vss_timestamp) = @_;
815 # set the correct time: VSS stores the local time as the timestamp, but subversion
816 # needs a gmtime. So we need to reverse adjust the timestamp in order to turn back
817 # the clock.
818 my($sec, $min, $hour, $day, $mon, $year) = gmtime($vss_timestamp);
819 my($faketime) = Time::Local::timelocal ($sec, $min, $hour, $day, $mon, $year);
820 ($sec, $min, $hour, $day, $mon, $year) = gmtime($faketime);
822 $year += 1900;
823 $mon += 1;
825 return sprintf("%4.4i-%2.2i-%2.2iT%2.2i:%2.2i:%2.2i.%6.6iZ",
826 $year, $mon, $day, $hour, $min, $sec, 0);
828 } # End SvnTimestamp
830 ###############################################################################
831 # add_error
832 ###############################################################################
833 sub add_error {
834 my($self, $msg) = @_;
836 push @{ $self->{errors} }, $msg;
837 } # End add_error