File Coverage

blib/lib/SVN/Mirror.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             package SVN::Mirror;
3             our $VERSION = '0.75';
4 9     9   229315 use SVN::Core;
  0            
  0            
5             use SVN::Repos;
6             use SVN::Fs;
7             use File::Spec::Unix;
8             use strict;
9              
10             =head1 NAME
11              
12             SVN::Mirror - Mirror remote repository to local Subversion repository
13              
14             =head1 SYNOPSIS
15              
16             my $m = SVN::Mirror->new (source => $url,
17             repos => '/path/to/repository',
18             target_path => '/mirror/project1'
19             repos_create => 1,
20             skip_to => 100
21             );
22             $m->init;
23             $m->run;
24              
25             =head1 DESCRIPTION
26              
27             SVN::Mirror allows you to mirror remote repository to your local
28             subversion repository. Supported types of remote repository are:
29              
30             =over
31              
32             =item Subversion
33              
34             with the L backend.
35              
36             =item CVS, Perforce
37              
38             with the L backend through the L framework.
39              
40             =back
41              
42             =cut
43              
44             use File::Spec;
45             use URI::Escape;
46             use SVN::Simple::Edit;
47              
48             use SVN::Mirror::Ra;
49              
50             sub _schema_class {
51             my ($url) = @_;
52             die "no source specificed" unless $url;
53             return 'SVN::Mirror::Ra' if $url =~ m/^(https?|file|svn(\+.*?)?):/;
54             if ($url =~ m/^git:/) {
55             eval {
56             require SVN::Mirror::Git; 1
57             } and return 'SVN::Mirror::Git';
58             warn "SVK required for git support.\n";
59             }
60             if ($url =~ m/^(p4|cvs|arch)/) {
61             eval {
62             require SVN::Mirror::VCP; 1
63             } and return 'SVN::Mirror::VCP';
64             warn "VCP required. Please install VCP and VCP::Dest::svk.\n";
65             }
66              
67             die "schema for $url not handled\n";
68             }
69              
70             sub new {
71             my $class = shift;
72             my $self = {};
73             %$self = @_;
74              
75             return bless $self, $class unless $class eq __PACKAGE__;
76              
77             # XXX: legacy argument to be removed.
78             $self->{repospath} ||= $self->{target};
79             $self->{repos_create} ||= $self->{target_create};
80              
81             die "no repository specified" unless $self->{repospath} || $self->{repos};
82              
83             die "no source specified" unless $self->{source} || $self->{get_source};
84              
85             $self->{pool} ||= SVN::Pool->new (undef);
86             if ($self->{repos_create} && !-e $self->{repospath}) {
87             $self->{repos} = SVN::Repos::create($self->{repospath},
88             undef, undef, undef, undef, $self->{pool});
89             }
90             elsif ($self->{repos}) {
91             $self->{repospath} = $self->{repos}->path;
92             }
93              
94             $self->{repos} ||= SVN::Repos::open ($self->{repospath}, $self->{pool});
95             my $fs = $self->{fs} = $self->{repos}->fs;
96              
97             my $root = $fs->revision_root ($fs->youngest_rev);
98             $self->{target_path} = File::Spec::Unix->canonpath("/$self->{target_path}");
99              
100             if ($root->check_path ($self->{target_path}) != $SVN::Node::none) {
101             $self->{rsource} = $root->node_prop ($self->{target_path}, 'svm:rsource');
102             $self->{source} ||= $root->node_prop ($self->{target_path}, 'svm:source')
103             or die "no source found on $self->{target_path}";
104             }
105              
106             return _schema_class ($self->{rsource} || $self->{source})->new (%$self);
107             }
108              
109             sub has_local {
110             my ($repos, $spec) = @_;
111             my $fs = $repos->fs;
112             my $root = $fs->revision_root ($fs->youngest_rev);
113             local $@;
114             # XXX:
115             my %mirrored = map {
116             my $m = SVN::Mirror->new (target_path => $_,
117             repos => $repos,
118             pool => SVN::Pool->new,
119             get_source => 1);
120             eval { $m->init };
121             $@ ? () : (join(':', $m->{source_uuid}, $m->{source_path}) => $_)
122             } list_mirror ($repos);
123             # XXX: gah!
124             my ($specanchor) =
125             map { (m|[/:]$| ?
126             substr ($spec, 0, length ($_)) eq $_
127             : substr ("$spec/", 0, length($_)+1) eq "$_/")
128             ? $_ : () } keys %mirrored;
129             return unless $specanchor;
130             my $path = $mirrored{$specanchor};
131             $spec =~ s/^\Q$specanchor\E//;
132             my $m = SVN::Mirror->new (target_path => $path,
133             repos => $repos,
134             pool => SVN::Pool->new,
135             get_source => 1);
136             eval { $m->init () };
137             return if $@;
138             if ($spec) {
139             $spec = "/$spec" if substr ($spec, 0, 1) ne '/';
140             $spec = '' if $spec eq '/';
141             }
142             return wantarray ? ($m, $spec) : $m;
143             }
144              
145             sub list_mirror {
146             my ($repos) = @_;
147             my $fs = $repos->fs;
148             my $root = $fs->revision_root ($fs->youngest_rev);
149             die "please upgrade the mirror state\n"
150             if grep {m/^svm:mirror:/} keys %{$root->node_proplist ('/')};
151              
152             my $prop = $root->node_prop ('/', 'svm:mirror') or return;
153             return $prop =~ m/^(.*)$/mg;
154             }
155              
156             sub is_mirrored {
157             my ($repos, $path) = @_;
158             my ($mpath) = map { substr ("$path/", 0, length($_)+1) eq "$_/" ? $_ : () } list_mirror ($repos);
159             return unless $mpath;
160             $path =~ s/^\Q$mpath\E//;
161              
162             my $m = SVN::Mirror->new (target_path => $mpath,
163             repos => $repos,
164             pool => SVN::Pool->new,
165             get_source => 1) or die $@;
166             eval { $m->init };
167             undef $@, return if $@;
168             return wantarray ? ($m, $path) : $m;
169             }
170              
171             sub load_fromrev {
172             my ($self) = @_;
173             my $fromrev;
174             # try without lock first
175             if (defined ($fromrev = $self->_do_load_fromrev)) {
176             return $self->{fromrev} = $fromrev;
177             }
178             $self->lock('mirror');
179             $fromrev = $self->_do_load_fromrev;
180             $self->unlock('mirror');
181             $self->{fromrev} = $fromrev if defined $fromrev;
182             return $fromrev;
183             }
184              
185             sub _do_load_fromrev {
186             my $self = shift;
187             my $fs = $self->{fs};
188             my $root = $fs->revision_root ($fs->youngest_rev);
189             my $changed = $root->node_created_rev ($self->{target_path});
190             my $prop = $fs->revision_prop ($changed, 'svm:headrev');
191             return unless $prop;
192             my %revs = map {split (':', $_)} $prop =~ m/^.*$/mg;
193             my $uuid = $self->{rsource_uuid} || $self->{source_uuid};
194             return $revs{$uuid};
195             }
196              
197             sub find_local_rev {
198             my ($self, $rrev, $uuid) = @_;
199              
200             # if uuid is the repository we talk to directly, return
201             # null for revisions larger than what we have
202             $uuid ||= $self->{source_uuid};
203             return if $uuid eq ($self->{rsource_uuid} || $self->{source_uuid})
204             && $rrev > ($self->{working} || $self->{fromrev});
205              
206             my $pool = SVN::Pool->new_default ($self->{pool});
207             my $fs = $self->{repos}->fs;
208              
209             my $rev = $self->_find_local_rev($rrev, $uuid);
210             return $rev if defined $rev;
211              
212             # try again with iterative, for the case that the source revision
213             # is something we are not mirroring.
214             my $old_pool = SVN::Pool->new;
215             my $new_pool = SVN::Pool->new;
216              
217             my $hist = $fs->revision_root ($fs->youngest_rev)->
218             node_history ($self->{target_path}, $old_pool);
219              
220             while ($hist = $hist->prev (1, $new_pool)) {
221             $rev = ($hist->location ($new_pool))[1];
222             my %rev = $self->find_remote_rev($rev, $self->{repos});
223             my $lrev = $rev{$uuid};
224             $old_pool->clear;
225             ($old_pool, $new_pool) = ($new_pool, $old_pool);
226              
227             # 0 would be the init change we had. not good for any use.
228             next unless $lrev;
229             return $rev if $rrev >= $lrev;
230             }
231             return;
232             }
233              
234             sub _find_local_rev {
235             my ($self, $rrev, $uuid, $path) = @_;
236             my $fs = $self->{repos}->fs;
237              
238             $path ||= $self->{target_path};
239             my @rev = (1, $fs->youngest_rev);
240              
241             my $id = $fs->revision_root($rev[1])->node_id($path);
242             my $pool = SVN::Pool->new_default;
243              
244             while ($rev[0] <= $rev[1]) {
245             $pool->clear;
246             my $rev = int(($rev[0]+$rev[1])/2);
247             my $root = $fs->revision_root($rev);
248             # In the revision we are looking at, the path must exist and
249             # related to the one we know
250             if ($root->check_path($path) &&
251             SVN::Fs::check_related($id, $root->node_id($path))) {
252             # normalise the revision so we can hit the headrev prop.
253             # But don't normalise when we are bounded to one revision,
254             # as this is likely the case where no path is touched.
255             my $nrev = $rev;
256             $nrev = ($root->node_history($path)->prev(0)->location)[1]
257             unless $rev[0] == $rev[1] || $nrev == $root->node_created_rev ($path);
258             my %rev = $self->find_remote_rev($nrev, $self->{repos});
259             my $found = $rev{$uuid};
260              
261             $rev[0] = $rev + 1, next unless defined $found;
262             return $nrev if $rrev == $found && !$fs->revision_prop ($nrev, 'svm:incomplete');
263             if ($rrev > $found) {
264             $rev[0] = $rev+1;
265             }
266             else {
267             $rev[1] = $rev-1;
268             }
269             }
270             else {
271             $rev[0] = $rev+1;
272             }
273             }
274             return;
275             }
276              
277             =head2 find_remote_rev
278              
279              
280              
281             =cut
282              
283             sub find_remote_rev {
284             my ($self, $rev, $repos) = @_;
285             $repos ||= $self->{repos};
286             my $fs = $repos->fs;
287             my $prop = $fs->revision_prop ($rev, 'svm:headrev') or return;
288             my %rev = map {split (':', $_, 2)} $prop =~ m/^.*$/mg;
289             return %rev if wantarray;
290             return ref($self) ? $rev{$self->{source_uuid}} || $rev{$self->{rsource_uuid}} :
291             (values %rev)[0];
292             }
293              
294             sub delete {
295             my ($self, $remove_props) = @_;
296             my $fs = $self->{repos}->fs;
297             my $newprop = join ('', map {"$_\n"} grep { $_ ne $self->{target_path}}
298             list_mirror ($self->{repos}));
299             my $txn = $fs->begin_txn ($fs->youngest_rev);
300             my $txnroot = $txn->root;
301             $txn->change_prop ("svn:author", 'svm');
302             $txn->change_prop ("svn:log", "SVM: discard mirror for $self->{target_path}");
303             $txnroot->change_node_prop ('/', 'svm:mirror', $newprop);
304             if ($remove_props) {
305             $txnroot->change_node_prop ($self->{target_path}, 'svm:source', undef);
306             $txnroot->change_node_prop ($self->{target_path}, 'svm:uuid', undef);
307             }
308             my $rev = $self->commit_txn($txn);
309             print "Committed revision $rev.\n";
310             }
311              
312             # prepare source
313             sub pre_init {}
314              
315             sub init {
316             my $self = shift;
317             my $pool = SVN::Pool->new_default ($self->{pool});
318              
319             if ($self->is_initialized) {
320             $self->pre_init (0);
321             $self->load_state ();
322             return 0;
323             }
324              
325             return $self->do_initialize;
326             }
327              
328             sub is_initialized {
329             my $self = shift;
330             my $headrev = $self->{headrev} ||= $self->{fs}->youngest_rev;
331             $self->{root} ||= $self->{fs}->revision_root ($headrev);
332              
333             if ($self->{target_path} eq '/') {
334             $self->{fs}->revision_root($self->{headrev})->node_prop('/', 'svm:source');
335             }
336             else {
337             # XXX: verify this is a valid mirror too.
338             $self->{root}->check_path ($self->{target_path}) != $SVN::Node::none;
339             }
340             }
341              
342             sub do_initialize {
343             my $self = shift;
344              
345             $self->pre_init (1);
346              
347             my $txn = $self->{fs}->begin_txn ($self->{headrev});
348             my $txnroot = $txn->root;
349             $self->mkpdir ($txnroot, $self->{target_path});
350              
351             my $source = $self->init_state ($txn);
352             my %mirrors = map { ($_ => 1) }
353             split(/\n/, $txnroot->node_prop ('/', 'svm:mirror') || '');
354             $mirrors{$self->{target_path}}++;
355              
356             $txnroot->change_node_prop ('/', 'svm:mirror', join("\n", (grep length, sort keys %mirrors), ''));
357             $txnroot->change_node_prop ($self->{target_path}, 'svm:source', $source);
358             $txnroot->change_node_prop ($self->{target_path}, 'svm:uuid', $self->{source_uuid});
359              
360             my $rev = $self->commit_txn($txn);
361             print "Committed revision $rev.\n";
362              
363             $self->{fs}->change_rev_prop ($rev, "svn:author", 'svm');
364             $self->{fs}->change_rev_prop
365             ($rev, "svn:log", "SVM: initializing mirror for $self->{target_path}");
366              
367             return $rev;
368             }
369              
370             sub relocate {
371             my $self = shift;
372             my $pool = SVN::Pool->new_default ($self->{pool});
373             my $headrev = $self->{headrev} = $self->{fs}->youngest_rev;
374             $self->{root} = $self->{fs}->revision_root ($headrev);
375              
376             $self->is_initialized
377             or die "Cannot relocate uninitialized path $self->{target_path}";
378              
379             $self->pre_init (0);
380             $self->load_state ();
381              
382             my $ra = $self->_new_ra (url => $self->{source});
383             my $ra_uuid = $ra->get_uuid;
384             die "Local and remote UUID differ." unless ($ra_uuid eq $self->{source_uuid} or $ra_uuid eq $self->{rsource_uuid});
385              
386             # Get latest revprops
387             my $old_prevs = $self->{fs}->revision_proplist(
388             $self->find_local_rev($self->{fromrev}) , $pool
389             );
390              
391             my $rev = $self->do_initialize;
392             $self->{fs}->change_rev_prop ($rev, $_ => $old_prevs->{$_})
393             for sort grep /^svm:/, keys %$old_prevs;
394              
395             $self->{fs}->change_rev_prop ($rev, 'svm:incomplete' => '*');
396              
397             return $rev;
398             }
399              
400             sub mergeback {
401             my ($self, $fromrev, $path, $rev) = @_;
402              
403             # verify $path is copied from $self->{target_path}
404              
405             # concat batch merge?
406             my $msg = $self->{fs}->revision_prop ($rev, 'svn:log');
407             $msg .= "\n\nmerged from rev $rev of repository ".$self->{fs}->get_uuid;
408              
409             my $editor = $self->get_merge_back_editor ('', $msg,
410             sub {warn "committed via RA"});
411              
412             # dir_delta ($path, $fromrev, $rev) for commit_editor
413             SVN::Repos::dir_delta($self->{fs}->revision_root ($fromrev), $path,
414             $SVN::Core::VERSION ge '0.36.0' ? '' : undef,
415             $self->{fs}->revision_root ($rev), $path,
416             $editor, undef,
417             1, 1, 0, 1
418             );
419             }
420              
421             sub mkpdir {
422             my ($self, $root, $dir) = @_;
423             my @dirs = File::Spec::Unix->splitdir($self->{target_path});
424             my $path = '';
425             my $new;
426              
427             while (@dirs) {
428             $path = File::Spec::Unix->join($path, shift @dirs);
429             my $kind = $self->{root}->check_path ($path);
430             if ($kind == $SVN::Core::node_none) {
431             $root->make_dir ($path, SVN::Pool->new);
432             $new = 1;
433             }
434             elsif ($kind != $SVN::Core::node_dir) {
435             die "something is in the way of mirror root($path)";
436             }
437             }
438             return $new;
439             }
440              
441             sub upgrade {
442             my ($repos) = @_;
443             my $fs = $repos->fs;
444             my $yrev = $fs->youngest_rev;
445              
446             # pre 0.40:
447             # svm:mirror:: in node_prop of /
448             # svm:headrev:
449              
450             my $txn = $fs->begin_txn ($yrev);
451             my $root = $txn->root;
452             my $prop = $root->node_proplist ('/');
453             my @mirrors;
454             for (grep {m/^svm:mirror:/} keys %$prop) {
455             $root->change_node_prop ('/', $_, undef);
456             push @mirrors, $prop->{$_};
457             }
458              
459             unless (@mirrors) {
460             print "nothing to upgrade\n";
461             $txn->abort;
462             return;
463             }
464              
465             $root->change_node_prop ('/', 'svm:mirror', join ('', map {"$_\n"} @mirrors));
466              
467             my $spool = SVN::Pool->new_default;
468             for (@mirrors) {
469             print "Upgrading $_.\n";
470             my $source = join ('', split ('!', $root->node_prop ($_, 'svm:source')));
471             my $uuid = $root->node_prop ($_, 'svm:uuid');
472             my $hist = $fs->revision_root ($yrev)->node_history ($_);
473             my $ipool = SVN::Pool->new_default_sub;
474             while ($hist = $hist->prev (0)) {
475             my (undef, $rev) = $hist->location;
476             next unless $rev;
477             my $rrev = $fs->revision_prop ($rev, "svm:headrev:$source");
478             if (defined $rrev) {
479             $fs->change_rev_prop ($rev, "svm:headrev:$source", undef);
480             $fs->change_rev_prop ($rev, "svm:headrev", "$uuid:$rrev\n");
481             }
482             else {
483             Carp::carp "no headrev" unless $source =~ m/^(?:cvs|p4)/;
484             }
485             $ipool->clear;
486             }
487             }
488              
489             my $rev = __PACKAGE__->commit_txn($txn);
490             $fs->change_rev_prop ($rev, "svn:author", 'svm');
491             $fs->change_rev_prop ($rev, "svn:log", 'SVM: upgrading svm mirror state.');
492             }
493              
494             sub commit_txn {
495             my ($self, $txn) = @_;
496             return ($txn->commit)[1];
497             }
498              
499             use Sys::Hostname;
500              
501             sub _lock_token {
502             my $token = $_[0]->{target_path};
503             $token =~ s/_/__/g;
504             $token =~ s{/}{_}g;
505             return "svm:lock:$_[1]:$token";
506             }
507              
508             sub lock {
509             my ($self, $what) = @_;
510             my $fs = $self->{fs};
511             my $token = $self->_lock_token ($what);
512             my $content = hostname.':'.$$;
513             my $where = join(' ', (caller(0))[0..2]);
514             die $where."\n".$self->{locked}{$what} if exists $self->{locked}{$what};
515             # This is not good enough but race condition should result in failed sync
516             # without corrupting repository.
517             LOCKED:
518             {
519             while (1) {
520             my $who = $fs->revision_prop (0, $token) or last LOCKED;
521             if ($who eq $content) {
522             $self->unlock ($what);
523             Carp::confess "recursive lock? $what $where $self->{locked}{$what}";
524             }
525             if ($self->{lock_message}) {
526             $self->{lock_message}->($self, $what, $who);
527             }
528             else {
529             print "Waiting for $what lock on $self->{target_path}: $who.\n";
530             }
531             sleep 1;
532             }
533             }
534             $fs->change_rev_prop (0, $token, $content);
535             $self->{locked}{$what} = $where;
536             }
537              
538             sub unlock {
539             my ($self, $what) = @_;
540             if ($what eq 'force') {
541             for (keys %{$self->{fs}->revision_proplist(0)}) {
542             $self->{fs}->change_rev_prop (0, $_, undef);
543             }
544             delete $self->{locked};
545             return;
546             }
547              
548             my $token = $self->_lock_token ($what);
549             if ($self->{locked}{$what}) {
550             $self->{fs}->change_rev_prop (0, $token, undef);
551             delete $self->{locked}{$what};
552             }
553             }
554              
555             =head1 AUTHORS
556              
557             Chia-liang Kao Eclkao@clkao.orgE
558              
559             =head1 COPYRIGHT
560              
561             Copyright 2003-2005 by Chia-liang Kao Eclkao@clkao.orgE.
562              
563             This program is free software; you can redistribute it and/or modify it
564             under the same terms as Perl itself.
565              
566             See L
567              
568             =cut
569              
570             1;