about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--Documentation/public-inbox-fetch.pod30
-rw-r--r--lib/PublicInbox/Fetch.pm21
-rwxr-xr-xscript/public-inbox-fetch4
-rw-r--r--t/lei-mirror.t12
-rw-r--r--t/v2mirror.t3
5 files changed, 61 insertions, 9 deletions
diff --git a/Documentation/public-inbox-fetch.pod b/Documentation/public-inbox-fetch.pod
index 7944fdcd..28d5638d 100644
--- a/Documentation/public-inbox-fetch.pod
+++ b/Documentation/public-inbox-fetch.pod
@@ -4,7 +4,7 @@ public-inbox-fetch - "git fetch" wrapper for v2 inbox mirrors
 
 =head1 SYNOPSIS
 
-public-inbox-fetch -C INBOX_DIR
+public-inbox-fetch [--exit-code] -C INBOX_DIR
 
 =head1 DESCRIPTION
 
@@ -31,6 +31,15 @@ file to speed up future invocations.
 
 Quiets down progress messages, also passed to L<git-fetch(1)>.
 
+=item --exit-code
+
+Exit with C<127> if no updates are done.  This can be used in
+shell scripts to avoid invoking L<public-inbox-index(1)> when
+there are no updates:
+
+        public-inbox-fetch -q --exit-code && public-inbox-index
+        test $? -eq 0 || exit $?
+
 =item -v
 
 =item --verbose
@@ -45,6 +54,23 @@ Whether to wrap L<git(1)> and L<curl(1)> commands with torsocks.
 
 Default: C<auto>
 
+=back
+
+=head1 EXIT CODES
+
+=over
+
+=item 127
+
+no updates when L</--exit-code> is used above
+
+=back
+
+public-inbox-fetch will also exit with curl L<curl(1)/EXIT CODES>
+as documented in the L<curl(1)> manpage (e.g. C<7> when curl cannot
+reach a host).  Likewise, L<git-fetch(1)> failures are also
+propagated to the user.
+
 =head1 CONTACT
 
 Feedback welcome via plain-text mail to L<mailto:meta@public-inbox.org>
@@ -60,4 +86,4 @@ License: AGPL-3.0+ L<https://www.gnu.org/licenses/agpl-3.0.txt>
 
 =head1 SEE ALSO
 
-L<public-inbox-index(1)>
+L<public-inbox-index(1)>, L<curl(1)>
diff --git a/lib/PublicInbox/Fetch.pm b/lib/PublicInbox/Fetch.pm
index 9ea55e9d..0539fe50 100644
--- a/lib/PublicInbox/Fetch.pm
+++ b/lib/PublicInbox/Fetch.pm
@@ -75,12 +75,22 @@ sub do_manifest ($$$) {
                 my $t1 = $cur->{modified} // next;
                 delete($mdiff->{$k}) if $f0 eq $f1 && $t0 == $t1;
         }
-        return unless keys %$mdiff;
+        unless (keys %$mdiff) {
+                $lei->child_error(127 << 8) if $lei->{opt}->{'exit-code'};
+                return;
+        }
         my (undef, $v1_path, @v2_epochs) =
                 PublicInbox::LeiMirror::deduce_epochs($mdiff, $ibx_uri->path);
         [ 200, $v1_path, \@v2_epochs, $muri, $ft, $mf ];
 }
 
+sub get_fingerprint2 {
+        my ($git_dir) = @_;
+        require Digest::SHA;
+        my $rd = popen_rd([qw(git show-ref)], undef, { -C => $git_dir });
+        Digest::SHA::sha256(do { local $/; <$rd> });
+}
+
 sub do_fetch {
         my ($cls, $lei, $cd) = @_;
         my $ibx_ver;
@@ -136,11 +146,14 @@ EOM
         }
         # n.b. this expects all epochs are from the same host
         my $torsocks = $lei->{curl}->torsocks($lei, $muri);
+        my $fp2 = $lei->{opt}->{'exit-code'} ? [] : undef;
+        my $xit = 127;
         for my $d (@git_dir) {
                 my $cmd;
                 my $opt = {}; # for spawn
                 if (-d $d) {
                         $opt->{-C} = $d;
+                        $fp2->[0] = get_fingerprint2($d) if $fp2;
                         $cmd = [ @$torsocks, fetch_cmd($lei, $opt) ];
                 } else {
                         my $e_uri = $ibx_uri->clone;
@@ -152,6 +165,7 @@ EOM
                                 PublicInbox::LeiMirror::clone_cmd($lei, $opt),
                                 $$e_uri, $d];
                         push @new_epoch, substr($epath, 5, -4) + 0;
+                        $xit = 0;
                 }
                 my $cerr = PublicInbox::LeiMirror::run_reap($lei, $cmd, $opt);
                 # do not bail on clone failure if we didn't have a manifest
@@ -159,6 +173,10 @@ EOM
                         $lei->child_error($cerr, "@$cmd failed");
                         return;
                 }
+                if ($fp2 && $xit) {
+                        $fp2->[1] = get_fingerprint2($d);
+                        $xit = 0 if $fp2->[0] ne $fp2->[1];
+                }
         }
         for my $i (@new_epoch) { $mg->epoch_cfg_set($i) }
         if ($ft) {
@@ -166,6 +184,7 @@ EOM
                 rename($fn, $mf) or die "E: rename($fn, $mf): $!\n";
                 $ft->unlink_on_destroy(0);
         }
