#!/usr/bin/perl package SVN::Mirror; our $VERSION = '0.73'; use SVN::Core; use SVN::Repos; use SVN::Fs; use File::Spec::Unix; use strict; =head1 NAME SVN::Mirror - Mirror remote repository to local Subversion repository =head1 SYNOPSIS my $m = SVN::Mirror->new (source => $url, repos => '/path/to/repository', target_path => '/mirror/project1' repos_create => 1, skip_to => 100 ); $m->init; $m->run; =head1 DESCRIPTION SVN::Mirror allows you to mirror remote repository to your local subversion repository. Supported types of remote repository are: =over =item Subversion with the L backend. =item CVS, Perforce with the L backend through the L framework. =back =cut use File::Spec; use URI::Escape; use SVN::Simple::Edit; use SVN::Mirror::Ra; sub _schema_class { my ($url) = @_; die "no source specificed" unless $url; return 'SVN::Mirror::Ra' if $url =~ m/^(https?|file|svn(\+.*?)?):/; if ($url =~ m/^git:/) { eval { require SVN::Mirror::Git; 1 } and return 'SVN::Mirror::Git'; warn "SVK required for git support.\n"; } if ($url =~ m/^(p4|cvs|arch)/) { eval { require SVN::Mirror::VCP; 1 } and return 'SVN::Mirror::VCP'; warn "VCP required. Please install VCP and VCP::Dest::svk.\n"; } die "schema for $url not handled\n"; } sub new { my $class = shift; my $self = {}; %$self = @_; return bless $self, $class unless $class eq __PACKAGE__; # XXX: legacy argument to be removed. $self->{repospath} ||= $self->{target}; $self->{repos_create} ||= $self->{target_create}; die "no repository specified" unless $self->{repospath} || $self->{repos}; die "no source specified" unless $self->{source} || $self->{get_source}; $self->{pool} ||= SVN::Pool->new (undef); if ($self->{repos_create} && !-e $self->{repospath}) { $self->{repos} = SVN::Repos::create($self->{repospath}, undef, undef, undef, undef, $self->{pool}); } elsif ($self->{repos}) { $self->{repospath} = $self->{repos}->path; } $self->{repos} ||= SVN::Repos::open ($self->{repospath}, $self->{pool}); my $fs = $self->{fs} = $self->{repos}->fs; my $root = $fs->revision_root ($fs->youngest_rev); $self->{target_path} = File::Spec::Unix->canonpath("/$self->{target_path}"); if ($root->check_path ($self->{target_path}) != $SVN::Node::none) { $self->{rsource} = $root->node_prop ($self->{target_path}, 'svm:rsource'); $self->{source} ||= $root->node_prop ($self->{target_path}, 'svm:source') or die "no source found on $self->{target_path}"; } return _schema_class ($self->{rsource} || $self->{source})->new (%$self); } sub has_local { my ($repos, $spec) = @_; my $fs = $repos->fs; my $root = $fs->revision_root ($fs->youngest_rev); local $@; # XXX: my %mirrored = map { my $m = SVN::Mirror->new (target_path => $_, repos => $repos, pool => SVN::Pool->new, get_source => 1); eval { $m->init }; $@ ? () : (join(':', $m->{source_uuid}, $m->{source_path}) => $_) } list_mirror ($repos); # XXX: gah! my ($specanchor) = map { (m|[/:]$| ? substr ($spec, 0, length ($_)) eq $_ : substr ("$spec/", 0, length($_)+1) eq "$_/") ? $_ : () } keys %mirrored; return unless $specanchor; my $path = $mirrored{$specanchor}; $spec =~ s/^\Q$specanchor\E//; my $m = SVN::Mirror->new (target_path => $path, repos => $repos, pool => SVN::Pool->new, get_source => 1); eval { $m->init () }; return if $@; if ($spec) { $spec = "/$spec" if substr ($spec, 0, 1) ne '/'; $spec = '' if $spec eq '/'; } return wantarray ? ($m, $spec) : $m; } sub list_mirror { my ($repos) = @_; my $fs = $repos->fs; my $root = $fs->revision_root ($fs->youngest_rev); die "please upgrade the mirror state\n" if grep {m/^svm:mirror:/} keys %{$root->node_proplist ('/')}; my $prop = $root->node_prop ('/', 'svm:mirror') or return; return $prop =~ m/^(.*)$/mg; } sub is_mirrored { my ($repos, $path) = @_; my ($mpath) = map { substr ("$path/", 0, length($_)+1) eq "$_/" ? $_ : () } list_mirror ($repos); return unless $mpath; $path =~ s/^\Q$mpath\E//; my $m = SVN::Mirror->new (target_path => $mpath, repos => $repos, pool => SVN::Pool->new, get_source => 1) or die $@; eval { $m->init }; undef $@, return if $@; return wantarray ? ($m, $path) : $m; } sub load_fromrev { my ($self) = @_; my $fromrev; # try without lock first if (defined ($fromrev = $self->_do_load_fromrev)) { return $self->{fromrev} = $fromrev; } $self->lock('mirror'); $fromrev = $self->_do_load_fromrev; $self->unlock('mirror'); $self->{fromrev} = $fromrev if defined $fromrev; return $fromrev; } sub _do_load_fromrev { my $self = shift; my $fs = $self->{fs}; my $root = $fs->revision_root ($fs->youngest_rev); my $changed = $root->node_created_rev ($self->{target_path}); my $prop = $fs->revision_prop ($changed, 'svm:headrev'); return unless $prop; my %revs = map {split (':', $_)} $prop =~ m/^.*$/mg; my $uuid = $self->{rsource_uuid} || $self->{source_uuid}; return $revs{$uuid}; } sub find_local_rev { my ($self, $rrev, $uuid) = @_; # if uuid is the repository we talk to directly, return # null for revisions larger than what we have $uuid ||= $self->{source_uuid}; return if $uuid eq ($self->{rsource_uuid} || $self->{source_uuid}) && $rrev > ($self->{working} || $self->{fromrev}); my $pool = SVN::Pool->new_default ($self->{pool}); my $fs = $self->{repos}->fs; my $rev = $self->_find_local_rev($rrev, $uuid); return $rev if defined $rev; # try again with iterative, for the case that the source revision # is something we are not mirroring. my $old_pool = SVN::Pool->new; my $new_pool = SVN::Pool->new; my $hist = $fs->revision_root ($fs->youngest_rev)-> node_history ($self->{target_path}, $old_pool); while ($hist = $hist->prev (1, $new_pool)) { $rev = ($hist->location ($new_pool))[1]; my %rev = $self->find_remote_rev($rev, $self->{repos}); my $lrev = $rev{$uuid}; $old_pool->clear; ($old_pool, $new_pool) = ($new_pool, $old_pool); # 0 would be the init change we had. not good for any use. next unless $lrev; return $rev if $rrev >= $lrev; } return; } sub _find_local_rev { my ($self, $rrev, $uuid, $path) = @_; my $fs = $self->{repos}->fs; $path ||= $self->{target_path}; my @rev = (1, $fs->youngest_rev); my $id = $fs->revision_root($rev[1])->node_id($path); my $pool = SVN::Pool->new_default; while ($rev[0] <= $rev[1]) { $pool->clear; my $rev = int(($rev[0]+$rev[1])/2); my $root = $fs->revision_root($rev); # In the revision we are looking at, the path must exist and # related to the one we know if ($root->check_path($path) && SVN::Fs::check_related($id, $root->node_id($path))) { # normalise the revision so we can hit the headrev prop. # But don't normalise when we are bounded to one revision, # as this is likely the case where no path is touched. my $nrev = $rev; $nrev = ($root->node_history($path)->prev(0)->location)[1] unless $rev[0] == $rev[1] || $nrev == $root->node_created_rev ($path); my %rev = $self->find_remote_rev($nrev, $self->{repos}); my $found = $rev{$uuid}; $rev[0] = $rev + 1, next unless defined $found; return $nrev if $rrev == $found && !$fs->revision_prop ($nrev, 'svm:incomplete'); if ($rrev > $found) { $rev[0] = $rev+1; } else { $rev[1] = $rev-1; } } else { $rev[0] = $rev+1; } } return; } =head2 find_remote_rev =cut sub find_remote_rev { my ($self, $rev, $repos) = @_; $repos ||= $self->{repos}; my $fs = $repos->fs; my $prop = $fs->revision_prop ($rev, 'svm:headrev') or return; my %rev = map {split (':', $_, 2)} $prop =~ m/^.*$/mg; return %rev if wantarray; return ref($self) ? $rev{$self->{source_uuid}} || $rev{$self->{rsource_uuid}} : (values %rev)[0]; } sub delete { my ($self, $remove_props) = @_; my $fs = $self->{repos}->fs; my $newprop = join ('', map {"$_\n"} grep { $_ ne $self->{target_path}} list_mirror ($self->{repos})); my $txn = $fs->begin_txn ($fs->youngest_rev); my $txnroot = $txn->root; $txn->change_prop ("svn:author", 'svm'); $txn->change_prop ("svn:log", "SVM: discard mirror for $self->{target_path}"); $txnroot->change_node_prop ('/', 'svm:mirror', $newprop); if ($remove_props) { $txnroot->change_node_prop ($self->{target_path}, 'svm:source', undef); $txnroot->change_node_prop ($self->{target_path}, 'svm:uuid', undef); } my $rev = $self->commit_txn($txn); print "Committed revision $rev.\n"; } # prepare source sub pre_init {} sub init { my $self = shift; my $pool = SVN::Pool->new_default ($self->{pool}); if ($self->is_initialized) { $self->pre_init (0); $self->load_state (); return 0; } return $self->do_initialize; } sub is_initialized { my $self = shift; my $headrev = $self->{headrev} ||= $self->{fs}->youngest_rev; $self->{root} ||= $self->{fs}->revision_root ($headrev); if ($self->{target_path} eq '/') { $self->{fs}->revision_root($self->{headrev})->node_prop('/', 'svm:source'); } else { # XXX: verify this is a valid mirror too. $self->{root}->check_path ($self->{target_path}) != $SVN::Node::none; } } sub do_initialize { my $self = shift; $self->pre_init (1); my $txn = $self->{fs}->begin_txn ($self->{headrev}); my $txnroot = $txn->root; $self->mkpdir ($txnroot, $self->{target_path}); my $source = $self->init_state ($txn); my %mirrors = map { ($_ => 1) } split(/\n/, $txnroot->node_prop ('/', 'svm:mirror') || ''); $mirrors{$self->{target_path}}++; $txnroot->change_node_prop ('/', 'svm:mirror', join("\n", (grep length, sort keys %mirrors), '')); $txnroot->change_node_prop ($self->{target_path}, 'svm:source', $source); $txnroot->change_node_prop ($self->{target_path}, 'svm:uuid', $self->{source_uuid}); my $rev = $self->commit_txn($txn); print "Committed revision $rev.\n"; $self->{fs}->change_rev_prop ($rev, "svn:author", 'svm'); $self->{fs}->change_rev_prop ($rev, "svn:log", "SVM: initializing mirror for $self->{target_path}"); return $rev; } sub relocate { my $self = shift; my $pool = SVN::Pool->new_default ($self->{pool}); my $headrev = $self->{headrev} = $self->{fs}->youngest_rev; $self->{root} = $self->{fs}->revision_root ($headrev); $self->is_initialized or die "Cannot relocate uninitialized path $self->{target_path}"; $self->pre_init (0); $self->load_state (); my $ra = $self->_new_ra (url => $self->{source}); my $ra_uuid = $ra->get_uuid; die "Local and remote UUID differ." unless ($ra_uuid eq $self->{source_uuid} or $ra_uuid eq $self->{rsource_uuid}); # Get latest revprops my $old_prevs = $self->{fs}->revision_proplist( $self->find_local_rev($self->{fromrev}) , $pool ); my $rev = $self->do_initialize; $self->{fs}->change_rev_prop ($rev, $_ => $old_prevs->{$_}) for sort grep /^svm:/, keys %$old_prevs; $self->{fs}->change_rev_prop ($rev, 'svm:incomplete' => '*'); return $rev; } sub mergeback { my ($self, $fromrev, $path, $rev) = @_; # verify $path is copied from $self->{target_path} # concat batch merge? my $msg = $self->{fs}->revision_prop ($rev, 'svn:log'); $msg .= "\n\nmerged from rev $rev of repository ".$self->{fs}->get_uuid; my $editor = $self->get_merge_back_editor ('', $msg, sub {warn "committed via RA"}); # dir_delta ($path, $fromrev, $rev) for commit_editor SVN::Repos::dir_delta($self->{fs}->revision_root ($fromrev), $path, $SVN::Core::VERSION ge '0.36.0' ? '' : undef, $self->{fs}->revision_root ($rev), $path, $editor, undef, 1, 1, 0, 1 ); } sub mkpdir { my ($self, $root, $dir) = @_; my @dirs = File::Spec::Unix->splitdir($self->{target_path}); my $path = ''; my $new; while (@dirs) { $path = File::Spec::Unix->join($path, shift @dirs); my $kind = $self->{root}->check_path ($path); if ($kind == $SVN::Core::node_none) { $root->make_dir ($path, SVN::Pool->new); $new = 1; } elsif ($kind != $SVN::Core::node_dir) { die "something is in the way of mirror root($path)"; } } return $new; } sub upgrade { my ($repos) = @_; my $fs = $repos->fs; my $yrev = $fs->youngest_rev; # pre 0.40: # svm:mirror:: in node_prop of / # svm:headrev: my $txn = $fs->begin_txn ($yrev); my $root = $txn->root; my $prop = $root->node_proplist ('/'); my @mirrors; for (grep {m/^svm:mirror:/} keys %$prop) { $root->change_node_prop ('/', $_, undef); push @mirrors, $prop->{$_}; } unless (@mirrors) { print "nothing to upgrade\n"; $txn->abort; return; } $root->change_node_prop ('/', 'svm:mirror', join ('', map {"$_\n"} @mirrors)); my $spool = SVN::Pool->new_default; for (@mirrors) { print "Upgrading $_.\n"; my $source = join ('', split ('!', $root->node_prop ($_, 'svm:source'))); my $uuid = $root->node_prop ($_, 'svm:uuid'); my $hist = $fs->revision_root ($yrev)->node_history ($_); my $ipool = SVN::Pool->new_default_sub; while ($hist = $hist->prev (0)) { my (undef, $rev) = $hist->location; my $rrev = $fs->revision_prop ($rev, "svm:headrev:$source"); if (defined $rrev) { $fs->change_rev_prop ($rev, "svm:headrev:$source", undef); $fs->change_rev_prop ($rev, "svm:headrev", "$uuid:$rrev\n"); } else { die "no headrev" unless $source =~ m/^(?:cvs|p4)/; } $ipool->clear; } } my $rev = __PACKAGE__->commit_txn($txn); $fs->change_rev_prop ($rev, "svn:author", 'svm'); $fs->change_rev_prop ($rev, "svn:log", 'SVM: upgrading svm mirror state.'); } sub commit_txn { my ($self, $txn) = @_; return ($txn->commit)[1]; } use Sys::Hostname; sub _lock_token { my $token = $_[0]->{target_path}; $token =~ s/_/__/g; $token =~ s{/}{_}g; return "svm:lock:$_[1]:$token"; } sub lock { my ($self, $what) = @_; my $fs = $self->{fs}; my $token = $self->_lock_token ($what); my $content = hostname.':'.$$; my $where = join(' ', (caller(0))[0..2]); die $where."\n".$self->{locked}{$what} if exists $self->{locked}{$what}; # This is not good enough but race condition should result in failed sync # without corrupting repository. LOCKED: { while (1) { my $who = $fs->revision_prop (0, $token) or last LOCKED; if ($who eq $content) { $self->unlock ($what); Carp::confess "recursive lock? $what $where $self->{locked}{$what}"; } if ($self->{lock_message}) { $self->{lock_message}->($self, $what, $who); } else { print "Waiting for $what lock on $self->{target_path}: $who.\n"; } sleep 1; } } $fs->change_rev_prop (0, $token, $content); $self->{locked}{$what} = $where; } sub unlock { my ($self, $what) = @_; if ($what eq 'force') { for (keys %{$self->{fs}->revision_proplist(0)}) { $self->{fs}->change_rev_prop (0, $_, undef); } delete $self->{locked}; return; } my $token = $self->_lock_token ($what); if ($self->{locked}{$what}) { $self->{fs}->change_rev_prop (0, $token, undef); delete $self->{locked}{$what}; } } =head1 AUTHORS Chia-liang Kao Eclkao@clkao.orgE =head1 COPYRIGHT Copyright 2003-2005 by Chia-liang Kao Eclkao@clkao.orgE. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L =cut 1;