about summary refs log tree commit homepage
path: root/lib
diff options
context:
space:
mode:
authorEric Wong <e@80x24.org>2016-03-12 03:41:29 +0000
committerEric Wong <e@80x24.org>2016-04-05 18:58:27 +0000
commit89abb41ac9a2ca0a4fff0ccf75edf0bc1d050d61 (patch)
tree740e28ed88cf7292e177353b4d1270837108305e /lib
parent78951743029a1d0074ff447c612b2750a83623e5 (diff)
downloadpublic-inbox-89abb41ac9a2ca0a4fff0ccf75edf0bc1d050d61.tar.gz
This needs to be cleaned up, but we shall support the
(potentially very expensive) diff view between arbitrary
revisions to avoid breaking existing URLs.

The diff parsing code will need to be consolidated
between this and the commit view.
Diffstat (limited to 'lib')
-rw-r--r--lib/PublicInbox/Repobrowse.pm3
-rw-r--r--lib/PublicInbox/RepobrowseGitDiff.pm252
2 files changed, 254 insertions, 1 deletions
diff --git a/lib/PublicInbox/Repobrowse.pm b/lib/PublicInbox/Repobrowse.pm
index 0b09800b..0c4cf144 100644
--- a/lib/PublicInbox/Repobrowse.pm
+++ b/lib/PublicInbox/Repobrowse.pm
@@ -24,7 +24,8 @@ use Plack::Request;
 use URI::Escape qw(uri_escape_utf8 uri_unescape);
 use PublicInbox::RepobrowseConfig;
 
-my %CMD = map { lc($_) => $_ } qw(Log Commit Tree Patch Blob Plain Tag Atom);
+my %CMD = map { lc($_) => $_ } qw(Log Commit Tree Patch Blob Plain Tag Atom
+        Diff);
 my %VCS = (git => 'Git');
 my %LOADED;
 