+        $lei->child_error($xit << 8) if $fp2 && $xit;
 }
 
 1;
diff --git a/script/public-inbox-fetch b/script/public-inbox-fetch
index 5d303574..d7d4ba47 100755
--- a/script/public-inbox-fetch
+++ b/script/public-inbox-fetch
@@ -16,12 +16,13 @@ options:
   --torsocks VAL      whether or not to wrap git and curl commands with
                       torsocks (default: `auto')
                       Must be one of: `auto', `no' or `yes'
+  --exit-code         exit with 127 if no updates
   --verbose | -v      increase verbosity (may be repeated)
     --quiet | -q      increase verbosity (may be repeated)
     -C DIR            chdir to specified directory
 EOF
 GetOptions($opt, qw(help|h quiet|q verbose|v+ C=s@ c=s@
-        no-torsocks torsocks=s)) or die $help;
+        no-torsocks torsocks=s exit-code)) or die $help;
 if ($opt->{help}) { print $help; exit };
 require PublicInbox::Fetch; # loads Admin
 PublicInbox::Admin::do_chdir(delete $opt->{C});
@@ -33,3 +34,4 @@ my $lei = bless {
         0 => *STDIN{GLOB}, 1 => *STDOUT{GLOB}, 2 => *STDERR{GLOB},
 }, 'PublicInbox::LEI';
 PublicInbox::Fetch->do_fetch($lei, '.');
+exit(($lei->{child_error} // 0) >> 8);
diff --git a/t/lei-mirror.t b/t/lei-mirror.t
index 5238b67c..9fdda5aa 100644
--- a/t/lei-mirror.t
+++ b/t/lei-mirror.t
@@ -111,12 +111,14 @@ SKIP: {
                 'all.git alternates created');
         ok(-f "$d/t2/manifest.js.gz", 'manifest saved');
         ok(!-e "$d/t2/mirror.done", 'no leftover mirror.done');
-        ok(run_script([qw(-fetch -C), "$d/t2"], undef, $opt),
+        ok(!run_script([qw(-fetch --exit-code -C), "$d/t2"], undef, $opt),
                 '-fetch succeeds w/ manifest.js.gz');
+        is($? >> 8, 127, '--exit-code gave 127');
         unlike($err, qr/git fetch/, 'no fetch done w/ manifest');
         unlink("$d/t2/manifest.js.gz") or xbail "unlink $!";
-        ok(run_script([qw(-fetch -C), "$d/t2"], undef, $opt),
+        ok(!run_script([qw(-fetch --exit-code -C), "$d/t2"], undef, $opt),
                 '-fetch succeeds w/o manifest.js.gz');
+        is($? >> 8, 127, '--exit-code gave 127');
         like($err, qr/git fetch/, 'fetch forced w/o manifest');
 
         ok(run_script([qw(-clone -q -C), $d, "$http/t1"], undef, $opt),
@@ -124,13 +126,15 @@ SKIP: {
         ok(-d "$d/t1", 'v1 cloned');
         ok(!-e "$d/t1/mirror.done", 'no leftover file');
         ok(-f "$d/t1/manifest.js.gz", 'manifest saved');
-        ok(run_script([qw(-fetch -C), "$d/t1"], undef, $opt),
+        ok(!run_script([qw(-fetch --exit-code -C), "$d/t1"], undef, $opt),
                 'fetching v1 works');
+        is($? >> 8, 127, '--exit-code gave 127');
         unlike($err, qr/git fetch/, 'no fetch done w/ manifest');
         unlink("$d/t1/manifest.js.gz") or xbail "unlink $!";
         my $before = [ glob("$d/t1/*") ];
-        ok(run_script([qw(-fetch -C), "$d/t1"], undef, $opt),
+        ok(!run_script([qw(-fetch --exit-code -C), "$d/t1"], undef, $opt),
                 'fetching v1 works w/o manifest.js.gz');
+        is($? >> 8, 127, '--exit-code gave 127');
         unlink("$d/t1/FETCH_HEAD"); # git internal
         like($err, qr/git fetch/, 'no fetch done w/ manifest');
         ok(unlink("$d/t1/manifest.js.gz"), 'manifest created');
diff --git a/t/v2mirror.t b/t/v2mirror.t
index 54ad6945..3df5d053 100644
--- a/t/v2mirror.t
+++ b/t/v2mirror.t
@@ -98,8 +98,9 @@ $ibx->cleanup;
 my @new_epochs;
 my $fetch_each_epoch = sub {
         my %before = map { $_ => 1 } glob("$tmpdir/m/git/*");
-        run_script([qw(-fetch -q)], undef, {-C => "$tmpdir/m"}) or
+        run_script([qw(-fetch --exit-code -q)], undef, {-C => "$tmpdir/m"}) or
                 xbail '-fetch fail';
+        is($?, 0, '--exit-code 0 after fetch updated');
         my @after = grep { !$before{$_} } glob("$tmpdir/m/git/*");
         push @new_epochs, @after;
 };