]> git.ipfire.org Git - thirdparty/public-inbox.git/commitdiff
repobrowse: qspawn + streaming for git commit display
authorEric Wong <e@80x24.org>
Wed, 11 Jan 2017 04:12:26 +0000 (04:12 +0000)
committerEric Wong <e@80x24.org>
Wed, 11 Jan 2017 04:13:18 +0000 (04:13 +0000)
This prevents "git show" processes from monopolizing
the system and allows us to better handle backpressure
from gigantic commits.

lib/PublicInbox/GetlineBody.pm
lib/PublicInbox/Qspawn.pm
lib/PublicInbox/RepobrowseGitCommit.pm
lib/PublicInbox/RepobrowseGitDiffCommon.pm
t/repobrowse_git_commit.t

index 5f32782886205641ac46dcbc7abd28949d0b0c32..ccc66e4884b41f2ce5464ca96ada8f66c6e4d53f 100644 (file)
@@ -9,8 +9,13 @@ use strict;
 use warnings;
 
 sub new {
-       my ($class, $rpipe, $end, $buf) = @_;
-       bless { rpipe => $rpipe, end => $end, buf => $buf }, $class;
+       my ($class, $rpipe, $end, $buf, $filter) = @_;
+       bless {
+               rpipe => $rpipe,
+               end => $end,
+               buf => $buf,
+               filter => $filter || 0,
+       }, $class;
 }
 
 # close should always be called after getline returns undef,
