1package SVN::Mirror::Ra; 2@ISA = ('SVN::Mirror'); 3$VERSION = '0.73'; 4use strict; 5use SVN::Core; 6use SVN::Repos; 7use SVN::Fs; 8use SVN::Delta; 9use SVN::Ra; 10use SVN::Client (); 11use constant OK => $SVN::_Core::SVN_NO_ERROR; 12 13sub new { 14 my $class = shift; 15 my $self = bless {}, $class; 16 %$self = @_; 17 $self->{source} =~ s{/+$}{}g; 18 19 @{$self}{qw/source source_root source_path/} = 20 _parse_source ($self->{source}); 21 22 @{$self}{qw/rsource rsource_root rsource_path/} = 23 _parse_source ($self->{rsource}) if $self->{rsource}; 24 25 return $self; 26} 27 28sub _parse_source { 29 my $source = shift; 30 my ($root, $path) = split ('!', $source, 2); 31 $path ||= ''; 32 return (join('', $root, $path), $root, $path) 33} 34 35sub _store_source { 36 my ($root, $path) = @_; 37 return join('!', $root, $path); 38} 39 40sub _get_prop { 41 my ($self, $ra, $path, $propname) = @_; 42} 43 44sub _is_descendent { 45 my ($parent, $child) = @_; 46 return 1 if $parent eq $child; 47 $parent = "$parent/" unless $parent eq '/'; 48 return $parent eq substr ($child, 0, length ($parent)); 49} 50 51sub _check_overlap { 52 my ($self) = @_; 53 my $fs = $self->{repos}->fs; 54 my $root = $fs->revision_root ($fs->youngest_rev); 55 for (map {$root->node_prop ($_, 'svm:source')} SVN::Mirror::list_mirror ($self->{repos})) { 56 my (undef, $source_root, $source_path) = _parse_source ($_); 57 next if $source_root ne $self->{source_root}; 58 die "Mirroring overlapping paths not supported\n" 59 if _is_descendent ($source_path, $self->{source_path}) 60 || _is_descendent ($self->{source_path}, $source_path); 61 } 62} 63 64sub init_state { 65 my ($self, $txn) = @_; 66 my $ra = $self->_new_ra (url => $self->{source}); 67 68 my $uuid = $self->{source_uuid} = $ra->get_uuid (); 69 my $source_root = $ra->get_repos_root (); 70 my $path = $self->{source}; 71 $txn->abort, die "source url not under source root" 72 if substr($path, 0, length($source_root), '') ne $source_root; 73 74 $self->{source_root} = $source_root; 75 $self->{source_path} = $path; 76 $self->{fromrev} = 0; 77 78 # XXX: abort txn before dying 79 $self->_check_overlap; 80 81 # check if the url exists 82 if ($ra->check_path ('', -1) != $SVN::Node::dir) { 83 $txn->abort; 84 die "$self->{source} is not a directory.\n"; 85 } 86 unless ($self->{source} eq $self->{source_root}) { 87 undef $ra; # bizzare perlgc 88 $ra = $self->_new_ra (url => $self->{source_root}); 89 } 90 91 # check if mirror source is already a mirror 92 # older SVN::RA will return Reporter so prop would be undef 93 my (undef, undef, $prop) = eval { $ra->get_dir ('', -1) }; 94 warn "Unable to read $ra->{url}, relay support disabled\n" if $@; 95 if ($prop && $prop->{'svm:mirror'}) { 96 my $rroot; 97 for ($prop->{'svm:mirror'} =~ m/^.*$/mg) { 98 if (_is_descendent ($_, $self->{source_path})) { 99 $rroot = $_; 100 last; 101 } 102 elsif (_is_descendent ($self->{source_path}, $_)) { 103 $txn->abort, die "Can't relay mirror outside mirror anchor $_"; 104 } 105 } 106 if ($rroot) { 107 $rroot =~ s|^/||; 108 (undef, undef, $prop) = $ra->get_dir ($rroot, -1); 109 $txn->abort, die "relayed mirror source doesn't not have svm:source" 110 unless exists $prop->{'svm:source'}; 111 @{$self}{qw/rsource rsource_root rsource_path/} = 112 @{$self}{qw/source source_root source_path/}; 113 $self->{rsource_uuid} = $uuid; 114 $self->{source_path} =~ s|^/\Q$rroot\E||; 115 @{$self}{qw/source source_uuid/} = @{$prop}{qw/svm:source svm:uuid/}; 116 $self->{source} .= '!' if index ($self->{source}, '!') == -1; 117 @{$self}{qw/source source_root source_path/} = 118 _parse_source ($self->{source}.$self->{source_path}); 119 120 $txn->abort, die "relayed source and source have same repository uuid" 121 if $self->{source_uuid} eq $self->{rsource_uuid}; 122 123 my $txnroot = $txn->root; 124 $txnroot->change_node_prop ($self->{target_path}, 'svm:rsource', 125 _store_source ($source_root, $path)); 126 $txnroot->change_node_prop ($self->{target_path}, 'svm:ruuid', 127 $uuid); 128 $txn->change_prop ("svm:headrev", "$self->{rsource_uuid}:$self->{fromrev}\n"); 129 130 return _store_source ($self->{source_root}, $self->{source_path}); 131 } 132 } 133 134 @{$self}{qw/rsource rsource_root rsource_path/} = 135 @{$self}{qw/source source_root source_path/}; 136 137 $self->{rsource_uuid} = $self->{source_uuid}; 138 139 $txn->change_prop ("svm:headrev", "$self->{rsource_uuid}:$self->{fromrev}\n"); 140 return _store_source ($source_root, $path); 141} 142 143sub load_state { 144 my ($self) = @_; 145 146 my $prop = $self->{root}->node_proplist ($self->{target_path}); 147 @{$self}{qw/source_uuid rsource_uuid/} = 148 @{$prop}{qw/svm:uuid svm:ruuid/}; 149 unless ($self->{rsource}) { 150 @{$self}{qw/rsource rsource_root rsource_path/} = 151 @{$self}{qw/source source_root source_path/}; 152 $self->{rsource_uuid} = $self->{source_uuid}; 153 } 154 155 die "please upgrade the mirror state\n" 156 if $self->{root}->node_prop ('/', join (':', 'svm:mirror', $self->{source_uuid}, 157 $self->{source_path} || '/')); 158 159 unless ($self->{ignore_lock}) { 160 die "no headrev" 161 unless defined $self->load_fromrev; 162 } 163 return; 164} 165 166sub _new_ra { 167 my ($self, %arg) = @_; 168 $self->{config} ||= SVN::Core::config_get_config(undef, $self->{pool}); 169 $self->{auth} ||= $self->_new_auth; 170 171 SVN::Ra->new( url => $self->{rsource}, 172 auth => $self->{auth}, 173 config => $self->{config}, 174 %arg); 175} 176 177sub _new_auth { 178 my ($self) = @_; 179 # create a subpool that is not automatically destroyed 180 my $pool = SVN::Pool::create (${$self->{pool}}); 181 $pool->default; 182 my ($baton, $ref) = SVN::Core::auth_open_helper([ 183 SVN::Client::get_simple_provider (), 184 SVN::Client::get_ssl_server_trust_file_provider (), 185 SVN::Client::get_username_provider (), 186 SVN::Client::get_simple_prompt_provider( $self->can('_simple_prompt'), 2), 187 SVN::Client::get_ssl_server_trust_prompt_provider( $self->can('_ssl_server_trust_prompt') ), 188 SVN::Client::get_ssl_client_cert_prompt_provider( $self->can('_ssl_client_cert_prompt'), 2 ), 189 SVN::Client::get_ssl_client_cert_pw_prompt_provider( $self->can('_ssl_client_cert_pw_prompt'), 2 ), 190 SVN::Client::get_username_prompt_provider( $self->can('_username_prompt'), 2), 191 ]); 192 $self->{auth_ref} = $ref; 193 return $baton; 194} 195 196sub _simple_prompt { 197 my ($cred, $realm, $default_username, $may_save, $pool) = @_; 198 199 if (defined $default_username and length $default_username) { 200 print "Authentication realm: $realm\n" if defined $realm and length $realm; 201 $cred->username($default_username); 202 } 203 else { 204 _username_prompt($cred, $realm, $may_save, $pool); 205 } 206 207 $cred->password(_read_password("Password for '" . $cred->username . "': ")); 208 $cred->may_save($may_save); 209 210 return OK; 211} 212 213sub _ssl_server_trust_prompt { 214 my ($cred, $realm, $failures, $cert_info, $may_save, $pool) = @_; 215 216 print "Error validating server certificate for '$realm':\n"; 217 218 print " - The certificate is not issued by a trusted authority. Use the\n", 219 " fingerprint to validate the certificate manually!\n" 220 if ($failures & $SVN::Auth::SSL::UNKNOWNCA); 221 222 print " - The certificate hostname does not match.\n" 223 if ($failures & $SVN::Auth::SSL::CNMISMATCH); 224 225 print " - The certificate is not yet valid.\n" 226 if ($failures & $SVN::Auth::SSL::NOTYETVALID); 227 228 print " - The certificate has expired.\n" 229 if ($failures & $SVN::Auth::SSL::EXPIRED); 230 231 print " - The certificate has an unknown error.\n" 232 if ($failures & $SVN::Auth::SSL::OTHER); 233 234 printf( 235 "Certificate information:\n". 236 " - Hostname: %s\n". 237 " - Valid: from %s until %s\n". 238 " - Issuer: %s\n". 239 " - Fingerprint: %s\n", 240 map $cert_info->$_, qw(hostname valid_from valid_until issuer_dname fingerprint) 241 ); 242 243 print( 244 $may_save 245 ? "(R)eject, accept (t)emporarily or accept (p)ermanently? " 246 : "(R)eject or accept (t)emporarily? " 247 ); 248 249 my $choice = lc(substr(<STDIN> || 'R', 0, 1)); 250 251 if ($choice eq 't') { 252 $cred->may_save(0); 253 $cred->accepted_failures($failures); 254 } 255 elsif ($may_save and $choice eq 'p') { 256 $cred->may_save(1); 257 $cred->accepted_failures($failures); 258 } 259 260 return OK; 261} 262 263sub _ssl_client_cert_prompt { 264 my ($cred, $realm, $may_save, $pool) = @_; 265 266 print "Client certificate filename: "; 267 chomp(my $filename = <STDIN>); 268 $cred->cert_file($filename); 269 270 return OK; 271} 272 273sub _ssl_client_cert_pw_prompt { 274 my ($cred, $realm, $may_save, $pool) = @_; 275 276 $cred->password(_read_password("Passphrase for '%s': ")); 277 278 return OK; 279} 280 281sub _username_prompt { 282 my ($cred, $realm, $may_save, $pool) = @_; 283 284 print "Authentication realm: $realm\n" if defined $realm and length $realm; 285 print "Username: "; 286 chomp(my $username = <STDIN>); 287 $username = '' unless defined $username; 288 289 $cred->username($username); 290 291 return OK; 292} 293 294sub _read_password { 295 my ($prompt) = @_; 296 297 print $prompt; 298 299 require Term::ReadKey; 300 Term::ReadKey::ReadMode('noecho'); 301 302 my $password = ''; 303 while (defined(my $key = Term::ReadKey::ReadKey(0))) { 304 last if $key =~ /[\012\015]/; 305 $password .= $key; 306 } 307 308 Term::ReadKey::ReadMode('restore'); 309 print "\n"; 310 311 return $password; 312} 313 314sub _revmap { 315 my ($self, $rev, $ra) = @_; 316 $ra ||= $self->{cached_ra}; 317 $SVN::Core::VERSION ge '1.1.0' ? 318 $ra->rev_prop ($rev, 'svm:headrev') : 319 $ra->rev_proplist ($rev)->{'svm:headrev'}; 320} 321 322sub committed { 323 my ($self, $revmap, $date, $sourcerev, $rev) = @_; 324 $self->{headrev} = $rev; 325 326 # Even though we set this on the transaction, we need to set it 327 # again after commit, since the fs will always make it the current 328 # time after committing. 329 $self->{fs}->change_rev_prop($rev, 'svn:date', $date); 330 331 $self->unlock ('mirror'); 332 print "Committed revision $rev from revision $sourcerev.\n"; 333} 334 335our $debug; 336 337sub mirror { 338 my ($self, $fromrev, $paths, $rev, $author, $date, $msg, $ppool) = @_; 339 my $ra; 340 341 if ($debug and eval { require BSD::Resource; 1 }) { 342 my ($usertime, $systemtime, 343 $maxrss, $ixrss, $idrss, $isrss, $minflt, $majflt, $nswap, 344 $inblock, $oublock, $msgsnd, $msgrcv, 345 $nsignals, $nvcsw, $nivcsw) = BSD::Resource::getrusage(); 346 print ">>> mirroring $rev:\n"; 347 print ">>> $usertime $systemtime $maxrss $ixrss $idrss $isrss\n"; 348 } 349 350 my $pool = SVN::Pool->new_default ($ppool); 351 my ($newrev, $revmap); 352 353 $ra = $self->{cached_ra} 354 if exists $self->{cached_ra_url} && 355 $self->{cached_ra_url} eq $self->{rsource}; 356 if ($ra && $self->{rsource} =~ m/^http/ && --$self->{cached_life} == 0) { 357 undef $ra; 358 } 359 $ra ||= $self->_new_ra; 360 361 $revmap = $self->_revmap ($rev, $ra) if $self->_relayed; 362 $revmap ||= ''; 363 364 my $txn = $self->{repos}->fs_begin_txn_for_commit 365 ($self->{fs}->youngest_rev, $author, $msg); 366 $txn->change_prop('svk:commit', '*') 367 if $self->{fs}->revision_prop(0, 'svk:notify-commit'); 368 369 $txn->change_prop('svn:date', $date); 370 # XXX: sync remote headrev too 371 $txn->change_prop('svm:headrev', $revmap."$self->{rsource_uuid}:$rev\n"); 372 $txn->change_prop('svm:incomplete', '*') 373 if $self->{rev_incomplete}; 374 375 my $editor = SVN::Mirror::Ra::NewMirrorEditor->new 376 ($self->{repos}->get_commit_editor2 377 ($txn, '', $self->{target_path}, $author, $msg, 378 sub { $newrev = $_[0]; 379 $self->committed ($revmap, $date, $rev, @_) })); 380 381 $self->{working} = $rev; 382 $editor->{mirror} = $self; 383 384 @{$self}{qw/cached_ra cached_ra_url/} = ($ra, $self->{rsource}); 385 386 $self->{cached_life} ||= 100; # some leak in ra_dav, so reconnect every 100 revs 387 $editor->{target} ||= '' if $SVN::Core::VERSION gt '0.36.0'; 388 389=begin NOTES 390 391The structure of mod_lists: 392 393* Key is the path of a changed path, a relative path to source_path. 394 This is what methods in MirrorEditor get its path, therefore easier 395 for them to look up information. 396 397* Value is a hash, containing the following values: 398 399 * action: 'A'dd, 'M'odify, 'D'elete, 'R'eplace 400 * remote_path: The path on remote depot 401 * remote_rev: The revision on remote depot 402 * local_rev: 403 * Not Add: -1 404 * Add but source is not in local depot: undef 405 * Add and source is in local depot: the source revision in local depot 406 * local_path: The mapped path of key, ie. the changed path, in local 407 depot. 408 * local_source_path: 409 * Source path is not in local depot: undef 410 * Source path is in local depot: a string 411 * source_node_kind: Only meaningful if action is 'A'. 412 413=cut 414 415 $editor->{mod_lists} = {}; 416 foreach ( keys %$paths ) { 417 my $spool = SVN::Pool->new_default; 418 my $item = $paths->{$_}; 419 s/\n/ /g; # XXX: strange edge case 420 my $href; 421 422 my $svn_lpath = my $local_path = $_; 423 if ( $editor->{anchor} ) { 424 $svn_lpath = $self->{rsource_root} . $svn_lpath; 425 $svn_lpath =~ s|^\Q$editor->{anchor}\E/?||; 426 my $source_path = $self->{rsource_path} || "/"; 427 $local_path =~ s|^\Q$source_path\E|$self->{target_path}|; 428 } else { 429 $svn_lpath =~ s|^\Q$self->{rsource_path}\E/?||; 430 $local_path = "$self->{target_path}/$svn_lpath"; 431 } 432 433 my $local_rev = -1; 434 unless ($item->copyfrom_rev == -1) { 435 $local_rev = $self->find_local_rev 436 ($item->copyfrom_rev, $self->{rsource_uuid}); 437 } 438 # XXX: the logic of the code here is a mess! 439 my ($action, $rpath, $rrev, $lrev) = 440 @$href{qw/action remote_path remote_rev local_rev local_path/} = 441 ( $item->action, 442 $item->copyfrom_path, 443 $item->copyfrom_rev, 444 $local_rev, 445 $local_path, 446 ); 447 # workaround fsfs remoet_path inconsistencies 448 $rpath = "/$rpath" if $rpath && substr ($rpath, 0, 1) ne '/'; 449 my ($src_lpath, $source_node_kind) = (undef, $SVN::Node::unknown); 450 # XXX: should check if the copy is within the anchor before resolving lrev 451 if ( defined $lrev && $lrev != -1 ) { 452 $src_lpath = $rpath; 453 # copy within mirror anchor 454 if ($src_lpath =~ s|^\Q$self->{rsource_path}\E/|$self->{target_path}/|) { 455 # $source_node_kind is used for deciding if we need reporter later 456 my $rev_root = $self->{fs}->revision_root ($lrev); 457 $source_node_kind = $rev_root->check_path ($src_lpath); 458 } 459 else { 460 ($src_lpath, $href->{local_rev}) = (undef, undef); 461 } 462 } 463 elsif ($rrev != -1) { 464 # The source is not in local depot. Invalidate this 465 # copy. 466 ($src_lpath, $href->{local_rev}) = 467 $self->{cb_copy_notify} 468 ? $self->{cb_copy_notify}->($self, $local_path, $rpath, $rrev) 469 : (undef, undef) 470 } 471 $src_lpath =~ s/%/%25/g if defined $src_lpath; 472 @$href{qw/local_source_path source_node_kind/} = 473 ( $src_lpath, $source_node_kind ); 474 475 # XXX: the loop should not reached here if changed path is 476 # not interesting to us, skip them at the beginning the the loop 477 if ( $_ eq $self->{rsource_path} or 478 index ("$_/", "$self->{rsource_path}/") == 0 ) { 479 $editor->{mod_lists}{$svn_lpath} = $href; 480 $editor->{mod_lists}{$svn_lpath}{path} = $svn_lpath; 481 } elsif ($rrev != -1 && $href->{action} eq 'A' && 482 index ($self->{rsource_path}, "$_/") == 0) { 483 # special case for the parent of the anchor is copied. 484 my $reanchor = $self->{rsource_path}; 485 my $path = length $svn_lpath ? "$svn_lpath/$reanchor" : $reanchor; 486 $reanchor =~ s{^\Q$_\E/}{}; 487 $href->{remote_path} .= '/'.$reanchor; 488 $href->{local_path} = $self->{target_path}; 489 $editor->{mod_lists}{$path} = $href; 490 $editor->{mod_lists}{$path}{path} = $path; 491 } 492 } 493 494 unless (keys %{$editor->{mod_lists}}) { 495 my $root = $editor->open_root($self->{headrev}); 496 $editor->change_dir_prop ($root, svm => undef); 497 $editor->close_directory($root); 498 $editor->close_edit; 499 } else { 500 my @mod_list = sort keys %{$editor->{mod_lists}}; 501 # mark item as directory that we are sure about. 502 # do not use !isdir for deciding the item is _not_ a directory. 503 for my $parent (@mod_list) { 504 for (@mod_list) { 505 next if $parent eq $_; 506 if (index ("$_/", "$parent/") == 0) { 507 $editor->{mod_lists}{$parent}{isdir} = 1; 508 last; 509 } 510 } 511 } 512 if (($self->{skip_to} && $self->{skip_to} <= $rev) || 513 grep { my $href = $editor->{mod_lists}{$_}; 514 !( ( ($href->{action} eq 'A' || $href->{action} eq 'R') 515 && ((defined $href->{local_rev} 516 && $href->{local_rev} != -1 517 && $href->{source_node_kind} == $SVN::Node::dir) 518 || ($href->{isdir}) 519 )) 520 || $href->{action} eq 'D' ) 521 } @mod_list ) { 522 my $pool = SVN::Pool->new_default_sub; 523 524 my $start = $fromrev || ($self->{skip_to} ? $fromrev : $rev-1); 525 my $reporter = 526 $ra->do_update ($rev, $editor->{target} || '', 1, $editor); 527 my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : (); 528 529 if ($fromrev == 0 || $start == 0) { 530 $reporter->set_path ('', $rev, 1, @lock); # start_empty 531 } 532 else { 533 $reporter->set_path ('', $start, 0, @lock); 534 } 535 536 $reporter->finish_report (); 537 } else { 538 # Copies only. Don't bother fetching full diff through network. 539 my $edit = SVN::Simple::Edit->new 540 (_editor => [$editor], 541 missing_handler => \&SVN::Simple::Edit::open_missing 542 ); 543 544 $edit->open_root ($self->{headrev}); 545 546 foreach (@mod_list) { 547 my $href = $editor->{mod_lists}{$_}; 548 my $action = $href->{action}; 549 550 if ($action eq 'D' || $action eq 'R') { 551 # XXX: bad pool usage here, but svn::simple::edit sucks 552 $edit->delete_entry($_); 553 } 554 555 # can't use a new pool for these, because we need to 556 # keep the parent. switch to svk dynamic editor when we can 557 if ($action eq 'A' || $action eq 'R') { 558 my $ret; 559 if (defined $href->{local_rev} && $href->{local_rev} != -1) { 560 $ret = $edit->copy_directory( $_, $href->{local_source_path}, 561 $href->{local_rev}); 562 } 563 else { 564 $ret = $edit->add_directory($_); 565 } 566 $edit->close_directory($_) if $ret; 567 } 568 } 569 $edit->close_edit (); 570 } 571 } 572 return if defined $self->{mirror}{skip_to} && 573 $self->{mirror}{skip_to} > $rev; 574 575 my $prop; 576 $prop = $ra->rev_proplist ($rev) if $self->{revprop}; 577 for (@{$self->{revprop}}) { 578 $self->{fs}->change_rev_prop($newrev, $_, $prop->{$_}) 579 if exists $prop->{$_}; 580 } 581} 582 583sub _relayed { $_[0]->{rsource} ne $_[0]->{source} } 584 585sub _debug_args { map { $_ = '' if !defined($_) } @_ } 586 587sub get_merge_back_editor { 588 my ($self, $path, $msg, $committed) = @_; 589 die "relayed merge back not supported yet" if $self->_relayed; 590 @{$self}{qw/cached_ra cached_ra_url/} = 591 ($self->_new_ra ( url => "$self->{source}$path"), "$self->{source}$path" ); 592 593 $self->{commit_ra} = $self->{cached_ra}; 594 $self->load_fromrev; 595 my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : (); 596 return ($self->{fromrev}, SVN::Delta::Editor->new 597 ($self->{cached_ra}->get_commit_editor ($msg, $committed, @lock))); 598} 599 600sub switch { 601 my ($self, $url) = @_; 602 my $ra = $self->_new_ra (url => $url); 603 # XXX: get proper uuid like init_state 604 die "uuid is different" unless $ra->get_uuid eq $self->{source_uuid}; 605 # warn "===> switching from $self->{source} to $url"; 606 # get a txn, change rsource and rsource_uuidto new url 607} 608 609sub get_latest_rev { 610 my ($self, $ra) = @_; 611 # don't care about real last-modified rev num unless in skip to mode. 612 return $ra->get_latest_revnum 613 unless $self->{skip_to}; 614 my ($rev, $headrev); 615 my $offset = 2; 616 617 # there were once get_log2, but it then was refactored by the svn_ra 618 # overhaul. We have to check the version. 619 # also, it's harmful to make use of the limited get_log for svn 1.2 620 # vs svnserve 1.1, it retrieves all logs and leave the connection 621 # in an inconsistent state. 622 if ($SVN::Core::VERSION ge '1.2.0' && $self->{rsource} !~ m/^svn/) { 623 $ra->get_log ([''], $ra->get_latest_revnum, 0, 1, 0, 1, 624 sub { $rev = $_[1] }); 625 } 626 else { 627 until (defined $rev) { 628 $headrev = $ra->get_latest_revnum 629 unless defined $headrev; 630 631 $headrev -= $offset; 632 $ra->get_log ([''], -1, $headrev, 633 ($SVN::Core::VERSION ge '1.2.0') ? (0) : (), 634 0, 1, 635 sub { $rev = $_[1] unless defined $rev}); 636 if ( $offset < $headrev ) { 637 $offset*=2; 638 } 639 else { 640 $offset = 2; 641 } 642 } 643 } 644 645 die 'fatal: unable to find last-modified revision' 646 unless defined $rev; 647 return $rev; 648} 649 650sub run { 651 my $self = shift; 652 my $ra = $self->_new_ra; 653 my $latestrev = $self->get_latest_rev ($ra); 654 655 $self->lock ('sync'); 656 $self->load_fromrev; 657 # there were code here to use find_local_rev, but it will get base that 658 # is too old for use, if there are relocate happening. 659 # but this might cause race condition, while we also have lock now, need 660 # to take a closer look. 661 $self->{headrev} = $self->{fs}->youngest_rev; 662 if ($self->{skip_to} && $self->{skip_to} =~ m/^HEAD(?:-(\d+))?/) { 663 $self->{skip_to} = $latestrev - ($1 || 0); 664 } 665 my $startrev = ($self->{skip_to} || 0); 666 $startrev = $self->{fromrev}+1 if $self->{fromrev}+1 > $startrev; 667 my $endrev = shift || -1; 668 if ($endrev && $endrev =~ m/^HEAD(?:-(\d+))?/) { 669 $endrev = $latestrev - ($1 || 0); 670 } 671 $endrev = $latestrev if $endrev == -1; 672 673 print "Syncing $self->{source}".($self->_relayed ? " via $self->{rsource}\n" : "\n"); 674 675 $self->unlock ('sync'), return 676 unless $endrev == -1 || $startrev <= $endrev; 677 678 print "Retrieving log information from $startrev to $endrev\n"; 679 680 my $firsttime = 1; 681 eval { 682 $ra->get_log ([''], $startrev, $endrev, 683 ($SVN::Core::VERSION ge '1.2.0') ? (0) : (), 684 1, 1, 685 sub { 686 my ($paths, $rev, $author, $date, $msg, $pool) = @_; 687 # for the first time, skip_to might not hit 688 # active revision in the tree. adjust to make it so. 689 if ($firsttime) { 690 $self->{skip_to} = $rev if defined $self->{skip_to}; 691 $firsttime = 0; 692 } 693 # move the anchor detection stuff to &mirror ? 694 if (defined $self->{skip_to} && $rev <= $self->{skip_to}) { 695 # XXX: get the logs for skipped changes 696 $self->{rev_incomplete} = 1; 697 $author = 'svm'; 698 $msg = sprintf('SVM: skipping changes %d-%d for %s', 699 $self->{fromrev}, $rev, $self->{rsource}); 700 } 701 else { 702 delete $self->{rev_incomplete}; 703 } 704 $self->mirror($self->{fromrev}, $paths, $rev, $author, 705 $date, $msg, $pool); 706 $self->{fromrev} = $rev; 707 }); 708 }; 709 710 delete $self->{cached_ra}; 711 delete $self->{cached_ra_url}; 712 713 $self->unlock ('sync'); 714 715 return unless $@; 716 if ($@ =~ /no item/) { 717 print "Mirror source already removed.\n"; 718 undef $@; 719 } 720 else { 721 die $@; 722 } 723} 724 725sub DESTROY { 726} 727 728package SVN::Mirror::Ra::NewMirrorEditor; 729our @ISA = ('SVN::Delta::Editor'); 730use strict; 731 732#use Smart::Comments '###', '####'; 733 734sub new { 735 my $class = shift; 736 my $self = $class->SUPER::new(@_); 737 return $self; 738} 739 740sub set_target_revision { 741 return; 742} 743 744# class method 745# _visited_path_item( $path, $pass_thru, $copied, $ref_mod ) 746# ref_mod is the reference item in mod_list. 747sub _visited_path_item { 748 return { path => shift, 749 pass_thru => shift, 750 copied => shift, 751 ref_mod => shift, 752 }; 753} 754 755# object method 756# visit_path( $path, @args ) 757# @args are as the same as _visited_path_item(). '-inherit' for 758# inheriting from the counterpart of the last path. 759# 760# Call this method whenever a directory is entered. 761sub visit_path { 762 my ($self, $path) = (shift, shift); 763 764 my $last = $self->{visited_paths}[-1]; 765 my @inherited = @$last{qw/pass_thru copied ref_mod/}; 766 my @args = map { my $o = shift @inherited; 767 ( ( $_ || '' ) eq '-inherit' ) ? $o : $_ 768 } @_; 769 push @{$self->{visited_paths}}, _visited_path_item( $path, @args ); 770 771 return $self; 772} 773 774# Call this method whenever a directory is left. 775sub leave_path { 776 my ($self) = @_; 777 778 my $last = $self->{visited_paths}[-1]; 779 pop @{$self->{visited_paths}}; 780 781 return $self; 782} 783 784sub is_pass_thru { $_[0]->{visited_paths}[-1]{pass_thru} } 785 786sub is_copied { $_[0]->{visited_paths}[-1]{copied} } 787 788sub _remove_entries_in_path { 789 my ($self, $path, $pb, $pool) = @_; 790 791 foreach ( sort grep $self->{mod_lists}{$_}{action} eq 'D', 792 keys %{$self->{mod_lists}} ) { 793 next unless m{^\Q$path\E/([^/]+)$}; 794 $self->delete_entry ($_, -1, $pb, $pool); 795 } 796} 797 798# Return undef if not in modified list, action otherwise. 799# 'A'dd, 'D'elete, 'R'eplace, 'M'odify 800sub _in_modified_list { 801 my ($self, $path) = @_; 802 803 if (exists $self->{mod_lists}{$path}) { 804 return $self->{mod_lists}{$path}{action}; 805 } else { 806 return; 807 } 808} 809 810# From source to target. Given a path what svn lib gives, get a path 811# where it should be. 812sub _translate_rel_path { 813 my ($self, $path) = @_; 814 815 if ( exists $self->{mod_lists}{$path} ) { 816 return $self->{mod_lists}{$path}{local_path}; 817 } else { 818 if ( $self->{anchor} ) { 819 $path = "$self->{anchor}/$path"; 820 $path =~ s|\Q$self->{mirror}{rsource_root}\E||; 821 } else { 822 $path = "$self->{mirror}{rsource_path}/$path"; 823 } 824 $path =~ s|^\Q$self->{mirror}{rsource_path}\E|$self->{mirror}{target_path}|; 825 return $path; 826 } 827 828} 829 830# If there's modifications under specified path, return true. 831sub _contains_mod_in_path { 832 my ($self, $path) = @_; 833 834 foreach ( reverse sort keys %{$self->{mod_lists}} ) { 835 return $self->{mod_lists}{$_} 836 if index ($_, $path, 0) == 0; 837 } 838 839 return; 840} 841 842# Given a path, return true if it is a copied path. 843sub _is_copy { 844 my ($self, $path) = @_; 845 846 return exists $self->{mod_lists}{$path} && 847 $self->{mod_lists}{$path}{remote_path}; 848} 849 850# Given a path, return source path and revision number in local depot. 851sub _get_copy_path_rev { 852 my ($self, $path) = @_; 853 854 return unless exists $self->{mod_lists}{$path}; 855 my ($cp_path, $cp_rev) = 856 @{$self->{mod_lists}{$path}}{qw/local_source_path local_rev/}; 857 return ($cp_path, $cp_rev); 858} 859 860sub open_root { 861 my ($self, $remoterev, $pool) =@_; 862 ### open_root()... 863 ### $remoterev 864 865 # {visited_paths} keeps track of visited paths. Parents at the 866 # beginning of array, and children the end. '' means '/'. $path 867 # passed to add_directory() and other methods are in the form of 868 # 'deep/path' instead of '/deep/path'. 869 $self->{visited_paths} = [ _visited_path_item( '', undef) ]; 870 871 $self->{root} = $self->SUPER::open_root($self->{mirror}{headrev}, $pool); 872} 873 874sub open_file { 875 my ($self,$path,$pb,undef,$pool) = @_; 876 ### open_file()... 877 ### $path 878 return undef unless $pb; 879 880 my $action = $self->_in_modified_list ($path); 881 ### Action for path is action... 882 ### $path 883 ### $action 884 if ( $self->is_pass_thru() && !$action ) { 885 #### Skip this file... 886 return undef; 887 } 888 if ( ($action || '') eq 'R' ) { 889 my $item = $self->{mod_lists}{$path}; 890 return $self->add_file($path, $pb, undef, -1, $pool) 891 unless defined $item->{remote_rev} xor defined $item->{local_rev}; 892 # If we are replacing with history and the source is out side 893 # of the mirror, assume assume a simple replace. Note that 894 # the server would send a delete+add if the source is actually 895 # unrelated. 896 } 897 898 ++$self->{changes}; 899 return $self->SUPER::open_file ($path, $pb, 900 $self->{mirror}{headrev}, $pool); 901} 902 903sub change_dir_prop { 904 my $self = shift; 905 my $baton = shift; 906 ### change_dir_prop()... 907 ### $_[0] 908 ### $_[1] 909 910 # filter wc specified stuff 911 return unless $baton; 912 return if $_[0] =~ /^svm:/; 913 return if $_[0] =~ /^svn:(?:entry|wc):/; 914 return $self->SUPER::change_dir_prop ($baton, @_) 915} 916 917sub change_file_prop { 918 my $self = shift; 919 ### change_file_prop()... 920 ### $_[1] 921 ### $_[2] 922 923 # filter wc specified stuff 924 return unless $_[0]; 925 return if $_[1] =~ /^svn:(?:entry|wc):/; 926 return $self->SUPER::change_file_prop (@_) 927} 928 929sub apply_textdelta { 930 my $self = shift; 931 return undef unless $_[0]; 932 ### apply_textdelta()... 933 ### $_[0] 934 935 $self->SUPER::apply_textdelta (@_); 936} 937 938sub open_directory { 939 my ($self,$path,$pb,undef,$pool) = @_; 940 ### open_directory()... 941 ### $path 942 return undef unless $pb; 943 944 if ( ($self->_in_modified_list($path) || '') eq 'R' ) { 945 ### Found an R item... 946 # if the path is replaced with history, from outside the 947 # mirror anchor... HATE 948 my $bogus_copy = $self->_is_copy($path) && !defined $self->{mod_lists}{$path}{local_source_path}; 949 if ($bogus_copy ) { 950 ##### Is a bogus... 951 $self->visit_path( $path, 952 0, 1, # copy but source not in local 953 $self->{mod_lists}{$path} ); 954 } 955 else { 956 ##### Call add_directory()... 957 return $self->add_directory($path, $pb, undef, -1, $pool); 958 } 959 } 960 961 my $dir_baton = $self->SUPER::open_directory ($path, $pb, 962 $self->{mirror}{headrev}, 963 $pool); 964 965 $self->visit_path( $path, '-inherit', '-inherit', '-inherit' ); 966 ### Visit info for this path: $self->{visited_paths}[-1] 967 968 if ($self->is_pass_thru()) { 969 ### Under latest copy, remove entries under path... 970 # $self->_enter_new_copied_path(); 971 $self->_remove_entries_in_path ($path, $dir_baton, $pool); 972 } 973 974 ++$self->{changes}; 975 return $dir_baton; 976} 977 978# Return an array of two elements: if pass thru and if copied. 979sub _visit_info_for_dir { 980 my $copyrev = shift; 981 982 if ( !defined($copyrev) ) { 983 ### Copy source is not in local depot... 984 return ( 0, 1 ); # copy but source not in local 985 } elsif ( $copyrev == -1 ) { 986 ### Usual action... 987 return ( 0, 0 ); # not a copy 988 } else { 989 ### Copy source is in local depot... 990 return ( 1, 1 ); # copy with source in local 991 } 992} 993 994=comment 995 996Please keep in mind that subversion's update editor is optimized for 997file system. That's why we need to keep many data in add_directory() 998because we need to deal with many different situations. 999 1000It means open_directory() or add_directory() (as well as counterparts 1001for files) is called depends on the existence of the target directory. 1002An add_diectory() call in a file system may be not necessary in a 1003repostiroy. If a directory is copied from another directory in a 1004repository, every directory or file under it needs a add_directory() 1005or add_file() call, but absolutely not necessary in another 1006repository, since they are brought automatically by the copy 1007operation. If some entries are deleted under the copied diectory, no 1008add_directory() and add_file() is necessary in a file system, but 1009explicit calls to delete_entry() are needed in a repository. 1010 1011=cut 1012 1013sub add_directory { 1014 my $self = shift; 1015 my $path = shift; 1016 my $pb = shift; 1017 my (undef,undef,$pool) = @_; 1018 my ($copypath, $copyrev) = $self->_get_copy_path_rev( $path ); 1019 my $crazy_replace; 1020 ### add_directory()... 1021 ### $path 1022 ### $copypath 1023 ### $copyrev 1024 return undef unless $pb; 1025 1026 # rules: 1027 # in mod_lists, not under copied path: 1028 # * A: add_directory() 1029 # * M: open_directory() 1030 # * R: delete_entry($path), add_directory() 1031 # under copied path, with local copy source: 1032 # * in mod_lists: 1033 # A: add_directory() 1034 # M: open_directory() 1035 # R: delete_entry($path), add_directory() 1036 # * not in mod_lists: 1037 # * Modifications in the path: 1038 # * open_directory(). 1039 # * No modification in the path: 1040 # * Ignore unconditionally. 1041 # under copied path, without local copy source: 1042 # ( add_directory() unconditionally ) 1043 1044 my $method = 'add_directory'; 1045 my $action = $self->_in_modified_list ($path); 1046 my $do_remove_items = undef; 1047 if (defined $self->{mirror}{skip_to} && 1048 $self->{mirror}{skip_to} >= $self->{mirror}{working}) { 1049 # no-op. 1050 } elsif ( $action ) { 1051 ### Change item. Action : $action 1052 my $item = $self->{mod_lists}{$path}; 1053 ### More info: $item 1054 1055 my @visit_info; 1056 if ( $action eq 'A' ) { 1057 ### Add a directory... 1058 @visit_info = _visit_info_for_dir( $copyrev ); 1059 if ( $visit_info[0] && $visit_info[1] ) { 1060 $do_remove_items = 1; 1061 splice (@_, 0, 2, $copypath, $copyrev); 1062 } 1063 push @visit_info, $item; 1064 } elsif ( $action eq 'M' ) { 1065 ### Modify a directory... 1066 $method = 'open_directory'; 1067 @visit_info = ( '-inherit', # as parent 1068 '-inherit', # as parent 1069 $item 1070 ); 1071 $do_remove_items = 1; 1072 } elsif ( $action eq 'R' ) { 1073 ### Replace a directory... 1074 $self->delete_entry ($path, 1075 $self->{mirror}{headrev}, 1076 $pb, $pool); 1077 1078 @visit_info = _visit_info_for_dir( $copyrev ); 1079 if ( $visit_info[0] && $visit_info[1] ) { 1080 $do_remove_items = 1; 1081 splice (@_, 0, 2, @$item{qw/local_source_path local_rev/}); 1082 } 1083 if ($copypath) { 1084 ++$crazy_replace; 1085 $visit_info[0] = 0; # don't pass thru for crazy replace 1086 } 1087 push @visit_info, $item; 1088 } 1089 $self->visit_path( $path, @visit_info ); 1090 } elsif ( $self->is_pass_thru() ) { 1091 ### Is pass thru... 1092 1093 # We are supposed to pass everything, but check if we have 1094 # modifications under current path. 1095 if ( (my $ref_mod = $self->_contains_mod_in_path ($path)) ) { 1096 ### Contains modifications under path... 1097 #### ref_mod : $ref_mod 1098 $do_remove_items = 1; 1099 if ( $ref_mod->{path} ne $path ) { $ref_mod = undef } 1100 $self->visit_path( $path, 1101 '-inherit', # should not pass thru 1102 '-inherit', # yes, copied 1103 $ref_mod # whatever previous node is 1104 ); 1105 $method = 'open_directory'; 1106 } else { 1107 ### No modifications under path. Bypass anything under it... 1108 return; 1109 } 1110 } elsif ( $self->is_copied() ) { 1111 ### Not pass thru, but is copied. Modifications under path... 1112 $method = 'open_directory'; 1113 $self->visit_path( $path, 1114 '-inherit', # pass_thru 1115 '-inherit', # copied 1116 '-inherit' # ref_mod 1117 ); 1118 } else { 1119 my $item = $self->{mod_lists}{$path}; 1120 ### path is not catched by conditionals... 1121 ### Action : $action 1122 ### Mod item : $item 1123 ### Last item in visited paths : $self->{visited_paths}[-1] 1124 $self->visit_path( $path, 1125 '-inherit', # pass_thru 1126 '-inherit', # copied 1127 '-inherit' # ref_mod 1128 ); 1129 } 1130 1131 ### Visit info for this path: $self->{visited_paths}[-1] 1132 1133 $method = "open_directory" if $path eq $self->{target}; 1134 my $tran_path = $self->_translate_rel_path ($path); 1135 $method = 'open_directory' 1136 if $tran_path eq $self->{mirror}{target_path}; 1137 1138 my $dir_baton; 1139 if ( $method eq 'open_directory' ) { 1140 my @args = @_; 1141 splice @args, 0, 2, $self->{mirror}{headrev}; 1142 $dir_baton = eval { 1143 $self->SUPER::open_directory ($tran_path, $pb, @args); 1144 }; 1145 if ( $@ ) { 1146 $dir_baton = $self->SUPER::add_directory ($tran_path, $pb, @_); 1147 } 1148 } else { 1149 $dir_baton = $self->SUPER::add_directory ($tran_path, $pb, @_); 1150 } 1151 1152 $self->_remove_entries_in_path ($path, $dir_baton, $pool) if $do_remove_items; 1153 1154 ++$self->{changes}; 1155 1156 if ($crazy_replace) { 1157 # When there's a replace with history, we need to replay the 1158 # diff between the base (which we reconstruct the replace 1159 # with) and the actual new revision. The problem is that 1160 # do_update gives us only the delta between our fromrev and 1161 # current rev, which is unusable if we are reconstructing the 1162 # copy. 1163 my $item = $self->{mod_lists}{$path}; 1164 my $remote_path = $item->{remote_path}; 1165 $remote_path =~ s/%/%25/g; 1166 my $ra = $self->{mirror}->_new_ra( url => "$self->{mirror}{source_root}$remote_path" ); 1167 my $compeditor = SVN::Mirror::Ra::CompositeEditor->new 1168 ( master_editor => $self, 1169 anchor => $path, anchor_baton => $dir_baton ); 1170 $path =~ s/%/%25/g; 1171 my ($reporter) = 1172 $ra->do_diff($self->{mirror}{working}, '', 1, 1, 1173 "$self->{mirror}{source}/$path", $compeditor); 1174 my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : (); 1175 $reporter->set_path('', $item->{remote_rev}, 0, @lock); 1176 $reporter->finish_report (); 1177 1178 $self->close_directory($dir_baton); 1179 return undef; 1180 } 1181 1182 return $dir_baton; 1183} 1184 1185sub add_file { 1186 my $self = shift; 1187 my $path = shift; 1188 my $pb = shift; 1189 my ($copypath, $copyrev) = $self->_get_copy_path_rev( $path ); 1190 ### add_file()... 1191 ### $path 1192 ### $copypath 1193 ### $copyrev 1194 return undef unless $pb; 1195 1196 my $method = 'add_file'; 1197 my $action = $self->_in_modified_list ($path); 1198 my $crazy_replace; 1199 1200 if ((defined $self->{mirror}{skip_to} 1201 && $self->{mirror}{skip_to} >= $self->{mirror}{working})) { 1202 ### Skiped... 1203 # no-op. add_file(). 1204 } elsif ( $action ) { 1205 ### With action: $action 1206 if ( !defined($copyrev) || $copyrev == -1) { 1207 # no-op 1208 } else { 1209 ### Come with a copy source. Use its information... 1210 ### $copypath 1211 ### $copyrev 1212 splice (@_, 0, 2, $copypath, $copyrev); 1213 } 1214 1215 if ($action eq 'M') { 1216 ### Modify... 1217 # splice @_, 0, 2, $self->{mirror}{headrev}; 1218 $method = 'open_file'; 1219 } elsif ($action eq 'R') { 1220 ### Replace... 1221 $self->delete_entry ($path, $self->{mirror}{headrev}, $pb, $_[-1]); 1222 if ($copypath) { 1223 ++$crazy_replace; 1224 } 1225 } 1226 } elsif ( $self->is_pass_thru() ) { 1227 ### Pass thru, and not in mod list. SKip it... 1228 return; 1229 } elsif ( $self->is_copied() ) { 1230 ### path is copied from somewhere. Accept it... 1231 # no-op. 1232 } else { 1233 my $item = $self->{mod_lists}{$path}; 1234 ### path is not catched by conditionals... 1235 ### Action : $action 1236 ### Mod item : $item 1237 ### Last item in visited paths : $self->{visited_paths}[-1] 1238 } 1239 1240 my $tran_path = $self->_translate_rel_path ($path); 1241 1242 # Why try open_file() first then add_file() later? I saw a weird 1243 # rev which looks like: 1244 # 1245 # A /path 1246 # A /path/foo 1247 # M /path/bar 1248 # A /path/baz 1249 # 1250 # /path/bar should be A because /path is A. Anyway, to accept 1251 # this rev, falling back to add_file() if open_file() fails will 1252 # do. 1253 # 1254 # - plasma 1255 ++$self->{changes}; 1256 if ($method eq 'open_file') { 1257 my @args = @_; 1258 splice @args, 0, 2, $self->{mirror}{headrev}; 1259 my $res = eval { 1260 $self->SUPER::open_file ($tran_path, $pb, @args); 1261 }; 1262 if (!$@) { return $res } 1263 } 1264 1265 my $file_baton = $self->SUPER::add_file ($tran_path, $pb, @_); 1266 1267 if ($crazy_replace) { 1268 my $item = $self->{mod_lists}{$path}; 1269 my $remote_path = $item->{remote_path}; 1270 $remote_path =~ s/%/%25/g; 1271 my ($anchor, $target) = "$self->{mirror}{rsource_root}$remote_path" =~ m{(.*)/([^/]+)}; 1272 my $ra = $self->{mirror}->_new_ra( url => $anchor ); 1273 my $compeditor = SVN::Mirror::Ra::CompositeEditor->new 1274 ( master_editor => $self, 1275 anchor => $path, anchor_baton => $pb, 1276 target => $target, target_baton => $file_baton ); 1277 1278 $path =~ s/%/%25/g; 1279 my ($reporter) = 1280 $ra->do_diff($self->{mirror}{working}, $target, 1, 1, 1281 "$self->{mirror}{rsource}/$path", $compeditor); 1282 my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : (); 1283 my ($tgt) = $path =~ m{([^/]+)$/}; 1284 $reporter->set_path('', $item->{remote_rev}, 0, @lock); 1285 $reporter->finish_report (); 1286 1287 $self->close_file($file_baton, undef); # XXX: md5 1288 1289 return undef; 1290 } 1291 1292 return $file_baton; 1293} 1294 1295sub close_directory { 1296 my $self = shift; 1297 my $baton = shift; 1298 ### close_directory()... 1299 ### $self->{visited_paths}[-1]{path} 1300 return unless $baton; 1301 1302 $self->leave_path(); 1303 1304 # 'touch' the root if there's no change. 1305 $self->change_dir_prop ( $baton, 'svm' => undef ) 1306 if $baton eq $self->{root} && !$self->{changes}; 1307 1308 $self->SUPER::close_directory ($baton, @_); 1309} 1310 1311sub close_file { 1312 my $self = shift; 1313 ### close_file()... 1314 return unless $_[0]; 1315 $self->SUPER::close_file(@_); 1316} 1317 1318sub delete_entry { 1319 my ($self, $path, $rev, $pb, $pool) = @_; 1320 ### delete_entry()... 1321 ### $path 1322 ### $rev 1323 return unless $pb; 1324 if ( $self->is_pass_thru() ) { 1325 my $action = $self->_in_modified_list($path) || ''; 1326 return unless $action eq 'D' || $action eq 'R'; 1327 } 1328 ++$self->{changes}; 1329 $self->SUPER::delete_entry ($path, $self->{mirror}{headrev}, 1330 $pb, $pool); 1331} 1332 1333sub close_edit { 1334 my ($self, $pool) = @_; 1335 ### close_edit()... 1336 1337 unless ($self->{root}) { 1338 # If we goes here, this must be an empty revision. We must 1339 # replicate an empty revision as well. 1340 $self->open_root ($self->{mirror}{headrev}, $pool); 1341 $self->SUPER::close_directory ($self->{root}, $pool); 1342 } 1343 delete $self->{root}; 1344 local $SIG{INT} = 'IGNORE'; 1345 local $SIG{TERM} = 'IGNORE'; 1346 1347 $self->{mirror}->lock ('mirror'); 1348 $self->SUPER::close_edit ($pool); 1349} 1350 1351package SVN::Mirror::Ra::CompositeEditor; 1352our @ISA = ('SVN::Delta::Editor'); 1353# XXX: this is from svk, should be merged 1354 1355sub AUTOLOAD { 1356 my ($self, @arg) = @_; 1357 my $func = our $AUTOLOAD; 1358 $func =~ s/^.*:://; 1359 1360 if ($func =~ m/^(?:add|open|delete)/) { 1361 return $self->{target_baton} 1362 if defined $self->{target} && $arg[0] eq $self->{target}; 1363 $arg[0] = length $arg[0] ? 1364 "$self->{anchor}/$arg[0]" : $self->{anchor}; 1365 } 1366 elsif ($func =~ m/^close_(?:file|directory)/) { 1367 if (defined $arg[0]) { 1368 return if $arg[0] eq $self->{anchor_baton}; 1369 return if defined $self->{target_baton} && 1370 $arg[0] eq $self->{target_baton}; 1371 } 1372 } 1373 1374 $self->{master_editor}->$func(@arg); 1375} 1376 1377sub set_target_revision {} 1378 1379sub open_root { 1380 my ($self, $base_revision) = @_; 1381 return $self->{anchor_baton}; 1382} 1383 1384sub close_edit {} 1385 13861; 1387