]> git.ipfire.org Git - thirdparty/public-inbox.git/commitdiff
repobrowse: add "plain" endpoint for cgit URI compat
authorEric Wong <e@80x24.org>
Sun, 3 Jan 2016 04:18:58 +0000 (04:18 +0000)
committerEric Wong <e@80x24.org>
Tue, 5 Apr 2016 18:58:27 +0000 (18:58 +0000)
This is similar to the "blob" endpoint, except it shows
trees as a directory.

lib/PublicInbox/RepoBrowse.pm
lib/PublicInbox/RepoBrowseGitBlob.pm
lib/PublicInbox/RepoBrowseGitPlain.pm [new file with mode: 0644]
lib/PublicInbox/RepoBrowseGitTree.pm
t/repobrowse_common_git.perl [new file with mode: 0644]
t/repobrowse_git_plain.t [new file with mode: 0644]

index 63228249036dea70b5ad5693d7e4c9361fc88458..74fa40fc236275c9713252696f0a53a20f2af6d3 100644 (file)
@@ -23,7 +23,7 @@ use warnings;
 use URI::Escape qw(uri_escape_utf8 uri_unescape);
 use PublicInbox::RepoConfig;
 
-my %CMD = map { lc($_) => $_ } qw(Log Commit Tree Patch Blob);
+my %CMD = map { lc($_) => $_ } qw(Log Commit Tree Patch Blob Plain);
 my %VCS = (git => 'Git');
 my %LOADED;
 
index d94f0d94c68e8cffc46b7a9439d1e3973b2f4ac3..3be069f74d47364737247ee2e3bb0e41fff24c49 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2015 all contributors <meta@public-inbox.org>
+# Copyright (C) 2015-2016 all contributors <meta@public-inbox.org>
 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
 
 # Show a blob as-is
@@ -6,11 +6,12 @@ package PublicInbox::RepoBrowseGitBlob;
 use strict;
 use warnings;
 use base qw(PublicInbox::RepoBrowseBase);
