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 $self->add_error("Attempt to re-add directory '$itempath' at "
209 . "revision $data->{revision_id}, skipping action: possibly "
215 my $success = $self->{repository
}->exists_parent ($itempath);
216 if(!defined($success)) {
217 $self->add_error("Path consistency failure while trying to add "
218 . "item '$itempath' at revision $data->{revision_id}; skipping");
221 elsif ($success == 0) {
222 $self->add_error("Parent path missing while trying to add "
223 . "item '$itempath' at revision $data->{revision_id}: adding missing "
225 $self->_create_svn_path ($nodes, $itempath);
228 my $node = Vss2Svn
::Dumpfile
::Node
->new();
229 $node->set_initial_props($itempath, $data);
230 if ($data->{is_binary
}) {
231 $node->add_prop('svn:mime-type', 'application/octet-stream');
233 if (defined $self->{auto_props
}) {
234 $node->add_props ($self->{auto_props
}->get_props ($itempath));
237 $node->{action
} = 'add';
239 if ($data->{itemtype
} == 2) {
240 $self->get_export_file($node, $data, $expdir);
243 # $self->track_modified($data->{physname}, $data->{revision_id}, $itempath);
244 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
250 ###############################################################################
252 ###############################################################################
253 sub _commit_handler
{
254 my($self, $itempath, $nodes, $data, $expdir) = @_;
256 if (!$self->{repository
}->exists ($itempath)) {
257 $self->add_error("Attempt to commit to non-existant file '$itempath' at "
258 . "revision $data->{revision_id}, changing to add; possibly "
259 . "missing recover");
260 return $self->_add_handler ($itempath, $nodes, $data, $expdir);
263 my $node = Vss2Svn
::Dumpfile
::Node
->new();
264 $node->set_initial_props($itempath, $data);
265 $node->{action
} = 'change';
267 if ($data->{itemtype
} == 2) {
268 $self->get_export_file($node, $data, $expdir);
271 # $self->track_modified($data->{physname}, $data->{revision_id}, $itempath);
272 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
276 } # End _commit_handler
278 ###############################################################################
280 ###############################################################################
281 sub _rename_handler
{
282 my($self, $itempath, $nodes, $data, $expdir) = @_;
284 # to rename a file in SVN, we must add "with history" then delete the orig.
286 my $newname = $data->{info
};
287 my $newpath = $itempath;
289 if ($data->{itemtype
} == 1) {
290 $newpath =~ s
:(.*/)?
.+$:$1$newname:;
292 $newpath =~ s
:(.*/)?
.*:$1$newname:;
295 if ($self->{repository
}->exists ($newpath)) {
296 $self->add_error("Attempt to rename item '$itempath' to '$newpath' at "
297 . "revision $data->{revision_id}, but destination already exists: possibly "
298 . "missing delete; skipping");
302 if (!$self->{repository
}->exists ($itempath)) {
303 $self->add_error("Attempt to rename item '$itempath' to '$newpath' at "
304 . "revision $data->{revision_id}, but source doesn't exists: possibly "
305 . "missing recover; skipping");
309 my $node = Vss2Svn
::Dumpfile
::Node
->new();
310 $node->set_initial_props($newpath, $data);
311 # change the properties according to the new name
312 if (defined $self->{auto_props
}) {
313 $node->add_props ($self->{auto_props
}->get_props ($newpath));
315 $node->{action
} = 'add';
317 my($copyrev, $copypath);
319 # ideally, we should be finding the last time the file was modified and
320 # copy it from there, but that becomes difficult to track...
321 $copyrev = $data->{revision_id
} - 1;
322 $copypath = $itempath;
324 $node->{copyrev
} = $copyrev;
325 $node->{copypath
} = $copypath;
329 # $self->track_modified($data->{physname}, $data->{revision_id}, $newpath);
330 # $self->track_version ($data->{physname}, $data->{version}, $newpath);
332 $node = Vss2Svn
::Dumpfile
::Node
->new();
333 $node->set_initial_props($itempath, $data);
334 $node->{action
} = 'delete';
335 $node->{hideprops
} = 1;
339 # We don't add this to %gDeleted since VSS doesn't treat a rename as an
340 # add/delete and therefore we wouldn't recover from this point
342 } # End _rename_handler
344 ###############################################################################
346 ###############################################################################
348 my($self, $itempath, $nodes, $data, $expdir) = @_;
350 if ($self->{repository
}->exists ($itempath)) {
351 $self->add_error("Attempt to share item '$data->{info}' to '$itempath' at "
352 . "revision $data->{revision_id}, but destination already exists: possibly "
353 . "missing delete; skipping");
357 # It could be possible that we share from a historically renamed item, so we don't check the source
358 # if ($self->{repository}->exists ($data->{info})) {
359 # $self->add_error("Attempt to share item '$itempath' to '$newpath' at "
360 # . "revision $data->{revision_id}, but destination already exists: possibly "
361 # . "missing delete; skipping");
365 my $node = Vss2Svn
::Dumpfile
::Node
->new();
366 $node->set_initial_props($itempath, $data);
367 $node->{action
} = 'add';
369 # @{ $node }{ qw(copyrev copypath) }
370 # = $self->last_modified_rev_path($data->{physname});
372 $self->get_revision ($data->{physname
}, $data->{version
}, $data->{info
});
373 $node->{copypath
} = $data->{info
};
375 if (!defined $node->{copyrev
} || !defined $node->{copypath
}) {
376 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
379 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
383 } # End _share_handler
385 ###############################################################################
387 ###############################################################################
388 sub _branch_handler
{
389 my($self, $itempath, $nodes, $data, $expdir) = @_;
391 # branching is a no-op in SVN
393 # since it is possible, that we refer to version prior to the branch later, we
394 # need to copy all internal information about the ancestor to the child.
395 if (defined $data->{info
}) {
396 # only copy versions, that are common between the branch source and the branch.
397 my $copy_version=$data->{version
};
398 while(--$copy_version > 0) {
399 if (defined $gVersion{$data->{info
}}->[$copy_version]) {
400 $gVersion{$data->{physname
}}->[$copy_version] =
401 $gVersion{$data->{info
}}->[$copy_version];
406 # # if the file is copied later, we need to track, the revision of this branch
407 # # see the shareBranchShareModify Test
408 # $self->track_modified($data->{physname}, $data->{revision_id}, $itempath);
409 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
411 } # End _branch_handler
413 ###############################################################################
415 ###############################################################################
417 my($self, $itempath, $nodes, $data, $expdir) = @_;
419 # moving in SVN is the same as renaming; add the new and delete the old
421 my $oldpath = $data->{info
};
423 if ($self->{repository
}->exists ($itempath)) {
424 $self->add_error("Attempt to move item '$oldpath' to '$itempath' at "
425 . "revision $data->{revision_id}, but destination already exists: possibly "
426 . "missing delete; skipping");
430 if (!$self->{repository
}->exists ($oldpath)) {
431 $self->add_error("Attempt to move item '$oldpath' to '$itempath' at "
432 . "revision $data->{revision_id}, but source doesn't exists: possibly "
433 . "missing recover; skipping");
437 my $success = $self->{repository
}->exists_parent($itempath);
438 if(!defined($success)) {
439 $self->add_error("Attempt to move item '$oldpath' to '$itempath' at "
440 . "revision $data->{revision_id}, but path consistency failure at dest");
443 elsif ($success == 0) {
444 $self->add_error("Parent path missing while trying to move "
445 . "item '$oldpath' to '$itempath' at "
446 . "revision $data->{revision_id}: adding missing parents");
447 $self->_create_svn_path ($nodes, $itempath);
450 my $node = Vss2Svn
::Dumpfile
::Node
->new();
451 $node->set_initial_props($itempath, $data);
452 $node->{action
} = 'add';
454 my($copyrev, $copypath);
456 $copyrev = $data->{revision_id
} - 1;
457 $copypath = $oldpath;
459 $node->{copyrev
} = $copyrev;
460 $node->{copypath
} = $copypath;
464 # the new move target is a valid path.
465 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
467 $node = Vss2Svn
::Dumpfile
::Node
->new();
468 $node->set_initial_props($oldpath, $data);
469 $node->{action
} = 'delete';
470 $node->{hideprops
} = 1;
472 # Deleted tracking is only necessary to be able to recover the item. But a move
473 # does not set a recover point, so we don't need to track the delete here. Additionally
474 # we do not have enough information for this operation.
475 # $self->track_deleted($data->{oldparentphys}, $data->{physname},
476 # $data->{revision_id}, $oldpath);
480 } # End _move_handler
482 ###############################################################################
484 ###############################################################################
485 sub _delete_handler
{
486 my($self, $itempath, $nodes, $data, $expdir) = @_;
488 if (!$self->{repository
}->exists ($itempath)) {
489 $self->add_error("Attempt to delete non-existent item '$itempath' at "
490 . "revision $data->{revision_id}: possibly "
491 . "missing recover/add/share; skipping");
495 my $node = Vss2Svn
::Dumpfile
::Node
->new();
496 $node->set_initial_props($itempath, $data);
497 $node->{action
} = 'delete';
498 $node->{hideprops
} = 1;
502 $self->track_deleted($data->{parentphys
}, $data->{physname
},
503 $data->{revision_id
}, $itempath);
505 } # End _delete_handler
507 ###############################################################################
509 ###############################################################################
510 sub _recover_handler
{
511 my($self, $itempath, $nodes, $data, $expdir) = @_;
513 if ($self->{repository
}->exists ($itempath)) {
514 $self->add_error("Attempt to recover existing item '$itempath' at "
515 . "revision $data->{revision_id}: possibly "
516 . "missing delete; change to commit");
517 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
520 my $node = Vss2Svn
::Dumpfile
::Node
->new();
521 $node->set_initial_props($itempath, $data);
522 $node->{action
} = 'add';
524 # for projects we want to go back to the revision just one before the deleted
525 # revision. For files, we need to go back to the specified revision, since
526 # the file could have been modified via a share.
527 my($copyrev, $copypath);
528 if (!defined ($data->{version
})) {
529 ($copyrev, $copypath)= $self->last_deleted_rev_path($data->{parentphys
},
535 $self->get_revision ($data->{physname
}, $data->{version
}, $data->{info
});
536 $copypath = $data->{info
};
539 if (!defined $copyrev || !defined $copypath) {
541 "Could not recover path $itempath at revision $data->{revision_id};"
542 . " unable to determine deleted revision or path");
546 $node->{copyrev
} = $copyrev;
547 $node->{copypath
} = $copypath;
549 if (defined ($data->{version
})) {
550 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
555 } # End _recover_handler
557 ###############################################################################
559 ###############################################################################
561 my($self, $itempath, $nodes, $data, $expdir) = @_;
563 if (!$self->{repository
}->exists ($itempath)) {
564 $self->add_error("Attempt to pin non-existing item '$itempath' at "
565 . "revision $data->{revision_id}: possibly "
566 . "missing recover; skipping");
571 $self->get_revision ($data->{physname
}, $data->{version
}, $data->{info
});
572 my $copypath = $data->{info
};
574 # if one of the necessary copy from attributes are unavailable we fall back
575 # to a complete checkin
576 if (defined $copyrev && defined $copypath) {
577 $data->{comment
} = "ported from $copypath r$copyrev";
579 # if (!defined $copyrev || !defined $copypath) {
580 return $self->_commit_handler ($itempath, $nodes, $data, $expdir);
583 my $node = Vss2Svn
::Dumpfile
::Node
->new();
584 $node->set_initial_props($itempath, $data);
585 $node->{action
} = 'add';
587 $node->{copyrev
} = $copyrev;
588 $node->{copypath
} = $copypath;
590 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
596 ###############################################################################
598 ###############################################################################
600 my($self, $itempath, $nodes, $data, $expdir) = @_;
602 if (!$self->{repository
}->exists ($itempath)) {
603 $self->add_error("Attempt to label non-existing item '$itempath' at "
604 . "revision $data->{revision_id}: possibly "
605 . "missing recover; skipping");
609 my $label = $data->{info
};
611 # It is possible that the label was deleted later, so we see here a label
612 # action, but no label was assigned. In this case, we only need to track
613 # the version->revision mapping, since the version could have been used
614 # as a valid share source.
615 if (defined ($label)) {
616 my $labeldir = $main::gCfg
{labeldir
};
618 if (defined $self->{label_mapper
}) {
619 my $mapping = $self->{label_mapper
}->remap ($main::gCfg
{labeldir
}, $label);
620 $labeldir = $mapping->{replacement
} if $mapping->{is_labeldir
};
621 $label = $mapping->{replacement
} unless $mapping->{is_labeldir
};
623 $labeldir =~ s
:\\:/:g
;
626 $label =~ s![\\/:*?"<>|]!_!g;
628 my $vssitempath = $itempath;
629 $vssitempath =~ s/^$main::gCfg{trunkdir}//;
630 my $labelpath = "$labeldir/$label$vssitempath";
632 $self->_create_svn_path ($nodes, $labelpath);
634 my $node = Vss2Svn
::Dumpfile
::Node
->new();
635 $node->set_initial_props($labelpath, $data);
636 $node->{action
} = 'add';
638 my $copyrev = $data->{revision_id
} - 1;
639 my $copypath = $itempath;
641 $node->{copyrev
} = $copyrev;
642 $node->{copypath
} = $copypath;
648 $self->track_version ($data->{physname
}, $data->{version
}, $itempath);
649 } # End _label_handler
651 ###############################################################################
653 ###############################################################################
655 my($self, $nodes, $dir) = @_;
657 my $node = Vss2Svn
::Dumpfile
::Node
->new();
658 my $data = { itemtype
=> 1, is_binary
=> 0 };
660 $node->set_initial_props($dir, $data);
661 $node->{action
} = 'add';
667 ###############################################################################
669 ###############################################################################
670 sub _create_svn_path
{
671 my($self, $nodes, $itempath) = @_;
673 my $missing_dirs = $self->{repository
}->get_missing_dirs($itempath);
675 foreach my $dir (@
$missing_dirs) {
676 $self->_add_svn_dir($nodes, $dir);
678 } # End _create_svn_path
680 ###############################################################################
682 ###############################################################################
684 my($self, $physname, $version, $itempath) = @_;
688 physname
=> $physname,
690 revision
=> $self->{revision
},
691 itempath
=> $itempath,
693 push @
{$self->{version_cache
}}, $record;
695 } # End track_version
698 ###############################################################################
700 ###############################################################################
702 my($self, $physname, $version, $itempath) = @_;
704 if (!defined($gVersion{$physname})) {
708 if (!exists($gVersion{$physname}->[$version])) {
712 return $gVersion{$physname}->[$version]->{$itempath};
716 ###############################################################################
718 ###############################################################################
720 my($self, $parentphys, $physname, $revision, $path) = @_;
722 $self->{deleted_cache
}->{$parentphys}->{$physname} =
724 revision
=> $revision,
728 } # End track_deleted
730 ###############################################################################
731 # last_deleted_rev_path
732 ###############################################################################
733 sub last_deleted_rev_path
{
734 my($self, $parentphys, $physname) = @_;
736 if (!defined($gDeleted{$parentphys})) {
737 return (undef, undef);
740 if (!defined($gDeleted{$parentphys}->{$physname})) {
741 return (undef, undef);
744 return @
{ $gDeleted{$parentphys}->{$physname} }{ qw(revision path) };
745 } # End last_deleted_rev_path
747 ###############################################################################
749 ###############################################################################
750 sub get_export_file
{
751 my($self, $node, $data, $expdir) = @_;
753 if (!defined($expdir)) {
755 } elsif (!defined($data->{version
})) {
757 "Attempt to retrieve file contents with unknown version number");
761 $node->{file
} = "$expdir/$data->{physname}.$data->{version}";
764 } # End get_export_file
766 ###############################################################################
768 ###############################################################################
770 my($self, $node) = @_;
771 my $fh = $self->{fh
};
773 # only in an add or rename action the propery array is set. So we have
774 # to lookup the eol-style flag again. The best thing is to query the
775 # property always temporarirly
777 if (defined $self->{auto_props
}) {
778 %tmpProps = $self->{auto_props
}->get_props ($node->{path
});
780 my $eolStyle = $tmpProps{'svn:eol-style'};
781 my $isNative = (defined $eolStyle && $eolStyle eq 'native') ?
1 : 0;
783 my $string = $node->get_headers();
785 $self->output_content($node->{hideprops
}?
undef : $node->{props
},
786 $node->{text
}, $node->{file
}, $isNative);
789 ###############################################################################
791 ###############################################################################
793 my($self, $props, $text, $file, $isNative) = @_;
795 my $fh = $self->{fh
};
797 $text = '' unless defined $text || defined $file;
801 my($propout, $textout) = ('') x
2;
803 if (defined($props)) {
804 foreach my $key (keys %$props) {
805 my $value = $props->{$key};
806 $propout .= 'K ' . length($key) . "\n$key\n";
807 if (defined $value) {
808 $propout .= 'V ' . length($value) . "\n$value\n";
811 $propout .= "V 0\n\n";
815 $propout .= "PROPS-END\n";
816 $proplen = length($propout);
820 $md5 = Digest
::MD5
->new if $self->{do_md5
};
822 # prevent errors due to non existing files
823 if(!defined $text && defined $file && !-e
$file) {
827 # convert CRLF -> LF before calculating the size and compute the md5
828 if(!defined $text && defined $file) {
830 my ($input, $output);
831 if (defined $isNative && $isNative) {
832 open ($input, "<:crlf", $file);
833 my $tmpFile = "$gTmpDir/crlf_to_lf.tmp.txt";
834 open ($output, ">", $tmpFile);
838 $md5->add($_) if $self->{do_md5
};
847 open ($input, "<", $file);
849 $md5->addfile($input) if $self->{do_md5
};
853 $md5->add($text) if $self->{do_md5
};
856 my $digest = $md5->hexdigest if $self->{do_md5
};
857 # print "digest: $digest\n";
859 if(!defined $text && defined $file) {
862 $textlen = length($text);
864 return if ($textlen + $proplen == 0);
867 print $fh "Prop-content-length: $proplen\n";
871 print $fh "Text-content-length: $textlen\n";
872 print $fh "Text-content-md5: $digest\n" if $self->{do_md5
};
875 print $fh "Content-length: " . ($proplen + $textlen)
878 if(!defined $text && defined $file) {
885 } # End output_content
887 ###############################################################################
889 ###############################################################################
891 my($self, $vss_timestamp) = @_;
893 return &SvnTimestamp
($vss_timestamp);
895 } # End svn_timestamp
897 ###############################################################################
899 ###############################################################################
901 my($vss_timestamp) = @_;
903 # set the correct time: VSS stores the local time as the timestamp, but subversion
904 # needs a gmtime. So we need to reverse adjust the timestamp in order to turn back
906 my($sec, $min, $hour, $day, $mon, $year) = gmtime($vss_timestamp);
907 my($faketime) = Time
::Local
::timelocal
($sec, $min, $hour, $day, $mon, $year);
908 ($sec, $min, $hour, $day, $mon, $year) = gmtime($faketime);
913 return sprintf("%4.4i-%2.2i-%2.2iT%2.2i:%2.2i:%2.2i.%6.6iZ",
914 $year, $mon, $day, $hour, $min, $sec, 0);
918 ###############################################################################
920 ###############################################################################
922 my($self, $msg) = @_;
924 push @
{ $self->{errors
} }, $msg;