Hi, On Wed, Mar 22, 2006, Marcelo Matus wrote: > How are you getting this file > SVN/Mirror/Ra.pm bee% dlocate SVN/Mirror/Ra.pm libsvn-mirror-perl: /usr/share/perl5/SVN/Mirror/Ra.pm
I attach the file, but you might want to check the Debian source package (you can grab it from packages.debian.org or from ftp.debian.org as two files: upstream tarball and Debian patch). > it seems subversion generates one, but it is only 214 lines long. Mine is 1261 lines long, that's a big difference. > also, this call: > $reporter->set_path ('', $start, @lock, 0); > is no where to be found in the *.pm files generated by subversion-1.3.0. Perhaps this is the problem then. > Are you sure you don't have a PATH variable set such as is using > and old version of Ra.pm? I doubt it's related to PATH, I only have the Debian package's version of the file, and inserting print statements results in them being printed. > Anyway, it fails calling 'set_path' > obj->set_path(report_baton, path, revision, start_empty, lock_token, > pool); > where the '6' one refers to 'lock_token', is @lock realy a string parameter, > could you print it out before the call?. Sure, I added a: warn "foo"[EMAIL PROTECTED]; before the set_path call, and it resulted in: Retrieving log information from 241 to 241 foo1 at /usr/share/perl5/SVN/Mirror/Ra.pm line 525. Bye, -- Loïc Minier <[EMAIL PROTECTED]> Current Earth status: NOT DESTROYED
package SVN::Mirror::Ra; @ISA = ('SVN::Mirror'); $VERSION = '0.68'; use strict; use SVN::Core; use SVN::Repos; use SVN::Fs; use SVN::Delta; use SVN::Ra; use SVN::Client (); use constant OK => $SVN::_Core::SVN_NO_ERROR; sub new { my $class = shift; my $self = bless {}, $class; %$self = @_; $self->{source} =~ s{/+$}{}g; @{$self}{qw/source source_root source_path/} = _parse_source ($self->{source}); @{$self}{qw/rsource rsource_root rsource_path/} = _parse_source ($self->{rsource}) if $self->{rsource}; return $self; } sub _parse_source { my $source = shift; my ($root, $path) = split ('!', $source, 2); $path ||= ''; return (join('', $root, $path), $root, $path) } sub _store_source { my ($root, $path) = @_; return join('!', $root, $path); } sub _get_prop { my ($self, $ra, $path, $propname) = @_; } sub _is_descendent { my ($parent, $child) = @_; return 1 if $parent eq $child; $parent = "$parent/" unless $parent eq '/'; return $parent eq substr ($child, 0, length ($parent)); } sub _check_overlap { my ($self) = @_; my $fs = $self->{repos}->fs; my $root = $fs->revision_root ($fs->youngest_rev); for (map {$root->node_prop ($_, 'svm:source')} SVN::Mirror::list_mirror ($self->{repos})) { my (undef, $source_root, $source_path) = _parse_source ($_); next if $source_root ne $self->{source_root}; die "Mirroring overlapping paths not supported\n" if _is_descendent ($source_path, $self->{source_path}) || _is_descendent ($self->{source_path}, $source_path); } } sub init_state { my ($self, $txn) = @_; my $ra = $self->_new_ra (url => $self->{source}); my $uuid = $self->{source_uuid} = $ra->get_uuid (); my $source_root = $ra->get_repos_root (); my $path = $self->{source}; $txn->abort, die "source url not under source root" if substr($path, 0, length($source_root), '') ne $source_root; $self->{source_root} = $source_root; $self->{source_path} = $path; $self->{fromrev} = 0; # XXX: abort txn before dying $self->_check_overlap; # check if the url exists if ($ra->check_path ('', -1) != $SVN::Node::dir) { $txn->abort; die "$self->{source} is not a directory.\n"; } unless ($self->{source} eq $self->{source_root}) { undef $ra; # bizzare perlgc $ra = $self->_new_ra (url => $self->{source_root}); } # check if mirror source is already a mirror # older SVN::RA will return Reporter so prop would be undef my (undef, undef, $prop) = $ra->get_dir ('', -1); if ($prop && $prop->{'svm:mirror'}) { my $rroot; for ($prop->{'svm:mirror'} =~ m/^.*$/mg) { if (_is_descendent ($_, $self->{source_path})) { $rroot = $_; last; } elsif (_is_descendent ($self->{source_path}, $_)) { $txn->abort, die "Can't relay mirror outside mirror anchor $_"; } } if ($rroot) { $rroot =~ s|^/||; (undef, undef, $prop) = $ra->get_dir ($rroot, -1); $txn->abort, die "relayed mirror source doesn't not have svm:source" unless exists $prop->{'svm:source'}; @{$self}{qw/rsource rsource_root rsource_path/} = @{$self}{qw/source source_root source_path/}; $self->{rsource_uuid} = $uuid; $self->{source_path} =~ s|^/\Q$rroot\E||; @{$self}{qw/source source_uuid/} = @{$prop}{qw/svm:source svm:uuid/}; $self->{source} .= '!' if index ($self->{source}, '!') == -1; @{$self}{qw/source source_root source_path/} = _parse_source ($self->{source}.$self->{source_path}); $txn->abort, die "relayed source and source have same repository uuid" if $self->{source_uuid} eq $self->{rsource_uuid}; my $txnroot = $txn->root; $txnroot->change_node_prop ($self->{target_path}, 'svm:rsource', _store_source ($source_root, $path)); $txnroot->change_node_prop ($self->{target_path}, 'svm:ruuid', $uuid); $txn->change_prop ("svm:headrev", "$self->{rsource_uuid}:$self->{fromrev}\n"); return _store_source ($self->{source_root}, $self->{source_path}); } } @{$self}{qw/rsource rsource_root rsource_path/} = @{$self}{qw/source source_root source_path/}; $self->{rsource_uuid} = $self->{source_uuid}; $txn->change_prop ("svm:headrev", "$self->{rsource_uuid}:$self->{fromrev}\n"); return _store_source ($source_root, $path); } sub load_state { my ($self) = @_; my $prop = $self->{root}->node_proplist ($self->{target_path}); @{$self}{qw/source_uuid rsource_uuid/} = @{$prop}{qw/svm:uuid svm:ruuid/}; unless ($self->{rsource}) { @{$self}{qw/rsource rsource_root rsource_path/} = @{$self}{qw/source source_root source_path/}; $self->{rsource_uuid} = $self->{source_uuid}; } die "please upgrade the mirror state\n" if $self->{root}->node_prop ('/', join (':', 'svm:mirror', $self->{source_uuid}, $self->{source_path} || '/')); unless ($self->{ignore_lock}) { die "no headrev" unless defined $self->load_fromrev; } return; } sub _new_ra { my ($self, %arg) = @_; $self->{config} ||= SVN::Core::config_get_config (undef, $self->{pool}); $self->{auth} ||= $self->_new_auth; SVN::Ra->new( url => $self->{rsource}, auth => $self->{auth}, config => $self->{config}, %arg); } sub _new_auth { my ($self) = @_; # create a subpool that is not automatically destroyed my $pool = SVN::Pool::create (${$self->{pool}}); $pool->default; my ($baton, $ref) = SVN::Core::auth_open_helper([ SVN::Client::get_simple_provider (), SVN::Client::get_ssl_server_trust_file_provider (), SVN::Client::get_username_provider (), SVN::Client::get_simple_prompt_provider( $self->can('_simple_prompt'), 2), SVN::Client::get_ssl_server_trust_prompt_provider( $self->can('_ssl_server_trust_prompt') ), SVN::Client::get_ssl_client_cert_prompt_provider( $self->can('_ssl_client_cert_prompt'), 2 ), SVN::Client::get_ssl_client_cert_pw_prompt_provider( $self->can('_ssl_client_cert_pw_prompt'), 2 ), SVN::Client::get_username_prompt_provider( $self->can('_username_prompt'), 2), ]); $self->{auth_ref} = $ref; return $baton; } sub _simple_prompt { my ($cred, $realm, $default_username, $may_save, $pool) = @_; if (defined $default_username and length $default_username) { print "Authentication realm: $realm\n" if defined $realm and length $realm; $cred->username($default_username); } else { _username_prompt($cred, $realm, $may_save, $pool); } $cred->password(_read_password("Password for '" . $cred->username . "': ")); $cred->may_save($may_save); return OK; } sub _ssl_server_trust_prompt { my ($cred, $realm, $failures, $cert_info, $may_save, $pool) = @_; print "Error validating server certificate for '$realm':\n"; print " - The certificate is not issued by a trusted authority. Use the\n", " fingerprint to validate the certificate manually!\n" if ($failures & $SVN::Auth::SSL::UNKNOWNCA); print " - The certificate hostname does not match.\n" if ($failures & $SVN::Auth::SSL::CNMISMATCH); print " - The certificate is not yet valid.\n" if ($failures & $SVN::Auth::SSL::NOTYETVALID); print " - The certificate has expired.\n" if ($failures & $SVN::Auth::SSL::EXPIRED); print " - The certificate has an unknown error.\n" if ($failures & $SVN::Auth::SSL::OTHER); printf( "Certificate information:\n". " - Hostname: %s\n". " - Valid: from %s until %s\n". " - Issuer: %s\n". " - Fingerprint: %s\n", map $cert_info->$_, qw(hostname valid_from valid_until issuer_dname fingerprint) ); print( $may_save ? "(R)eject, accept (t)emporarily or accept (p)ermanently? " : "(R)eject or accept (t)emporarily? " ); my $choice = lc(substr(<STDIN> || 'R', 0, 1)); if ($choice eq 't') { $cred->may_save(0); $cred->accepted_failures($failures); } elsif ($may_save and $choice eq 'p') { $cred->may_save(1); $cred->accepted_failures($failures); } return OK; } sub _ssl_client_cert_prompt { my ($cred, $realm, $may_save, $pool) = @_; print "Client certificate filename: "; chomp(my $filename = <STDIN>); $cred->cert_file($filename); return OK; } sub _ssl_client_cert_pw_prompt { my ($cred, $realm, $may_save, $pool) = @_; $cred->password(_read_password("Passphrase for '%s': ")); return OK; } sub _username_prompt { my ($cred, $realm, $may_save, $pool) = @_; print "Authentication realm: $realm\n" if defined $realm and length $realm; print "Username: "; chomp(my $username = <STDIN>); $username = '' unless defined $username; $cred->username($username); return OK; } sub _read_password { my ($prompt) = @_; print $prompt; require Term::ReadKey; Term::ReadKey::ReadMode('noecho'); my $password = ''; while (defined(my $key = Term::ReadKey::ReadKey(0))) { last if $key =~ /[\012\015]/; $password .= $key; } Term::ReadKey::ReadMode('restore'); print "\n"; return $password; } sub _revmap { my ($self, $rev, $ra) = @_; $ra ||= $self->{cached_ra}; $SVN::Core::VERSION ge '1.1.0' ? $ra->rev_prop ($rev, 'svm:headrev') : $ra->rev_proplist ($rev)->{'svm:headrev'}; } sub committed { my ($self, $revmap, $date, $sourcerev, $rev) = @_; $self->{fs}->change_rev_prop($rev, 'svn:date', $date); # sync remote headrev too $self->{fs}->change_rev_prop($rev, 'svm:headrev', $revmap."$self->{rsource_uuid}:$sourcerev\n"); $self->{fs}->change_rev_prop($rev, 'svm:incomplete', '*') if $self->{rev_incomplete}; $self->{headrev} = $rev; $self->unlock ('mirror'); print "Committed revision $rev from revision $sourcerev.\n"; } our $debug; sub mirror { my ($self, $fromrev, $paths, $rev, $author, $date, $msg, $ppool) = @_; my $ra; if ($debug and eval { require BSD::Resource; 1 }) { my ($usertime, $systemtime, $maxrss, $ixrss, $idrss, $isrss, $minflt, $majflt, $nswap, $inblock, $oublock, $msgsnd, $msgrcv, $nsignals, $nvcsw, $nivcsw) = BSD::Resource::getrusage(); print ">>> mirroring $rev:\n"; print ">>> $usertime $systemtime $maxrss $ixrss $idrss $isrss\n"; } my $pool = SVN::Pool->new_default ($ppool); my ($newrev, $revmap); $ra = $self->{cached_ra} if exists $self->{cached_ra_url} && $self->{cached_ra_url} eq $self->{rsource}; if ($ra && $self->{rsource} =~ m/^http/ && --$self->{cached_life} == 0) { undef $ra; } $ra ||= $self->_new_ra; $revmap = $self->_revmap ($rev, $ra) if $self->_relayed; $revmap ||= ''; my $editor = SVN::Mirror::Ra::MirrorEditor->new ($self->{repos}->get_commit_editor ('', $self->{target_path}, $author, $msg, sub { $newrev = $_[0]; $self->committed ($revmap, $date, $rev, @_) })); $self->{working} = $rev; $editor->{mirror} = $self; @{$self}{qw/cached_ra cached_ra_url/} = ($ra, $self->{rsource}); if ( ( $fromrev == 0 # WTF do we need to check this? # || !(defined $fromrev && $self->find_local_rev($fromrev, $self->{rsource_uuid})) ) && $self->{rsource} ne $self->{rsource_root} ) { (undef, $editor->{anchor}, $editor->{target}) = File::Spec::Unix->splitpath($editor->{anchor} || $self->{rsource}); chop $editor->{anchor}; $ra = $self->_new_ra ( url => $editor->{anchor} ); undef $self->{cached_ra}; # bizzare perlgc @{$self}{qw/cached_ra cached_ra_url/} = ($ra, $editor->{anchor}); } $self->{cached_life} ||= 100; $editor->{target} ||= '' if $SVN::Core::VERSION gt '0.36.0'; =begin NOTES The structure of mod_lists: * Key is the path of a changed path, a relative path to source_path. This is what methods in MirrorEditor get its path, therefore easier for them to look up information. * Value is a hash, containing the following values: * action: 'A'dd, 'M'odify, 'D'elete, 'R'eplace * remote_path: The path on remote depot * remote_rev: The revision on remote depot * local_rev: * Not Add: -1 * Add but source is not in local depot: undef * Add and source is in local depot: the source revision in local depot * local_path: The mapped path of key, ie. the changed path, in local depot. * local_source_path: * Source path is not in local depot: undef * Source path is in local depot: a string * source_node_kind: Only meaningful if action is 'A'. =cut $editor->{mod_lists} = {}; foreach ( keys %$paths ) { my $spool = SVN::Pool->new_default; my $item = $paths->{$_}; s/\n/ /g; # XXX: strange edge case my $href; my $svn_lpath = my $local_path = $_; if ( $editor->{anchor} ) { $svn_lpath = $self->{rsource_root} . $svn_lpath; $svn_lpath =~ s|^\Q$editor->{anchor}\E/?||; my $source_path = $self->{rsource_path} || "/"; $local_path =~ s|^\Q$source_path\E|$self->{target_path}|; } else { $svn_lpath =~ s|^\Q$self->{rsource_path}\E/?||; $local_path = "$self->{target_path}/$svn_lpath"; } my $local_rev = -1; unless ($item->copyfrom_rev == -1) { $local_rev = $self->find_local_rev ($item->copyfrom_rev, $self->{rsource_uuid}); } # XXX: the logic of the code here is a mess! my ($action, $rpath, $rrev, $lrev) = @$href{qw/action remote_path remote_rev local_rev local_path/} = ( $item->action, $item->copyfrom_path, $item->copyfrom_rev, $local_rev, $local_path, ); # workaround fsfs remoet_path inconsistencies $rpath = "/$rpath" if $rpath && substr ($rpath, 0, 1) ne '/'; my ($src_lpath, $source_node_kind) = (undef, $SVN::Node::unknown); # XXX: should check if the copy is within the anchor before resolving lrev if ( defined $lrev && $lrev != -1 ) { $src_lpath = $rpath; # copy within mirror anchor if ($src_lpath =~ s|^\Q$self->{rsource_path}\E/|$self->{target_path}/|) { # $source_node_kind is used for deciding if we need reporter later my $rev_root = $self->{fs}->revision_root ($lrev); $source_node_kind = $rev_root->check_path ($src_lpath); } else { ($src_lpath, $href->{local_rev}) = (undef, undef); } } elsif ($rrev != -1) { # The source is not in local depot. Invalidate this # copy. ($src_lpath, $href->{local_rev}) = $self->{cb_copy_notify} ? $self->{cb_copy_notify}->($self, $local_path, $rpath, $rrev) : (undef, undef) } @$href{qw/local_source_path source_node_kind/} = ( $src_lpath, $source_node_kind ); # XXX: the loop should not reached here if changed path is # not interesting to us, skip them at the beginning the the loop if ( $_ eq $self->{rsource_path} or index ("$_/", "$self->{rsource_path}/") == 0 ) { $editor->{mod_lists}{$svn_lpath} = $href; } elsif ($rrev != -1 && $href->{action} eq 'A' && index ($self->{rsource_path}, "$_/") == 0) { # special case for the parent of the anchor is copied. my $reanchor = $self->{rsource_path}; $reanchor =~ s{^\Q$_\E/}{}; $href->{remote_path} .= '/'.$reanchor; $href->{local_path} = $self->{target_path}; $editor->{mod_lists}{length $svn_lpath ? "$svn_lpath/$reanchor" : $reanchor} = $href; } } unless (keys %{$editor->{mod_lists}}) { my $root = $editor->open_root($self->{headrev}); $editor->change_dir_prop ($root, svm => undef); $editor->close_directory($root); $editor->close_edit; } else { my @mod_list = sort keys %{$editor->{mod_lists}}; # mark item as directory that we are sure about. # do not use !isdir for deciding the item is _not_ a directory. for my $parent (@mod_list) { for (@mod_list) { next if $parent eq $_; if (index ("$_/", "$parent/") == 0) { $editor->{mod_lists}{$parent}{isdir} = 1; last; } } } if ( ($self->{skip_to} && $self->{skip_to} <= $rev) || grep { my $href = $editor->{mod_lists}{$_}; !( ( ($href->{action} eq 'A' || $href->{action} eq 'R') && ((defined $href->{local_rev} && $href->{local_rev} != -1 && $href->{source_node_kind} == $SVN::Node::dir) || ($href->{isdir}) )) || $href->{action} eq 'D' ) } @mod_list ) { my $pool = SVN::Pool->new_default_sub; my $start = $fromrev || ($self->{skip_to} ? $fromrev : $rev-1); my $reporter = $ra->do_update ($rev, $editor->{target} || '', 1, $editor); my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : (); # XXX warn "pouet"[EMAIL PROTECTED]; $reporter->set_path ('', $start, @lock, 0); $reporter->finish_report (); } else { # Copies only. Don't bother fetching full diff through network. my $edit = SVN::Simple::Edit->new (_editor => [$editor], missing_handler => \&SVN::Simple::Edit::open_missing ); $edit->open_root ($self->{headrev}); foreach (@mod_list) { my $href = $editor->{mod_lists}{$_}; my $action = $href->{action}; if ($action eq 'D' || $action eq 'R') { # XXX: bad pool usage here, but svn::simple::edit sucks $edit->delete_entry($_); } # can't use a new pool for these, because we need to # keep the parent. switch to svk dynamic editor when we can if ($action eq 'A' || $action eq 'R') { if (defined $href->{local_rev} && $href->{local_rev} != -1) { $edit->copy_directory ($_, $href->{local_source_path}, $href->{local_rev}); } else { $edit->add_directory($_); } $edit->close_directory ($_); } } $edit->close_edit (); } } return if defined $self->{mirror}{skip_to} && $self->{mirror}{skip_to} > $rev; my $prop; $prop = $ra->rev_proplist ($rev) if $self->{revprop}; for (@{$self->{revprop}}) { $self->{fs}->change_rev_prop($newrev, $_, $prop->{$_}) if exists $prop->{$_}; } } sub _relayed { $_[0]->{rsource} ne $_[0]->{source} } sub get_merge_back_editor { my ($self, $path, $msg, $committed) = @_; die "relayed merge back not supported yet" if $self->_relayed; @{$self}{qw/cached_ra cached_ra_url/} = ($self->_new_ra ( url => "$self->{source}$path"), "$self->{source}$path" ); $self->{commit_ra} = $self->{cached_ra}; $self->load_fromrev; my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : (); return ($self->{fromrev}, SVN::Delta::Editor->new ($self->{cached_ra}->get_commit_editor ($msg, $committed, @lock))); } sub switch { my ($self, $url) = @_; my $ra = $self->_new_ra (url => $url); # XXX: get proper uuid like init_state die "uuid is different" unless $ra->get_uuid eq $self->{source_uuid}; # warn "===> switching from $self->{source} to $url"; # get a txn, change rsource and rsource_uuidto new url } sub get_latest_rev { my ($self, $ra) = @_; # don't care about real last-modified rev num unless in skip to mode. return $ra->get_latest_revnum unless $self->{skip_to}; my ($rev, $headrev); my $offset = 2; until (defined $rev) { # there were once get_log2, but it then was refactored by the svn_ra # overhaul. We have to check the version. # also, it's harmful to make use of the limited get_log for svn 1.2 # vs svnserve 1.1, it retrieves all logs and leave the connection # in an inconsistent state. if ($SVN::Core::VERSION ge '1.2.0' && $self->{rsource} !~ m/^svn/) { $ra->get_log ([''], -1, 1, 1, 0, 1, sub { $rev = $_[1] }); } else { $headrev = $ra->get_latest_revnum unless defined $headrev; $headrev -= $offset; $ra->get_log ([''], -1, $headrev, ($SVN::Core::VERSION ge '1.2.0') ? (0) : (), 0, 1, sub { $rev = $_[1] unless defined $rev}); if ( $offset < $headrev ) { $offset*=2; } else { $offset = 2; } } } die 'fatal: unable to find last-modified revision' unless defined $rev; return $rev; } sub run { my $self = shift; my $ra = $self->_new_ra; my $latestrev = $self->get_latest_rev ($ra); $self->lock ('sync'); $self->load_fromrev; # there were code here to use find_local_rev, but it will get base that # is too old for use, if there are relocate happening. # but this might cause race condition, while we also have lock now, need # to take a closer look. $self->{headrev} = $self->{fs}->youngest_rev; if ($self->{skip_to} && $self->{skip_to} =~ m/^HEAD(?:-(\d+))?/) { $self->{skip_to} = $latestrev - ($1 || 0); } my $startrev = ($self->{skip_to} || 0); $startrev = $self->{fromrev}+1 if $self->{fromrev}+1 > $startrev; my $endrev = shift || -1; if ($endrev && $endrev =~ m/^HEAD(?:-(\d+))?/) { $endrev = $latestrev - ($1 || 0); } $endrev = $latestrev if $endrev == -1; print "Syncing $self->{source}".($self->_relayed ? " via $self->{rsource}\n" : "\n"); $self->unlock ('sync'), return unless $endrev == -1 || $startrev <= $endrev; print "Retrieving log information from $startrev to $endrev\n"; my $firsttime = 1; eval { $ra->get_log ([''], $startrev, $endrev, ($SVN::Core::VERSION ge '1.2.0') ? (0) : (), 1, 1, sub { my ($paths, $rev, $author, $date, $msg, $pool) = @_; # for the first time, skip_to might not hit # active revision in the tree. adjust to make it so. if ($firsttime) { $self->{skip_to} = $rev if defined $self->{skip_to}; $firsttime = 0; } # move the anchor detection stuff to &mirror ? if (defined $self->{skip_to} && $rev <= $self->{skip_to}) { # XXX: get the logs for skipped changes $self->{rev_incomplete} = 1; $author = 'svm'; $msg = sprintf('SVM: skipping changes %d-%d for %s', $self->{fromrev}, $rev, $self->{rsource}); } else { delete $self->{rev_incomplete}; } $self->mirror($self->{fromrev}, $paths, $rev, $author, $date, $msg, $pool); $self->{fromrev} = $rev; }); }; delete $self->{cached_ra}; delete $self->{cached_ra_url}; $self->unlock ('sync'); return unless $@; if ($@ =~ /no item/) { print "Mirror source already removed.\n"; undef $@; } else { die $@; } } sub DESTROY { } package SVN::Mirror::Ra::MirrorEditor; our @ISA = ('SVN::Delta::Editor'); use strict; our $debug = 0; =begin NOTES 1. The path passed to methods in MirrorEditor is a relative path under $self->{anchor} (if exists) or $self->{mirror}{source_root}. 2. MirrorEditor can fetch usable information in $self->{mod_lists} if exists. 3. If we need to call other method in MirrorEditor, the path must be in the style of #1 above. 4. The diff text passed through network is patch-like. If a directory is copied, there needs to be one delete_entry() call for the original directory, then a LOT of add_directory() and add_file() calls for each one of directories and files underneath the new directory. This behavior is easy to handle for a real file system, but hard to work correct for another subversion repository. A lot of code below, especially in add_directory() and add_file(), are specialised for handling different conditions. =cut sub new { my $class = shift; my $self = $class->SUPER::new(@_); return $self; } sub set_target_revision { return; } sub open_root { my ($self, $remoterev, $pool) [EMAIL PROTECTED]; print "MirrorEditor::open_root($remoterev)\n" if $debug; # {copied_paths} stores paths that are copied. Because there # might be copied paths beneath another one, we need it to be an # array. $self->{copied_paths} = [ ]; # {visited_paths} keeps track of visited paths. Parents at the # beginning of array, and children the end. '' means '/'. $path # passed to add_directory() and other methods are in the form of # 'deep/path' instead of '/deep/path'. $self->{visited_paths} = [ '' ]; $self->{root} = $self->SUPER::open_root($self->{mirror}{headrev}, $pool); } sub open_directory { my ($self,$path,$pb,undef,$pool) = @_; print "MirrorEditor::open_directory($path)\n" if $debug; return undef unless $pb; if ( ($self->_in_modified_list($path) || '') eq 'R' ) { # if the path is replaced with history, from outside the # mirror anchor... HATE my $bogus_copy = $self->_is_copy($path) && !defined $self->{mod_lists}{$path}{local_source_path}; if ($bogus_copy ) { $self->_create_new_copied_path ($path); } else { return $self->add_directory($path, $pb, undef, -1, $pool); } } my $dir_baton = $self->SUPER::open_directory ($path, $pb, $self->{mirror}{headrev}, $pool); push @{$self->{visited_paths}}, $path; if ($self->_under_latest_copypath($path)) { $self->_enter_new_copied_path(); $self->_remove_entries_in_path ($path, $dir_baton, $pool); } return $dir_baton; } sub open_file { my ($self,$path,$pb,undef,$pool) = @_; print "MirrorEditor::open_file($path)\n" if $debug; return undef unless $pb; my $action = $self->_in_modified_list ($path); if ($self->_under_latest_copypath ($path) && $self->_is_under_true_copy ($path) && !$action) { return undef; } $self->{opening} = $path; return $self->SUPER::open_file ($path, $pb, $self->{mirror}{headrev}, $pool); } sub change_dir_prop { my $self = shift; my $baton = shift; print "MirrorEditor::change_dir_prop($_[0], $_[1])\n" if $debug; # filter wc specified stuff return unless $baton; return if $_[0] =~ /^svm:/; return $self->SUPER::change_dir_prop ($baton, @_) unless $_[0] =~ /^svn:(?:entry|wc):/; } sub change_file_prop { my $self = shift; print "MirrorEditor::change_file_prop($_[1], $_[2])\n" if $debug; # filter wc specified stuff return unless $_[0]; return $self->SUPER::change_file_prop (@_) unless $_[1] =~ /^svn:(?:entry|wc):/; } # From source to target. Given a path what svn lib gives, get a path # where it should be. sub _translate_rel_path { my ($self, $path) = @_; if ( exists $self->{mod_lists}{$path} ) { return $self->{mod_lists}{$path}{local_path}; } else { if ( $self->{anchor} ) { $path = "$self->{anchor}/$path"; $path =~ s|\Q$self->{mirror}{rsource_root}\E||; } else { $path = "$self->{mirror}{rsource_path}/$path"; } $path =~ s|^\Q$self->{mirror}{rsource_path}\E|$self->{mirror}{target_path}|; return $path; } } sub _remove_entries_in_path { my ($self, $path, $pb, $pool) = @_; foreach ( sort grep $self->{mod_lists}{$_}{action} eq 'D', keys %{$self->{mod_lists}} ) { next unless m{^\Q$path\E/([^/]+)$}; $self->delete_entry ($_, -1, $pb, $pool); } } # If there's modifications under specified path, return true. sub _contains_mod_in_path { my ($self, $path) = @_; foreach ( reverse sort keys %{$self->{mod_lists}} ) { return $_ if index ($_, $path, 0) == 0; } return; } # Given a path, return true if it is a copied path. sub _is_copy { my ($self, $path) = @_; return exists $self->{mod_lists}{$path} && $self->{mod_lists}{$path}{remote_path}; } # Given a path, return source path and revision number in local depot. sub _get_copy_path_rev { my ($self, $path) = @_; return unless exists $self->{mod_lists}{$path}; my ($cp_path, $cp_rev) = @{$self->{mod_lists}{$path}}{qw/local_source_path local_rev/}; use URI::Escape; return (uri_escape($cp_path), $cp_rev); } # Given a path, return true if it's under the lastest visited copied path. sub _under_latest_copypath { my ($self, $path) = @_; return (@{$self->{copied_paths}} && index ($path, $self->{copied_paths}[-1]{path}, 0) == 0); } # Return undef if not in modified list, action otherwise. # 'A'dd, 'D'elete, 'R'eplace, 'M'odify sub _in_modified_list { my ($self, $path) = @_; if (exists $self->{mod_lists}{$path}) { return $self->{mod_lists}{$path}{action}; } else { return; } } sub _is_under_copy { return scalar @{$_[0]->{copied_paths}} } # Given a path, return true if the the path is under a copied path # which has a valid source in local repo. Otherwise return false. sub _is_under_true_copy { my ($self, $path) = @_; return unless scalar @{$self->{copied_paths}}; $path = $self->{copied_paths}[-1]{path}; return $self->{mod_lists}{$path}{local_rev}; } # Given a path, return true if the the path is under a copied path # which has no valid source in local repo. Otherwise return false. sub _is_under_false_copy { my ($self, $path) = @_; return unless scalar @{$self->{copied_paths}}; $path = $self->{copied_paths}[-1]{path}; return !defined ($self->{mod_lists}{$path}{local_source_path}); } # The following three methods are used to keep tracks of copied paths. # _create_new_copied_path($path): call this when you are ready to enter # a new copied path. # _enter_new_copied_path(): call this when enter a directory under a # copied path. Use _is_under_true_copy() to see if it is true. # _leave_new_copied_path(): call this when leave a directory under a # copied path. Use _is_under_true_copy() to see if it is true. sub _create_new_copied_path { my $self = shift; my $path = shift; push @{$self->{copied_paths}}, { path => $path, child_depth => 0 }; } sub _enter_new_copied_path { $_[0]->{copied_paths}[-1]{child_depth}++ } sub _leave_new_copied_path { my $self = shift; if ($self->{copied_paths}[-1]{child_depth} == 0) { pop @{$self->{copied_paths}}; } else { $self->{copied_paths}[-1]{child_depth}--; } } sub add_directory { my $self = shift; my $path = shift; my $pb = shift; my ($cp_path,$cp_rev,$pool) = @_; if ($debug) { my ($cp_path, $cp_rev) = $self->_get_copy_path_rev( $path ); $cp_path = "" unless defined $cp_path; $cp_rev = "" unless defined $cp_rev; print "MirrorEditor::add_directory($path, $cp_path, $cp_rev)\n"; } return undef unless $pb; # rules: # in mod_lists, not under copied path: # * A: add_directory() # * M: open_directory() # * R: delete_entry($path), add_directory() # under copied path, with local copy source: # * in mod_lists: # A: add_directory() # M: open_directory() # R: delete_entry($path), add_directory() # * not in mod_lists: # * Modifications in the path: # * open_directory(). # * No modification in the path: # * Ignore unconditionally. # under copied path, without local copy source: # ( add_directory() unconditionally ) # # re-organize: # The path is equal to $self->{target}: # * open_directory(). # under copied path, without local copy source: # * add_directory() unconditionally # under copied path, with local copy source, and not in mod_lists: # * Modifications in the path: # * open_directory(). # * No modification in the path: # * Ignore unconditionally. # in mod_lists: # * A: add_directory() # * M: open_directory() # * R: delete_entry($path), add_directory() # else: # raise an error, let others have a chance to give me complains. # # Rules for 'A', 'M', and 'R': # 1. If the path is in the modified list: # action is 'A': # 1a. The source rev is undef, meaning the copy source is not in # local depo. Pass whatever underneath the path # unconditionally. # 1b. The source rev is an positive integer. Use add_directory # method. # 1c. The source rev is -1, don't tweak @_. Use add_directory method. # action is 'M': # 1d. If the action is 'M', tweak @_ and use open_directory. # action is 'R': # 1e. delete the entry first. If the path has source rev, # supply source path and revision to add_directory. my $method = 'add_directory'; my $is_copy = 0; my $action = $self->_in_modified_list ($path); if (defined $self->{mirror}{skip_to} && $self->{mirror}{skip_to} >= $self->{mirror}{working}) { # no-op. } elsif ($self->_under_latest_copypath ($path) && $self->_is_under_false_copy ($path)) { # no-op. add_directory(). $self->_enter_new_copied_path (); } elsif ($self->_under_latest_copypath ($path) && $self->_is_under_true_copy ($path) && !$action) { if ($self->_contains_mod_in_path ($path)) { $is_copy = 1; $method = 'open_directory'; $self->_enter_new_copied_path (); } else { # don't do anything. return; } } elsif ($action) { my $item = $self->{mod_lists}{$path}; if ($action eq 'A') { my ($copypath, $copyrev) = $self->_get_copy_path_rev ($path); if (!defined($copyrev)) { # 1a. $self->_create_new_copied_path ($path); } elsif ($copyrev == -1) { # 1c. $self->_enter_new_copied_path () if $self->_is_under_copy (); } else { # 1b. $is_copy = 1; splice (@_, 0, 2, $copypath, $copyrev); $self->_create_new_copied_path ($path); } } elsif ($action eq 'M') { # 1d. $method = 'open_directory'; $self->_enter_new_copied_path () if $self->_under_latest_copypath ($path); } elsif ($action eq 'R') { # 1e. $self->delete_entry ($path, $self->{mirror}{headrev}, $pb, $pool); my ($copypath, $copyrev) = $self->_get_copy_path_rev ($path); if (defined ($copyrev) && $copyrev >= 0) { $is_copy = 1; splice (@_, 0, 2, @$item{qw/local_source_path local_rev/}); $self->_create_new_copied_path ($path); } elsif ( !defined ($copyrev) ) { $self->_create_new_copied_path ($path); } else { $self->_enter_new_copied_path () if $self->_under_latest_copypath ($path); } } } else { # raise error. die "Oh no, no more exceptions! add_directory() failed."; } $method = "open_directory" if $path eq $self->{target}; push @{$self->{visited_paths}}, $path; my $tran_path = $self->_translate_rel_path ($path); $method = 'open_directory' if $tran_path eq $self->{mirror}{target_path}; splice @_, 0, 2, $self->{mirror}{headrev} if $method eq "open_directory"; my $dir_baton; $method = "SUPER::$method"; $dir_baton = $self->$method($tran_path, $pb, @_); # Always 'touch' the directory, even for empty modifications. $self->change_dir_prop ( $dir_baton, 'svm' => undef, $pool ); $self->_remove_entries_in_path ($path, $dir_baton, $pool) if $is_copy; return $dir_baton; } sub apply_textdelta { my $self = shift; return undef unless $_[0]; print "MirrorEditor::apply_textdelta($_[0])\n" if $debug; $self->SUPER::apply_textdelta (@_); } sub close_directory { my $self = shift; my $baton = shift; print "MirrorEditor::close_directory()\n" if $debug; return unless $baton; my $path = pop @{$self->{visited_paths}}; $self->_leave_new_copied_path () if $self->_under_latest_copypath ($path); $self->SUPER::close_directory ($baton, @_); } sub close_file { my $self = shift; print "MirrorEditor::close_file()\n" if $debug; return unless $_[0]; $self->SUPER::close_file(@_); } sub add_file { my $self = shift; my $path = shift; my $pb = shift; if ($debug) { my ($cp_path, $cp_rev) = $self->_get_copy_path_rev( $path ); $cp_path = "" unless defined $cp_path; $cp_rev = "" unless defined $cp_rev; print "MirrorEditor::add_file($path, $cp_path, $cp_rev)\n" if $debug; } return undef unless $pb; # rules: # 1. If the path is in the modified list: # 1a. If the path is a copy, source path and rev must be # specified, and use add_file method. (action could be 'A' or # 'R') # 1b. If the path is not a copy, don't tweak @_. Use add_file # method. (action could be 'A' or 'R') # 1c. If the action is 'M', tweak @_ and use open_file. # 1d. If the action is 'R', delete the entry first. Then do # proper method. # 2. The path is not in the modified list, which means one of its # parent directory must be copied from other place: # 2a: If the path is not copied from a directory which exists in # local depot, pass it unconditionally. # 2b: If the path is under the latest visited copied path, reject # the file. # 2c: Reject the file my $method = 'add_file'; my $action = $self->_in_modified_list ($path); if ((defined $self->{mirror}{skip_to} && $self->{mirror}{skip_to} >= $self->{mirror}{working}) || ($self->_under_latest_copypath ($path) && $self->_is_under_false_copy ($path)) ) { # no-op. add_file(). } elsif ($self->_under_latest_copypath ($path) && $self->_is_under_true_copy ($path) && !$action) { # ignore the path. return; } elsif ($action) { my ($copypath, $copyrev) = $self->_get_copy_path_rev ($path); if ( !defined($copyrev) || $copyrev == -1) { # 1b. no-op } else { # 1a. splice (@_, 0, 2, $copypath, $copyrev); } if ($action eq 'M') { # 1c. splice @_, 0, 2, $self->{mirror}{headrev}; $method = 'open_file'; } elsif ($action eq 'R') { # 1d. $self->delete_entry ($path, $self->{mirror}{headrev}, $pb, $_[-1]); # XXX: why should this fail and ignore the following applytext? # return undef; } } else { # raise error. die "Oh no, no more exceptions! add_file() failed."; } my $tran_path = $self->_translate_rel_path ($path); if ($method eq 'add_file') { $self->SUPER::add_file ($tran_path, $pb, @_); } else { $self->SUPER::open_file ($tran_path, $pb, @_); } } sub delete_entry { my ($self, $path, $rev, $pb, $pool) = @_; print "MirrorEditor::delete_entry($path, $rev)\n" if $debug; return unless $pb; if ($self->_under_latest_copypath($path)) { my $action = $self->_in_modified_list($path) || ''; return unless $action eq 'D' || $action eq 'R'; } $self->SUPER::delete_entry ($path, $self->{mirror}{headrev}, $pb, $pool); } sub close_edit { my ($self, $pool) = @_; print "MirrorEditor::close_edit()\n" if $debug; unless ($self->{root}) { # If we goes here, this must be an empty revision. We must # replicate an empty revision as well. $self->open_root ($self->{mirror}{headrev}, $pool); $self->SUPER::close_directory ($self->{root}, $pool); } delete $self->{root}; local $SIG{INT} = 'IGNORE'; local $SIG{TERM} = 'IGNORE'; $self->{mirror}->lock ('mirror'); $self->SUPER::close_edit ($pool); } 1;