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