| # Copyright (C) all contributors <meta@public-inbox.org> |
| # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> |
| |
| # show any VCS object, similar to "git show" |
| # |
| # This can use a "solver" to reconstruct blobs based on git |
| # patches (with abbreviated OIDs in the header). However, the |
| # abbreviated OIDs must match exactly what's in the original |
| # email (unless a normal code repo already has the blob). |
| # |
| # In other words, we can only reliably reconstruct blobs based |
| # on links generated by ViewDiff (and only if the emailed |
| # patches apply 100% cleanly to published blobs). |
| |
| package PublicInbox::ViewVCS; |
| use strict; |
| use v5.10.1; |
| use File::Temp 0.19 (); # newdir |
| use PublicInbox::SolverGit; |
| use PublicInbox::Git; |
| use PublicInbox::GitAsyncCat; |
| use PublicInbox::WwwStream qw(html_oneshot); |
| use PublicInbox::Linkify; |
| use PublicInbox::Tmpfile; |
| use PublicInbox::ViewDiff qw(flush_diff uri_escape_path); |
| use PublicInbox::View; |
| use PublicInbox::Eml; |
| use PublicInbox::OnDestroy; |
| use Text::Wrap qw(wrap); |
| use PublicInbox::Hval qw(ascii_html to_filename prurl utf8_maybe); |
| use POSIX qw(strftime); |
| use autodie qw(open seek truncate); |
| use Fcntl qw(SEEK_SET); |
| my $hl = eval { |
| require PublicInbox::HlMod; |
| PublicInbox::HlMod->new; |
| }; |
| |
| my %QP_MAP = ( A => 'oid_a', a => 'path_a', b => 'path_b' ); |
| our $MAX_SIZE = 1024 * 1024; # TODO: configurable |
| my $BIN_DETECT = 8000; # same as git |
| my $SHOW_FMT = '--pretty=format:'.join('%n', '%P', '%p', '%H', '%T', '%s', '%f', |
| '%an <%ae> %ai', '%cn <%ce> %ci', '%b%x00'); |
| |
| my %GIT_MODE = ( |
| '100644' => ' ', # blob |
| '100755' => 'x', # executable blob |
| '040000' => 'd', # tree |
| '120000' => 'l', # symlink |
| '160000' => 'g', # commit (gitlink) |
| ); |
| |
| # TODO: not fork safe, but we don't fork w/o exec in PublicInbox::WWW |
| my (@solver_q, $solver_lim); |
| my $solver_nr = 0; |
| |
| sub html_page ($$;@) { |
| my ($ctx, $code) = @_[0, 1]; |
| my $wcb = delete $ctx->{-wcb}; |
| $ctx->{-upfx} //= '../../'; # from "/$INBOX/$OID/s/" |
| my $res = html_oneshot($ctx, $code, @_[2..$#_]); |
| $wcb ? $wcb->($res) : $res; |
| } |
| |
| sub dbg_log ($) { |
| my ($ctx) = @_; |
| my $log = delete $ctx->{lh} // die 'BUG: already captured debug log'; |
| if (!CORE::seek($log, 0, SEEK_SET)) { |
| warn "seek(log): $!"; |
| return '<pre>debug log seek error</pre>'; |
| } |
| $log = eval { PublicInbox::IO::read_all $log } // do { |
| warn "read(log): $@"; |
| return '<pre>debug log read error</pre>'; |
| }; |
| return '' if $log eq ''; |
| $ctx->{-linkify} //= PublicInbox::Linkify->new; |
| "<hr><pre>debug log:\n\n". |
| $ctx->{-linkify}->to_html($log).'</pre>'; |
| } |
| |
| sub stream_blob_parse_hdr { # {parse_hdr} for Qspawn |
| my ($r, $bref, $ctx) = @_; |
| my ($git, $oid, $type, $size, $di) = @{$ctx->{-res}}; |
| my @cl = ('Content-Length', $size); |
| if (!defined $r) { # sysread error |
| html_page($ctx, 500, dbg_log($ctx)); |
| } elsif (index($$bref, "\0") >= 0) { |
| [200, [qw(Content-Type application/octet-stream), @cl] ]; |
| } else { |
| my $n = length($$bref); |
| if ($n >= $BIN_DETECT || $n == $size) { |
| return [200, [ 'Content-Type', |
| 'text/plain; charset=UTF-8', @cl ] ]; |
| } |
| if ($r == 0) { |
| my $log = dbg_log($ctx); |
| warn "premature EOF on $oid $log"; |
| return html_page($ctx, 500, $log); |
| } |
| undef; # bref keeps growing |
| } |
| } |
| |
| sub stream_large_blob ($$) { |
| my ($ctx, $res) = @_; |
| $ctx->{-res} = $res; |
| my ($git, $oid, $type, $size, $di) = @$res; |
| my $cmd = $git->cmd('cat-file', $type, $oid); |
| my $qsp = PublicInbox::Qspawn->new($cmd); |
| $ctx->{env}->{'qspawn.wcb'} = $ctx->{-wcb}; |
| $qsp->psgi_yield($ctx->{env}, undef, \&stream_blob_parse_hdr, $ctx); |
| } |
| |
| sub show_other_result ($$) { # future-proofing |
| my ($bref, $ctx) = @_; |
| if (my $qsp_err = delete $ctx->{-qsp_err}) { |
| return html_page($ctx, 500, dbg_log($ctx) . |
| "git show error:$qsp_err"); |
| } |
| my $l = PublicInbox::Linkify->new; |
| utf8_maybe($$bref); |
| html_page($ctx, 200, '<pre>', $l->to_html($$bref), '</pre><hr>', |
| dbg_log($ctx)); |
| } |
| |
| sub cmt_title { # git->cat_async callback |
| my ($bref, $oid, $type, $size, $ctx_cb) = @_; |
| utf8_maybe($$bref); |
| my $title = $$bref =~ /\r?\n\r?\n([^\r\n]+)\r?\n?/ ? $1 : ''; |
| # $ctx_cb is [ $ctx, $cmt_fin ] |
| push @{$ctx_cb->[0]->{-cmt_pt}}, ascii_html($title); |
| } |
| |
| sub do_cat_async { |
| my ($arg, $cb, @req) = @_; |
| # favor git(1) over Gcf2 (libgit2) for SHA-256 support |
| my $ctx = ref $arg eq 'ARRAY' ? $arg->[0] : $arg; |
| $ctx->{git}->cat_async($_, $cb, $arg) for @req; |
| if ($ctx->{env}->{'pi-httpd.async'}) { |
| $ctx->{git}->watch_async; |
| } else { # synchronous, generic PSGI |
| $ctx->{git}->cat_async_wait; |
| } |
| } |
| |
| sub do_check_async { |
| my ($ctx, $cb, @req) = @_; |
| if ($ctx->{env}->{'pi-httpd.async'}) { |
| async_check($ctx, $_, $cb, $ctx) for @req; |
| } else { # synchronous, generic PSGI |
| $ctx->{git}->check_async($_, $cb, $ctx) for @req; |
| $ctx->{git}->check_async_wait; |
| } |
| } |
| |
| sub cmt_hdr_prep { # psgi_qx cb for "git show" commit output |
| my ($fh, $ctx, $cmt_fin) = @_; |
| return if $ctx->{-qsp_err_h}; # let cmt_fin handle it |
| seek $fh, 0, SEEK_SET; |
| my $buf = do { local $/ = "\0"; <$fh> } // die "readline: $!"; |
| chop($buf) eq "\0" or die 'no NUL in git show -z output'; |
| utf8_maybe($buf); # non-UTF-8 commits exist |
| chomp $buf; |
| (my $P, my $p, @{$ctx->{cmt_info}}) = split(/\n/, $buf, 9); |
| truncate $fh, 0; |
| return unless $P; |
| seek $fh, 0, SEEK_SET; |
| my $qsp_p = PublicInbox::Qspawn->new($ctx->{git}->cmd(qw(show |
| --encoding=UTF-8 --pretty=format:%n -M --stat -p), $ctx->{oid}), |
| undef, { 1 => $fh }); |
| $qsp_p->{qsp_err} = \($ctx->{-qsp_err_p} = ''); |
| $qsp_p->psgi_qx($ctx->{env}, undef, \&cmt_patch_prep, $ctx, $cmt_fin); |
| @{$ctx->{-cmt_P}} = split / /, $P; |
| @{$ctx->{-cmt_p}} = split / /, $p; # abbreviated |
| do_cat_async([$ctx, $cmt_fin], \&cmt_title, @{$ctx->{-cmt_P}}); |
| } |
| |
| sub read_patchid { # psgi_qx cb |
| my ($bref, $ctx, $cmt_fin) = @_; |
| my ($patchid) = split(/ /, $$bref); # ignore commit |
| $ctx->{-q_value_html} = "patchid:$patchid" if defined $patchid; |
| } |
| |
| sub cmt_patch_prep { # psgi_qx cb |
| my ($fh, $ctx, $cmt_fin) = @_; |
| return if $ctx->{-qsp_err_p}; # let cmt_fin handle error |
| return if -s $fh > $MAX_SIZE; # too big to show, too big to patch-id |
| seek $fh, 0, SEEK_SET; |
| my $qsp = PublicInbox::Qspawn->new( |
| $ctx->{git}->cmd(qw(patch-id --stable)), |
| undef, { 0 => $fh }); |
| $qsp->{qsp_err} = \$ctx->{-qsp_err_p}; |
| $qsp->psgi_qx($ctx->{env}, undef, \&read_patchid, $ctx, $cmt_fin); |
| } |
| |
| sub ibx_url_for { |
| my ($ctx) = @_; |
| $ctx->{ibx} and return; # fall back to $upfx |
| $ctx->{git} or die 'BUG: no {git}'; |
| if (my $ALL = $ctx->{www}->{pi_cfg}->ALL) { |
| if (defined(my $u = $ALL->base_url($ctx->{env}))) { |
| return wantarray ? ($u) : $u; |
| } |
| } |
| my @ret; |
| if (my $ibx_names = $ctx->{git}->{ibx_names}) { |
| my $by_name = $ctx->{www}->{pi_cfg}->{-by_name}; |
| for my $name (@$ibx_names) { |
| my $ibx = $by_name->{$name} // do { |
| warn "inbox `$name' no longer exists\n"; |
| next; |
| }; |
| $ibx->isrch // next; |
| my $u = defined($ibx->{url}) ? |
| prurl($ctx->{env}, $ibx->{url}) : $name; |
| $u .= '/' if substr($u, -1) ne '/'; |
| push @ret, $u; |
| } |
| } |
| wantarray ? (@ret) : $ret[0]; |
| } |
| |
| sub prep_merge_titles ($) { |
| my ($in_titles, @s, $t, @lines); |
| chomp(@lines = split /^/ms, $_[0]); |
| for (@lines) { |
| if (/^\* /) { # * branch/name: |
| $in_titles = 1; |
| } elsif ($in_titles) { # commit titles |
| if (s/^ //) { # break up Xapian phrases |
| $t = join " AND\n ", map { qq{s:"$_"} } |
| split /["\x{201c}\x{201d}]+/; |
| push @s, $t if $t ne 's:"..."'; |
| } else { # trailing text or trailers? |
| undef $in_titles; |
| } |
| } # else: preamble text |
| } |
| @s ? \@s : undef; |
| } |
| |
| sub cmt_fin { # OnDestroy cb for `git show' commit output |
| my ($ctx) = @_; |
| my ($eh, $ep) = delete @$ctx{qw(-qsp_err_h -qsp_err_p)}; |
| if ($eh || $ep) { |
| my $e = join(' - ', grep defined, $eh, $ep); |
| return html_page($ctx, 500, dbg_log($ctx) . |
| "git show/patch-id error:$e"); |
| } |
| $ctx->{-linkify} //= PublicInbox::Linkify->new; |
| my $upfx = $ctx->{-upfx} = '../../'; # from "/$INBOX/$OID/s/" |
| my ($H, $T, $s, $f, $au, $co, $bdy) = @{delete $ctx->{cmt_info}}; |
| # try to keep author and committer dates lined up |
| my $x = length($au) - length($co); |
| if ($x > 0) { |
| $x = ' ' x $x; |
| $co =~ s/>/>$x/; |
| } elsif ($x < 0) { |
| $x = ' ' x (-$x); |
| $au =~ s/>/>$x/; |
| } |
| $_ = ascii_html($_) for ($au, $co); |
| my ($merge_titles, $ibx_url, $ibx_url_html, $alt); |
| $ibx_url = ibx_url_for($ctx); |
| if (defined $ibx_url) { |
| $ibx_url =~ m!://! or |
| substr($ibx_url, 0, 0, '../../../'); |
| $ibx_url_html = ascii_html($ibx_url); |
| $alt = " `$ibx_url_html'"; |
| } else { |
| $ibx_url = $ibx_url_html = $upfx; |
| $alt = ''; |
| } |
| $au =~ s!(> +)([0-9]{4,}-\S+ \S+)! |
| my ($gt, $t) = ($1, $2); |
| $t =~ tr/ :-//d; |
| qq($gt<a |
| href="$ibx_url?t=$t" |
| title="list contemporary emails">$2</a>) |
| !e; |
| |
| my $title_html = $ctx->{-title_html} = $ctx->{-linkify}->to_html($s); |
| my ($P, $p, $pt) = delete @$ctx{qw(-cmt_P -cmt_p -cmt_pt)}; |
| $_ = qq(<a href="$upfx$_/s/">).shift(@$p).'</a> '.shift(@$pt) for @$P; |
| if (@$P == 1) { |
| $x = qq{ (<a |
| href="$f.patch">patch</a>)\n <a href=#parent>parent</a> $P->[0]}; |
| } elsif (@$P > 1) { |
| $merge_titles = 1; |
| $x = qq(\n <a href=#parents>parents</a> $P->[0]\n); |
| shift @$P; |
| $x .= qq( $_\n) for @$P; |
| chop $x; |
| } else { |
| $x = ' (<a href=#root_commit>root commit</a>)'; |
| } |
| PublicInbox::WwwStream::html_init($ctx); |
| my $zfh = $ctx->zfh; |
| print $zfh <<EOM; |
| <pre> <a href=#commit>commit</a> $H$x |
| <a href=#tree>tree</a> <a href="$upfx$T/s/?b=">$T</a> |
| author $au |
| committer $co |
| |
| <b>$title_html</b> |
| EOM |
| if (length($bdy)) { |
| print $zfh "\n", $ctx->{-linkify}->to_html($bdy); |
| $merge_titles = prep_merge_titles $bdy if $merge_titles; |
| } |
| undef $bdy; # free memory |
| my $fh = delete $ctx->{patch_fh}; |
| if (-s $fh > $MAX_SIZE) { |
| print $zfh '</pre><hr><pre>patch is too large to show</pre>'; |
| } else { # prepare flush_diff: |
| seek $fh, 0, SEEK_SET; |
| PublicInbox::IO::read_all $fh, -s _, \$x; |
| utf8_maybe($x); |
| $ctx->{-apfx} = $ctx->{-spfx} = $upfx; |
| $x =~ s/\r?\n/\n/gs; |
| $ctx->{-anchors} = {} if $x =~ /^diff --git /sm; |
| flush_diff($ctx, \$x); # undefs $x |
| # TODO: should there be another textarea which attempts to |
| # search for the exact email which was applied to make this |
| # commit? |
| print $zfh '</pre>'; |
| my ($rows, $q) = PublicInbox::View::dfqry_text $ctx, $s; |
| print $zfh <<EOM if $rows; |
| <hr><form action="$ibx_url" |
| id=related><pre>find related emails, including ancestors/descendants/conflicts |
| <textarea name=q cols=78 rows=$rows>$q</textarea> |
| <input type=submit value="search$alt" |
| />\t(<a href="${ibx_url}_/text/help/">help</a>)</pre></form> |
| EOM |
| } |
| if (ref $merge_titles) { |
| print $zfh <<EOM; |
| <hr><form action="$ibx_url" id=merged><pre>find merged patch emails |
| <textarea name=q cols=78 |
| EOM |
| my $nr = scalar @$merge_titles; |
| $merge_titles = ascii_html(join ") OR\n(", @$merge_titles); |
| print $zfh 'rows=', $nr, '>(', $merge_titles, <<EOM; |
| )</textarea> |
| <input type=submit value="search$alt" |
| /></pre></form> |
| EOM |
| undef $merge_titles; |
| } |
| chop($x = <<EOM); |
| <hr><pre>glossary |
| -------- |
| <dfn |
| id=commit>Commit</dfn> objects reference one tree, and zero or more parents. |
| |
| Single <dfn |
| id=parent>parent</dfn> commits can typically generate a patch in |
| unified diff format via `git format-patch'. |
| |
| Multiple <dfn id=parents>parents</dfn> means the commit is a merge. |
| |
| <dfn id=root_commit>Root commits</dfn> have no ancestor. Note that it is |
| possible to have multiple root commits when merging independent histories. |
| |
| Every commit references one top-level <dfn id=tree>tree</dfn> object.</pre> |
| EOM |
| delete($ctx->{-wcb})->($ctx->html_done($x)); |
| } |
| |
| sub stream_patch_parse_hdr { # {parse_hdr} for Qspawn |
| my ($r, $bref, $ctx) = @_; |
| if (!defined $r) { # sysread error |
| html_page($ctx, 500, dbg_log($ctx)); |
| } elsif (index($$bref, "\n\n") >= 0) { |
| my $eml = bless { hdr => $bref }, 'PublicInbox::Eml'; |
| my $fn = to_filename($eml->header('Subject') // ''); |
| $fn = substr($fn // 'PATCH-no-subject', 6); # drop "PATCH-" |
| return [ 200, [ 'Content-Type', 'text/plain; charset=UTF-8', |
| 'Content-Disposition', |
| qq(inline; filename=$fn.patch) ] ]; |
| } elsif ($r == 0) { |
| my $log = dbg_log($ctx); |
| warn "premature EOF on $ctx->{patch_oid} $log"; |
| return html_page($ctx, 500, $log); |
| } else { |
| undef; # bref keeps growing until "\n\n" |
| } |
| } |
| |
| sub show_patch ($$) { |
| my ($ctx, $res) = @_; |
| my ($git, $oid) = @$res; |
| my $cmd = $git->cmd(qw(format-patch -1 --stdout -C), |
| "--signature=git format-patch -1 --stdout -C $oid", $oid); |
| my $qsp = PublicInbox::Qspawn->new($cmd); |
| $ctx->{env}->{'qspawn.wcb'} = $ctx->{-wcb}; |
| $ctx->{patch_oid} = $oid; |
| $qsp->psgi_yield($ctx->{env}, undef, \&stream_patch_parse_hdr, $ctx); |
| } |
| |
| sub show_commit ($$) { |
| my ($ctx, $res) = @_; |
| return show_patch($ctx, $res) if ($ctx->{fn} // '') =~ /\.patch\z/; |
| my ($git, $oid) = @$res; |
| # patch-id needs two passes, and we use the initial show to ensure |
| # a patch embedded inside the commit message body doesn't get fed |
| # to patch-id: |
| open $ctx->{patch_fh}, '+>', "$ctx->{-tmp}/show"; |
| my $qsp_h = PublicInbox::Qspawn->new($git->cmd('show', $SHOW_FMT, |
| qw(--encoding=UTF-8 -z --no-notes --no-patch), $oid), |
| undef, { 1 => $ctx->{patch_fh} }); |
| $qsp_h->{qsp_err} = \($ctx->{-qsp_err_h} = ''); |
| my $cmt_fin = on_destroy \&cmt_fin, $ctx; |
| $ctx->{git} = $git; |
| $ctx->{oid} = $oid; |
| $qsp_h->psgi_qx($ctx->{env}, undef, \&cmt_hdr_prep, $ctx, $cmt_fin); |
| } |
| |
| sub show_other ($$) { # just in case... |
| my ($ctx, $res) = @_; |
| my ($git, $oid, $type, $size) = @$res; |
| $size > $MAX_SIZE and return html_page($ctx, 200, |
| ascii_html($type)." $oid is too big to show\n". dbg_log($ctx)); |
| my $cmd = $git->cmd(qw(show --encoding=UTF-8 |
| --no-color --no-abbrev), $oid); |
| my $qsp = PublicInbox::Qspawn->new($cmd); |
| $qsp->{qsp_err} = \($ctx->{-qsp_err} = ''); |
| $qsp->psgi_qx($ctx->{env}, undef, \&show_other_result, $ctx); |
| } |
| |
| sub show_tree_result ($$) { |
| my ($bref, $ctx) = @_; |
| if (my $qsp_err = delete $ctx->{-qsp_err}) { |
| return html_page($ctx, 500, dbg_log($ctx) . |
| "git ls-tree -z error:$qsp_err"); |
| } |
| my @ent = split(/\0/, $$bref); |
| my $qp = delete $ctx->{qp}; |
| my $l = $ctx->{-linkify} //= PublicInbox::Linkify->new; |
| my $pfx = $ctx->{-path} // $qp->{b}; # {-path} is from RepoTree |
| $$bref = "<pre><a href=#tree>tree</a> $ctx->{tree_oid}"; |
| # $REPO/tree/$path already sets {-upfx} |
| my $upfx = $ctx->{-upfx} //= '../../'; |
| if (defined $pfx) { |
| $pfx =~ s!/+\z!!s; |
| if (my $t = $ctx->{-obj}) { # $t eq "$tip:$path" |
| $t = ascii_html($t); |
| $pfx .= '/' if $pfx ne ''; |
| $$bref .= <<EOM |
| \n\$ git ls-tree -l $t # shows similar output on the CLI |
| EOM |
| } elsif ($pfx eq '') { |
| $$bref .= " (root)\n"; |
| } else { |
| my $x = ascii_html($pfx); |
| $pfx .= '/'; |
| $$bref .= qq( <a href=#path>path</a>: $x</a>\n); |
| } |
| } else { |
| $pfx = ''; |
| $$bref .= qq[ (<a href=#path>path</a> unknown)\n]; |
| } |
| my ($x, $m, $t, $oid, $sz, $f, $n, $gitlink); |
| $$bref .= "\n size name"; |
| for (@ent) { |
| ($x, $f) = split(/\t/, $_, 2); |
| undef $_; |
| ($m, $t, $oid, $sz) = split(/ +/, $x, 4); |
| $m = $GIT_MODE{$m} // '?'; |
| utf8_maybe($f); |
| $n = ascii_html($f); |
| if ($m eq 'g') { # gitlink submodule commit |
| $$bref .= "\ng\t\t$n @ <a\nhref=#g>commit</a>$oid"; |
| $gitlink = 1; |
| next; |
| } |
| my $q = 'b='.ascii_html(uri_escape_path($pfx.$f)); |
| if ($m eq 'd') { $n .= '/' } |
| elsif ($m eq 'x') { $n = "<b>$n</b>" } |
| elsif ($m eq 'l') { $n = "<i>$n</i>" } |
| $$bref .= qq(\n$m\t$sz\t<a\nhref="$upfx$oid/s/?$q">$n</a>); |
| } |
| $$bref .= dbg_log($ctx); |
| $$bref .= <<EOM; |
| <hr><pre>glossary |
| -------- |
| <dfn |
| id=tree>Tree</dfn> objects belong to commits or other tree objects. Trees may |
| reference blobs, sub-trees, or (rarely) commits of submodules. |
| |
| <dfn |
| id=path>Path</dfn> names are stored in tree objects, but trees do not know |
| their own path name. A tree's path name comes from their parent tree, |
| or it is the root tree referenced by a commit object. Thus, this web UI |
| relies on the `b=' URI parameter as a hint to display the path name. |
| EOM |
| |
| $$bref .= <<EOM if $gitlink; |
| |
| <dfn title="submodule commit" |
| id=g>Commit</dfn> objects may be stored in trees to reference submodules.</pre> |
| EOM |
| chop $$bref; |
| html_page($ctx, 200, $$bref); |
| } |
| |
| sub show_tree ($$) { # also used by RepoTree |
| my ($ctx, $res) = @_; |
| my ($git, $oid, undef, $size) = @$res; |
| $size > $MAX_SIZE and return html_page($ctx, 200, |
| "tree $oid is too big to show\n". dbg_log($ctx)); |
| my $cmd = $git->cmd(qw(ls-tree -z -l --no-abbrev), $oid); |
| my $qsp = PublicInbox::Qspawn->new($cmd); |
| $ctx->{tree_oid} = $oid; |
| $qsp->{qsp_err} = \($ctx->{-qsp_err} = ''); |
| $qsp->psgi_qx($ctx->{env}, undef, \&show_tree_result, $ctx); |
| } |
| |
| # returns seconds offset from git TZ offset |
| sub tz_adj ($) { |
| my ($tz) = @_; # e.g "-0700" |
| $tz = int($tz); |
| my $mm = $tz < 0 ? -$tz : $tz; |
| $mm = int($mm / 100) * 60 + ($mm % 100); |
| $mm = $tz < 0 ? -$mm : $mm; |
| ($mm * 60); |
| } |
| |
| sub show_tag_result { # git->cat_async callback |
| my ($bref, $oid, $type, $size, $ctx) = @_; |
| utf8_maybe($$bref); |
| my $l = PublicInbox::Linkify->new; |
| $$bref = $l->to_html($$bref); |
| $$bref =~ s!^object ([a-f0-9]+)!object <a |
| href=../../$1/s/>$1</a>!; |
| |
| $$bref =~ s/^(tagger .*> )([0-9]+) ([\-+]?[0-9]+)/$1.strftime( |
| '%Y-%m-%d %H:%M:%S', gmtime($2 + tz_adj($3)))." $3"/sme; |
| # TODO: download link |
| html_page($ctx, 200, '<pre>', $$bref, '</pre>', dbg_log($ctx)); |
| } |
| |
| sub show_tag ($$) { |
| my ($ctx, $res) = @_; |
| my ($git, $oid) = @$res; |
| $ctx->{git} = $git; |
| do_cat_async($ctx, \&show_tag_result, $oid); |
| } |
| |
| # user_cb for SolverGit, called as: user_cb->($result_or_error, $uarg) |
| sub solve_result { |
| my ($res, $ctx) = @_; |
| my $hints = delete $ctx->{hints}; |
| $res or return html_page($ctx, 404, 'Not found', dbg_log($ctx)); |
| ref($res) eq 'ARRAY' or |
| return html_page($ctx, 500, 'Internal error', dbg_log($ctx)); |
| |
| my ($git, $oid, $type, $size, $di) = @$res; |
| return show_commit($ctx, $res) if $type eq 'commit'; |
| return show_tree($ctx, $res) if $type eq 'tree'; |
| return show_tag($ctx, $res) if $type eq 'tag'; |
| return show_other($ctx, $res) if $type ne 'blob'; |
| my $fn = $di->{path_b} // $hints->{path_b}; |
| my $paths = $ctx->{-paths} //= do { |
| my $path = to_filename($fn // 'blob') // 'blob'; |
| my $raw_more = qq[(<a\nhref="$path">raw</a>)]; |
| my @def; |
| |
| # XXX not sure if this is the correct wording |
| if (defined($fn)) { |
| $raw_more .= qq( |
| name: ${\ascii_html($fn)} \t # note: path name is non-authoritative<a |
| href="#pathdef" id=top>(*)</a>); |
| $def[0] = "<hr><pre\nid=pathdef>" . |
| '(*) Git path names are given by the tree(s) the blob belongs to. |
| Blobs themselves have no identifier aside from the hash of its contents.'. |
| qq(<a\nhref="#top">^</a></pre>); |
| } |
| [ $path, $raw_more, @def ]; |
| }; |
| $ctx->{-q_value_html} //= do { |
| my $s = defined($fn) ? 'dfn:'.ascii_html($fn).' ' : ''; |
| $s.'dfpost:'.substr($oid, 0, 7); |
| }; |
| |
| if ($size > $MAX_SIZE) { |
| return stream_large_blob($ctx, $res) if defined $ctx->{fn}; |
| return html_page($ctx, 200, <<EOM . dbg_log($ctx)); |
| <pre><b>Too big to show, download available</b> |
| blob $oid $size bytes $paths->[1]</pre> |
| EOM |
| } |
| bless $ctx, 'PublicInbox::WwwStream'; # for DESTROY |
| $ctx->{git} = $git; |
| do_cat_async($ctx, \&show_blob, $oid); |
| } |
| |
| sub show_blob { # git->cat_async callback |
| my ($blob, $oid, $type, $size, $ctx) = @_; |
| if (!$blob) { |
| my $e = "Failed to retrieve generated blob ($oid)"; |
| warn "$e ($ctx->{git}->{git_dir}) type=$type"; |
| return html_page($ctx, 500, "<pre><b>$e</b></pre>".dbg_log($ctx)) |
| } |
| |
| my $bin = index(substr($$blob, 0, $BIN_DETECT), "\0") >= 0; |
| if (defined $ctx->{fn}) { |
| my $h = [ 'Content-Length', $size, 'Content-Type' ]; |
| push(@$h, ($bin ? 'application/octet-stream' : 'text/plain')); |
| return delete($ctx->{-wcb})->([200, $h, [ $$blob ]]); |
| } |
| |
| my ($path, $raw_more, @def) = @{delete $ctx->{-paths}}; |
| $bin and return html_page($ctx, 200, |
| "<pre>blob $oid $size bytes (binary)" . |
| " $raw_more</pre>".dbg_log($ctx)); |
| |
| # TODO: detect + convert to ensure validity |
| utf8_maybe($$blob); |
| my $nl = ($$blob =~ s/\r?\n/\n/sg); |
| my $pad = length($nl); |
| |
| ($ctx->{-linkify} //= PublicInbox::Linkify->new)->linkify_1($$blob); |
| my $ok = $hl->do_hl($blob, $path) if $hl; |
| if ($ok) { |
| $blob = $ok; |
| } else { |
| $$blob = ascii_html($$blob); |
| } |
| |
| # using some of the same CSS class names and ids as cgit |
| my $x = "<pre>blob $oid $size bytes $raw_more</pre>" . |
| "<hr /><table\nclass=blob>". |
| "<tr><td\nclass=linenumbers><pre>"; |
| # scratchpad in this loop is faster here than `printf $zfh': |
| $x .= sprintf("<a id=n$_ href=#n$_>% ${pad}u</a>\n", $_) for (1..$nl); |
| $x .= '</pre></td><td><pre> </pre></td>'. # pad for non-CSS users |
| "<td\nclass=lines><pre\nstyle='white-space:pre'><code>"; |
| html_page($ctx, 200, $x, $ctx->{-linkify}->linkify_2($$blob), |
| '</code></pre></td></tr></table>'.dbg_log($ctx), @def); |
| } |
| |
| sub start_solver ($) { |
| my ($ctx) = @_; |
| while (my ($from, $to) = each %QP_MAP) { |
| my $v = $ctx->{qp}->{$from} // next; |
| $ctx->{hints}->{$to} = $v if $v ne ''; |
| } |
| $ctx->{-next_solver} = on_destroy \&next_solver; |
| ++$solver_nr; |
| $ctx->{-tmp} = File::Temp->newdir("solver.$ctx->{oid_b}-XXXX", |
| TMPDIR => 1); |
| $ctx->{lh} or open $ctx->{lh}, '+>>', "$ctx->{-tmp}/solve.log"; |
| my $solver = PublicInbox::SolverGit->new($ctx->{ibx}, |
| \&solve_result, $ctx); |
| $solver->{limiter} = $solver_lim; |
| $solver->{gits} //= [ $ctx->{git} ]; |
| $solver->{tmp} = $ctx->{-tmp}; # share tmpdir |
| # PSGI server will call this immediately and give us a callback (-wcb) |
| $solver->solve(@$ctx{qw(env lh oid_b hints)}); |
| } |
| |
| # run the next solver job when done and DESTROY-ed |
| sub next_solver { |
| --$solver_nr; |
| # XXX FIXME: client may've disconnected if it waited a long while |
| start_solver(shift(@solver_q) // return); |
| } |
| |
| sub may_start_solver ($) { |
| my ($ctx) = @_; |
| $solver_lim //= $ctx->{www}->{pi_cfg}->limiter('codeblob'); |
| if ($solver_nr >= $solver_lim->{max}) { |
| @solver_q > 128 ? html_page($ctx, 503, 'too busy') |
| : push(@solver_q, $ctx); |
| } else { |
| start_solver($ctx); |
| } |
| } |
| |
| # GET /$INBOX/$GIT_OBJECT_ID/s/ |
| # GET /$INBOX/$GIT_OBJECT_ID/s/$FILENAME |
| sub show ($$;$) { |
| my ($ctx, $oid_b, $fn) = @_; |
| @$ctx{qw(oid_b fn)} = ($oid_b, $fn); |
| sub { |
| $ctx->{-wcb} = $_[0]; # HTTP write callback |
| may_start_solver $ctx; |
| }; |
| } |
| |
| 1; |