X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FLeiMirror.pm;h=afe12bb30dd719e545224b348575ff52b82b3f92;hb=0830817c132cb105291104937a645d30efe06db1;hp=e284f55deb6133cb25eaaf0113426ff7517434d6;hpb=9b285533321b533a5257fd071e4200723c37aa8b;p=public-inbox.git diff --git a/lib/PublicInbox/LeiMirror.pm b/lib/PublicInbox/LeiMirror.pm index e284f55d..afe12bb3 100644 --- a/lib/PublicInbox/LeiMirror.pm +++ b/lib/PublicInbox/LeiMirror.pm @@ -16,23 +16,24 @@ use Carp qw(croak); use URI; use PublicInbox::Config; use PublicInbox::Inbox; +use PublicInbox::Git; use PublicInbox::LeiCurl; use PublicInbox::OnDestroy; -use Digest::SHA qw(sha256_hex sha1_hex); +use PublicInbox::SHA qw(sha256_hex sha1_hex); use POSIX qw(strftime); our $LIVE; # pid => callback our $FGRP_TODO; # objstore -> [ fgrp mirror objects ] our $TODO; # reference => [ non-fgrp mirror objects ] +our @PUH; # post-update hooks sub keep_going ($) { $LIVE && (!$_[0]->{lei}->{child_error} || $_[0]->{lei}->{opt}->{'keep-going'}); } -sub _wq_done_wait { # dwaitpid callback (via wq_eof) - my ($arg, $pid) = @_; - my ($mrr, $lei) = @$arg; +sub _wq_done_wait { # awaitpid cb (via wq_eof) + my ($pid, $mrr, $lei) = @_; if ($?) { $lei->child_error($?); } elsif (!$lei->{child_error}) { @@ -181,13 +182,16 @@ sub _write_inbox_config { my $buf = delete($self->{'txt._/text/config/raw'}) // return; my $dst = $self->{cur_dst} // $self->{dst}; my $f = "$dst/inbox.config.example"; - open my $fh, '>', $f or die "open($f): $!"; - print $fh $buf or die "print: $!"; - chmod(0444 & ~umask, $fh) or die "chmod($f): $!"; my $mtime = delete $self->{'mtime._/text/config/raw'}; - $fh->flush or die "flush($f): $!"; - if (defined $mtime) { - utime($mtime, $mtime, $fh) or die "utime($f): $!"; + if (sysopen(my $fh, $f, O_CREAT|O_EXCL|O_WRONLY)) { + print $fh $buf or die "print: $!"; + chmod(0444 & ~umask, $fh) or die "chmod($f): $!"; + $fh->flush or die "flush($f): $!"; + if (defined $mtime) { + utime($mtime, $mtime, $fh) or die "utime($f): $!"; + } + } elsif (!$!{EEXIST}) { + die "open($f): $!"; } my $cfg = PublicInbox::Config->git_config_dump($f, $self->{lei}->{2}); my $ibx = $self->{ibx} = {}; @@ -232,7 +236,7 @@ sub index_cloned_inbox { my ($k) = ($sw =~ /\A([\w-]+)/); $opt->{$k} = $lei->{opt}->{$k}; } - # force synchronous dwaitpid for v2: + # force synchronous awaitpid for v2: local $PublicInbox::DS::in_loop = 0; my $cfg = PublicInbox::Config->new(undef, $lei->{2}); my $env = PublicInbox::Admin::index_prepare($opt, $cfg); @@ -255,7 +259,8 @@ sub run_reap { sub start_cmd { my ($self, $cmd, $opt, $fini) = @_; do_reap($self); - $self->{lei}->qerr("# @$cmd"); + utf8::decode(my $msg = "# @$cmd"); + $self->{lei}->qerr($msg); return if $self->{dry_run}; $LIVE->{spawn($cmd, undef, $opt)} = [ \&reap_cmd, $self, $cmd, $fini ] } @@ -272,6 +277,8 @@ sub fetch_args ($$) { ($lei->{opt}->{jobs} // 1) > 1; push @cmd, '-v' if $lei->{opt}->{verbose}; push(@cmd, '-p') if $lei->{opt}->{prune}; + PublicInbox::Git::version() >= ((2 << 24) | (29 << 16)) and + push(@cmd, '--no-write-fetch-head'); @cmd; } @@ -296,7 +303,7 @@ sub fgrp_update { my $cmd = [ 'git', "--git-dir=$fgrp->{cur_dst}", qw(update-ref --stdin -z) ]; my $lei = $fgrp->{lei}; - my $pack = PublicInbox::OnDestroy->new($$, \&pack_dst, $fgrp); + my $pack = PublicInbox::OnDestroy->new($$, \&satellite_done, $fgrp); start_cmd($fgrp, $cmd, { 0 => $r, 2 => $lei->{2} }, $pack); close $r or die "close(r): $!"; return if $fgrp->{dry_run}; @@ -316,9 +323,10 @@ sub fgrp_update { close($w) or warn "E: close(update-ref --stdin): $! (need git 1.8.5+)\n"; } -sub pack_dst { # packs lightweight satellite repos +sub satellite_done { my ($fgrp) = @_; pack_refs($fgrp, $fgrp->{cur_dst}); + run_puh($fgrp); } sub pack_refs { @@ -376,6 +384,7 @@ sub fgrp_fetch_all { push(@fetch, "-j$j") if $j; while (my ($osdir, $fgrpv) = each %$todo) { my $f = "$osdir/config"; + return if !keep_going($self); # clobber group from previous run atomically my $cmd = ['git', "--git-dir=$osdir", qw(config -f), @@ -424,20 +433,9 @@ sub forkgroup_prep { my $key = $self->{-key} // die 'BUG: no -key'; my $rn = substr(sha256_hex($key), 0, 16); 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): $!"; + my $f = "$self->{cur_dst}/config"; + open my $fh, '+>>', $f or die "open:($f): $!"; print $fh <{dry_run}) { + my $alt = File::Spec->rel2abs("$dir/objects"); + 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): $!"; + } bless { %$self, -osdir => $dir, -remote => $rn, -uri => $uri }, __PACKAGE__; } sub fp_done { - my ($self, $cb, @arg) = @_; + my ($self, $cmd, $cb, @arg) = @_; + if ($?) { + $self->{lei}->err("@$cmd failed (\$?=$?) (non-fatal)"); + $? = 0; # don't let it influence normal exit + } return if !keep_going($self); my $fh = delete $self->{-show_ref} // die 'BUG: no show-ref output'; seek($fh, SEEK_SET, 0) or die "seek(show_ref): $!"; @@ -481,8 +496,10 @@ sub cmp_fp_do { 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, $cb, @arg); - start_cmd($self, $cmd, $opt, $done); + do_reap($self); + $self->{lei}->qerr("# @$cmd"); + $LIVE->{spawn($cmd, undef, $opt)} = [ \&fp_done, $self, $cmd, + $cb, @arg ]; } sub resume_fetch { @@ -491,17 +508,16 @@ sub resume_fetch { my $dst = $self->{cur_dst} // $self->{dst}; my @git = ('git', "--git-dir=$dst"); my $opt = { 2 => $self->{lei}->{2} }; - my $rn = 'origin'; # configurable? + my $rn = 'random'.int(rand(1 << 30)); 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); + push @git, '-c', "remote.$rn.$_"; } my $cmd = [ @{$self->{-torsocks}}, @git, fetch_args($self->{lei}, $opt), $rn ]; push @$cmd, '-P' if $self->{lei}->{prune}; # --prune-tags implied - start_cmd($self, $cmd, $opt, $fini); + my $run_puh = PublicInbox::OnDestroy->new($$, \&run_puh, $self, $fini); + ++$self->{chg}->{nr_chg}; + start_cmd($self, $cmd, $opt, $run_puh); } sub fgrp_enqueue { @@ -518,6 +534,7 @@ sub fgrp_enqueue { $fgrp->{dry_run} ? $fgrp->{lei}->qerr("# @cmd @kv") : run_die([@cmd, @kv], undef, $opt); } + ++$fgrp->{chg}->{nr_chg}; push @{$FGRP_TODO->{$fgrp->{-osdir}}}, $fgrp; } @@ -550,7 +567,9 @@ sub clone_v1 { "$self->{dst}$ref"; } } - start_cmd($self, $cmd, $opt, $fini); + ++$self->{chg}->{nr_chg}; + start_cmd($self, $cmd, $opt, PublicInbox::OnDestroy->new($$, + \&run_puh, $self, $fini)); } if (!$self->{-is_epoch} && $lei->{opt}->{'inbox-config'} =~ /\A(?:always|v1)\z/s) { @@ -669,6 +688,22 @@ sub atomic_write ($$$) { ft_rename($ft, "$dn/$bn", 0666); } +sub run_next_puh { + my ($self) = @_; + my $puh = shift @{$self->{-puh_todo}} // return delete($self->{-fini}); + my $fini = PublicInbox::OnDestroy->new($$, \&run_next_puh, $self); + my $cmd = [ @$puh, ($self->{cur_dst} // $self->{dst}) ]; + my $opt = +{ map { $_ => $self->{lei}->{$_} } (0..2) }; + start_cmd($self, $cmd, undef, $opt, $fini); +} + +sub run_puh { + my ($self, $fini) = @_; + $self->{-fini} = $fini; + @{$self->{-puh_todo}} = @PUH; + run_next_puh($self); +} + # modifies the to-be-written manifest entry, and sets values from it, too sub update_ent { my ($self) = @_; @@ -701,6 +736,7 @@ sub update_ent { } if (my $symlinks = $self->{-ent}->{symlinks}) { my $top = File::Spec->rel2abs($self->{dst}); + push @{$self->{-new_symlinks}}, @$symlinks; for my $p (@$symlinks) { my $ln = "$top/$p"; $ln =~ tr!/!/!s; @@ -717,6 +753,7 @@ sub update_ent { } } symlink($tgt, $ln) or die "symlink($tgt, $ln): $!"; + ++$self->{chg}->{nr_chg}; } } if (defined(my $t = $self->{-ent}->{modified})) { @@ -953,9 +990,13 @@ sub clone_all { # resolve multi-level references while ($m && defined($nxt = $m->{$x}->{reference})) { exists($todo->{$nxt}) or last; - die < 1000; -E: dependency loop detected (`$x' => `$nxt') + if (++$nr > 1000) { + $m->{$x}->{reference} = undef; + $m->{$nxt}->{reference} = undef; + warn < `$nxt'), breaking EOM + } $x = $nxt; } my $y = delete $todo->{$x} // next; # already done @@ -987,6 +1028,38 @@ sub dump_manifest ($$) { utime($mtime, $mtime, "$ft") or die "utime(..., $ft): $!"; } +sub dump_project_list ($$) { + my ($self, $m) = @_; + my $f = $self->{'-project-list'} // return; + my $old = PublicInbox::Git::try_cat($f); + my %new; + + open my $dh, '<', '.' or die "open(.): $!"; + chdir($self->{dst}) or die "chdir($self->{dst}): $!"; + my @local = grep { -e $_ ? ($new{$_} = undef) : 1 } split(/\n/s, $old); + chdir($dh) or die "chdir(restore): $!"; + + $new{substr($_, 1)} = 1 for keys %$m; # drop leading '/' + my @list = sort keys %new; + my @remote = grep { !defined($new{$_}) } @list; + my %lnk = map { substr($_, 1) => undef } @{$self->{-new_symlinks}}; + @remote = grep { !exists($lnk{$_}) } @remote; + + warn <{src}: +EOM + warn "\t", $_, "\n" for @remote; + warn <splitpath($f); + $self->{chg}->{nr_chg} += scalar(@remote) + scalar(@local); + my $new = join("\n", @list, ''); + atomic_write($dn, $bn, $new) if $new ne $old; +} + # FIXME: this gets confused by single inbox instance w/ global manifest.js.gz sub try_manifest { my ($self) = @_; @@ -1015,7 +1088,10 @@ sub try_manifest { } # bail out if curl -z/--timecond hit 304 Not Modified, $ft will be empty - return $lei->qerr("# $manifest unchanged") if -f $manifest && !-s $ft; + if (-f $manifest && !-s $ft) { + $lei->child_error(127 << 8) if $lei->{opt}->{'exit-code'}; + return $lei->qerr("# $manifest unchanged"); + } my $m = eval { decode_manifest($ft, $ft, $uri) }; if ($@) { @@ -1024,6 +1100,7 @@ sub try_manifest { } local $self->{chg} = {}; local $self->{-local_manifest} = load_current_manifest($self); + local $self->{-new_symlinks} = []; my ($path_pfx, $n, $multi) = multi_inbox($self, \$path, $m); return $lei->child_error(1, $multi) if !ref($multi); my $v2 = delete $multi->{v2}; @@ -1100,7 +1177,10 @@ EOM warn(<{chg}->{nr_chg} && $lei->{opt}->{'exit-code'} and + $lei->child_error(127 << 8); } sub start_clone_url { @@ -1115,19 +1195,28 @@ sub do_mirror { # via wq_io_do or public-inbox-clone $self->{dry_run} = 1 if $lei->{opt}->{'dry-run'}; umask($lei->{client_umask}) if defined $lei->{client_umask}; $self->{-initial_clone} = 1 if !-d $self->{dst}; + local @PUH; + if (defined(my $puh = $lei->{opt}->{'post-update-hook'})) { + require Text::ParseWords; + @PUH = map { [ Text::ParseWords::shellwords($_) ] } @$puh; + } 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' - # we support --objstore= and --manifest= with '' (empty string) - for my $default (qw(objstore manifest.js.gz)) { - my ($k) = (split(/\./, $default))[0]; + # we support these switches with '' (empty string). + # defaults match example conf distributed with grokmirror + my @pairs = qw(objstore objstore manifest manifest.js.gz + project-list projects.list); + while (@pairs) { + my ($k, $default) = splice(@pairs, 0, 2); my $v = $lei->{opt}->{$k} // next; $v = $default if $v eq ''; $v = "$self->{dst}/$v" if $v !~ m!\A\.{0,2}/!; $self->{"-$k"} = $v; } + local $LIVE = {}; local $TODO = {}; local $FGRP_TODO = {};