]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/LeiMirror.pm
lei_mirror: delay configuring forkgroups
[public-inbox.git] / lib / PublicInbox / LeiMirror.pm
index 04d9494c604388ca4e9c0f832b9233c653cb9ac7..0e8689ca71c0d4a0c447076aa748ac578627f653 100644 (file)
@@ -18,7 +18,7 @@ use PublicInbox::Config;
 use PublicInbox::Inbox;
 use PublicInbox::LeiCurl;
 use PublicInbox::OnDestroy;
-use Digest::SHA qw(sha256_hex);
+use Digest::SHA qw(sha256_hex sha1_hex);
 
 our $LIVE; # pid => callback
 
@@ -256,7 +256,7 @@ sub run_reap {
        $ret;
 }
 
-sub start_clone {
+sub start_cmd {
        my ($self, $cmd, $opt, $fini) = @_;
        do_reap($self);
        $self->{lei}->qerr("# @$cmd");
@@ -279,9 +279,9 @@ sub fetch_args ($$) {
 }
 
 sub upr { # feed `git update-ref --stdin -z' verbosely
-       my ($lei, $w, $op, $ref, $oid) = @_;
-       $lei->qerr("# $op $ref $oid") if $lei->{opt}->{verbose};
-       print $w "$op $ref\0$oid\0" or die "print(w): $!";
+       my ($lei, $w, $op, @rest) = @_; # ($ref, $oid) = @rest
+       $lei->qerr("# $op @rest") if $lei->{opt}->{verbose};
+       print $w "$op ", join("\0", @rest, '') or die "print(w): $!";
 }
 
 sub fgrp_update {
@@ -306,7 +306,8 @@ sub fgrp_update {
                my $new = delete $src{$ref};
                my $old = $dst{$ref};
                if (defined $new) {
-                       upr($lei, $w, 'update', $ref, $new) if $new ne $old;
+                       $new eq $old or
+                               upr($lei, $w, 'update', $ref, $new, $old);
                } else {
                        upr($lei, $w, 'delete', $ref, $old);
                }
@@ -315,7 +316,12 @@ sub fgrp_update {
                upr($lei, $w, 'create', $ref, $oid);
        }
        close($w) or warn "E: close(update-ref --stdin): $! (need git 1.8.5+)\n";
-       $LIVE->{$pid} = [ \&reap_cmd, $fgrp, $cmd ];
+       my $pack = PublicInbox::OnDestroy->new($$, \&pack_dst, $fgrp);
+       $LIVE->{$pid} = [ \&reap_cmd, $fgrp, $cmd, $pack ];
+}
+
+sub pack_dst { # packs lightweight satellite repos
+       my ($fgrp) = @_;
        pack_refs($fgrp, $fgrp->{cur_dst});
 }
 
@@ -428,10 +434,10 @@ sub forkgroup_prep {
        my $os = $self->{-objstore} // return;
        my $fg = $self->{-ent}->{forkgroup} // return;
        my $dir = "$os/$fg.git";
-       my @cmd = ('git', "--git-dir=$dir", 'config');
-       my $opt = +{ map { $_ => $self->{lei}->{$_} } (0..2) };
        if (!-d $dir && !$self->{dry_run}) {
                PublicInbox::Import::init_bare($dir);
+               my @cmd = ('git', "--git-dir=$dir", 'config');
+               my $opt = { 2 => $self->{lei}->{2} };
                for ('repack.useDeltaIslands=true',
                                'pack.island=refs/remotes/([^/]+)/') {
                        run_die([@cmd, split(/=/, $_, 2)], undef, $opt);
@@ -439,15 +445,6 @@ sub forkgroup_prep {
        }
        my $key = $self->{-key} // die 'BUG: no -key';
        my $rn = substr(sha256_hex($key), 0, 16);
-       unless ($self->{dry_run}) {
-               # --no-tags is required to avoid conflicts
-               for ("url=$uri", "fetch=+refs/*:refs/remotes/$rn/*",
-                               'tagopt=--no-tags') {
-                       my @kv = split(/=/, $_, 2);
-                       $kv[0] = "remote.$rn.$kv[0]";
-                       run_die([@cmd, @kv], undef, $opt);
-               }
-       }
        if (!-d $self->{cur_dst} && !$self->{dry_run}) {
                my $alt = File::Spec->rel2abs("$dir/objects");
                PublicInbox::Import::init_bare($self->{cur_dst});
@@ -474,7 +471,87 @@ sub forkgroup_prep {
 EOM
                close $fh or die "close($f): $!";
        }
-       bless { %$self, -osdir => $dir, -remote => $rn }, __PACKAGE__;
+       bless {
+               %$self, -osdir => $dir, -remote => $rn, -uri => $uri
+       }, __PACKAGE__;
+}
+
+sub fp_done {
+       my ($self, $go_fetch) = @_;
+       my $fh = delete $self->{-show_ref} // die 'BUG: no show-ref output';
+       seek($fh, SEEK_SET, 0) or die "seek(show_ref): $!";
+       $self->{-ent} // die 'BUG: no -ent';
+       my $A = $self->{-ent}->{fingerprint} // die 'BUG: no fingerprint';
+       my $B = sha1_hex(do { local $/; <$fh> } // die("read(show_ref): $!"));
+       return if $A ne $B; # $go_fetch->DESTROY fires
+       $go_fetch->cancel;
+       $self->{lei}->qerr("# $self->{-key} up-to-date");
+}
+
+sub cmp_fp_fetch {
+       my ($self, $go_fetch) = @_;
+       # $go_fetch is either resume_fetch or fgrp_enqueue
+       my $new = $self->{-ent}->{fingerprint} // die 'BUG: no fingerprint';
+       my $key = $self->{-key} // die 'BUG: no -key';
+       if (my $cur_ent = $self->{-local_manifest}->{$key}) {
+               # runs go_fetch->DESTROY run if eq
+               return $go_fetch->cancel if $cur_ent->{fingerprint} eq $new;
+       }
+       my $dst = $self->{cur_dst} // $self->{dst};
+       my $cmd = ['git', "--git-dir=$dst", 'show-ref'];
+       my $opt = { 2 => $self->{lei}->{2} };
+       open($opt->{1}, '+>', undef) or die "open(tmp): $!";
+       $self->{-show_ref} = $opt->{1};
+       my $done = PublicInbox::OnDestroy->new($$, \&fp_done, $self, $go_fetch);
+       start_cmd($self, $cmd, $opt, $done);
+}
+
+sub resume_fetch_maybe {
+       my ($self, $uri, $fini) = @_;
+       my $go_fetch = PublicInbox::OnDestroy->new($$, \&resume_fetch, @_);
+       cmp_fp_fetch($self, $go_fetch) if $self->{-ent} &&
+                               defined($self->{-ent}->{fingerprint});
+}
+
+sub resume_fetch {
+       my ($self, $uri, $fini) = @_;
+       my $dst = $self->{cur_dst} // $self->{dst};
+       my @git = ('git', "--git-dir=$dst");
+       my $opt = +{ map { $_ => $self->{lei}->{$_} } (0..2) };
+       my $rn = 'origin'; # configurable?
+       for ("url=$uri", "fetch=+refs/*:refs/*", 'mirror=true') {
+               my @kv = split(/=/, $_, 2);
+               $kv[0] = "remote.$rn.$kv[0]";
+               next if $self->{dry_run};
+               run_die([@git, 'config', @kv], undef, $opt);
+       }
+       my $cmd = [ @{$self->{-torsocks}}, @git,
+                       fetch_args($self->{lei}, $opt), $rn ];
+       start_cmd($self, $cmd, $opt, $fini);
+}
+
+sub fgrp_enqueue_maybe {
+       my ($self, $fgrp) = @_;
+       my $enq = PublicInbox::OnDestroy->new($$, \&fgrp_enqueue, $self, $fgrp);
+       cmp_fp_fetch($self, $enq) if $self->{-ent} &&
+                                       defined($self->{-ent}->{fingerprint});
+       # $enq->DESTROY calls fgrp_enqueue otherwise
+}
+
+sub fgrp_enqueue {
+       my ($self, $fgrp) = @_;
+       my $opt = { 2 => $self->{lei}->{2} };
+       # --no-tags is required to avoid conflicts
+       my $u = $fgrp->{-uri} // die 'BUG: no {-uri}';
+       my $rn = $fgrp->{-remote} // die 'BUG: no {-remote}';
+       my @cmd = ('git', "--git-dir=$fgrp->{-osdir}", 'config');
+       for ("url=$u", "fetch=+refs/*:refs/remotes/$rn/*", 'tagopt=--no-tags') {
+               my @kv = split(/=/, $_, 2);
+               $kv[0] = "remote.$rn.$kv[0]";
+               $self->{dry_run} ? $self->{lei}->qerr("# @cmd @kv") :
+                               run_die([@cmd, @kv], undef, $opt);
+       }
+       push @{$self->{fgrp_todo}->{$fgrp->{-osdir}}}, $fgrp;
 }
 
 sub clone_v1 {
@@ -487,10 +564,14 @@ sub clone_v1 {
        $self->{-torsocks} //= $curl->torsocks($lei, $uri) or return;
        my $dst = $self->{cur_dst} // $self->{dst};
        my $fini = PublicInbox::OnDestroy->new($$, \&v1_done, $self);
+       my $resume = -d $dst;
        if (my $fgrp = forkgroup_prep($self, $uri)) {
                $fgrp->{-fini} = $fini;
-               push @{$self->{fgrp_todo}->{$fgrp->{-osdir}}}, $fgrp;
-       } else { # normal fetch
+               $resume ? fgrp_enqueue_maybe($self, $fgrp) :
+                               fgrp_enqueue($self, $fgrp);
+       } elsif ($resume) {
+               resume_fetch_maybe($self, $uri, $fini);
+       } else { # normal clone
                my $cmd = [ @{$self->{-torsocks}},
                                clone_cmd($lei, my $opt = {}), "$uri", $dst ];
                if (defined($self->{-ent})) {
@@ -500,7 +581,7 @@ sub clone_v1 {
                                                "$self->{dst}$ref";
                        }
                }
-               start_clone($self, $cmd, $opt, $fini);
+               start_cmd($self, $cmd, $opt, $fini);
        }
        if (!$self->{-is_epoch} && $lei->{opt}->{'inbox-config'} =~
                                /\A(?:always|v1)\z/s) {
@@ -597,7 +678,7 @@ sub reap_cmd { # async, called via SIGCHLD
        $? = 0; # don't let it influence normal exit
        if ($cerr) {
                kill('TERM', keys %$LIVE);
-               $self->{lei}->child_error($cerr, "@$cmd failed");
+               $self->{lei}->child_error($cerr, "@$cmd failed (\$?=$cerr)");
        }
 }
 
@@ -607,7 +688,10 @@ sub v1_done { # called via OnDestroy
        _write_inbox_config($self);
        my $dst = $self->{cur_dst} // $self->{dst};
        if (defined(my $o = $self->{-ent} ? $self->{-ent}->{owner} : undef)) {
-               run_die([qw(git config -f), "$dst/config", 'gitweb.owner', $o]);
+               my $key = $self->{-key} // die 'BUG: no -key';
+               my $cur = $self->{-local_manifest}->{$key}->{owner} // "\0";
+               $cur eq $o or run_die([qw(git config -f),
+                                       "$dst/config", 'gitweb.owner', $o]);
        }
        my $o = "$dst/objects";
        if (open(my $fh, '<', my $fn = "$o/info/alternates")) {;
@@ -726,6 +810,19 @@ sub decode_manifest ($$$) {
        $m;
 }
 
+sub load_current_manifest ($) {
+       my ($self) = @_;
+       my $fn = $self->{-manifest} // return;
+       if (open(my $fh, '<', $fn)) {
+               decode_manifest($fh, $fn, $fn);
+       } elsif ($!{ENOENT}) { # non-fatal, we can just do it slowly
+               warn "open($fn): $!\n";
+               undef;
+       } else {
+               die "open($fn): $!\n";
+       }
+}
+
 sub multi_inbox ($$$) {
        my ($self, $path, $m) = @_;
        my $incl = $self->{lei}->{opt}->{include};
@@ -819,6 +916,7 @@ EOM
                        last; # restart %$todo iteration
                }
        }
+       do_reap($self, 1); # finish all fingerprint checks
        fgrp_fetch_all($self);
        do_reap($self, 1);
 }
@@ -861,6 +959,7 @@ sub try_manifest {
                warn $@;
                return try_scrape($self);
        }
+       local $self->{-local_manifest} = load_current_manifest($self);
        my ($path_pfx, $n, $multi) = multi_inbox($self, \$path, $m);
        return $lei->child_error(1, $multi) if !ref($multi);
        my $v2 = delete $multi->{v2};
@@ -941,10 +1040,13 @@ sub do_mirror { # via wq_io_do or public-inbox-clone
                $ic =~ /\A(?:v1|v2|always|never)\z/s or die <<"";
 --inbox-config must be one of `always', `v2', `v1', or `never'
 
-               if (defined(my $os = $lei->{opt}->{objstore})) {
-                       $os = 'objstore' if $os eq ''; # --objstore w/o args
-                       $os = "$self->{dst}/$os" if $os !~ m!\A/!;
-                       $self->{-objstore} = $os;
+               # we support --objstore= and --manifest= with '' (empty string)
+               for my $default (qw(objstore manifest.js.gz)) {
+                       my ($k) = (split(/\./, $default))[0];
+                       my $v = $lei->{opt}->{$k} // next;
+                       $v = $default if $v eq '';
+                       $v = "$self->{dst}/$v" if $v !~ m!\A/!;
+                       $self->{"-$k"} = $v;
                }
                local $LIVE;
                my $iv = $lei->{opt}->{'inbox-version'} //