about summary refs log tree commit homepage
path: root/lib/PublicInbox/RepoGitSrc.pm
diff options
context:
space:
mode:
authorEric Wong <e@80x24.org>2017-03-02 23:39:49 +0000
committerEric Wong <e@80x24.org>2017-03-02 23:39:49 +0000
commit16b1fbe36cc39a351ef9810b9018d36df833a941 (patch)
tree6a354e1ab5177993fb11bd5ae0e43400d66ffbce /lib/PublicInbox/RepoGitSrc.pm
parenta4df292ed39f6e02a7d57326f291583339cb562d (diff)
downloadpublic-inbox-16b1fbe36cc39a351ef9810b9018d36df833a941.tar.gz
This is shorter, and makes more sense as the endpoint
displays both tree listings and actual blob sources.
This will also make rewriting existing URLs from cgit
installations easier.
Diffstat (limited to 'lib/PublicInbox/RepoGitSrc.pm')
-rw-r--r--lib/PublicInbox/RepoGitSrc.pm249
1 files changed, 249 insertions, 0 deletions
diff --git a/lib/PublicInbox/RepoGitSrc.pm b/lib/PublicInbox/RepoGitSrc.pm
new file mode 100644
index 00000000..1546830f
--- /dev/null
+++ b/lib/PublicInbox/RepoGitSrc.pm
@@ -0,0 +1,249 @@
+# Copyright (C) 2015 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+package PublicInbox::RepoGitSrc;
+use strict;
+use warnings;
+use base qw(PublicInbox::RepoBase);
+use PublicInbox::Hval qw(utf8_html);
+use PublicInbox::Qspawn;
+
+my %GIT_MODE = (
+        '100644' => ' ', # blob
+        '100755' => 'x', # executable blob
+        '040000' => 'd', # tree
+        '120000' => 'l', # symlink
+        '160000' => 'g', # commit (gitlink)
+);
+
+my $BINARY_MSG = "Binary file, save using the 'raw' link above";
+my $MAX_ASYNC = 65536; # same as pipe size on Linux
+my $BIN_DETECT = 8000; # same as git (buffer_is_binary in git.git)
+
+sub call_git_src {
+        my ($self, $req) = @_;
+        my $repo = $req->{-repo};
+        my $git = $repo->{git};
+        my $tip = $req->{tip} || $req->{repo}->tip;
+        sub {
+                my ($res) = @_;
+                $git->check_async($req->{env}, "$tip:$req->{expath}", sub {
+                        my ($info) = @_;
+                        my ($hex, $type, $size) = @$info;
+                        unless (defined $type) {
+                                return $res->([404,
+                                        ['Content-Type','text/plain'],
+                                        ['Not Found']]);
+                        }
+                        show_tree($self, $req, $res, $hex, $type, $size);
+                });
+        }
+}
+
+sub show_tree {
+        my ($self, $req, $res, $hex, $type, $size) = @_;
+        my $opts = { nofollow => 1 };
+        my $title = "tree: ".utf8_html($req->{expath});
+        $req->{thtml} = $self->html_start($req, $title, $opts) . "\n";
+        if ($type eq 'tree') {
+                $opts->{noindex} = 1;
+                git_tree_show($req, $res, $hex);
+        } elsif ($type eq 'blob') {
+                git_blob_show($req, $res, $hex, $size);
+        } else {
+                $res->([404, ['Content-Type', 'text/plain; charset=UTF-8'],
+                         ["Unrecognized type ($type) for $hex\n"]]);
+        }
+}
+
+sub cur_path {
+        my ($req) = @_;
+        my @ex = @{$req->{extra}} or return '<b>root</b>';
+        my $s;
+        my $tip = $req->{tip} || $req->{repo}->tip;
+        my $rel = $req->{relcmd};
+        # avoid relative paths, here, we don't want to propagate
+        # trailing-slash URLs although we tolerate them
+        $s = "<a\nhref=\"${rel}src/$tip\">root</a>/";
+        my $cur = pop @ex;
+        my @t;
+        $s .= join('/', (map {
+                push @t, $_;
+                my $e = PublicInbox::Hval->utf8($_, join('/', @t));
+                my $ep = $e->as_path;
+                my $eh = $e->as_html;
+                "<a\nhref=\"${rel}src/$tip/$ep\">$eh</a>";
+        } @ex), '<b>'.utf8_html($cur).'</b>');
+}
+
+sub git_blob_sed ($$$) {
+        my ($req, $hex, $size) = @_;
+        my $pfx = $req->{tpfx};
+        my $nl = 0;
+        my $bytes = 0;
+        my @lines;
+        my $buf = '';
+        my $rel = $req->{relcmd};
+        my $tip = $req->{tip} || $req->{repo}->tip;
+        my $raw = join('/', "${rel}raw", $tip, @{$req->{extra}});
+        $raw = PublicInbox::Hval->utf8($raw)->as_path;
+        my $t = cur_path($req);
+        my $end = '';
+        $req->{thtml} .= qq{\npath: $t\n\nblob $hex} .
+                        qq{\t$size bytes (<a\nhref="$raw">raw</a>)};
+        $req->{lstart} = '</pre><hr/><pre>';
+        my $s;
+
+        sub {
+                my $dst = delete $req->{thtml} || '';
+                if (defined $_[0]) {
+                        return '' if $bytes < 0; # binary
+                        if ($bytes <= $BIN_DETECT) {
+                                if (index($_[0], "\0") >= 0) {
+                                        $bytes = -1;
+                                        $s = delete $req->{lstart} and
+                                                $dst .= $s;
+                                        $dst .= "\n";
+                                        $dst .= $BINARY_MSG;
+                                        return $dst .= '</pre></body></html>';
+                                }
+                        }
+                        $bytes += bytes::length($_[0]);
+                        $buf .= $_[0];
+                        $_[0] = ''; # save some memory
+                        $s = delete $req->{lstart} and $dst .= $s;
+                        @lines = split(/\r?\n/, $buf, -1);
+                        $buf = pop @lines; # last line, careful...
+                } else { # EOF
+                        $s = delete $req->{lstart} and $dst .= $s;
+                        @lines = split(/\r?\n/, $buf, -1);
+                        $buf = pop @lines;
+                        $end .= '</pre></body></html>';
+                }
+                foreach (@lines) {
+                        ++$nl;
+                        $dst .= "<a\nid=n$nl>";
+                        $dst .= sprintf("% 5u</a>\t", $nl);
+                        $dst .= utf8_html($_);
+                        $dst .= "\n";
+                }
+                @lines = ();
+                if ($end && defined $buf && $buf ne '') {
+                        ++$nl;
+                        $dst .= "<a\nid=n$nl>";
+                        $dst .= sprintf("% 5u</a>\t", $nl);
+                        $dst .= utf8_html($buf);
+                        $buf = undef;
+                        $dst .= "\n\\ No newline at end of file";
+                }
+                $dst .= $end;
+        }
+}
+
+sub git_blob_show {
+        my ($req, $res, $hex, $size) = @_;
+        my $sed = git_blob_sed($req, $hex, $size);
+        my $git = $req->{-repo}->{git};
+        if ($size <= $MAX_ASYNC) {
+                my $buf = ''; # we slurp small files
+                $git->cat_async($req->{env}, $hex, sub {
+                        my ($r) = @_;
+                        my $ref = ref($r);
+                        return if $ref eq 'ARRAY'; # redundant info
+                        if ($ref eq 'SCALAR') {
+                                $buf .= $$r;
+                                if (bytes::length($buf) == $size) {
+                                        my $fh = $res->([200,
+                                                ['Content-Type',
+                                                 'text/html; charset=UTF-8']]);
+                                        $fh->write($sed->($buf));
+                                        $fh->write($sed->(undef));
+                                        $fh->close;
+                                }
+                                return;
+                        }
+                        my $cb = $res or return;
+                        $res = undef;
+                        $cb->([500,
+                                ['Content-Type', 'text/plain; charset=UTF-8'],
+                                [ 'Error' ]]);
+                });
+        } else {
+                $res->([200, ['Content-Type', 'text/plain; charset=UTF-8'],
+                        [ 'Too big' ]]);
+        }
+}
+
+sub git_tree_sed ($) {
+        my ($req) = @_;
+        my @lines;
+        my $buf = '';
+        my $pfx = $req->{tpfx};
+        my $end;
+        sub {
+                my $dst = delete $req->{thtml} || '';
+                if (defined $_[0]) {
+                        @lines = split(/\0/, $buf .= $_[0]);
+                        $buf = pop @lines if @lines;
+                } else {
+                        @lines = split(/\0/, $buf);
+                        $end = '</pre></body></html>';
+                }
+                for (@lines) {
+                        my ($m, $x, $s, $path) =
+                                        (/\A(\S+) \S+ (\S+)( *\S+)\t(.+)\z/s);
+                        $m = $GIT_MODE{$m} or next;
+                        $path = PublicInbox::Hval->utf8($path);
+                        my $ref = $path->as_path;
+                        $path = $path->as_html;
+
+                        if ($m eq 'g') {
+                                # TODO: support cross-repository gitlinks
+                                $dst .= 'g' . (' ' x 15) . "$path @ $x\n";
+                                next;
+                        }
+                        elsif ($m eq 'd') { $path = "$path/" }
+                        elsif ($m eq 'x') { $path = "<b>$path</b>" }
+                        elsif ($m eq 'l') { $path = "<i>$path</i>" }
+                        $s =~ s/\s+//g;
+
+                        # 'raw' and 'log' links intentionally omitted
+                        # for brevity and speed
+                        $dst .= qq($m\t).
+                                qq($s\t<a\nhref="$pfx/$ref">$path</a>\n);
+                }
+                $dst;
+        }
+}
+
+sub git_tree_show {
+        my ($req, $res, $hex) = @_;
+        my $git = $req->{-repo}->{git};
+        my $cmd = $git->cmd(qw(ls-tree -l -z), $git->abbrev, $hex);
+        my $rdr = { 2 => $git->err_begin };
+        my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr);
+        my $t = cur_path($req);
+        my $pfx;
+
+        $req->{thtml} .= "\npath: $t\n\n<b>mode\tsize\tname</b>\n";
+        if (defined(my $last = $req->{extra}->[-1])) {
+                $pfx = PublicInbox::Hval->utf8($last)->as_path;
+        } elsif (defined(my $tip = $req->{tip})) {
+                $pfx = $tip;
+        } else {
+                $pfx = 'src/' . $req->{-repo}->tip;
+        }
+        $req->{tpfx} = $pfx;
+        my $env = $req->{env};
+        $env->{'qspawn.response'} = $res;
+        $qsp->psgi_return($env, undef, sub {
+                my ($r) = @_;
+                if (defined $r) {
+                        $env->{'qspawn.filter'} = git_tree_sed($req);
+                        [ 200, [ 'Content-Type', 'text/html' ] ];
+                } else {
+                        [ 500, [ 'Content-Type', 'text/plain' ], [ $git->err ]];
+                }
+        });
+}
+
+1;