about summary refs log tree commit homepage
path: root/lib/PublicInbox/RepobrowseGitDiff.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/PublicInbox/RepobrowseGitDiff.pm')
-rw-r--r--lib/PublicInbox/RepobrowseGitDiff.pm131
1 files changed, 131 insertions, 0 deletions
diff --git a/lib/PublicInbox/RepobrowseGitDiff.pm b/lib/PublicInbox/RepobrowseGitDiff.pm
new file mode 100644
index 00000000..02a2c562
--- /dev/null
+++ b/lib/PublicInbox/RepobrowseGitDiff.pm
@@ -0,0 +1,131 @@
+# 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);
+use PublicInbox::RepobrowseGitDiffCommon qw/git_diffstat_emit
+        git_diff_ab_index git_diff_ab_hdr git_diff_ab_hunk/;
+
+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, '--');
+        my $expath = $req->{expath};
+        push @cmd, $expath if defined $expath;
+        $req->{rpipe} = $git->popen(\@cmd, undef, { 2 => $git->err_begin });
+        my $env = $req->{cgi}->env;
+        my $err = $env->{'psgi.errors'};
+        my ($vin);
+        $req->{dbuf} = '';
+        $req->{p} = [ $id2 ];
+        $req->{h} = $id;
+        my $end = sub {
+                if (my $fh = delete $req->{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;
+                } elsif (my $res = delete $req->{res}) {
+                        $res->($self->r(500));
+                }
+                if (my $rpipe = delete $req->{rpipe}) {
+                        $rpipe->close; # _may_ be Danga::Socket::close
+                }
+        };
+        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 = $req->{rpipe}->sysread($req->{dbuf}, 8192, $off);
+                return $fail->() unless defined $n;
+                return $end->() if $n == 0;
+                if (my $res = delete $req->{res}) {
+                        my $h = ['Content-Type', 'text/html; charset=UTF-8'];
+                        my $fh = $req->{fh} = $res->([200, $h]);
+                        my $o = { nofollow => 1, noindex => 1 };
+                        my $ex = defined $expath ? " $expath" : '';
+                        $fh->write($self->html_start($req, 'diff', $o).
+                                        "\n\n".
+                                        utf8_html("git diff-tree -r -M -B -D ".
+                                                "$id2 $id --$ex"). "\n\n");
+                }
+                git_diff_to_html($req);
+        };
+        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->() }
+                }
+        }
+}
+
+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($1, $2, $3);
+        } elsif ($l =~ /^@@ (\S+) (\S+) @@(.*)$/) { # regular
+                $l = git_diff_ab_hunk($req, $1, $2, $3);
+        } else {
+                $l = utf8_html($l);
+        }
+        $l .= "\n";
+}
+
+sub git_diff_to_html {
+        my ($req) = @_;
+        my $fh = $req->{fh};
+        if (!$req->{diff_state}) {
+                my ($stat, $buf) = split(/\0\0/, $req->{dbuf}, 2);
+                return unless defined $buf;
+                $req->{dbuf} = $buf;
+                git_diffstat_emit($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 '';
+        }
+}
+
+1;