X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FLeiMirror.pm;h=8b276336f288258a4a45a385ee37ee325802b384;hb=02d0dfd3f22f8f7c5d1e189dbe29034b9da9f510;hp=de4cdc2293835e30be430176761b3633ccf9145c;hpb=0d20437e59f7f1fc97e8004a30d873ea79a3a8a4;p=public-inbox.git diff --git a/lib/PublicInbox/LeiMirror.pm b/lib/PublicInbox/LeiMirror.pm index de4cdc22..8b276336 100644 --- a/lib/PublicInbox/LeiMirror.pm +++ b/lib/PublicInbox/LeiMirror.pm @@ -18,6 +18,7 @@ use PublicInbox::Config; use PublicInbox::Inbox; use PublicInbox::LeiCurl; use PublicInbox::OnDestroy; +use Digest::SHA qw(sha256_hex); our $LIVE; # pid => callback @@ -111,9 +112,9 @@ sub clone_cmd { @cmd; } -sub ft_rename ($$$) { - my ($ft, $dst, $open_mode) = @_; - my @st = stat($dst); +sub ft_rename ($$$;$) { + my ($ft, $dst, $open_mode, $fh) = @_; + my @st = stat($fh // $dst); my $mode = @st ? ($st[2] & 07777) : ($open_mode & ~umask); chmod($mode, $ft) or croak "E: chmod($ft): $!"; require File::Copy; @@ -121,6 +122,21 @@ sub ft_rename ($$$) { $ft->unlink_on_destroy(0); } +sub do_reap ($;$) { + my ($self, $jobs) = @_; + $jobs //= $self->{-jobs} //= $self->{lei}->{opt}->{jobs} // 1; + $jobs = 1 if $jobs < 1; + while (keys(%$LIVE) >= $jobs) { + my $pid = waitpid(-1, 0) // die "waitpid(-1): $!"; + if (my $x = delete $LIVE->{$pid}) { + my $cb = shift @$x; + $cb->(@$x) if $cb; + } else { + warn "reaped unknown PID=$pid ($?)\n"; + } + } +} + sub _get_txt_start { # non-fatal my ($self, $endpoint, $fini) = @_; my $uri = URI->new($self->{cur_src} // $self->{src}); @@ -133,8 +149,7 @@ sub _get_txt_start { # non-fatal my $opt = { 0 => $lei->{0}, 1 => $lei->{1}, 2 => $lei->{2} }; my $cmd = $self->{curl}->for_uri($lei, $uri, qw(-f --compressed -R -o), $ft->filename); - my $jobs = $lei->{opt}->{jobs} // 1; - reap_live() while keys(%$LIVE) >= $jobs; + do_reap($self); $lei->qerr("# @$cmd"); return if $self->{dry_run}; $self->{"-get_txt.$endpoint"} = [ $ft, $cmd, $uri ]; @@ -202,8 +217,6 @@ sub set_description ($) { sub index_cloned_inbox { my ($self, $iv) = @_; my $lei = $self->{lei}; - eval { set_description($self) }; - warn $@ if $@; # n.b. public-inbox-clone works w/o (SQLite || Xapian) # lei is useless without Xapian + SQLite @@ -245,11 +258,228 @@ sub run_reap { sub start_clone { my ($self, $cmd, $opt, $fini) = @_; - my $jobs = $self->{lei}->{opt}->{jobs} // 1; - reap_live() while keys(%$LIVE) >= $jobs; + do_reap($self); $self->{lei}->qerr("# @$cmd"); return if $self->{dry_run}; - $LIVE->{spawn($cmd, undef, $opt)} = [ \&reap_clone, $self, $cmd, $fini ] + $LIVE->{spawn($cmd, undef, $opt)} = [ \&reap_cmd, $self, $cmd, $fini ] +} + +sub fetch_args ($$) { + my ($lei, $opt) = @_; + my @cmd; # (git --git-dir=...) to be added by caller + $opt->{$_} = $lei->{$_} for (0..2); + # we support "-c $key=$val" for arbitrary git config options + # e.g.: git -c http.proxy=socks5h://127.0.0.1:9050 + push(@cmd, '-c', $_) for @{$lei->{opt}->{c} // []}; + push @cmd, 'fetch'; + push @cmd, '-q' if $lei->{opt}->{quiet} || + ($lei->{opt}->{jobs} // 1) > 1; + push @cmd, '-v' if $lei->{opt}->{verbose}; + @cmd; +} + +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): $!"; +} + +sub fgrp_update { + my ($fgrp) = @_; + my $srcfh = delete $fgrp->{srcfh} or return; + my $dstfh = delete $fgrp->{dstfh} or return; + seek($srcfh, SEEK_SET, 0) or die "seek(src): $!"; + seek($dstfh, SEEK_SET, 0) or die "seek(dst): $!"; + my %src = map { chomp; split(/\0/) } (<$srcfh>); + close $srcfh; + my %dst = map { chomp; split(/\0/) } (<$dstfh>); + close $dstfh; + pipe(my ($r, $w)) or die "pipe: $!"; + my $cmd = [ 'git', "--git-dir=$fgrp->{cur_dst}", + qw(update-ref --stdin -z) ]; + my $lei = $fgrp->{lei}; + $lei->qerr("# @$cmd"); + my $opt = { 0 => $r, 1 => $lei->{1}, 2 => $lei->{2} }; + my $pid = spawn($cmd, undef, $opt); + close $r or die "close(r): $!"; + for my $ref (keys %dst) { + my $new = delete $src{$ref}; + my $old = $dst{$ref}; + if (defined $new) { + upr($lei, $w, 'update', $ref, $new) if $new ne $old; + } else { + upr($lei, $w, 'delete', $ref, $old); + } + } + while (my ($ref, $oid) = each %src) { + upr($lei, $w, 'create', $ref, $oid); + } + close($w) or warn "E: close(update-ref --stdin): $! (need git 1.8.5+)\n"; + 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}); +} + +sub pack_refs { + my ($self, $git_dir) = @_; + do_reap($self); + my $cmd = [ 'git', "--git-dir=$git_dir", qw(pack-refs --all --prune) ]; + $self->{lei}->qerr("# @$cmd"); + return if $self->{dry_run}; + my $opt = { 1 => $self->{lei}->{1}, 2 => $self->{lei}->{2} }; + $LIVE->{spawn($cmd, undef, $opt)} = [ \&reap_cmd, $self, $cmd ]; +} + +sub fgrpv_done { + my ($fgrpv) = @_; + return if !$LIVE; + my $pid; + my $first = $fgrpv->[0] // die 'BUG: no fgrpv->[0]'; + pack_refs($first, $first->{-osdir}); # objstore refs always packed + for my $fgrp (@$fgrpv) { + my $rn = $fgrp->{-remote}; + my %opt = map { $_ => $fgrp->{lei}->{$_} } (0..2); + + my $update_ref = $fgrp->{dry_run} ? undef : + PublicInbox::OnDestroy->new($$, \&fgrp_update, $fgrp); + + my $src = [ 'git', "--git-dir=$fgrp->{-osdir}", 'for-each-ref', + "--format=refs/%(refname:lstrip=3)%00%(objectname)", + "refs/remotes/$rn/" ]; + do_reap($fgrp); + $fgrp->{lei}->qerr("# @$src >SRC"); + if ($update_ref) { + open(my $fh, '+>', undef) or die "open(src): $!"; + $pid = spawn($src, undef, { %opt, 1 => $fh }); + $fgrp->{srcfh} = $fh; + $LIVE->{$pid} = [ \&reap_cmd, $fgrp, $src, $update_ref ] + } + my $dst = [ 'git', "--git-dir=$fgrp->{cur_dst}", 'for-each-ref', + '--format=%(refname)%00%(objectname)' ]; + do_reap($fgrp); + $fgrp->{lei}->qerr("# @$dst >DST"); + if ($update_ref) { + open(my $fh, '+>', undef) or die "open(dst): $!"; + $pid = spawn($dst, undef, { %opt, 1 => $fh }); + $fgrp->{dstfh} = $fh; + $LIVE->{$pid} = [ \&reap_cmd, $fgrp, $dst, $update_ref ] + } + } +} + +sub fgrp_fetch_all { + my ($self) = @_; + my $todo = delete $self->{fgrp_todo} or return; + keys(%$todo) or return; + + # Rely on the fgrptmp remote groups in the config file rather + # than listing all remotes since the remote name list may exceed + # system argv limits: + my $grp = 'fgrptmp'; + + my @git = (@{$self->{-torsocks}}, 'git'); + my $j = $self->{lei}->{opt}->{jobs}; + my $opt = {}; + my @fetch = do { + local $self->{lei}->{opt}->{jobs} = 1; + (fetch_args($self->{lei}, $opt), + qw(--no-tags --multiple)); + }; + push(@fetch, "-j$j") if $j; + my $pid; + while (my ($osdir, $fgrpv) = each %$todo) { + my $f = "$osdir/config"; + + # clobber group from previous run atomically + my $cmd = ['git', "--git-dir=$osdir", qw(config -f), + $f, '--unset-all', "remotes.$grp"]; + $self->{lei}->qerr("# @$cmd"); + if (!$self->{dry_run}) { + $pid = spawn($cmd); + waitpid($pid, 0) // die "waitpid: $!"; + die "E: @$cmd: \$?=$?" if ($? && ($? >> 8) != 5); + + # update the config atomically via O_APPEND while + # respecting git-config locking + sysopen(my $lk, "$f.lock", O_CREAT|O_EXCL|O_WRONLY) + or die "open($f.lock): $!"; + open my $fh, '>>', $f or die "open(>>$f): $!"; + $fh->autoflush(1); + my $buf = join('', "[remotes]\n", + map { "\t$grp = $_->{-remote}\n" } @$fgrpv); + print $fh $buf or die "print($f): $!"; + close $fh or die "close($f): $!"; + unlink("$f.lock") or die "unlink($f.lock): $!"; + } + + $cmd = [ @git, "--git-dir=$osdir", @fetch, $grp ]; + do_reap($self); + $self->{lei}->qerr("# @$cmd"); + my $end = PublicInbox::OnDestroy->new($$, \&fgrpv_done, $fgrpv); + return if $self->{dry_run}; + $pid = spawn($cmd, undef, $opt); + $LIVE->{$pid} = [ \&reap_cmd, $self, $cmd, $end ]; + } +} + +# keep this idempotent for future use by public-inbox-fetch +sub forkgroup_prep { + my ($self, $uri) = @_; + $self->{-ent} // return; + 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); + for ('repack.useDeltaIslands=true', + 'pack.island=refs/remotes/([^/]+)/') { + run_die([@cmd, split(/=/, $_, 2)], undef, $opt); + } + } + 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}); + my $o = "$self->{cur_dst}/objects"; + my $f = "$o/info/alternates"; + my $l = File::Spec->abs2rel($alt, File::Spec->rel2abs($o)); + open my $fh, '+>>', $f or die "open($f): $!"; + seek($fh, SEEK_SET, 0) or die "seek($f): $!"; + chomp(my @cur = <$fh>); + if (!grep(/\A\Q$l\E\z/, @cur)) { + say $fh $l or die "say($f): $!"; + } + close $fh or die "close($f): $!"; + $f = "$self->{cur_dst}/config"; + open $fh, '+>>', $f or die "open:($f): $!"; + print $fh < $dir, -remote => $rn }, __PACKAGE__; } sub clone_v1 { @@ -259,15 +489,24 @@ sub clone_v1 { my $uri = URI->new($self->{cur_src} // $self->{src}); defined($lei->{opt}->{epoch}) and die "$uri is a v1 inbox, --epoch is not supported\n"; - my $pfx = $curl->torsocks($lei, $uri) or return; + $self->{-torsocks} //= $curl->torsocks($lei, $uri) or return; my $dst = $self->{cur_dst} // $self->{dst}; my $fini = PublicInbox::OnDestroy->new($$, \&v1_done, $self); - my $cmd = [ @$pfx, clone_cmd($lei, my $opt = {}), "$uri", $dst ]; - my $ref = $self->{-ent} ? $self->{-ent}->{reference} : undef; - defined($ref) && -e "$self->{dst}$ref" and - push @$cmd, '--reference', "$self->{dst}$ref"; - start_clone($self, $cmd, $opt, $fini); - + if (my $fgrp = forkgroup_prep($self, $uri)) { + $fgrp->{-fini} = $fini; + push @{$self->{fgrp_todo}->{$fgrp->{-osdir}}}, $fgrp; + } else { # normal fetch + my $cmd = [ @{$self->{-torsocks}}, + clone_cmd($lei, my $opt = {}), "$uri", $dst ]; + if (defined($self->{-ent})) { + if (defined(my $ref = $self->{-ent}->{reference})) { + -e "$self->{dst}$ref" and + push @$cmd, '--reference', + "$self->{dst}$ref"; + } + } + start_clone($self, $cmd, $opt, $fini); + } if (!$self->{-is_epoch} && $lei->{opt}->{'inbox-config'} =~ /\A(?:always|v1)\z/s) { _get_txt_start($self, '_/text/config/raw', $fini); @@ -278,7 +517,7 @@ sub clone_v1 { (!defined($d) && !$nohang) and _get_txt_start($self, 'description', $fini); - reap_live() until ($nohang || !keys(%$LIVE)); # for non-manifest clone + $nohang or do_reap($self, 1); # for non-manifest clone } sub parse_epochs ($$) { @@ -357,13 +596,13 @@ EOM } } -sub reap_clone { # async, called via SIGCHLD +sub reap_cmd { # async, called via SIGCHLD my ($self, $cmd) = @_; my $cerr = $?; $? = 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)"); } } @@ -376,16 +615,24 @@ sub v1_done { # called via OnDestroy run_die([qw(git config -f), "$dst/config", 'gitweb.owner', $o]); } my $o = "$dst/objects"; - if (open(my $fh, '<', "$o/info/alternates")) { - chomp(my @l = <$fh>); - for (@l) { $_ = File::Spec->abs2rel($_, $o)."\n" } - my $f = File::Temp->new(TEMPLATE => '.XXXX', DIR => "$o/info"); - print $f @l; - $f->flush or die "flush($f): $!"; - rename($f->filename, "$o/info/alternates") or - die "rename($f, $o/info/alternates): $!"; - $f->unlink_on_destroy(0); + if (open(my $fh, '<', my $fn = "$o/info/alternates")) {; + my $base = File::Spec->rel2abs($o); + my @l = <$fh>; + my $ft; + for (@l) { + next unless m!\A/!; + $_ = File::Spec->abs2rel($_, $base); + $ft //= File::Temp->new(TEMPLATE => '.XXXX', + DIR => "$o/info"); + } + if ($ft) { + print $ft @l or die "print($ft): $!"; + $ft->flush or die "flush($ft): $!"; + ft_rename($ft, $fn, 0666, $fh); + } } + eval { set_description($self) }; + warn $@ if $@; return if ($self->{-is_epoch} || $self->{lei}->{opt}->{'inbox-config'} ne 'always'); write_makefile($dst, 1); @@ -410,25 +657,17 @@ sub v2_done { # called via OnDestroy } write_makefile($dst, 2); undef $lck; # unlock + eval { set_description($self) }; + warn $@ if $@; index_cloned_inbox($self, 2); } -sub reap_live { - my $pid = waitpid(-1, 0) // die "waitpid(-1): $!"; - if (my $x = delete $LIVE->{$pid}) { - my $cb = shift @$x; - $cb->(@$x); - } else { - warn "reaped unknown PID=$pid ($?)\n"; - } -} - sub clone_v2_prep ($$;$) { my ($self, $v2_epochs, $m) = @_; # $m => manifest.js.gz hashref my $lei = $self->{lei}; my $curl = $self->{curl} //= PublicInbox::LeiCurl->new($lei) or return; my $first_uri = (map { $_->[0] } values %$v2_epochs)[0]; - my $pfx = $curl->torsocks($lei, $first_uri) or return; + $self->{-torsocks} //= $curl->torsocks($lei, $first_uri) or return; my $dst = $self->{cur_dst} // $self->{dst}; my $want = parse_epochs($lei->{opt}->{epoch}, $v2_epochs); my $task = $m ? bless { %$self }, __PACKAGE__ : $self; @@ -454,7 +693,7 @@ failed to extract epoch number from $src } } if (!$want || $want->{$nr}) { - my $etask = bless { %$task }, __PACKAGE__; + my $etask = bless { %$task, -key => $key }, __PACKAGE__; $etask->{-ent} = $ent; # may have {reference} $etask->{cur_src} = $src; $etask->{cur_dst} = $edst; @@ -585,7 +824,8 @@ EOM last; # restart %$todo iteration } } - reap_live() while keys(%$LIVE); + fgrp_fetch_all($self); + do_reap($self, 1); } sub dump_manifest ($$) { @@ -608,6 +848,7 @@ sub try_manifest { my $uri = URI->new($self->{src}); my $lei = $self->{lei}; my $curl = $self->{curl} //= PublicInbox::LeiCurl->new($lei) or return; + $self->{-torsocks} //= $curl->torsocks($lei, $uri) or return; my $path = $uri->path; chop($path) eq '/' or die "BUG: $uri not canonicalized"; $uri->path($path . '/manifest.js.gz'); @@ -629,6 +870,7 @@ sub try_manifest { return $lei->child_error(1, $multi) if !ref($multi); my $v2 = delete $multi->{v2}; local $self->{todo} = {}; + local $self->{fgrp_todo} = {}; # { objstore_dir => [fgrp, ...] } if ($v2) { for my $name (sort keys %$v2) { my $epochs = delete $v2->{$name}; @@ -664,6 +906,7 @@ EOM die("BUG: no `$name' in manifest"); $task->{cur_src} = "$uri"; $task->{cur_dst} = $task->{dst}; + $task->{-key} = $name; if ($n > 1) { $task->{cur_dst} .= $name; $task->{cur_src} .= $name; @@ -696,12 +939,18 @@ sub start_clone_url { sub do_mirror { # via wq_io_do or public-inbox-clone my ($self) = @_; my $lei = $self->{lei}; + $self->{dry_run} = 1 if $lei->{opt}->{'dry-run'}; umask($lei->{client_umask}) if defined $lei->{client_umask}; eval { my $ic = $lei->{opt}->{'inbox-config'} //= 'always'; $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; + } local $LIVE; my $iv = $lei->{opt}->{'inbox-version'} // return start_clone_url($self);