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',
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'};
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;
$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';
}
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";
$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);
# 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};
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;
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 ($$$) {
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) = @_;
$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));
@base ? "$base/{$from => $to}" : "$from => $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;
$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;