@@ -20,8 +25,13 @@ sub DESTROY { $_[0]->close }
 
 sub getline {
        my ($self) = @_;
+       my $filter = $self->{filter};
+       return if $filter == -1; # last call was EOF
+
        my $buf = delete $self->{buf}; # initial buffer
-       defined $buf ? $buf : $self->{rpipe}->getline;
+       $buf = $self->{rpipe}->getline unless defined $buf;
+       $self->{filter} = -1 unless defined $buf; # set EOF for next call
+       $filter ? $filter->($buf) : $buf;
 }
 
 sub close {
index cd1079a0c5f1ce92be5e431879fc1d123fb2accd..eead5a5baa1f4dbe5cedf9d269431b7f67ea2f7f 100644 (file)
@@ -9,6 +9,7 @@ package PublicInbox::Qspawn;
 use strict;
 use warnings;
 use PublicInbox::Spawn qw(popen_rd);
+require Plack::Util;
 my $def_limiter;
 
 sub new ($$$;) {
@@ -60,11 +61,25 @@ sub start {
        }
 }
 
+# create a filter for "push"-based streaming PSGI writes used by HTTPD::Async
+sub filter_fh ($$) {
+       my ($fh, $filter) = @_;
+       Plack::Util::inline_object(
+               close => sub {
+                       $fh->write($filter->(undef));
+                       $fh->close;
+               },
+               write => sub {
+                       $fh->write($filter->($_[0]));
+               });
+}
+
 sub psgi_return {
        my ($self, $env, $limiter, $parse_hdr) = @_;
        my ($fh, $rpipe);
        my $end = sub {
-               if (my $err = $self->finish) {
+               my $err = $self->finish;
+               if ($err && !$env->{'qspawn.quiet'}) {
                        $err = join(' ', @{$self->{args}->[0]}).": $err\n";
                        $env->{'psgi.errors'}->print($err);
                }
@@ -84,6 +99,7 @@ sub psgi_return {
        my $cb = sub {
                my $r = $rd_hdr->() or return;
                $rd_hdr = undef;
+               my $filter = delete $env->{'qspawn.filter'};
                if (scalar(@$r) == 3) { # error
                        if ($async) {
                                $async->close; # calls rpipe->close
@@ -94,11 +110,12 @@ sub psgi_return {
                        $res->($r);
                } elsif ($async) {
                        $fh = $res->($r); # scalar @$r == 2
+                       $fh = filter_fh($fh, $filter) if $filter;
                        $async->async_pass($env->{'psgix.io'}, $fh, \$buf);
                } else { # for synchronous PSGI servers
                        require PublicInbox::GetlineBody;
                        $r->[2] = PublicInbox::GetlineBody->new($rpipe, $end,
-                                                               $buf);
+                                                               $buf, $filter);
                        $res->($r);
                }
        };
index 2577ea3452aa7670fac256b2c5fe8781d3dec998..328d2d4044fc62b9063ab16b8b5e5301f37b2826 100644 (file)
@@ -17,8 +17,8 @@ use warnings;
 use base qw(PublicInbox::RepobrowseBase);
 use PublicInbox::Hval qw(utf8_html to_attr);
 use PublicInbox::RepobrowseGit qw(git_unquote git_commit_title);
-use PublicInbox::RepobrowseGitDiffCommon qw/git_diffstat_emit
-       git_diff_ab_index git_diff_ab_hdr git_diff_ab_hunk/;
+use PublicInbox::RepobrowseGitDiffCommon;
+use PublicInbox::Qspawn;
 
 use constant GIT_FMT => '--pretty=format:'.join('%n',
        '%H', '%h', '%s', '%an <%ae>', '%ai', '%cn <%ce>', '%ci',
@@ -29,14 +29,12 @@ use constant CC_MERGE => " This is a merge, showing combined diff:\n\n";
 
 sub commit_header {
        my ($self, $req) = @_;
-       my $res = delete $req->{res} or die "BUG: missing res\n";
        my ($H, $h, $s, $au, $ad, $cu, $cd, $t, $p, $D, $rest) =
                split("\n", $req->{dbuf}, 11);
        $s = utf8_html($s);
        $au = utf8_html($au);
        $cu = utf8_html($cu);
        my @p = split(' ', $p);
-       my $fh = $req->{fh} = $res->([200, ['Content-Type'=>'text/html']]);
 
        my $rel = $req->{relcmd};
        my $q = $req->{'q'};
@@ -70,7 +68,7 @@ sub commit_header {
                my $p = $p[0];
                $x .= git_parent_line('   parent', $p, $q, $git, $rel, $path);
        } elsif ($np > 1) {
-               $req->{help} = CC_MERGE;
+               $req->{mhelp} = CC_MERGE;
                my @common = ($q, $git, $rel, $path);
                my @t = @p;
                my $p = shift @t;
@@ -83,68 +81,45 @@ sub commit_header {
        $x .= $s;
        $x .= "</b>\n\n";
        my $bx00;
+
+       # FIXME: deal with excessively long commit message bodies
        ($bx00, $req->{dbuf}) = split("\0", $rest, 2);
-       $fh->write($x .= utf8_html($bx00) . "<a\nid=D>---</a>\n");
        $req->{anchors} = {};
        $req->{h} = $h;
        $req->{p} = \@p;
+       $x .= utf8_html($bx00) . "<a\nid=D>---</a>\n";
 }
 
-sub git_diff_cc_line_i ($$) {
-       my ($req, $l) = @_;
-       my $cmt = '[a-f0-9]+';
-
-       if ($l =~ m{^diff --git ("?a/.+) ("?b/.+)$}) { # regular
-               git_diff_ab_hdr($req, $1, $2) . "\n";
-       } elsif ($l =~ m{^diff --(cc|combined) (.+)$}) {
-               git_diff_cc_hdr($req, $1, $2) . "\n";
-       } elsif ($l =~ /^index ($cmt)\.\.($cmt)(.*)$/o) { # regular
-               git_diff_ab_index($1, $2, $3) . "\n";
-       } elsif ($l =~ /^@@ (\S+) (\S+) @@(.*)$/) { # regular
-               git_diff_ab_hunk($req, $1, $2, $3) . "\n";
-       } elsif ($l =~ /^index ($cmt,[^\.]+)\.\.($cmt)(.*)$/o) { # --cc
-               git_diff_cc_index($req, $1, $2, $3) . "\n";
-       } elsif ($l =~ /^(@@@+) (\S+.*\S+) @@@+(.*)$/) { # --cc
-               git_diff_cc_hunk($req, $1, $2, $3) . "\n";
-       } else {
-               utf8_html($l) . "\n";
-       }
-}
-
-sub git_commit_stream ($$$$) {
-       my ($self, $req, $fail, $end) = @_;
+sub git_commit_sed ($$) {
+       my ($self, $req) = @_;
+       git_diff_sed_init($req);
        my $dbuf = \($req->{dbuf});
-       my $off = length($$dbuf);
-       my $n = $req->{rpipe}->sysread($$dbuf, 8192, $off);
-       return $fail->() unless defined $n;
-       return $end->() if $n == 0;
-       my $res = $req->{res};
-       if ($res) {
-               return if index($$dbuf, "\0") < 0;
-               commit_header($self, $req);
-               return if $$dbuf eq '';
-       }
-       my $fh = $req->{fh};
-       if (!$req->{diff_state}) {
-               my ($stat, $buf) = split("\0\0", $$dbuf, 2);
-               return unless defined $buf;
-               $$dbuf = $buf;
-               git_diffstat_emit($req, $fh, $stat);
-               $req->{diff_state} = 1;
-       }
-       my @buf = split("\n", $$dbuf, -1);
-       $$dbuf = pop @buf; # last line, careful...
-       if (@buf) {
-               my $s = delete($req->{help}) || '';
-               $s .= git_diff_cc_line_i($req, $_) foreach @buf;
-               $fh->write($s) if $s ne '';
+
+       # this filters for $fh->write or $body->getline (see Qspawn)
+       sub {
+               my $dst = '';
+               if (defined $_[0]) { # $_[0] == scalar buffer
+                       $$dbuf .= $_[0];
+                       if ($req->{dstate} == DSTATE_INIT) {
+                               return $dst if index($$dbuf, "\0") < 0;
+                               $req->{dstate} = DSTATE_STAT;
+                               $dst .= commit_header($self, $req);
+                       }
+                       git_diff_sed_run(\$dst, $req);
+               } else { # undef means EOF from "git show", flush the last bit
+                       git_diff_sed_close(\$dst, $req);
+                       $dst .= CC_EMPTY if delete $req->{mhelp};
+                       show_unchanged(\$dst, $req);
+                       $dst .= '</pre></body></html>';
+               }
+               $dst;
        }
 }
 
-sub call_git_commit {
+sub call_git_commit { # RepobrowseBase calls this
        my ($self, $req) = @_;
-
-       my $q = PublicInbox::RepobrowseGitQuery->new($req->{env});
+       my $env = $req->{env};
+       my $q = PublicInbox::RepobrowseGitQuery->new($env);
        my $id = $q->{id};
        $id eq '' and $id = 'HEAD';
 
@@ -156,72 +131,30 @@ sub call_git_commit {
        }
 
        my $git = $req->{repo_info}->{git};
-       my $cmd = [ qw(show -z --numstat -p --encoding=UTF-8
+       my $cmd = [ 'git', "--git-dir=$git->{git_dir}", qw(show
+                       -z --numstat -p --encoding=UTF-8
                        --no-notes --no-color -c),
                        $git->abbrev, GIT_FMT, $id, '--' ];
-       $req->{rpipe} = $git->popen($cmd, undef, { 2 => $git->err_begin });
-       my $env = $req->{env};
-       my $err = $env->{'psgi.errors'};
-       my $vin;
-       $req->{dbuf} = '';
-       my $end = sub {
-               if (my $fh = delete $req->{fh}) {
-                       my $dbuf = delete $req->{dbuf};
-                       if (!$req->{diff_state}) {
-                               my ($stat, $buf) = split("\0\0", $dbuf, 2);
-                               $dbuf = defined $buf ? $buf : '';
-                               git_diffstat_emit($req, $fh, $stat);
-                               $req->{diff_state} = 1;
-                       }
-                       my @buf = split("\n", $dbuf, -1);
-                       if (@buf) {
-                               my $s = delete($req->{help}) || '';
-                               $s .= git_diff_cc_line_i($req, $_) foreach @buf;
-                               $fh->write($s) if $s ne '';
-                       }
-                       $fh->write(CC_EMPTY) if delete($req->{help});
-                       show_unchanged($req, $fh);
-                       $fh->write('</pre></body></html>');
-                       $fh->close;
-               } elsif (my $res = delete $req->{res}) {
-                       git_commit_404($req, $res);
-               }
-               if (my $rpipe = delete $req->{rpipe}) {
-                       $rpipe->close; # _may_ be Danga::Socket::close
-               }
-               # zero the error file for now, be careful about printing
-               # $id to psgi.errors w/o sanitizing...
-               $git->err;
-       };
-       my $fail = sub {
-               if ($!{EAGAIN} || $!{EINTR}) {
-                       select($vin, undef, undef, undef) if defined $vin;
-                       # $vin is undef on async, so this is a noop on EAGAIN
-                       return;
-               }
-               my $e = $!;
-               $end->();
-               $err->print("git show ($git->{git_dir}): $e\n");
-       };
+       my $rdr = { 2 => $git->err_begin };
+       my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr);
        $req->{'q'} = $q;
-       my $cb = sub { # read git-show output and stream to client
-               git_commit_stream($self, $req, $fail, $end);
-       };
-       if (my $async = $env->{'pi-httpd.async'}) {
-               $req->{rpipe} = $async->($req->{rpipe}, $cb);
-               sub { $req->{res} = $_[0] } # let Danga::Socket handle the rest
-       } else { # synchronous loop for other PSGI servers
-               $vin = '';
-               vec($vin, fileno($req->{rpipe}), 1) = 1;
-               sub {
-                       $req->{res} = $_[0];
-                       while ($req->{rpipe}) { $cb->() }
+       $env->{'qspawn.quiet'} = 1;
+       $qsp->psgi_return($env, undef, sub { # parse header
+               my ($r, $bref) = @_;
+               if (!defined $r) {
+                       my $errmsg = $git->err;
+                       [ 500, [ 'Content-Type', 'text/html' ], [ $errmsg ] ];
+               } elsif ($r == 0) {
+                       git_commit_404($req);
+               } else {
+                       $env->{'qspawn.filter'} = git_commit_sed($self, $req);
+                       [ 200, [ 'Content-Type', 'text/html' ] ];
                }
-       }
+       });
 }
 
 sub git_commit_404 {
-       my ($req, $res) = @_;
+       my ($req) = @_;
        my $x = 'Missing commit or path';
        my $pfx = "$req->{relcmd}commit";
 
@@ -231,70 +164,10 @@ sub git_commit_404 {
        $x .= "<a\nhref=\"$pfx$qs\">$try the latest commit in HEAD</a>\n";
        $x .= '</pre></body>';
 
-       $res->([404, ['Content-Type'=>'text/html'], [ $x ]]);
-}
-
-sub git_diff_cc_hdr {
-       my ($req, $combined, $path) = @_;
-       my $html_path = utf8_html($path);
-       $path = git_unquote($path);
-       my $anchor = to_attr($path);
-       delete $req->{anchors}->{$anchor};
-       my $cc = $req->{cc} = PublicInbox::Hval->utf8($path);
-       $req->{path_cc} = $cc->as_path;
-       qq(<a\nid="$anchor">diff</a> --$combined $html_path);
-}
-
-# index abcdef09,01234567..76543210
-sub git_diff_cc_index {
-       my ($req, $before, $last, $end) = @_;
-       $end = utf8_html($end);
-       my @before = split(',', $before);
-       $req->{pobj_cc} = \@before;
-
-       # not wasting bandwidth on links here, yet
-       # links in hunk headers are far more useful with line offsets
-       "index $before..$last$end";
-}
-
-# @@@ -1,2 -3,4 +5,6 @@@ (combined diff)
-sub git_diff_cc_hunk {
-       my ($req, $at, $offs, $ctx) = @_;
-       my @offs = split(' ', $offs);
-       my $last = pop @offs;
-       my @p = @{$req->{p}};
-       my @pobj = @{$req->{pobj_cc}};
-       my $path = $req->{path_cc};
-       my $rel = $req->{relcmd};
-       my $rv = $at;
-
-       # special 'cc' action as we don't have reliable paths from parents
-       my $ppath = "${rel}cc/$path";
-       foreach my $off (@offs) {
-               my $p = shift @p;
-               my $obj = shift @pobj; # blob SHA-1
-               my ($n) = ($off =~ /\A-(\d+)/); # line number
-
-               if ($n == 0) { # new file (does this happen with --cc?)
-                       $rv .= " $off";
-               } else {
-                       $rv .= " <a\nhref=\"$ppath?id=$p&obj=$obj#n$n\">";
-                       $rv .= "$off</a>";
-               }
-       }
-
-       # we can use the normal 'tree' endpoint for the result
-       my ($n) = ($last =~ /\A\+(\d+)/); # line number
-       if ($n == 0) { # deleted file (does this happen with --cc?)
-               $rv .= " $last";
-       } else {
-               my $h = $req->{h};
-               $rv .= qq( <a\nrel=nofollow);
-               $rv .= qq(\nhref="${rel}tree/$path?id=$h#n$n">$last</a>);
-       }
-       $rv .= " $at" . utf8_html($ctx);
+       [404, ['Content-Type'=>'text/html'], [ $x ]];
 }
 
+# FIXME: horrifically expensive...
 sub git_parent_line {
        my ($pfx, $p, $q, $git, $rel, $path) = @_;
        my $qs = $q->qs(id => $p);
@@ -305,12 +178,12 @@ sub git_parent_line {
 
 # do not break anchor links if the combined diff doesn't show changes:
 sub show_unchanged {
-       my ($req, $fh) = @_;
+       my ($dst, $req) = @_;
 
        my @unchanged = sort keys %{$req->{anchors}};
        return unless @unchanged;
        my $anchors = $req->{anchors};
-       my $s = "\n There are uninteresting changes from this merge.\n" .
+       $$dst .= "\n There are uninteresting changes from this merge.\n" .
                qq( See the <a\nhref="#P">parents</a>, ) .
                "or view final state(s) below:\n\n";
        my $rel = $req->{relcmd};
@@ -320,11 +193,10 @@ sub show_unchanged {
                my $p = PublicInbox::Hval->utf8(git_unquote($fn));
                $p = $p->as_path;
                $fn = utf8_html($fn);
-               $s .= qq(\t<a\nrel=nofollow);
-               $s .= qq(\nid="$anchor"\nhref="${rel}tree/$p$qs">);
-               $s .= "$fn</a>\n";
+               $$dst .= qq(\t<a\nrel=nofollow);
+               $$dst .= qq(\nid="$anchor"\nhref="${rel}tree/$p$qs">);
+               $$dst .= "$fn</a>\n";
        }
-       $fh->write($s);
 }
 
 1;
index 9ed24d0324ec84579f585b2fcb983dea0ff080fa..ac38aa0a250956d6cf7237fdac35804f16b39a44 100644 (file)
@@ -10,6 +10,8 @@ use PublicInbox::Hval qw/utf8_html to_attr/;
 use base qw/Exporter/;
 our @EXPORT_OK = qw/git_diffstat_emit
        git_diff_ab_index git_diff_ab_hdr git_diff_ab_hunk/;
+our @EXPORT = qw/git_diff_sed_init git_diff_sed_close git_diff_sed_run
+       DSTATE_INIT DSTATE_STAT DSTATE_LINES/;
 
 # index abcdef89..01234567
 sub git_diff_ab_index ($$$) {
@@ -41,6 +43,18 @@ sub git_diff_ab_hdr ($$$) {
        qq(<a\nid="$anchor">diff</a> --git $html_a $html_b);
 }
 
+# diff (--cc|--combined)
+sub git_diff_cc_hdr {
+       my ($req, $combined, $path) = @_;
+       my $html_path = utf8_html($path);
+       $path = git_unquote($path);
+       my $anchor = to_attr($path);
+       delete $req->{anchors}->{$anchor};
+       my $cc = $req->{cc} = PublicInbox::Hval->utf8($path);
+       $req->{path_cc} = $cc->as_path;
+       qq(<a\nid="$anchor">diff</a> --$combined $html_path);
+}
+
 # @@ -1,2 +3,4 @@ (regular diff)
 sub git_diff_ab_hunk ($$$$) {
        my ($req, $ca, $cb, $ctx) = @_;
@@ -72,6 +86,56 @@ sub git_diff_ab_hunk ($$$$) {
        $rv . ' @@' . utf8_html($ctx);
 }
 
+# index abcdef09,01234567..76543210
+sub git_diff_cc_index {
+       my ($req, $before, $last, $end) = @_;
+       $end = utf8_html($end);
+       my @before = split(',', $before);
+       $req->{pobj_cc} = \@before;
+
+       # not wasting bandwidth on links here, yet
+       # links in hunk headers are far more useful with line offsets
+       "index $before..$last$end";
+}
+
+# @@@ -1,2 -3,4 +5,6 @@@ (combined diff)
+sub git_diff_cc_hunk {
+       my ($req, $at, $offs, $ctx) = @_;
+       my @offs = split(' ', $offs);
+       my $last = pop @offs;
+       my @p = @{$req->{p}};
+       my @pobj = @{$req->{pobj_cc}};
+       my $path = $req->{path_cc};
+       my $rel = $req->{relcmd};
+       my $rv = $at;
+
+       # special 'cc' action as we don't have reliable paths from parents
+       my $ppath = "${rel}cc/$path";
+       foreach my $off (@offs) {
+               my $p = shift @p;
+               my $obj = shift @pobj; # blob SHA-1
+               my ($n) = ($off =~ /\A-(\d+)/); # line number
+
+               if ($n == 0) { # new file (does this happen with --cc?)
+                       $rv .= " $off";
+               } else {
+                       $rv .= " <a\nhref=\"$ppath?id=$p&obj=$obj#n$n\">";
+                       $rv .= "$off</a>";
+               }
+       }
+
+       # we can use the normal 'tree' endpoint for the result
+       my ($n) = ($last =~ /\A\+(\d+)/); # line number
+       if ($n == 0) { # deleted file (does this happen with --cc?)
+               $rv .= " $last";
+       } else {
+               my $h = $req->{h};
+               $rv .= qq( <a\nrel=nofollow);
+               $rv .= qq(\nhref="${rel}tree/$path?id=$h#n$n">$last</a>);
+       }
+       $rv .= " $at" . utf8_html($ctx);
+}
+
 sub git_diffstat_rename ($$$) {
        my ($req, $from, $to) = @_;
        my $anchor = to_attr(git_unquote($to));
@@ -94,7 +158,7 @@ sub git_diffstat_rename ($$$) {
        @base ? "$base/{$from =&gt; $to}" : "$from =&gt; $to";
 }
 
-sub git_diffstat_emit ($$$) {
+sub git_diffstat_emit ($$$) { # XXX deprecated
        my ($req, $fh, undef) = @_;
        my @stat = split("\0", $_[2]); # avoiding copy for $_[2]
        my $nr = 0;
@@ -132,4 +196,137 @@ sub git_diffstat_emit ($$$) {
        $fh->write($s);
 }
 
+sub DSTATE_INIT () { 0 }
+sub DSTATE_STAT () { 1 }
+sub DSTATE_LINES () { 2 }
+
+sub git_diff_sed_init ($) {
+       my ($req) = @_;
+       $req->{dbuf} = '';
+       $req->{ndiff} = $req->{nchg} = $req->{nadd} = $req->{ndel} = 0;
+       $req->{dstate} = DSTATE_INIT;
+}
+
+sub git_diff_sed_stat ($$) {
+       my ($dst, $req) = @_;
+       my @stat = split(/\0/, $req->{dbuf}, -1);
+       my $eos;
+       my $nchg = \($req->{nchg});
+       my $nadd = \($req->{nadd});
+       my $ndel = \($req->{ndel});
+       if (!$req->{dstat_started}) {
+               $req->{dstat_started} = 1;
+               if ($req->{mhelp}) {
+                       if ($stat[0] eq '') {
+                               shift @stat;
+                       } else {
+                               warn
+'initial merge diffstat line was not empty';
+                       }
+               } else {
+                       $stat[0] =~ s/\A\n//s or warn
+'failed to remove initial newline from diffstat';
+               }
+       }
+       while (defined(my $l = shift @stat)) {
+               if ($l eq '') {
+                       $eos = 1 if $stat[0] && $stat[0] =~ /\Ad/; # "diff --"
+                       last;
+               } elsif ($l =~ /\Adiff /) {
+                       unshift @stat, $l;
+                       $eos = 1;
+                       last;
+               }
+               $l =~ /\A(\S+)\t+(\S+)\t+(.*)/ or next;
+               my ($add, $del, $fn) = ($1, $2, $3);
+               if ($fn ne '') { # normal modification
+                       my $anchor = to_attr(git_unquote($fn));
+                       $req->{anchors}->{$anchor} = $fn;
+                       $l = utf8_html($fn);
+                       $l = qq(<a\nhref="#$anchor">$l</a>);
+               } else { # rename
+                       # incomplete...
+                       if (scalar(@stat) < 2) {
+                               unshift @stat, $l;
+                               last;
+                       }
+                       my $from = shift @stat;
+                       my $to = shift @stat;
+                       $l = git_diffstat_rename($req, $from, $to);
+               }
+
+               # text changes show numerically, Binary does not
+               if ($add =~ /\A\d+\z/) {
+                       $$nadd += $add;
+                       $$ndel += $del;
+                       $add = "+$add";
+                       $del = "-$del";
+               }
+               ++$$nchg;
+               my $num = sprintf('% 6s/%-6s', $del, $add);
+               $$dst .= " $num\t$l\n";
+       }
+
+       $req->{dbuf} = join("\0", @stat);
+       return unless $eos;
+
+       $req->{dstate} = DSTATE_LINES;
+       $$dst .= "\n $$nchg ";
+       $$dst .= $$nchg  == 1 ? 'file changed, ' : 'files changed, ';
+       $$dst .= $$nadd;
+       $$dst .= $$nadd == 1 ? ' insertion(+), ' : ' insertions(+), ';
+       $$dst .= $$ndel;
+       $$dst .= $$ndel == 1 ? " deletion(-)\n\n" : " deletions(-)\n\n";
+}
+
+sub git_diff_sed_lines ($$) {
+       my ($dst, $req) = @_;
+
+       # TODO: discard diffs if they are too big
+
+       my @dlines = split(/\n/, $req->{dbuf}, -1);
+       $req->{dbuf} = '';
+
+       if (my $help = delete $req->{mhelp}) {
+               $$dst .= $help; # CC_MERGE
+       }
+
+       # don't touch the last line, it may not be terminated
+       $req->{dbuf} .= pop @dlines;
+
+       my $ndiff = \($req->{ndiff});
+       my $cmt = '[a-f0-9]+';
+       while (defined(my $l = shift @dlines)) {
+               if ($l =~ m{\Adiff --git ("?a/.+) ("?b/.+)\z}) { # regular
+                       $$dst .= git_diff_ab_hdr($req, $1, $2) . "\n";
+               } elsif ($l =~ m{\Adiff --(cc|combined) (.+)\z}) {
+                       $$dst .= git_diff_cc_hdr($req, $1, $2) . "\n";
+               } elsif ($l =~ /\Aindex ($cmt)\.\.($cmt)(.*)\z/o) { # regular
+                       $$dst .= git_diff_ab_index($1, $2, $3) . "\n";
+               } elsif ($l =~ /\A@@ (\S+) (\S+) @@(.*)\z/) { # regular
+                       $$dst .= git_diff_ab_hunk($req, $1, $2, $3) . "\n";
+               } elsif ($l =~ /\Aindex ($cmt,[^\.]+)\.\.($cmt)(.*)$/o) {  #--cc
+                       $$dst .= git_diff_cc_index($req, $1, $2, $3) . "\n";
+               } elsif ($l =~ /\A(@@@+) (\S+.*\S+) @@@+(.*)\z/) { # --cc
+                       $$dst .= git_diff_cc_hunk($req, $1, $2, $3) . "\n";
+               } else {
+                       $$dst .= utf8_html($l) . "\n";
+               }
+               ++$$ndiff;
+       }
+}
+
+sub git_diff_sed_run ($$) {
+       my ($dst, $req) = @_;
+       $req->{dstate} == DSTATE_STAT and git_diff_sed_stat($dst, $req);
+       $req->{dstate} == DSTATE_LINES and git_diff_sed_lines($dst, $req);
+       undef;
+}
+
+sub git_diff_sed_close ($$) {
+       my ($dst, $req) = @_;
+       $$dst .= utf8_html(delete $req->{dbuf});
+       undef;
+}
+
 1;
index 969a0b5e82cdccf5849b1f10d6113822b6f05776..ed2d6d56d762c40cd625892f45dfa3b2fd71e878 100644 (file)
@@ -25,7 +25,7 @@ test_psgi($test->{app}, sub {
        like($body, qr!</html>\z!, 'response body finished');
 
        $res = $cb->(GET($req.$q));
-       is($res->code, 404, 'got 404 response for default');
+       is($res->code, 404, 'got 404 response for bad id');
 });
 
 done_testing();