1 package Vss2Svn
::Dumpfile
;
3 use Vss2Svn
::Dumpfile
::Node
;
4 use Vss2Svn
::Dumpfile
::SanityChecker
;
5 use Vss2Svn
::Dumpfile
::AutoProps
;
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
33 ###############################################################################
35 ###############################################################################
37 my($class, $fh, $autoprops) = @_;
46 repository
=> Vss2Svn
::Dumpfile
::SanityChecker
->new(),
47 auto_props
=> $autoprops,
50 # prevent perl from doing line-ending conversions
53 my $old = select($fh);
57 print $fh "SVN-fs-dump-format-version: 2\n\n";
59 $self = bless($self, $class);
64 ###############################################################################
66 ###############################################################################
76 ###############################################################################
78 ###############################################################################
80 my($self, $data) = @_;
81 my($revision, $author, $timestamp, $comment) =
82 @
{ $data }{qw(revision_id author timestamp comment)};
87 print $fh "\nRevision-number: $revision\n";
89 $comment = '' if !defined($comment);
90 $author = '' if !defined($author);
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 ###############################################################################
107 ###############################################################################
109 my($self, $data, $expdir) = @_;
111 my $action = $data->{action
};
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)) {
134 $handler = $gHandlers{$this_action};
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);
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
};
172 ###############################################################################
174 ###############################################################################
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 "
183 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
186 $self->add_error("Attempt to re-add directory '$itempath' at "
187 . "revision $data->{revision_id}, skipping action: possibly "
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");
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 "
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);
228 ###############################################################################
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);
254 } # End _commit_handler
256 ###############################################################################
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:;
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");
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");
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;
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;
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 ###############################################################################
320 ###############################################################################
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");
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");
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});
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);
357 } # End _share_handler
359 ###############################################################################
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 ###############################################################################
389 ###############################################################################
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");
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");
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");
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;
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);
454 } # End _move_handler
456 ###############################################################################
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");
469 my $node = Vss2Svn
::Dumpfile
::Node
->new();
470 $node->set_initial_props($itempath, $data);
471 $node->{action
} = 'delete';
472 $node->{hideprops
} = 1;
476 $self->track_deleted($data->{parentphys
}, $data->{physname
},
477 $data->{revision_id
}, $itempath);
479 } # End _delete_handler
481 ###############################################################################
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
},
509 $self->get_revision ($data->{physname
}, $data->{version
}, $data->{info
});
510 $copypath = $data->{info
};
513 if (!defined $copyrev || !defined $copypath) {
515 "Could not recover path $itempath at revision $data->{revision_id};"
516 . " unable to determine deleted revision or path");
520 $node->{copyrev
} = $copyrev;
521 $node->{copypath
} = $copypath;
523 if (defined ($data->{version
})) {
524 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
529 } # End _recover_handler
531 ###############################################################################
533 ###############################################################################
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");
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);
567 ###############################################################################
569 ###############################################################################
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");
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;
609 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
610 } # End _label_handler
612 ###############################################################################
614 ###############################################################################
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';
628 ###############################################################################
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 ###############################################################################
643 ###############################################################################
645 my($self, $physname, $version, $itempath) = @_;
649 physname
=> $physname,
651 revision
=> $self->{revision
},
652 itempath
=> $itempath,
654 push @
{$self->{version_cache
}}, $record;
656 } # End track_version
659 ###############################################################################
661 ###############################################################################
663 my($self, $physname, $version, $itempath) = @_;
665 if (!defined($gVersion{$physname})) {
669 if (!exists($gVersion{$physname}->[$version])) {
673 return $gVersion{$physname}->[$version]->{$itempath};
677 ###############################################################################
679 ###############################################################################
681 my($self, $parentphys, $physname, $revision, $path) = @_;
683 $self->{deleted_cache
}->{$parentphys}->{$physname} =
685 revision
=> $revision,
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)) {
716 } elsif (!defined($data->{version
})) {
718 "Attempt to retrieve file contents with unknown version number");
722 my $file = "$expdir/$data->{physname}.$data->{version}";
724 if (!open EXP
, "$file") {
725 $self->add_error("Could not open export file '$file'");
731 # $node->{text} = join('', <EXP>);
732 $node->{text
} = do { local( $/ ) ; <EXP
> } ;
738 } # End get_export_contents
740 ###############################################################################
742 ###############################################################################
744 my($self, $node) = @_;
745 my $fh = $self->{fh
};
747 my $string = $node->get_headers();
749 $self->output_content($node->{hideprops
}?
undef : $node->{props
},
753 ###############################################################################
755 ###############################################################################
757 my($self, $props, $text) = @_;
759 my $fh = $self->{fh
};
761 $text = '' unless defined $text;
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";
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);
787 print $fh "Prop-content-length: $proplen\n";
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 ###############################################################################
801 ###############################################################################
803 my($self, $vss_timestamp) = @_;
805 return &SvnTimestamp
($vss_timestamp);
807 } # End svn_timestamp
809 ###############################################################################
811 ###############################################################################
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
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);
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);
830 ###############################################################################
832 ###############################################################################
834 my($self, $msg) = @_;
836 push @
{ $self->{errors
} }, $msg;