X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FGit.pm;h=ff3ac40f59511aab560b114d4ad73eecc3f067b2;hb=24f55535e78be0f1fa394bcd1aebc8c027e937c7;hp=78b47096f63087c8ff49da51c9eff67a09929777;hpb=a93c7e471ca70bdb476c2285cf894e319b38f160;p=public-inbox.git diff --git a/lib/PublicInbox/Git.pm b/lib/PublicInbox/Git.pm index 78b47096..ff3ac40f 100644 --- a/lib/PublicInbox/Git.pm +++ b/lib/PublicInbox/Git.pm @@ -16,20 +16,22 @@ use Errno qw(EINTR EAGAIN ENOENT); use File::Glob qw(bsd_glob GLOB_NOSORT); use File::Spec (); use Time::HiRes qw(stat); -use PublicInbox::Spawn qw(popen_rd spawn); +use PublicInbox::Spawn qw(popen_rd which); use PublicInbox::Tmpfile; use IO::Poll qw(POLLIN); use Carp qw(croak carp); use Digest::SHA (); -use PublicInbox::DS qw(dwaitpid); +use PublicInbox::DS qw(awaitpid); our @EXPORT_OK = qw(git_unquote git_quote); our $PIPE_BUFSIZ = 65536; # Linux default our $in_cleanup; our $RDTIMEO = 60_000; # milliseconds our $async_warn; # true in read-only daemons -use constant MAX_INFLIGHT => (POSIX::PIPE_BUF * 3) / - 65; # SHA-256 hex size + "\n" in preparation for git using non-SHA1 +# 512: POSIX PIPE_BUF minimum (see pipe(7)) +# 3: @$inflight is flattened [ $OID, $cb, $arg ] +# 65: SHA-256 hex size + "\n" in preparation for git using non-SHA1 +use constant MAX_INFLIGHT => 512 * 3 / 65; my %GIT_ESC = ( a => "\a", @@ -44,7 +46,6 @@ my %GIT_ESC = ( ); my %ESC_GIT = map { $GIT_ESC{$_} => $_ } keys %GIT_ESC; - # unquote pathnames used by git, see quote.c::unquote_c_style.c in git.git sub git_unquote ($) { return $_[0] unless ($_[0] =~ /\A"(.*)"\z/); @@ -120,6 +121,26 @@ sub _bidi_pipe { } return; } + + state $EXE_ST = ''; # pack('dd', st_ctime, st_size); + my $exe = which('git') // die "git not found in $ENV{PATH}"; + my @st = stat($exe) or die "stat: $!"; + my $st = pack('dd', $st[10], $st[7]); + state $VER; + if ($st ne $EXE_ST) { + my $rd = popen_rd([ $exe, '--version' ]); + my $v = readline($rd); + $v =~ /\b([0-9]+(?:\.[0-9]+){2})/ or die + "$exe --version output: $v # unparseable"; + my @v = split(/\./, $1, 3); + $VER = ($v[0] << 24) | ($v[1] << 16) | $v[2]; + $EXE_ST = $st; + } + + # git 2.31.0+ supports -c core.abbrev=no, don't bother with + # core.abbrev=64 since not many releases had SHA-256 prior to 2.31 + my $abbr = $VER < (2 << 24 | 31 << 16) ? 40 : 'no'; + pipe(my ($out_r, $out_w)) or $self->fail("pipe failed: $!"); my $rdr = { 0 => $out_r, pgid => 0 }; my $gd = $self->{git_dir}; @@ -127,8 +148,8 @@ sub _bidi_pipe { $rdr->{-C} = $gd; $gd = $1; } - my @cmd = (qw(git), "--git-dir=$gd", - qw(-c core.abbrev=40 cat-file), $batch); + my @cmd = ($exe, "--git-dir=$gd", '-c', "core.abbrev=$abbr", + 'cat-file', $batch); if ($err) { my $id = "git.$self->{git_dir}$batch.err"; my $fh = tmpfile($id) or $self->fail("tmpfile($id): $!"); @@ -136,13 +157,14 @@ sub _bidi_pipe { $rdr->{2} = $fh; } my ($in_r, $p) = popen_rd(\@cmd, undef, $rdr); - $self->{$pid} = $p; + awaitpid($self->{$pid} = $p, undef); $self->{"$pid.owner"} = $$; $out_w->autoflush(1); if ($^O eq 'linux') { # 1031: F_SETPIPE_SZ fcntl($out_w, 1031, 4096); fcntl($in_r, 1031, 4096) if $batch eq '--batch-check'; } + $out_w->blocking(0); $self->{$out} = $out_w; $self->{$in} = $in_r; } @@ -201,7 +223,9 @@ sub cat_async_retry ($$) { for (my $i = 0; $i < @$inflight; $i += 3) { $buf .= "$inflight->[$i]\n"; } + $self->{out}->blocking(1); # brand new pipe, should never block print { $self->{out} } $buf or $self->fail("write error: $!"); + $self->{out}->blocking(0); my $req = shift @$inflight; unshift(@$inflight, \$req); # \$ref to indicate retried @@ -303,13 +327,26 @@ sub check_async_begin ($) { $self->{inflight_c} = []; } +sub write_all { + my ($self, $out, $buf, $read_step, $inflight) = @_; + $read_step->($self, $inflight) while @$inflight >= MAX_INFLIGHT; + do { + my $w = syswrite($out, $buf); + if (defined $w) { + return if $w == length($buf); + substr($buf, 0, $w, ''); # sv_chop + } elsif ($! != EAGAIN) { + $self->fail("write: $!"); + } + $read_step->($self, $inflight); + } while (1); +} + sub check_async ($$$$) { my ($self, $oid, $cb, $arg) = @_; my $inflight_c = $self->{inflight_c} // check_async_begin($self); - while (scalar(@$inflight_c) >= MAX_INFLIGHT) { - check_async_step($self, $inflight_c); - } - print { $self->{out_c} } $oid, "\n" or $self->fail("write error: $!"); + write_all($self, $self->{out_c}, $oid."\n", + \&check_async_step, $inflight_c); push(@$inflight_c, $oid, $cb, $arg); } @@ -339,9 +376,9 @@ sub _destroy { delete @$self{($rbuf, $in, $out)}; delete $self->{$err} if $err; # `err_c' - # GitAsyncCat::event_step may delete {pid} - my $p = delete $self->{$pid} or return; - dwaitpid($p) if $$ == $self->{"$pid.owner"}; + # GitAsyncCat::event_step may delete {$pid} + my $p = delete($self->{$pid}) // return; + awaitpid($p) if $$ == $self->{"$pid.owner"}; } sub async_abort ($) { @@ -426,6 +463,7 @@ sub cleanup { scalar(@{$self->{inflight} // []})); local $in_cleanup = 1; delete $self->{async_cat}; + delete $self->{async_chk}; async_wait_all($self); delete $self->{inflight}; delete $self->{inflight_c}; @@ -451,24 +489,37 @@ sub DESTROY { cleanup(@_) } sub local_nick ($) { # don't show full FS path, basename should be OK: - $_[0]->{git_dir} =~ m!/([^/]+?)(?:/*\.git/*)?\z! ? "$1.git" : '???'; + $_[0]->{nick} // ($_[0]->{git_dir} =~ m!/([^/]+?)(?:/*\.git/*)?\z! ? + "$1.git" : undef); } sub host_prefix_url ($$) { my ($env, $url) = @_; return $url if index($url, '//') >= 0; - my $scheme = $env->{'psgi.url_scheme'}; my $host_port = $env->{HTTP_HOST} // "$env->{SERVER_NAME}:$env->{SERVER_PORT}"; - "$scheme://$host_port". ($env->{SCRIPT_NAME} || '/') . $url; + my $sn = $env->{SCRIPT_NAME} // ''; + "$env->{'psgi.url_scheme'}://$host_port$sn/$url"; } +sub base_url { # for coderepos, PSGI-only + my ($self, $env) = @_; # env - PSGI env + my $nick = $self->{nick} // return undef; + my $url = host_prefix_url($env, ''); + # for mount in Plack::Builder + $url .= '/' if substr($url, -1, 1) ne '/'; + $url . $nick . '/'; +} + +sub isrch {} # TODO + sub pub_urls { my ($self, $env) = @_; if (my $urls = $self->{cgit_url}) { - return map { host_prefix_url($env, $_) } @$urls; + map { host_prefix_url($env, $_) } @$urls; + } else { + (base_url($self, $env) // '???'); } - (local_nick($self)); } sub cat_async_begin { @@ -482,10 +533,7 @@ sub cat_async_begin { sub cat_async ($$$;$) { my ($self, $oid, $cb, $arg) = @_; my $inflight = $self->{inflight} // cat_async_begin($self); - while (scalar(@$inflight) >= MAX_INFLIGHT) { - cat_async_step($self, $inflight); - } - print { $self->{out} } $oid, "\n" or $self->fail("write error: $!"); + write_all($self, $self->{out}, $oid."\n", \&cat_async_step, $inflight); push(@$inflight, $oid, $cb, $arg); } @@ -498,13 +546,31 @@ sub modified ($) { (split(/ /, <$fh> // time))[0] + 0; # integerize for JSON } +sub try_cat { + my ($path) = @_; + open(my $fh, '<', $path) or return ''; + local $/; + <$fh> // ''; +} + +sub cat_desc ($) { + my $desc = try_cat($_[0]); + chomp $desc; + utf8::decode($desc); + $desc =~ s/\s+/ /smg; + $desc eq '' ? undef : $desc; +} + sub description { - my $desc = ''; - if (open(my $fh, '<:utf8', "$_[0]->{git_dir}/description")) { - local $/ = "\n"; - chomp($desc = <$fh> // ''); - } - $desc eq '' ? 'Unnamed repository' : $desc; + cat_desc("$_[0]->{git_dir}/description") // 'Unnamed repository'; +} + +sub cloneurl { + my ($self, $env) = @_; + $self->{cloneurl} // do { + my @urls = split(/\s+/s, try_cat("$self->{git_dir}/cloneurl")); + scalar(@urls) ? ($self->{cloneurl} = \@urls) : undef; + } // [ substr(base_url($self, $env), 0, -1) ]; } # for grokmirror, which doesn't read gitweb.description