move branches/Dirk/pin_handler back to trunk
[vss2svn.git] / script / Vss2Svn / Dumpfile.pm
blobd32ef254b316dbe5d2178efa4186855b2fb44a4e
1 package Vss2Svn::Dumpfile;
3 use Vss2Svn::Dumpfile::Node;
4 use Vss2Svn::Dumpfile::SanityChecker;
5 use Encode qw(from_to);
7 use warnings;
8 use strict;
10 our %gHandlers =
12 ADD => \&_add_handler,
13 COMMIT => \&_commit_handler,
14 RENAME => \&_rename_handler,
15 SHARE => \&_share_handler,
16 BRANCH => \&_branch_handler,
17 MOVE => \&_move_handler,
18 DELETE => \&_delete_handler,
19 RECOVER => \&_recover_handler,
20 PIN => \&_pin_handler,
21 LABEL => \&_label_handler,
24 # Keep track of when paths were modified or deleted, for subsequent copies
25 # or recovers.
27 #our %gModified = ();
28 our %gDeleted = ();
29 our %gVersion = ();
31 ###############################################################################
32 # new
33 ###############################################################################
34 sub new {
35 my($class, $fh) = @_;
37 my $self =
39 fh => $fh,
40 revision => 0,
41 errors => [],
42 deleted_cache => {},
43 version_cache => [],
44 repository => Vss2Svn::Dumpfile::SanityChecker->new(),
47 # prevent perl from doing line-ending conversions
48 binmode($fh);
50 my $old = select($fh);
51 $| = 1;
52 select($old);
54 print $fh "SVN-fs-dump-format-version: 2\n\n";
56 $self = bless($self, $class);
57 return $self;
59 } # End new
61 ###############################################################################
62 # finish
63 ###############################################################################
64 sub finish {
65 my($self) = @_;
67 my $fh = $self->{fh};
69 print $fh "\n\n";
71 } # End finish
73 ###############################################################################
74 # begin_revision
75 ###############################################################################
76 sub begin_revision {
77 my($self, $data) = @_;
78 my($revision, $author, $timestamp, $comment) =
79 @{ $data }{qw(revision_id author timestamp comment)};
81 my $props = [];
82 my $fh = $self->{fh};
84 print $fh "\nRevision-number: $revision\n";
86 $comment = '' if !defined($comment);
87 $author = '' if !defined($author);
89 # convert to utf8
90 from_to ($comment, "windows-1252", "utf8");
91 from_to ($author, "windows-1252", "utf8");
93 if ($revision > 0) {
94 push @$props, ['svn:log', $comment];
95 push @$props, ['svn:author', $author];
98 push @$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 $node->{action} = 'add';
210 if ($data->{itemtype} == 2) {
211 $self->get_export_contents($node, $data, $expdir);
214 # $self->track_modified($data->{physname}, $data->{revision_id}, $itempath);
215 $self->track_version ($data->{physname}, $data->{version}, $itempath);
217 push @$nodes, $node;
219 } # End _add_handler
221 ###############################################################################
222 # _commit_handler
223 ###############################################################################
224 sub _commit_handler {
225 my($self, $itempath, $nodes, $data, $expdir) = @_;
227 if (!$self->{repository}->exists ($itempath)) {
228 $self->add_error("Attempt to commit to non-existant file '$itempath' at "
229 . "revision $data->{revision_id}, changing to add; possibly "
230 . "missing recover");
231 return $self->_add_handler ($itempath, $nodes, $data, $expdir);
234 my $node = Vss2Svn::Dumpfile::Node->new();
235 $node->set_initial_props($itempath, $data);
236 $node->{action} = 'change';
238 if ($data->{itemtype} == 2) {
239 $self->get_export_contents($node, $data, $expdir);
242 # $self->track_modified($data->{physname}, $data->{revision_id}, $itempath);
243 $self->track_version ($data->{physname}, $data->{version}, $itempath);
245 push @$nodes, $node;
247 } # End _commit_handler
249 ###############################################################################
250 # _rename_handler
251 ###############################################################################
252 sub _rename_handler {
253 my($self, $itempath, $nodes, $data, $expdir) = @_;
255 # to rename a file in SVN, we must add "with history" then delete the orig.
257 my $newname = $data->{info};
258 my $newpath = $itempath;
260 if ($data->{itemtype} == 1) {
261 $newpath =~ s:(.*/)?.+$:$1$newname:;
262 } else {
263 $newpath =~ s:(.*/)?.*:$1$newname:;
266 if ($self->{repository}->exists ($newpath)) {
267 $self->add_error("Attempt to rename item '$itempath' to '$newpath' at "
268 . "revision $data->{revision_id}, but destination already exists: possibly "
269 . "missing delete; skipping");
270 return 0;
273 if (!$self->{repository}->exists ($itempath)) {
274 $self->add_error("Attempt to rename item '$itempath' to '$newpath' at "
275 . "revision $data->{revision_id}, but source doesn't exists: possibly "
276 . "missing recover; skipping");
277 return 0;
280 my $node = Vss2Svn::Dumpfile::Node->new();
281 $node->set_initial_props($newpath, $data);
282 $node->{action} = 'add';
284 my($copyrev, $copypath);
286 # ideally, we should be finding the last time the file was modified and
287 # copy it from there, but that becomes difficult to track...
288 $copyrev = $data->{revision_id} - 1;
289 $copypath = $itempath;
291 $node->{copyrev} = $copyrev;
292 $node->{copypath} = $copypath;
294 push @$nodes, $node;
296 # $self->track_modified($data->{physname}, $data->{revision_id}, $newpath);
297 # $self->track_version ($data->{physname}, $data->{version}, $newpath);
299 $node = Vss2Svn::Dumpfile::Node->new();
300 $node->set_initial_props($itempath, $data);
301 $node->{action} = 'delete';
302 $node->{hideprops} = 1;
304 push @$nodes, $node;
306 # We don't add this to %gDeleted since VSS doesn't treat a rename as an
307 # add/delete and therefore we wouldn't recover from this point
309 } # End _rename_handler
311 ###############################################################################
312 # _share_handler
313 ###############################################################################
314 sub _share_handler {
315 my($self, $itempath, $nodes, $data, $expdir) = @_;
317 if ($self->{repository}->exists ($itempath)) {
318 $self->add_error("Attempt to share item '$data->{info}' to '$itempath' at "
319 . "revision $data->{revision_id}, but destination already exists: possibly "
320 . "missing delete; skipping");
321 return 0;
324 # It could be possible that we share from a historically renamed item, so we don't check the source
325 # if ($self->{repository}->exists ($data->{info})) {
326 # $self->add_error("Attempt to share item '$itempath' to '$newpath' at "
327 # . "revision $data->{revision_id}, but destination already exists: possibly "
328 # . "missing delete; skipping");
329 # return 0;
332 my $node = Vss2Svn::Dumpfile::Node->new();
333 $node->set_initial_props($itempath, $data);
334 $node->{action} = 'add';
336 # @{ $node }{ qw(copyrev copypath) }
337 # = $self->last_modified_rev_path($data->{physname});
338 $node->{copyrev} =
339 $self->get_revision ($data->{physname}, $data->{version}, $data->{info});
340 $node->{copypath} = $data->{info};
342 if (!defined $node->{copyrev} || !defined $node->{copypath}) {
343 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
346 $self->track_version ($data->{physname}, $data->{version}, $itempath);
348 push @$nodes, $node;
350 } # End _share_handler
352 ###############################################################################
353 # _branch_handler
354 ###############################################################################
355 sub _branch_handler {
356 my($self, $itempath, $nodes, $data, $expdir) = @_;
358 # branching is a no-op in SVN
360 # # if the file is copied later, we need to track, the revision of this branch
361 # # see the shareBranchShareModify Test
362 # $self->track_modified($data->{physname}, $data->{revision_id}, $itempath);
363 $self->track_version ($data->{physname}, $data->{version}, $itempath);
365 } # End _branch_handler
367 ###############################################################################
368 # _move_handler
369 ###############################################################################
370 sub _move_handler {
371 my($self, $itempath, $nodes, $data, $expdir) = @_;
373 # moving in SVN is the same as renaming; add the new and delete the old
375 my $newpath = $data->{info};
377 if ($self->{repository}->exists ($newpath)) {
378 $self->add_error("Attempt to move item '$itempath' to '$newpath' at "
379 . "revision $data->{revision_id}, but destination already exists: possibly "
380 . "missing delete; skipping");
381 return 0;
384 if (!$self->{repository}->exists ($itempath)) {
385 $self->add_error("Attempt to move item '$itempath' to '$newpath' at "
386 . "revision $data->{revision_id}, but source doesn't exists: possibly "
387 . "missing recover; skipping");
388 return 0;
391 my $node = Vss2Svn::Dumpfile::Node->new();
392 $node->set_initial_props($newpath, $data);
393 $node->{action} = 'add';
395 my($copyrev, $copypath);
397 $copyrev = $data->{revision_id} - 1;
398 $copypath = $itempath;
400 $node->{copyrev} = $copyrev;
401 $node->{copypath} = $copypath;
403 push @$nodes, $node;
405 # $self->track_modified($data->{physname}, $data->{revision_id}, $newpath);
406 # $self->track_version ($data->{physname}, $data->{version}, $newpath);
408 $node = Vss2Svn::Dumpfile::Node->new();
409 $node->set_initial_props($itempath, $data);
410 $node->{action} = 'delete';
411 $node->{hideprops} = 1;
413 push @$nodes, $node;
415 } # End _move_handler
417 ###############################################################################
418 # _delete_handler
419 ###############################################################################
420 sub _delete_handler {
421 my($self, $itempath, $nodes, $data, $expdir) = @_;
423 if (!$self->{repository}->exists ($itempath)) {
424 $self->add_error("Attempt to delete non-existent item '$itempath' at "
425 . "revision $data->{revision_id}: possibly "
426 . "missing recover/add/share; skipping");
427 return 0;
430 my $node = Vss2Svn::Dumpfile::Node->new();
431 $node->set_initial_props($itempath, $data);
432 $node->{action} = 'delete';
433 $node->{hideprops} = 1;
435 push @$nodes, $node;
437 $self->track_deleted($data->{parentphys}, $data->{physname},
438 $data->{revision_id}, $itempath);
440 } # End _delete_handler
442 ###############################################################################
443 # _recover_handler
444 ###############################################################################
445 sub _recover_handler {
446 my($self, $itempath, $nodes, $data, $expdir) = @_;
448 if ($self->{repository}->exists ($itempath)) {
449 $self->add_error("Attempt to recover existing item '$itempath' at "
450 . "revision $data->{revision_id}: possibly "
451 . "missing delete; change to commit");
452 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
455 my $node = Vss2Svn::Dumpfile::Node->new();
456 $node->set_initial_props($itempath, $data);
457 $node->{action} = 'add';
459 # for projects we want to go back to the revision just one before the deleted
460 # revision. For files, we need to go back to the specified revision, since
461 # the file could have been modified via a share.
462 my($copyrev, $copypath);
463 if (!defined ($data->{version})) {
464 ($copyrev, $copypath)= $self->last_deleted_rev_path($data->{parentphys},
465 $data->{physname});
466 $copyrev -= 1;
468 else {
469 $copyrev =
470 $self->get_revision ($data->{physname}, $data->{version}, $data->{info});
471 $copypath = $data->{info};
474 if (!defined $copyrev || !defined $copypath) {
475 $self->add_error(
476 "Could not recover path $itempath at revision $data->{revision_id};"
477 . " unable to determine deleted revision or path");
478 return 0;
481 $node->{copyrev} = $copyrev;
482 $node->{copypath} = $copypath;
484 if (defined ($data->{version})) {
485 $self->track_version ($data->{physname}, $data->{version}, $itempath);
488 push @$nodes, $node;
490 } # End _recover_handler
492 ###############################################################################
493 # _pin_handler
494 ###############################################################################
495 sub _pin_handler {
496 my($self, $itempath, $nodes, $data, $expdir) = @_;
498 if (!$self->{repository}->exists ($itempath)) {
499 $self->add_error("Attempt to pin non-existing item '$itempath' at "
500 . "revision $data->{revision_id}: possibly "
501 . "missing recover; skipping");
502 return 0;
505 my $copyrev =
506 $self->get_revision ($data->{physname}, $data->{version}, $data->{info});
507 my $copypath = $data->{info};
509 # if one of the necessary copy from attributes are unavailable we fall back
510 # to a complete checkin
511 if (!defined $copyrev || !defined $copypath) {
512 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
515 my $node = Vss2Svn::Dumpfile::Node->new();
516 $node->set_initial_props($itempath, $data);
517 $node->{action} = 'add';
519 $node->{copyrev} = $copyrev;
520 $node->{copypath} = $copypath;
522 $self->track_version ($data->{physname}, $data->{version}, $itempath);
524 push @$nodes, $node;
526 } # End _pin_handler
528 ###############################################################################
529 # _label_handler
530 ###############################################################################
531 sub _label_handler {
532 my($self, $itempath, $nodes, $data, $expdir) = @_;
534 if (!$self->{repository}->exists ($itempath)) {
535 $self->add_error("Attempt to label non-existing item '$itempath' at "
536 . "revision $data->{revision_id}: possibly "
537 . "missing recover; skipping");
538 return 0;
541 my $label = $data->{info};
543 # It is possible that the label was deleted later, so we see here a label
544 # action, but no label was assigned. In this case, we only need to track
545 # the version->revision mapping, since the version could have been used
546 # as a valid share source.
547 if (defined ($label)) {
548 my $uniquepath = join('.', @$data{ qw(physname version) });
549 my $labelpath = "$main::gCfg{labeldir}/$data->{info}$itempath";
551 $self->_create_svn_path ($nodes, $labelpath);
553 my $node = Vss2Svn::Dumpfile::Node->new();
554 $node->set_initial_props($labelpath, $data);
555 $node->{action} = 'add';
557 my $copyrev = $data->{revision_id} - 1;
558 my $copypath = $itempath;
560 $node->{copyrev} = $copyrev;
561 $node->{copypath} = $copypath;
563 push @$nodes, $node;
567 $self->track_version ($data->{physname}, $data->{version}, $itempath);
568 } # End _label_handler
570 ###############################################################################
571 # _add_svn_dir
572 ###############################################################################
573 sub _add_svn_dir {
574 my($self, $nodes, $dir) = @_;
576 my $node = Vss2Svn::Dumpfile::Node->new();
577 my $data = { itemtype => 1, is_binary => 0 };
579 $node->set_initial_props($dir, $data);
580 $node->{action} = 'add';
582 push @$nodes, $node;
583 } # End _add_svn_dir
586 ###############################################################################
587 # _create_svn_path
588 ###############################################################################
589 sub _create_svn_path {
590 my($self, $nodes, $itempath) = @_;
592 my $missing_dirs = $self->{repository}->get_missing_dirs($itempath);
594 foreach my $dir (@$missing_dirs) {
595 $self->_add_svn_dir($nodes, $dir);
597 } # End _create_svn_path
599 ###############################################################################
600 # track_version
601 ###############################################################################
602 sub track_version {
603 my($self, $physname, $version, $itempath) = @_;
605 my $record =
607 physname => $physname,
608 version => $version,
609 revision => $self->{revision},
610 itempath => $itempath,
612 push @{$self->{version_cache}}, $record;
614 } # End track_version
617 ###############################################################################
618 # get_revision
619 ###############################################################################
620 sub get_revision {
621 my($self, $physname, $version, $itempath) = @_;
623 if (!defined($gVersion{$physname})) {
624 return (undef);
627 if (!exists($gVersion{$physname}->[$version])) {
628 return (undef);
631 return $gVersion{$physname}->[$version]->{$itempath};
633 } # End get_revision
635 ###############################################################################
636 # track_deleted
637 ###############################################################################
638 sub track_deleted {
639 my($self, $parentphys, $physname, $revision, $path) = @_;
641 $self->{deleted_cache}->{$parentphys}->{$physname} =
643 revision => $revision,
644 path => $path,
647 } # End track_deleted
649 ###############################################################################
650 # last_deleted_rev_path
651 ###############################################################################
652 sub last_deleted_rev_path {
653 my($self, $parentphys, $physname) = @_;
655 if (!defined($gDeleted{$parentphys})) {
656 return (undef, undef);
659 if (!defined($gDeleted{$parentphys}->{$physname})) {
660 return (undef, undef);
663 return @{ $gDeleted{$parentphys}->{$physname} }{ qw(revision path) };
664 } # End last_deleted_rev_path
666 ###############################################################################
667 # get_export_contents
668 ###############################################################################
669 sub get_export_contents {
670 my($self, $node, $data, $expdir) = @_;
672 if (!defined($expdir)) {
673 return 0;
674 } elsif (!defined($data->{version})) {
675 $self->add_error(
676 "Attempt to retrieve file contents with unknown version number");
677 return 0;
680 my $file = "$expdir/$data->{physname}.$data->{version}";
682 if (!open EXP, "$file") {
683 $self->add_error("Could not open export file '$file'");
684 return 0;
687 binmode(EXP);
689 # $node->{text} = join('', <EXP>);
690 $node->{text} = do { local( $/ ) ; <EXP> } ;
692 close EXP;
694 return 1;
696 } # End get_export_contents
698 ###############################################################################
699 # output_node
700 ###############################################################################
701 sub output_node {
702 my($self, $node) = @_;
703 my $fh = $self->{fh};
705 my $string = $node->get_headers();
706 from_to ($string, "windows-1252", "utf8");
707 print $fh $string;
708 $self->output_content($node->{hideprops}? undef : $node->{props},
709 $node->{text});
710 } # End output_node
712 ###############################################################################
713 # output_content
714 ###############################################################################
715 sub output_content {
716 my($self, $props, $text) = @_;
718 my $fh = $self->{fh};
720 $text = '' unless defined $text;
722 my $proplen = 0;
723 my $textlen = 0;
724 my($propout, $textout) = ('') x 2;
726 my($key, $value);
728 if (defined($props)) {
729 foreach my $prop (@$props) {
730 ($key, $value) = @$prop;
731 $propout .= 'K ' . length($key) . "\n$key\nV " . length($value)
732 . "\n$value\n";
735 $propout .= "PROPS-END\n";
736 $proplen = length($propout);
739 $textlen = length($text);
740 return if ($textlen + $proplen == 0);
742 if ($proplen > 0) {
743 print $fh "Prop-content-length: $proplen\n";
746 if ($textlen > 0) {
747 print $fh "Text-content-length: $textlen\n";
750 print $fh "Content-length: " . ($proplen + $textlen)
751 . "\n\n$propout$text\n";
753 } # End output_content
755 ###############################################################################
756 # svn_timestamp
757 ###############################################################################
758 sub svn_timestamp {
759 my($self, $vss_timestamp) = @_;
761 return &SvnTimestamp($vss_timestamp);
763 } # End svn_timestamp
765 ###############################################################################
766 # SvnTimestamp
767 ###############################################################################
768 sub SvnTimestamp {
769 my($vss_timestamp) = @_;
771 my($sec, $min, $hour, $day, $mon, $year) = gmtime($vss_timestamp);
773 $year += 1900;
774 $mon += 1;
776 return sprintf("%4.4i-%2.2i-%2.2iT%2.2i:%2.2i:%2.2i.%6.6iZ",
777 $year, $mon, $day, $hour, $min, $sec, 0);
779 } # End SvnTimestamp
781 ###############################################################################
782 # add_error
783 ###############################################################################
784 sub add_error {
785 my($self, $msg) = @_;
787 push @{ $self->{errors} }, $msg;
788 } # End add_error