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;
-# 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
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';
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);
--- /dev/null
+# 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;
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')) {
}
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;
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 {
# 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;
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...
}
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;
$pfx = 'tree/';
}
+ my $plain_pfx = join('/', "${rel}plain", @{$req->{extra}}, '');
while (defined(my $l = <$ls>)) {
chomp $l;
my ($m, $t, $x, $s, $path) =
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>');
}
--- /dev/null
+#!/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';
--- /dev/null
+# 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();