From: Eric Wong Date: Sun, 3 Jan 2016 04:18:58 +0000 (+0000) Subject: repobrowse: add "plain" endpoint for cgit URI compat X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=2e84aabd2d81868ce1ed909a2b04103ec3c7e622;p=thirdparty%2Fpublic-inbox.git repobrowse: add "plain" endpoint for cgit URI compat This is similar to the "blob" endpoint, except it shows trees as a directory. --- diff --git a/lib/PublicInbox/RepoBrowse.pm b/lib/PublicInbox/RepoBrowse.pm index 632282490..74fa40fc2 100644 --- a/lib/PublicInbox/RepoBrowse.pm +++ b/lib/PublicInbox/RepoBrowse.pm @@ -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; diff --git a/lib/PublicInbox/RepoBrowseGitBlob.pm b/lib/PublicInbox/RepoBrowseGitBlob.pm index d94f0d94c..3be069f74 100644 --- a/lib/PublicInbox/RepoBrowseGitBlob.pm +++ b/lib/PublicInbox/RepoBrowseGitBlob.pm @@ -1,4 +1,4 @@ -# Copyright (C) 2015 all contributors +# Copyright (C) 2015-2016 all contributors # License: AGPL-3.0+ # 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 index 000000000..e16195dd2 --- /dev/null +++ b/lib/PublicInbox/RepoBrowseGitPlain.pm @@ -0,0 +1,81 @@ +# Copyright (C) 2015-2016 all contributors +# License: AGPL-3.0+ +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
 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 = "

$title

    "; + if (@ex) { + if ($tslash) { + $t .= qq(
  • ../
  • ); + } else { + $t .= qq(
  • ../
  • ); + 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("$title". + $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(
  • $n
  • )) + } + $fh->write('
'); + $fh->close; + } +} + +1; diff --git a/lib/PublicInbox/RepoBrowseGitTree.pm b/lib/PublicInbox/RepoBrowseGitTree.pm index 275c562ed..b361a9c08 100644 --- a/lib/PublicInbox/RepoBrowseGitTree.pm +++ b/lib/PublicInbox/RepoBrowseGitTree.pm @@ -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(''. - PublicInbox::Hval::PRE); + $fh->write(''. PublicInbox::Hval::STYLE . + ''); 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(''); $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('
Binary file cannot be displayed
'); } 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('
');
 		$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('
');
+	$fh->write(qq($_\n)) foreach (1..$n);
+	$fh->write('
'); } sub git_tree_show { my ($req, $fh, $git, $hex, $q) = @_; - + $fh->write('
');
 	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 = "$path" }
 		elsif ($m eq 'l') { $path = "$path" }
 
-		$ref = $pfx.$ref.$qs;
-		$fh->write("$m log raw $s $path\n");
+		$fh->write(qq($m log raw) .
+			qq( $s $path\n));
 	}
 	$fh->write('
'); } diff --git a/t/repobrowse_common_git.perl b/t/repobrowse_common_git.perl new file mode 100644 index 000000000..869ab3826 --- /dev/null +++ b/t/repobrowse_common_git.perl @@ -0,0 +1,67 @@ +#!/usr/bin/perl -w +# Copyright (C) 2016 all contributors +# License: AGPL-3.0+ +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 index 000000000..23c9e0f50 --- /dev/null +++ b/t/repobrowse_git_plain.t @@ -0,0 +1,28 @@ +# Copyright (C) 2016 all contributors +# License: AGPL-3.0+ +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}, '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}, '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();