diff --git a/lib/PublicInbox/RepobrowseGitDiff.pm b/lib/PublicInbox/RepobrowseGitDiff.pm
new file mode 100644
index 00000000..7e137adb
--- /dev/null
+++ b/lib/PublicInbox/RepobrowseGitDiff.pm
@@ -0,0 +1,252 @@
+# Copyright (C) 2016 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# shows the /diff endpoint for git repositories for cgit compatibility
+# usage: /repo.git/diff?id=COMMIT_ID&id2=COMMIT_ID2
+#
+# FIXME: much duplicated code between this and RepobrowseGitCommit.pm
+#
+# We probably will not link to this outright because it's expensive,
+# but exists to preserve URL compatibility.
+package PublicInbox::RepobrowseGitDiff;
+use strict;
+use warnings;
+use base qw(PublicInbox::RepobrowseBase);
+use PublicInbox::Hval qw(utf8_html to_attr);
+use PublicInbox::RepobrowseGit qw(git_unquote git_commit_title);
+
+sub call_git_diff {
+        my ($self, $req) = @_;
+        my $git = $req->{repo_info}->{git};
+        my $q = PublicInbox::RepobrowseGitQuery->new($req->{cgi});
+        my $id = $q->{id};
+        my $id2 = $q->{id2};
+
+        my @cmd = (qw(diff-tree -z --numstat -p --encoding=UTF-8
+                        --no-notes --no-color -M -B -D -r),
+                        $id2, $id, '--');
+        if (defined(my $expath = $req->{expath})) {
+                push @cmd, $expath;
+        }
+        my $rpipe = $git->popen(\@cmd, undef, { 2 => $git->err_begin });
+        my $env = $req->{cgi}->env;
+        my $err = $env->{'psgi.errors'};
+        my ($res, $vin, $fh);
+        $req->{dbuf} = '';
+        $req->{p} = [ $id2 ];
+        $req->{h} = $id;
+
+        my $end = sub {
+                if ($fh) {
+                        # write out the last bit that was buffered
+                        my @buf = split(/\n/, delete $req->{dbuf}, -1);
+                        my $s = '';
+                        $s .= git_diff_line_i($req, $_) foreach @buf;
+                        $s .= '</pre></body></html>';
+                        $fh->write($s);
+
+                        $fh->close;
+                        $fh = undef;
+                } elsif ($res) {
+                        $res->($self->r(500));
+                }
+                if ($rpipe) {
+                        $rpipe->close; # _may_ be Danga::Socket::close
+                        $rpipe = undef;
+                }
+        };
+        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 diff ($git->{git_dir}): $e\n");
+        };
+        my $cb = sub {
+                my $off = length($req->{dbuf});
+                my $n = $rpipe->sysread($req->{dbuf}, 8192, $off);
+                return $fail->() unless defined $n;
+                return $end->() if $n == 0;
+                if ($res) {
+                        my $h = ['Content-Type', 'text/html; charset=UTF-8'];
+                        $fh = $res->([200, $h]);
+                        $res = undef;
+                        my $o = { nofollow => 1, noindex => 1 };
+                        $fh->write($self->html_start($req, 'diff', $o)."\n");
+                }
+                git_diff_to_html($req, $fh) if $fh;
+        };
+        if (my $async = $env->{'pi-httpd.async'}) {
+                $rpipe = $async->($rpipe, $cb);
+                sub { ($res) = @_ } # let Danga::Socket handle the rest.
+        } else { # synchronous loop for other PSGI servers
+                $vin = '';
+                vec($vin, fileno($rpipe), 1) = 1;
+                sub {
+                        ($res) = @_;
+                        while ($rpipe) { $cb->() }
+                }
+        }
+}
+
+sub git_diffstat_to_html ($$$) {
+        my ($req, $fh, undef) = @_;
+        my @stat = split("\0", $_[2]); # avoiding copy for $_[2]
+        my $nr = 0;
+        my ($nadd, $ndel) = (0, 0);
+        my $s = '';
+        while (defined(my $l = shift @stat)) {
+                $l =~ s/\n?(\S+)\t+(\S+)\t+// or next;
+                my ($add, $del) = ($1, $2);
+                if ($add =~ /\A\d+\z/) {
+                        $nadd += $add;
+                        $ndel += $del;
+                        $add = "+$add";
+                        $del = "-$del";
+                }
+                my $num = sprintf('% 6s/%-6s', $del, $add);
+                if (length $l) {
+                        my $anchor = to_attr(git_unquote($l));
+                        $req->{anchors}->{$anchor} = $l;
+                        $l = utf8_html($l);
+                        $l = qq(<a\nhref="#$anchor">$l</a>);
+                } else {
+                        my $from = shift @stat;
+                        my $to = shift @stat;
+                        $l = git_diffstat_rename($req, $from, $to);
+                }
+                ++$nr;
+                $s .= ' '.$num."\t".$l."\n";
+        }
+        $s .= "\n $nr ";
+        $s .= $nr == 1 ? 'file changed, ' : 'files changed, ';
+        $s .= $nadd;
+        $s .= $nadd == 1 ? ' insertion(+), ' : ' insertions(+), ';
+        $s .= $ndel;
+        $s .= $ndel == 1 ? " deletion(-)\n\n" : " deletions(-)\n\n";
+        $fh->write($s);
+}
+
+sub git_diff_line_i {
+        my ($req, $l) = @_;
+        my $cmt = '[a-f0-9]+';
+
+        if ($l =~ m{^diff --git ("?a/.+) ("?b/.+)$}) { # regular
+                $l = git_diff_ab_hdr($req, $1, $2);
+        } elsif ($l =~ /^index ($cmt)\.\.($cmt)(.*)$/o) { # regular
+                $l = git_diff_ab_index($req, $1, $2, $3);
+        } elsif ($l =~ /^@@ (\S+) (\S+) @@(.*)$/) { # regular
+                $l = git_diff_ab_hunk($req, $1, $2, $3);
+        } elsif ($l =~ /^index ($cmt,[^\.]+)\.\.($cmt)(.*)$/o) { # --cc
+                $l = git_diff_cc_index($req, $1, $2, $3);
+        } else {
+                $l = utf8_html($l);
+        }
+        $l .= "\n";
+}
+
+sub git_diff_to_html {
+        my ($req, $fh) = @_;
+        if (!$req->{diff_state}) {
+                my ($stat, $buf) = split(/\0\0/, $req->{dbuf}, 2);
+                return unless defined $buf;
+                $req->{dbuf} = $buf;
+                git_diffstat_to_html($req, $fh, $stat);
+                $req->{diff_state} = 1;
+        }
+        my @buf = split(/\n/, $req->{dbuf}, -1);
+        $req->{dbuf} = pop @buf; # last line, careful...
+        if (@buf) {
+                my $s = '';
+                $s .= git_diff_line_i($req, $_) foreach @buf;
+                $fh->write($s) if $s ne '';
+        }
+}
+
+sub git_diffstat_rename {
+        my ($req, $from, $to) = @_;
+        my $anchor = to_attr(git_unquote($to));
+        $req->{anchors}->{$anchor} = $to;
+        my @from = split('/', $from);
+        my @to = split('/', $to);
+        my $orig_to = $to;
+        my ($base, @base);
+        while (@to && @from && $to[0] eq $from[0]) {
+                push @base, shift(@to);
+                shift @from;
+        }
+
+        $base = utf8_html(join('/', @base)) if @base;
+        $from = utf8_html(join('/', @from));
+        $to = PublicInbox::Hval->utf8(join('/', @to), $orig_to);
+        my $tp = $to->as_path;
+        my $th = $to->as_html;
+        $to = qq(<a\nhref="#$anchor">$th</a>);
+        @base ? "$base/{$from =&gt; $to}" : "$from =&gt; $to";
+}
+
+# index abcdef89..01234567
+sub git_diff_ab_index {
+        my ($req, $xa, $xb, $end) = @_;
+        # not wasting bandwidth on links here, yet
+        # links in hunk headers are far more useful with line offsets
+        $end = utf8_html($end);
+        "index $xa..$xb$end";
+}
+
+# diff --git a/foo.c b/bar.c
+sub git_diff_ab_hdr {
+        my ($req, $fa, $fb) = @_;
+        my $html_a = utf8_html($fa);
+        my $html_b = utf8_html($fb);
+        $fa = git_unquote($fa);
+        $fb = git_unquote($fb);
+        $fa =~ s!\Aa/!!;
+        $fb =~ s!\Ab/!!;
+        my $anchor = to_attr($fb);
+        delete $req->{anchors}->{$anchor};
+        $fa = $req->{fa} = PublicInbox::Hval->utf8($fa);
+        $fb = $req->{fb} = PublicInbox::Hval->utf8($fb);
+        $req->{path_a} = $fa->as_path;
+        $req->{path_b} = $fb->as_path;
+
+        # not wasting bandwidth on links here
+        # links in hunk headers are far more useful with line offsets
+        qq(<a\nhref=#D\nid="$anchor">diff</a> --git $html_a $html_b);
+}
+
+# @@ -1,2 +3,4 @@ (regular diff)
+sub git_diff_ab_hunk {
+        my ($req, $ca, $cb, $ctx) = @_;
+        my ($na) = ($ca =~ /\A-(\d+)/);
+        my ($nb) = ($cb =~ /\A\+(\d+)/);
+
+        # we add "rel=nofollow" here to reduce load on search engines, here
+        my $rel = $req->{relcmd};
+        my $rv = '@@ ';
+        if (defined($na) && $na == 0) { # new file
+                $rv .= $ca;
+        } else {
+                $na = defined $na ? "#n$na" : '';
+                my $p = $req->{p}->[0];
+                $rv .= qq(<a\nrel=nofollow);
+                $rv .= qq(\nhref="${rel}tree/$req->{path_a}?id=$p$na">);
+                $rv .= "$ca</a>";
+        }
+        $rv .= ' ';
+        if (defined($nb) && $nb == 0) { # deleted file
+                $rv .= $cb;
+        } else {
+                my $h = $req->{h};
+                $nb = defined $nb ? "#n$nb" : '';
+                $rv .= qq(<a\nrel=nofollow);
+                $rv .= qq(\nhref="${rel}tree/$req->{path_b}?id=$h$nb">);
+                $rv .= "$cb</a>";
+        }
+        $rv . ' @@' . utf8_html($ctx);
+}
+
+1;