+use base qw(Exporter);
+our @EXPORT = qw(git_blob_mime_type git_blob_stream_response);
 
 sub call_git_blob {
        my ($self, $req) = @_;
        my $git = $req->{repo_info}->{git};
-       my $to_read = 8000; # git uses this size to detect binary files
        my $q = PublicInbox::RepoBrowseQuery->new($req->{cgi});
        my $id = $q->{id};
        $id eq '' and $id = 'HEAD';
@@ -24,35 +25,46 @@ sub call_git_blob {
        my ($r, $buf);
        my $left = $size;
        if ($type eq 'blob') {
-               my $base = $req->{extra}->[-1];
-               $type = $self->mime_type($base) if defined $base;
-               unless ($type) {
-                       $to_read = $left if $to_read > $left;
-                       $r = read($cat, $buf, $to_read);
-                       if (!defined $r || $r <= 0) {
-                               $git->cat_file_finish($left);
-                               return;
-                       }
-                       $left -= $r;
-                       $type = (index($buf, "\0") < 0) ?
-                               'text/plain' :
-                               'application/octet-stream';
-               }
+               $type = git_blob_mime_type($self, $req, $cat, \$buf, \$left);
        } elsif ($type eq 'commit' || $type eq 'tag') {
                $type = 'text/plain';
        } else {
                $type = 'application/octet-stream';
        }
+       git_blob_stream_response($git, $cat, $size, $type, $buf, $left);
+}
+
+sub git_blob_mime_type {
+       my ($self, $req, $cat, $buf, $left) = @_;
+       my $base = $req->{extra}->[-1];
+       my $type = $self->mime_type($base) if defined $base;
+       return $type if $type;
+
+       my $to_read = 8000; # git uses this size to detect binary files
+       $to_read = $$left if $to_read > $$left;
+       my $r = read($cat, $$buf, $to_read);
+       if (!defined $r || $r <= 0) {
+               my $git = $req->{repo_info}->{git};
+               $git->cat_file_finish($$left);
+               return;
+       }
+       $$left -= $r;
+       (index($buf, "\0") < 0) ?  'text/plain' : 'application/octet-stream';
+}
+
+sub git_blob_stream_response {
+       my ($git, $cat, $size, $type, $buf, $left) = @_;
 
        sub {
                my ($res) = @_;
+               my $to_read = 8192;
                eval {
                        my $fh = $res->([ 200, ['Content-Length' => $size,
                                                'Content-Type' => $type]]);
                        $fh->write($buf) if defined $buf;
                        while ($left > 0) {
                                $to_read = $left if $to_read > $left;
-                               $r = read($cat, $buf, $to_read);
+                               my $r = read($cat, $buf, $to_read);
                                last if (!defined $r || $r <= 0);
                                $left -= $r;
                                $fh->write($buf);
diff --git a/lib/PublicInbox/RepoBrowseGitPlain.pm b/lib/PublicInbox/RepoBrowseGitPlain.pm
new file mode 100644 (file)
index 0000000..e16195d
--- /dev/null
@@ -0,0 +1,81 @@
+# Copyright (C) 2015-2016 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+package PublicInbox::RepoBrowseGitPlain;
+use strict;
+use warnings;
+use base qw(PublicInbox::RepoBrowseBase);
+use PublicInbox::RepoBrowseGitBlob;
+use PublicInbox::Hval qw(utf8_html);
+
+sub call_git_plain {
+       my ($self, $req) = @_;
+       my $git = $req->{repo_info}->{git};
+       my $q = PublicInbox::RepoBrowseQuery->new($req->{cgi});
+       my $id = $q->{id};
+       $id eq '' and $id = 'HEAD';
+
+       if (length(my $expath = $req->{expath})) {
+               $id .= ":$expath";
+       } else {
+               $id .= ':';
+       }
+       my ($cat, $hex, $type, $size) = $git->cat_file_begin($id);
+       return unless defined $cat;
+
+       my ($r, $buf);
+       my $left = $size;
+       if ($type eq 'blob') {
+               $type = git_blob_mime_type($self, $req, $cat, \$buf, \$left);
+       } elsif ($type eq 'commit' || $type eq 'tag') {
+               $type = 'text/plain';
+       } elsif ($type eq 'tree') {
+               $git->cat_file_finish($left);
+               return git_tree_plain($req, $git, $hex);
+       } else {
+               $type = 'application/octet-stream';
+       }
+       git_blob_stream_response($git, $cat, $size, $type, $buf, $left);
+}
+
+# This should follow the cgit DOM structure in case anybody depends on it,
+# not using <pre> here as we don't expect people to actually view it much
+sub git_tree_plain {
+       my ($req, $git, $hex) = @_;
+
+       my @ex = @{$req->{extra}};
+       my $rel = $req->{relcmd};
+       my $title = utf8_html(join('/', '', @ex, ''));
+       my $tslash = $req->{tslash};
+       my $pfx = $tslash ? './' : 'plain/';
+       my $t = "<h2>$title</h2><ul>";
+       if (@ex) {
+               if ($tslash) {
+                       $t .= qq(<li><a\nhref="../">../</a></li>);
+               } else  {
+                       $t .= qq(<li><a\nhref="./">../</a></li>);
+                       my $last = PublicInbox::Hval->utf8($ex[-1])->as_href;
+                       $pfx = "$last/";
+               }
+       }
+       my $ls = $git->popen(qw(ls-tree --name-only -z --abbrev=12), $hex);
+       sub {
+               my ($res) = @_;
+               my $fh = $res->([ 200, ['Content-Type' => 'text/html']]);
+               $fh->write("<html><head><title>$title</title></head><body>".
+                               $t);
+
+               local $/ = "\0";
+               while (defined(my $n = <$ls>)) {
+                       chomp $n;
+                       $n = PublicInbox::Hval->utf8($n);
+                       my $ref = $n->as_path;
+                       $n = $n->as_html;
+
+                       $fh->write(qq(<li><a\nhref="$pfx$ref">$n</a></li>))
+               }
+               $fh->write('</ul></body></html>');
+               $fh->close;
+       }
+}
+
+1;
index 275c562ed5fa6b22d6c2d10dbab3d14b9cf155a3..b361a9c08199d93fe601f6c2987971c5ce696f55 100644 (file)
@@ -17,12 +17,15 @@ my %GIT_MODE = (
 sub git_tree_stream {
        my ($self, $req, $res) = @_; # res: Plack callback
        my @extra = @{$req->{extra}};
+       my $git = $req->{repo_info}->{git};
        my $q = PublicInbox::RepoBrowseQuery->new($req->{cgi});
        my $id = $q->{id};
-       $id eq '' and $id = 'HEAD';
+       if ($id eq '') {
+               chomp($id = $git->qx(qw(rev-parse --short=10 HEAD)));
+               $q->{id} = $id;
+       }
 
        my $obj = "$id:$req->{expath}";
-       my $git = $req->{repo_info}->{git};
        my ($hex, $type, $size) = $git->check($obj);
 
        if (!defined($type) || ($type ne 'blob' && $type ne 'tree')) {
@@ -31,13 +34,15 @@ sub git_tree_stream {
        }
 
        my $fh = $res->([200, ['Content-Type'=>'text/html; charset=UTF-8']]);
-       $fh->write('<html><head><title></title></head><body>'.
-                        PublicInbox::Hval::PRE);
+       $fh->write('<html><head>'. PublicInbox::Hval::STYLE .
+               '<title></title></head><body>');
 
        if ($type eq 'tree') {
                git_tree_show($req, $fh, $git, $hex, $q);
        } elsif ($type eq 'blob') {
                git_blob_show($fh, $git, $hex);
+       } else {
+               # TODO
        }
        $fh->write('</body></html>');
        $fh->close;
@@ -50,7 +55,7 @@ sub call_git_tree {
 
 sub git_blob_binary {
        my ($fh) = @_;
-       $fh->write("Binary file cannot be displayed\n");
+       $fh->write('<pre>Binary file cannot be displayed</pre>');
 }
 
 sub git_blob_show {
@@ -58,9 +63,9 @@ sub git_blob_show {
        # ref: buffer_is_binary in git.git
        my $to_read = 8000; # git uses this size to detect binary files
        my $text_p;
+       my $n = 0;
        $git->cat_file($hex, sub {
                my ($cat, $left) = @_; # $$left == $size
-               my $n = 0;
                $to_read = $$left if $to_read > $$left;
                my $r = read($cat, my $buf, $to_read);
                return unless defined($r) && $r > 0;
@@ -68,7 +73,9 @@ sub git_blob_show {
 
                return git_blob_binary($fh) if (index($buf, "\0") >= 0);
 
+               $fh->write('<table><tr><td><pre>');
                $text_p = 1;
+
                while (1) {
                        my @buf = split(/\r?\n/, $buf, -1);
                        $buf = pop @buf; # last line, careful...
@@ -87,11 +94,16 @@ sub git_blob_show {
                }
                0;
        });
+
+       # line numbers go in a second column:
+       $fh->write('</pre></td><td><pre>');
+       $fh->write(qq(<a\nhref="#n$_">$_</a>\n)) foreach (1..$n);
+       $fh->write('</pre></td></tr></table>');
 }
 
 sub git_tree_show {
        my ($req, $fh, $git, $hex, $q) = @_;
-
+       $fh->write('<pre>');
        my $ls = $git->popen(qw(ls-tree --abbrev=16 -l -z), $hex);
        local $/ = "\0";
        my $pfx;
@@ -124,6 +136,7 @@ sub git_tree_show {
                $pfx = 'tree/';
        }
 
+       my $plain_pfx = join('/', "${rel}plain", @{$req->{extra}}, '');
        while (defined(my $l = <$ls>)) {
                chomp $l;
                my ($m, $t, $x, $s, $path) =
@@ -142,8 +155,8 @@ sub git_tree_show {
                elsif ($m eq 'x') { $path = "<b>$path</b>" }
                elsif ($m eq 'l') { $path = "<i>$path</i>" }
 
-               $ref = $pfx.$ref.$qs;
-               $fh->write("$m log raw $s <a\nhref=\"$ref\">$path</a>\n");
+               $fh->write(qq($m log <a\nhref="$plain_pfx$ref$qs">raw</a>) .
+                       qq( $s <a\nhref="$pfx$ref$qs">$path</a>\n));
        }
        $fh->write('</pre>');
 }
diff --git a/t/repobrowse_common_git.perl b/t/repobrowse_common_git.perl
new file mode 100644 (file)
index 0000000..869ab38
--- /dev/null
@@ -0,0 +1,67 @@
+#!/usr/bin/perl -w
+# Copyright (C) 2016 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+use strict;
+use warnings;
+use Test::More;
+use Data::Dumper;
+use File::Temp qw/tempdir/;
+use Cwd qw/getcwd/;
+my @mods = qw(HTTP::Request::Common Plack::Request Plack::Test URI::Escape);
+foreach my $mod (@mods) {
+       eval "require $mod";
+       plan skip_all => "$mod missing for $0" if $@;
+}
+
+sub dechunk ($) {
+       my ($res) = @_;
+       my $s = $res->content;
+       if (lc($res->header('Transfer-Encoding')) eq 'chunked') {
+               my $rv = '';
+               while ($s =~ s/\A([a-f0-9]+)\r\n//i) { # no comment support :x
+                       my $n = hex($1) or last;
+                       $rv .= substr($s, 0, $n);
+                       $s = substr($s, $n);
+                       $s =~ s/\A\r\n// or die "broken parsing in $s\n";
+               }
+               $s =~ s/\A\r\n// or die "broken end parsing in $s\n";
+               $s = $rv;
+       }
+       $s;
+}
+
+use_ok $_ foreach @mods;
+my $git_dir = tempdir(CLEANUP => 1);
+my $psgi = "examples/repobrowse.psgi";
+my $app;
+ok(-f $psgi, 'psgi example for repobrowse.psgi found');
+{
+       is(system(qw(git init -q --bare), $git_dir), 0, 'created git directory');
+       my @cmd = ('git', "--git-dir=$git_dir", 'fast-import', '--quiet');
+       my $fi_data = getcwd().'/t/git.fast-import-data';
+       ok(-r $fi_data, "fast-import data readable (or run test at top level)");
+       my $pid = fork;
+       defined $pid or die "fork failed: $!\n";
+       if ($pid == 0) {
+               open STDIN, '<', $fi_data or die "open $fi_data: $!\n";
+               exec @cmd;
+               die "failed exec: ",join(' ', @cmd),": $!\n";
+       }
+       waitpid $pid, 0;
+       is($?, 0, 'fast-import succeeded');
+       my $repo_config = "$git_dir/pi_repo_config";
+       my $fh;
+       ok((open $fh, '>', $repo_config and
+               print $fh '[repo "test.git"]', "\n",
+                       "\t", "path = $git_dir", "\n" and
+               close $fh), 'created repo config');
+       local $ENV{PI_REPO_CONFIG} = $repo_config;
+       ok($app = require $psgi, 'loaded PSGI app');
+}
+
+# return value
+bless {
+       psgi => $psgi,
+       git_dir => $git_dir,
+       app => $app,
+}, 'RepoBrowse::TestGit';
diff --git a/t/repobrowse_git_plain.t b/t/repobrowse_git_plain.t
new file mode 100644 (file)
index 0000000..23c9e0f
--- /dev/null
@@ -0,0 +1,28 @@
+# Copyright (C) 2016 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+use strict;
+use warnings;
+my $test = require './t/repobrowse_common_git.perl';
+
+test_psgi($test->{app}, sub {
+       my ($cb) = @_;
+
+       my $req = 'http://example.com/test.git/plain/dir';
+       my $res = $cb->(GET($req));
+       is(200, $res->code, 'got 200 response from dir');
+       my $noslash_body = dechunk($res);
+       like($noslash_body, qr{href="dir/dur">dur</a></li>}, 'path ok w/o slash');
+
+       my $slash = $req . '/';
+       my $r2 = $cb->(GET($slash));
+       is(200, $r2->code, 'got 200 response from dir');
+       my $slash_body = dechunk($r2);
+       like($slash_body, qr{href="\./dur\">dur</a></li>}, 'path ok w/ slash');
+
+       $req = 'http://example.com/test.git/plain/foo.txt';
+       my $blob = $cb->(GET($req));
+       is($blob->header('Content-Type'), 'text/plain', 'got text/plain blob');
+       is($blob->content, "-----\nhello\nworld\n", 'raw blob passed');
+});
+
+done_testing();