X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FLeiMirror.pm;h=4dedac9b36c5be711a19844d3b471806726712ef;hb=431bb226ccd7b219ad79bd52090eac10d2aa1bb6;hp=bfbfd8434a2f5d51c5051df64ecb269235c4de3b;hpb=b70592ae75f1251c2b116c8bb58a671534c650b4;p=public-inbox.git diff --git a/lib/PublicInbox/LeiMirror.pm b/lib/PublicInbox/LeiMirror.pm index bfbfd843..4dedac9b 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 $FGRP_TODO; # objstore -> [[ to resume ], [ to clone ]] 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}) { @@ -235,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); @@ -258,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 ] } @@ -275,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; } @@ -284,6 +288,19 @@ sub upr { # feed `git update-ref --stdin -z' verbosely print $w "$op ", join("\0", @rest, '') or die "print(w): $!"; } +sub start_update_ref { + my ($fgrp) = @_; + pipe(my ($r, $w)) or die "pipe: $!"; + my $cmd = [ 'git', "--git-dir=$fgrp->{cur_dst}", + qw(update-ref --stdin -z) ]; + my $pack = PublicInbox::OnDestroy->new($$, \&satellite_done, $fgrp); + start_cmd($fgrp, $cmd, { 0 => $r, 2 => $fgrp->{lei}->{2} }, $pack); + close $r or die "close(r): $!"; + $fgrp->{dry_run} ? undef : $w; +} + +sub upref_warn { warn "E: close(update-ref --stdin): $! (need git 1.8.5+)\n" } + sub fgrp_update { my ($fgrp) = @_; return if !keep_going($fgrp); @@ -295,14 +312,9 @@ sub fgrp_update { 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 $w = start_update_ref($fgrp) or return; my $lei = $fgrp->{lei}; - my $pack = PublicInbox::OnDestroy->new($$, \&pack_dst, $fgrp); - start_cmd($fgrp, $cmd, { 0 => $r, 2 => $lei->{2} }, $pack); - close $r or die "close(r): $!"; - return if $fgrp->{dry_run}; + my $ndel; for my $ref (keys %dst) { my $new = delete $src{$ref}; my $old = $dst{$ref}; @@ -311,17 +323,33 @@ sub fgrp_update { upr($lei, $w, 'update', $ref, $new, $old); } else { upr($lei, $w, 'delete', $ref, $old); + ++$ndel; } } - while (my ($ref, $oid) = each %src) { - upr($lei, $w, 'create', $ref, $oid); + # git's ref files backend doesn't allow directory/file conflicts + # between `delete' and `create' ops: + if ($ndel && scalar(keys %src)) { + $fgrp->{-create_refs} = \%src; + } else { + 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"; + close($w) or upref_warn(); } -sub pack_dst { # packs lightweight satellite repos +sub satellite_done { my ($fgrp) = @_; - pack_refs($fgrp, $fgrp->{cur_dst}); + if (my $create = delete $fgrp->{-create_refs}) { + my $w = start_update_ref($fgrp) or return; + while (my ($ref, $oid) = each %$create) { + upr($fgrp->{lei}, $w, 'create', $ref, $oid); + } + close($w) or upref_warn(); + } else { + pack_refs($fgrp, $fgrp->{cur_dst}); + run_puh($fgrp); + } } sub pack_refs { @@ -373,23 +401,45 @@ sub fgrp_fetch_all { my $opt = {}; my @fetch = do { local $self->{lei}->{opt}->{jobs} = 1; - (fetch_args($self->{lei}, $opt), - qw(--no-tags --multiple)); + (fetch_args($self->{lei}, $opt), qw(--no-tags --multiple)); }; push(@fetch, "-j$j") if $j; - while (my ($osdir, $fgrpv) = each %$todo) { + while (my ($osdir, $fgrp_old_new) = each %$todo) { my $f = "$osdir/config"; return if !keep_going($self); + my ($fgrpv, $new) = @$fgrp_old_new; + @$fgrpv = sort { $b->{-sort} <=> $a->{-sort} } @$fgrpv; + push @$fgrpv, @$new; # $new is ordered by references + my $cmd = ['git', "--git-dir=$osdir", qw(config -f), $f ]; # 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}) { - my $pid = spawn($cmd, undef, { 2 => $self->{lei}->{2} }); + for ("remotes.$grp") { # TODO: hideRefs + my $c = [ @$cmd, '--unset-all', $_ ]; + $self->{lei}->qerr("# @$c"); + next if $self->{dry_run}; + my $pid = spawn($c, undef, $opt); waitpid($pid, 0) // die "waitpid: $!"; - die "E: @$cmd: \$?=$?" if ($? && ($? >> 8) != 5); + die "E: @$c \$?=$?" if ($? && ($? >> 8) != 5); + } + + # permanent configs: + my $cfg = PublicInbox::Config->git_config_dump($f); + for my $fgrp (@$fgrpv) { + my $u = $fgrp->{-uri} // die 'BUG: no {-uri}'; + my $rn = $fgrp->{-remote} // die 'BUG: no {-remote}'; + for ("url=$u", "fetch=+refs/*:refs/remotes/$rn/*", + 'tagopt=--no-tags') { + my ($k, $v) = split(/=/, $_, 2); + $k = "remote.$rn.$k"; + next if ($cfg->{$k} // '') eq $v; + my $c = [@$cmd, $k, $v]; + $fgrp->{lei}->qerr("# @$c"); + next if $fgrp->{dry_run}; + run_die($c, undef, $opt); + } + } + if (!$self->{dry_run}) { # update the config atomically via O_APPEND while # respecting git-config locking sysopen(my $lk, "$f.lock", O_CREAT|O_EXCL|O_WRONLY) @@ -402,7 +452,6 @@ sub fgrp_fetch_all { close $fh or die "close($f): $!"; unlink("$f.lock") or die "unlink($f.lock): $!"; } - $cmd = [ @git, "--git-dir=$osdir", @fetch, $grp ]; my $end = PublicInbox::OnDestroy->new($$, \&fgrpv_done, $fgrpv); start_cmd($self, $cmd, $opt, $end); @@ -418,12 +467,15 @@ sub forkgroup_prep { my $dir = "$os/$fg.git"; 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); - } + my $f = "$dir/config"; + open my $fh, '+>>', $f or die "open:($f): $!"; + print $fh <{-key} // die 'BUG: no -key'; my $rn = substr(sha256_hex($key), 0, 16); @@ -503,34 +555,24 @@ 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 { my ($fgrp, $end) = @_; # $end calls fgrp_fetch_all return if !keep_going($fgrp); - my $opt = { 2 => $fgrp->{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]"; - $fgrp->{dry_run} ? $fgrp->{lei}->qerr("# @cmd @kv") : - run_die([@cmd, @kv], undef, $opt); - } - push @{$FGRP_TODO->{$fgrp->{-osdir}}}, $fgrp; + ++$fgrp->{chg}->{nr_chg}; + my $dst = $FGRP_TODO->{$fgrp->{-osdir}} //= [ [], [] ]; # [ old, new ] + push @{$dst->[defined($fgrp->{-sort} ? 0 : 1)]}, $fgrp; } sub clone_v1 { @@ -548,8 +590,12 @@ sub clone_v1 { my $resume = -d $dst; if (my $fgrp = forkgroup_prep($self, $uri)) { $fgrp->{-fini} = $fini; - $resume ? cmp_fp_do($fgrp, \&fgrp_enqueue, $end) - : fgrp_enqueue($fgrp, $end); + if ($resume) { + $fgrp->{-sort} = $fgrp->{-ent}->{modified}; + cmp_fp_do($fgrp, \&fgrp_enqueue, $end); + } else { # new repo, save for last + fgrp_enqueue($fgrp, $end); + } } elsif ($resume) { cmp_fp_do($self, \&resume_fetch, $uri, $fini); } else { # normal clone @@ -562,7 +608,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) { @@ -683,17 +731,17 @@ sub atomic_write ($$$) { sub run_next_puh { my ($self) = @_; - my $puh = shift @{$self->{-puh_todo}} // return; + 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_post_update_hooks { - my ($self) = @_; - my $puh = $self->{-puh} // return; - @{$self->{-puh_todo}} = @$puh; +sub run_puh { + my ($self, $fini) = @_; + $self->{-fini} = $fini; + @{$self->{-puh_todo}} = @PUH; run_next_puh($self); } @@ -746,6 +794,7 @@ sub update_ent { } } symlink($tgt, $ln) or die "symlink($tgt, $ln): $!"; + ++$self->{chg}->{nr_chg}; } } if (defined(my $t = $self->{-ent}->{modified})) { @@ -788,7 +837,6 @@ sub v1_done { # called via OnDestroy } eval { set_description($self) }; warn $@ if $@; - run_post_update_hooks($self); return if ($self->{-is_epoch} || $self->{lei}->{opt}->{'inbox-config'} ne 'always'); write_makefile($dst, 1); @@ -1048,7 +1096,9 @@ EOM warn "\t", $_, "\n" for @local; my (undef, $dn, $bn) = File::Spec->splitpath($f); - atomic_write($dn, $bn, join("\n", @list, '')); + $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 @@ -1079,7 +1129,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 ($@) { @@ -1167,6 +1220,8 @@ W: The following exist and have not been converted to symlinks EOM dump_project_list($self, $m); ft_rename($ft, $manifest, 0666); + !$self->{chg}->{nr_chg} && $lei->{opt}->{'exit-code'} and + $lei->child_error(127 << 8); } sub start_clone_url { @@ -1181,12 +1236,10 @@ 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; - for (@$puh) { - my $pfx = [ Text::ParseWords::shellwords($_) ]; - push @{$self->{-puh}}, $pfx; - } + @PUH = map { [ Text::ParseWords::shellwords($_) ] } @$puh; } eval { my $ic = $lei->{opt}->{'inbox-config'} //= 'always';