X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FGit.pm;h=2ae5eff9c01614a13f0781561c9aed64d1a52838;hb=081918279573b209c27a91e443c37df597bcd43f;hp=917fa4a16e96b76a4a45ebd560035a4bac1b78a4;hpb=c118d783a22a2080964899266c91c1a1ac69f481;p=public-inbox.git
diff --git a/lib/PublicInbox/Git.pm b/lib/PublicInbox/Git.pm
index 917fa4a1..2ae5eff9 100644
--- a/lib/PublicInbox/Git.pm
+++ b/lib/PublicInbox/Git.pm
@@ -1,4 +1,4 @@
-# Copyright (C) 2014-2020 all contributors
+# Copyright (C) 2014-2021 all contributors
# License: GPLv2 or later
#
# Used to read files from a git repository without excessive forking.
@@ -12,17 +12,20 @@ use v5.10.1;
use parent qw(Exporter);
use POSIX ();
use IO::Handle; # ->autoflush
-use Errno qw(EINTR);
+use Errno qw(EINTR EAGAIN);
use File::Glob qw(bsd_glob GLOB_NOSORT);
use File::Spec ();
use Time::HiRes qw(stat);
use PublicInbox::Spawn qw(popen_rd);
use PublicInbox::Tmpfile;
+use IO::Poll qw(POLLIN);
use Carp qw(croak);
use Digest::SHA ();
+use PublicInbox::DS qw(dwaitpid);
our @EXPORT_OK = qw(git_unquote git_quote);
our $PIPE_BUFSIZ = 65536; # Linux default
our $in_cleanup;
+our $RDTIMEO = 60_000; # milliseconds
use constant MAX_INFLIGHT =>
(($^O eq 'linux' ? 4096 : POSIX::_POSIX_PIPE_BUF()) * 3)
@@ -94,9 +97,9 @@ sub alternates_changed {
sub last_check_err {
my ($self) = @_;
my $fh = $self->{err_c} or return;
- sysseek($fh, 0, 0) or fail($self, "sysseek failed: $!");
+ sysseek($fh, 0, 0) or $self->fail("sysseek failed: $!");
defined(sysread($fh, my $buf, -s $fh)) or
- fail($self, "sysread failed: $!");
+ $self->fail("sysread failed: $!");
$buf;
}
@@ -105,24 +108,25 @@ sub _bidi_pipe {
if ($self->{$pid}) {
if (defined $err) { # "err_c"
my $fh = $self->{$err};
- sysseek($fh, 0, 0) or fail($self, "sysseek failed: $!");
- truncate($fh, 0) or fail($self, "truncate failed: $!");
+ sysseek($fh, 0, 0) or $self->fail("sysseek failed: $!");
+ truncate($fh, 0) or $self->fail("truncate failed: $!");
}
return;
}
my ($out_r, $out_w);
- pipe($out_r, $out_w) or fail($self, "pipe failed: $!");
+ pipe($out_r, $out_w) or $self->fail("pipe failed: $!");
my @cmd = (qw(git), "--git-dir=$self->{git_dir}",
qw(-c core.abbrev=40 cat-file), $batch);
my $redir = { 0 => $out_r };
if ($err) {
my $id = "git.$self->{git_dir}$batch.err";
- my $fh = tmpfile($id) or fail($self, "tmpfile($id): $!");
+ my $fh = tmpfile($id) or $self->fail("tmpfile($id): $!");
$self->{$err} = $fh;
$redir->{2} = $fh;
}
my ($in_r, $p) = popen_rd(\@cmd, undef, $redir);
$self->{$pid} = $p;
+ $self->{"$pid.owner"} = $$;
$out_w->autoflush(1);
if ($^O eq 'linux') { # 1031: F_SETPIPE_SZ
fcntl($out_w, 1031, 4096);
@@ -132,6 +136,8 @@ sub _bidi_pipe {
$self->{$in} = $in_r;
}
+sub poll_in ($) { IO::Poll::_poll($RDTIMEO, fileno($_[0]), my $ev = POLLIN) }
+
sub my_read ($$$) {
my ($fh, $rbuf, $len) = @_;
my $left = $len - length($$rbuf);
@@ -140,9 +146,12 @@ sub my_read ($$$) {
$r = sysread($fh, $$rbuf, $PIPE_BUFSIZ, length($$rbuf));
if ($r) {
$left -= $r;
+ } elsif (defined($r)) { # EOF
+ return 0;
} else {
- next if (!defined($r) && $! == EINTR);
- return $r;
+ next if ($! == EAGAIN and poll_in($fh));
+ next if $! == EINTR; # may be set by sysread or poll_in
+ return; # unrecoverable error
}
}
\substr($$rbuf, 0, $len, '');
@@ -154,9 +163,15 @@ sub my_readline ($$) {
if ((my $n = index($$rbuf, "\n")) >= 0) {
return substr($$rbuf, 0, $n + 1, '');
}
- my $r = sysread($fh, $$rbuf, $PIPE_BUFSIZ, length($$rbuf));
- next if $r || (!defined($r) && $! == EINTR);
- return defined($r) ? '' : undef; # EOF or error
+ my $r = sysread($fh, $$rbuf, $PIPE_BUFSIZ, length($$rbuf))
+ and next;
+
+ # return whatever's left on EOF
+ return substr($$rbuf, 0, length($$rbuf)+1, '') if defined($r);
+
+ next if ($! == EAGAIN and poll_in($fh));
+ next if $! == EINTR; # may be set by sysread or poll_in
+ return; # unrecoverable error
}
}
@@ -174,7 +189,7 @@ sub cat_async_retry ($$$$$) {
for (my $i = 0; $i < @$inflight; $i += 3) {
$buf .= "$inflight->[$i]\n";
}
- print { $self->{out} } $buf or fail($self, "write error: $!");
+ print { $self->{out} } $buf or $self->fail("write error: $!");
unshift(@$inflight, \$req, $cb, $arg); # \$ref to indicate retried
cat_async_step($self, $inflight); # take one step
@@ -204,7 +219,8 @@ sub cat_async_step ($$) {
$type = 'missing';
$oid = ref($req) ? $$req : $req if $oid eq '';
} else {
- $self->fail("Unexpected result from async git cat-file: $head");
+ my $err = $! ? " ($!)" : '';
+ $self->fail("bad result from async cat-file: $head$err");
}
$self->{cat_rbuf} = $rbuf if $$rbuf ne '';
eval { $cb->($bref, $oid, $type, $size, $arg) };
@@ -224,17 +240,16 @@ sub batch_prepare ($) {
}
sub _cat_file_cb {
- my ($bref, undef, undef, $size, $result) = @_;
- @$result = ($bref, $size);
+ my ($bref, $oid, $type, $size, $result) = @_;
+ @$result = ($bref, $oid, $type, $size);
}
sub cat_file {
- my ($self, $oid, $sizeref) = @_;
+ my ($self, $oid) = @_;
my $result = [];
cat_async($self, $oid, \&_cat_file_cb, $result);
cat_async_wait($self);
- $$sizeref = $result->[1] if $sizeref;
- $result->[0];
+ wantarray ? @$result : $result->[0];
}
sub check_async_step ($$) {
@@ -251,7 +266,7 @@ sub check_async_step ($$) {
# https://public-inbox.org/git/20190118033845.s2vlrb3wd3m2jfzu@dcvr/T/
if ($hex eq 'dangling' || $hex eq 'notdir' || $hex eq 'loop') {
my $ret = my_read($self->{in_c}, $rbuf, $type + 1);
- fail($self, defined($ret) ? 'read EOF' : "read: $!") if !$ret;
+ $self->fail(defined($ret) ? 'read EOF' : "read: $!") if !$ret;
}
$self->{chk_rbuf} = $rbuf if $$rbuf ne '';
eval { $cb->($hex, $type, $size, $arg, $self) };
@@ -280,7 +295,7 @@ sub check_async ($$$$) {
while (scalar(@$inflight_c) >= MAX_INFLIGHT) {
check_async_step($self, $inflight_c);
}
- print { $self->{out_c} } $oid, "\n" or fail($self, "write error: $!");
+ print { $self->{out_c} } $oid, "\n" or $self->fail("write error: $!");
push(@$inflight_c, $oid, $cb, $arg);
}
@@ -312,10 +327,7 @@ sub _destroy {
# GitAsyncCat::event_step may delete {pid}
my $p = delete $self->{$pid} or return;
-
- # PublicInbox::DS may not be loaded
- eval { PublicInbox::DS::dwaitpid($p, undef, undef) };
- waitpid($p, 0) if $@; # wait synchronously if not in event loop
+ dwaitpid($p) if $$ == $self->{"$pid.owner"};
}
sub cat_async_abort ($) {
@@ -333,25 +345,41 @@ sub cat_async_abort ($) {
cleanup($self);
}
-sub fail {
+sub fail { # may be augmented in subclasses
my ($self, $msg) = @_;
cat_async_abort($self);
croak(ref($self) . ' ' . ($self->{git_dir} // '') . ": $msg");
}
+# $git->popen(qw(show f00)); # or
+# $git->popen(qw(show f00), { GIT_CONFIG => ... }, { 2 => ... });
sub popen {
- my ($self, @cmd) = @_;
- @cmd = ('git', "--git-dir=$self->{git_dir}", @cmd);
- popen_rd(\@cmd);
+ my ($self, $cmd) = splice(@_, 0, 2);
+ $cmd = [ 'git', "--git-dir=$self->{git_dir}",
+ ref($cmd) ? @$cmd : ($cmd, grep { defined && !ref } @_) ];
+ popen_rd($cmd, grep { !defined || ref } @_); # env and opt
}
+# same args as popen above
sub qx {
- my ($self, @cmd) = @_;
- my $fh = $self->popen(@cmd);
- local $/ = "\n";
- return <$fh> if wantarray;
- local $/;
- <$fh>
+ my $fh = popen(@_);
+ if (wantarray) {
+ my @ret = <$fh>;
+ close $fh; # caller should check $?
+ @ret;
+ } else {
+ local $/;
+ my $ret = <$fh>;
+ close $fh; # caller should check $?
+ $ret;
+ }
+}
+
+sub date_parse {
+ my $self = shift;
+ map {
+ substr($_, length('--max-age='), -1)
+ } $self->qx('rev-parse', map { "--since=$_" } @_);
}
# check_async and cat_async may trigger the other, so ensure they're
@@ -399,7 +427,7 @@ sub local_nick ($) {
my $ret = '???';
# don't show full FS path, basename should be OK:
if ($self->{git_dir} =~ m!/([^/]+)(?:/\.git)?\z!) {
- $ret = "/path/to/$1";
+ $ret = "$1.git";
}
wantarray ? ($ret) : $ret;
}
@@ -435,24 +463,10 @@ sub cat_async ($$$;$) {
while (scalar(@$inflight) >= MAX_INFLIGHT) {
cat_async_step($self, $inflight);
}
- print { $self->{out} } $oid, "\n" or fail($self, "write error: $!");
+ print { $self->{out} } $oid, "\n" or $self->fail("write error: $!");
push(@$inflight, $oid, $cb, $arg);
}
-sub async_prefetch {
- my ($self, $oid, $cb, $arg) = @_;
- if (my $inflight = $self->{inflight}) {
- # we could use MAX_INFLIGHT here w/o the halving,
- # but lets not allow one client to monopolize a git process
- if (scalar(@$inflight) < int(MAX_INFLIGHT/2)) {
- print { $self->{out} } $oid, "\n" or
- fail($self, "write error: $!");
- return push(@$inflight, $oid, $cb, $arg);
- }
- }
- undef;
-}
-
sub extract_cmt_time {
my ($bref, undef, undef, undef, $modified) = @_;
@@ -483,14 +497,13 @@ sub modified ($) {
# templates/this--description in git.git
sub manifest_entry {
my ($self, $epoch, $default_desc) = @_;
- my ($fh, $pid) = $self->popen('show-ref');
+ my $fh = $self->popen('show-ref');
my $dig = Digest::SHA->new(1);
while (read($fh, my $buf, 65536)) {
$dig->add($buf);
}
- close $fh;
- waitpid($pid, 0);
- return if $?; # empty, uninitialized git repo
+ close $fh or return; # empty, uninitialized git repo
+ undef $fh; # for open, below
my $git_dir = $self->{git_dir};
my $ent = {
fingerprint => $dig->hexdigest,