diff options
author | Eric Wong <e@80x24.org> | 2015-12-12 04:15:48 +0000 |
---|---|---|
committer | Eric Wong <e@80x24.org> | 2016-04-05 18:58:27 +0000 |
commit | b30bc98b0296dae835bbdfce727239fd644972e2 (patch) | |
tree | b3ab46bb6ff28f3c49115a00898072c5e1b0124d | |
parent | 487bb3b2e43db8e8ebc0340039f7f22b79d45b25 (diff) | |
download | public-inbox-b30bc98b0296dae835bbdfce727239fd644972e2.tar.gz |
Basically stealing URLs and parameters from cgit as much as we can. This could get get interesting...
-rw-r--r-- | examples/repo-browse.psgi | 25 | ||||
-rw-r--r-- | lib/PublicInbox/RepoBrowse.pm | 85 | ||||
-rw-r--r-- | lib/PublicInbox/RepoBrowseBase.pm | 22 | ||||
-rw-r--r-- | lib/PublicInbox/RepoBrowseCommit.pm | 120 | ||||
-rw-r--r-- | lib/PublicInbox/RepoBrowseLog.pm | 91 | ||||
-rw-r--r-- | lib/PublicInbox/RepoBrowseQuery.pm | 40 | ||||
-rw-r--r-- | lib/PublicInbox/RepoConfig.pm | 56 |
7 files changed, 439 insertions, 0 deletions
diff --git a/examples/repo-browse.psgi b/examples/repo-browse.psgi new file mode 100644 index 00000000..b02b8e8b --- /dev/null +++ b/examples/repo-browse.psgi @@ -0,0 +1,25 @@ +#!/usr/bin/perl -w +# Copyright (C) 2015 all contributors <meta@public-inbox.org> +# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> +# Note: this is part of our test suite, update t/plack.t if this changes +# Usage: plackup [OPTIONS] /path/to/this/file +use strict; +use warnings; +use PublicInbox::RepoBrowse; +use Plack::Request; +use Plack::Builder; +my $have_deflater = eval { require Plack::Middleware::Deflater; 1 }; +my $repo_browse = PublicInbox::RepoBrowse->new; + +builder { + if ($have_deflater) { + enable 'Deflater', + content_type => [ 'text/html', 'text/plain', + 'application/atom+xml' ]; + } + enable 'Head'; + sub { + my $req = Plack::Request->new(@_); + $repo_browse->run($req, $req->method); + } +} diff --git a/lib/PublicInbox/RepoBrowse.pm b/lib/PublicInbox/RepoBrowse.pm new file mode 100644 index 00000000..5865d65d --- /dev/null +++ b/lib/PublicInbox/RepoBrowse.pm @@ -0,0 +1,85 @@ +# Copyright (C) 2015 all contributors <meta@public-inbox.org> +# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> + +# Version control system (VCS) repository viewer like cgit or gitweb, +# but with optional public-inbox archive integration. +# This uses cgit-compatible PATH_INFO URLs. +# This may be expanded to support other Free Software VCSes such as +# Subversion and Mercurial, so not just git +# +# Same web design principles as PublicInbox::WWW for supporting the +# lowest common denominators (see bottom of Documentation/design_www.txt) +# +# This allows an M:N relationship between "normal" repos for project +# and public-inbox (ssoma) git repositories where N may be zero. +# In other words, RepoBrowse must work for repositories without +# any public-inbox at all; or with multiple public-inboxes. +# And the rest of public-inbox will always work without a "normal" +# code repo for the project. + +package PublicInbox::RepoBrowse; +use strict; +use warnings; +use URI::Escape qw(uri_escape_utf8 uri_unescape); +use PublicInbox::RepoConfig; + +my %CMD = map { lc($_) => $_ } qw(Log Commit); + +sub new { + my ($class, $file) = @_; + bless { rconfig => PublicInbox::RepoConfig->new($file) }, $class; +} + +# simple response for errors +sub r { [ $_[0], ['Content-Type' => 'text/plain'], [ join(' ', @_, "\n") ] ] } + +sub run { + my ($self, $cgi, $method) = @_; + return r(405, 'Method Not Allowed') if ($method !~ /\AGET|HEAD\z/); + + # URL syntax: / repo [ / cmd [ / path ] ] + # cmd: log | commit | diff | tree | view | blob | snapshot + # repo and path may both contain '/' + my $rconfig = $self->{rconfig}; + my (undef, $repo_path, @extra) = split(m{/+}, $cgi->path_info, -1); + + return r404() unless $repo_path; + my $repo_info; + until ($repo_info = $rconfig->lookup($repo_path)) { + my $p = shift @extra or last; + $repo_path .= "/$p"; + } + return r404() unless $repo_info; + + my $req = { + repo_info => $repo_info, + path => \@extra, + cgi => $cgi, + rconfig => $rconfig, + }; + + my $cmd = shift @extra; + if (defined $cmd && length $cmd) { + my $mod = $CMD{$cmd}; + return r404() unless defined $mod; + if (index($mod, ':') < 0) { + $mod = "PublicInbox::RepoBrowse$mod"; + eval "require $mod"; + $CMD{$cmd} = $mod unless $@; + } + $req->{relcmd} = '../' x scalar(@extra); + my $rv = eval { $mod->new->call($req) }; + $rv || r404(); + } else { + $req->{relcmd} = defined $cmd ? '' : './'; + summary($req); + } +} + +sub summary { + r404(); +} + +sub r404 { r(404, 'Not Found') } + +1; diff --git a/lib/PublicInbox/RepoBrowseBase.pm b/lib/PublicInbox/RepoBrowseBase.pm new file mode 100644 index 00000000..cd9e66de --- /dev/null +++ b/lib/PublicInbox/RepoBrowseBase.pm @@ -0,0 +1,22 @@ +# Copyright (C) 2015 all contributors <meta@public-inbox.org> +# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> +package PublicInbox::RepoBrowseBase; +use strict; +use warnings; +require PublicInbox::RepoBrowseQuery; +require PublicInbox::Hval; + +sub new { bless {}, shift } + +sub call { + my ($self, $req) = @_; + my $vcs = $req->{repo_info}->{vcs}; + my $rv = eval { + no strict 'refs'; + my $sub = 'call_'.$vcs; + $self->$sub($req); + }; + $@ ? [ 500, ['Content-Type'=>'text/plain'], [] ] : $rv; +} + +1; diff --git a/lib/PublicInbox/RepoBrowseCommit.pm b/lib/PublicInbox/RepoBrowseCommit.pm new file mode 100644 index 00000000..ed7fa3f1 --- /dev/null +++ b/lib/PublicInbox/RepoBrowseCommit.pm @@ -0,0 +1,120 @@ +# Copyright (C) 2015 all contributors <meta@public-inbox.org> +# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> + +package PublicInbox::RepoBrowseCommit; +use strict; +use warnings; +use base qw(PublicInbox::RepoBrowseBase); +use PublicInbox::Git; + +use constant GIT_FMT => '--pretty=format:'.join('%n', + '%H', '%s', '%an <%ae>', '%ai', '%cn <%ce>', '%ci', + '%t', '%p', '%D', '%b%x00'); + +sub git_commit_stream { + my ($req, $q, $H, $log, $fh) = @_; + my $l; + my $s = PublicInbox::Hval->new_oneline($l = <$log>)->as_html; # subject + my $au = PublicInbox::Hval->new_oneline($l = <$log>)->as_html; # author + chomp(my $ad = <$log>); + my $cu = PublicInbox::Hval->new_oneline($l = <$log>)->as_html; + chomp(my $cd = <$log>); + chomp(my $t = <$log>); # tree + chomp(my $p = <$log>); # parents + my @p = split(' ', $p); + chomp(my $D = <$log>); # TODO: decorate + + my $rel = $req->{relcmd}; + my $x = "<html><head><title>$s</title></head><body>" . + "<pre\nstyle='white-space:pre-wrap'>" . + " commit $H" . + " author $au\t$ad\n" . + "committer $cu\t$cd\n" . + " tree <a\nhref=\"${rel}tree?id=$t\">$t</a>\n"; + if (scalar(@p) == 1) { + $x .= ' parent '; + $x .= qq(<a\nhref="${rel}commit?id=$p[0]">$p[0]</a>\n); + } elsif (scalar(@p) > 1) { + $x .= ' merge '; + $x .= join(' ', map { + qq(<a\nhref="${rel}commit?id=$_">$_</a>) + } @p); + $x .= "\n"; + } + $fh->write($x .= "\n$s\n\n"); + + # body: + local $/ = "\0"; + $x = PublicInbox::Hval->new($l = <$log>)->as_html; # body + $fh->write($x); + + # diff + local $/ = "\n"; + my $cmt = '[a-f0-9]+'; + my @href; + while (defined($l = <$log>)) { + if ($l =~ /^index ($cmt)\.\.($cmt)(.*)$/o) { # regular + my $end = $3; + my @l = ($1, $2); + @href = git_blob_hrefs($rel, @l); + @l = git_blob_links(\@href, \@l); + $l = "index $l[0]..$l[1]$end"; + } elsif ($l =~ /^@@ (\S+) (\S+) @@(.*)$/) { # regular + my $ctx = $3; + my @l = ($1, $2); + @l = git_blob_links(\@href, \@l); + $l = "@@ $l[0] $l[1] @@".$ctx; + } elsif ($l =~ /^index ($cmt,[^\.]+)\.\.($cmt)(.*)$/o) { # --cc + my @l = (split(',', $1), $2); + my $end = $3; + @href = git_blob_hrefs($rel, @l); + @l = git_blob_links(\@href, \@l); + my $res = pop @l; + $l = 'index '.join(',', @l)."..$res$end"; + } elsif ($l =~ /^(@@@+) (\S+.*\S+) @@@+(.*)$/) { # --cc + my ($at, $ctx) = ($1, $3); + my @l = split(' ', $2); + @l = git_blob_links(\@href, \@l); + $l = join(' ', $at, @l, $at) . $ctx; + } else { + $l = PublicInbox::Hval->new($l)->as_html; + } + $fh->write($l . "\n"); + } + $fh->write('</pre></body></html>'); +} + +sub call_git { + my ($self, $req) = @_; + my $repo_info = $req->{repo_info}; + my $path = $repo_info->{path}; + + my $q = PublicInbox::RepoBrowseQuery->new($req->{cgi}); + my $id = $q->{id}; + $id eq '' and $id = 'HEAD'; + my $git = $repo_info->{git} ||= PublicInbox::Git->new($path); + my $log = $git->popen(qw(show --no-notes --no-color + --abbrev=16 --irreversible-delete), + GIT_FMT, $id); + my $H = <$log>; + defined $H or return; + sub { + my ($res) = @_; # Plack callback + my $fh = $res->([200, ['Content-Type'=>'text/html']]); + git_commit_stream($req, $q, $H, $log, $fh); + $fh->close; + } +} + +sub git_blob_hrefs { + my ($rel, @ids) = @_; + map { "<a\nhref=\"${rel}blob?id=$_\"" } @ids; +} + +sub git_blob_links { + my ($hrefs, $labels) = @_; + my $i = 0; + map { $hrefs->[$i++].">$_</a>" } @$labels; +} + +1; diff --git a/lib/PublicInbox/RepoBrowseLog.pm b/lib/PublicInbox/RepoBrowseLog.pm new file mode 100644 index 00000000..c8d3f0ac --- /dev/null +++ b/lib/PublicInbox/RepoBrowseLog.pm @@ -0,0 +1,91 @@ +# Copyright (C) 2015 all contributors <meta@public-inbox.org> +# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> + +package PublicInbox::RepoBrowseLog; +use strict; +use warnings; +use base qw(PublicInbox::RepoBrowseBase); +require POSIX; # strftime +use PublicInbox::Git; + +sub call_git { + my ($self, $req) = @_; + my $repo_info = $req->{repo_info}; + my $path = $repo_info->{path}; + my $max = $repo_info->{max_commit_count} || 50; + $max = int($max); + $max = 50 if $max == 0; + + my $q = PublicInbox::RepoBrowseQuery->new($req->{cgi}); + my $h = $q->{h}; + $h eq '' and $h = 'HEAD'; + + my $fmt = '%h%x00%ct%x00%s%x00%D'; + $fmt .= '%x00%an%x00%b' if $q->{showmsg}; + $fmt .= '%x00%n'; + + my $ofs = $q->{ofs}; + $h .= "~$ofs" if $ofs =~ /\A\d+\z/; + + my $git = $repo_info->{git} ||= PublicInbox::Git->new($path); + my $log = $git->popen(qw(log --no-notes --no-color + --abbrev-commit --abbrev=16), + "--format=$fmt", "-$max", $h); + sub { + my ($res) = @_; # Plack callback + my $fh = $res->([200, ['Content-Type'=>'text/html']]); + git_log_stream($req, $q, $log, $fh); + $fh->close; + } +} + +sub git_log_stream { + my ($req, $q, $log, $fh) = @_; + my $desc = $req->{repo_info}->{desc_html}; + my ($x, $author); + my $showmsg = $q->{showmsg}; + + if ($showmsg) { + my $qs = $q->qs(showmsg => ''); + $x = qq{(<a\nhref="./$qs">collapse</a></th><th>Author}; + } else { + my $qs = $q->qs(showmsg => 1); + $x = qq{(<a\nhref="./$qs">expand</a>)}; + } + + $fh->write("<html><head><title>$desc" . + "</title></head><body><p>$desc</p><table><tr>" . + "<th>Date</th><th>Commit message $x</th></tr>"); + my %ac; + local $/ = "\0\n\n"; + my $rel = $req->{relcmd}; + while (defined(my $line = <$log>)) { + my ($id, $ct, $s, $D, $an, $b) = split("\0", $line); + $line = undef; + $s = PublicInbox::Hval->new_oneline($s)->as_html; + + # cannot rely on --date=format-local:... yet, + # it is too new (September 2015) + $ct = POSIX::strftime('%Y-%m-%d', gmtime($ct)); + + # TODO: handle $D (decorate) + $s = "<tr><td>$ct</td>" . + qq(<td><a\nhref="${rel}commit?id=$id">$s</a></td>); + if (defined $b) { + my $ah = $ac{$an} ||= + PublicInbox::Hval->new_oneline($an)->as_html; + $b = PublicInbox::Hval->new($b)->as_html; + $s .= "<td>$ah</td></tr>" . + "<tr><td colspan=3><pre\n" . + "style='white-space:pre-wrap'>$b</pre>". + '</td></tr>'; + } else { + $s .= '</tr>'; + } + $fh->write($s); + } + + $fh->write('</table></body></html>'); +} + +1; diff --git a/lib/PublicInbox/RepoBrowseQuery.pm b/lib/PublicInbox/RepoBrowseQuery.pm new file mode 100644 index 00000000..979a2b68 --- /dev/null +++ b/lib/PublicInbox/RepoBrowseQuery.pm @@ -0,0 +1,40 @@ +# Copyright (C) 2015 all contributors <meta@public-inbox.org> +# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> + +package PublicInbox::RepoBrowseQuery; +use strict; +use warnings; +use PublicInbox::Hval; + +sub new { + my ($class, $cgi) = @_; + my $self = bless {}, $class; + + foreach my $k (qw(id h showmsg ofs)) { + my $v = $cgi->param($k); + $self->{$k} = defined $v ? $v : ''; + } + $self; +} + +sub qs { + my ($self, %over) = @_; + + if (keys %over) { + my $tmp = bless { %$self }, ref($self); + foreach my $k (keys %over) { $tmp->{$k} = $over{$k}; } + $self = $tmp; + } + + my @qs; + foreach my $k (qw(id h showmsg ofs)) { + my $v = $self->{$k}; + + next if ($v eq ''); + $v = PublicInbox::Hval->new($v)->as_href; + push @qs, "$k=$v"; + } + scalar(@qs) ? ('?' . join('&', @qs)) : ''; +} + +1; diff --git a/lib/PublicInbox/RepoConfig.pm b/lib/PublicInbox/RepoConfig.pm new file mode 100644 index 00000000..dc99e7df --- /dev/null +++ b/lib/PublicInbox/RepoConfig.pm @@ -0,0 +1,56 @@ +# Copyright (C) 2015 all contributors <meta@public-inbox.org> +# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> +package PublicInbox::RepoConfig; +use strict; +use warnings; +use PublicInbox::Config qw/try_cat/; +require PublicInbox::Hval; + +sub new { + my ($class, $file) = @_; + $file = default_file() unless defined($file); + my $self = bless PublicInbox::Config::git_config_dump($file), $class; + $self->{-cache} = {}; + $self; +} + +sub default_file { + my $f = $ENV{PI_REPO_CONFIG}; + return $f if defined $f; + PublicInbox::Config::config_dir() . '/repo_config'; +} + +# Returns something like: +# { +# path => '/home/git/foo.git', +# description => 'foo repo', +# cloneurl => "git://example.com/foo.git\nhttp://example.com/foo.git", +# publicinbox => '/home/pub/foo-public.git', +# } +sub lookup { + my ($self, $repo_path) = @_; # "git.git" + my $rv; + + $rv = $self->{-cache}->{$repo_path} and return $rv; + + my $path = $self->{"repo.$repo_path.path"}; + (defined $path && -d $path) or return; + $rv->{path} = $path; + + foreach my $key (qw(description cloneurl)) { + $rv->{$key} = try_cat("$path/$key"); + } + + $rv->{desc_html} = + PublicInbox::Hval->new_oneline($rv->{description})->as_html; + + foreach my $key (qw(publicinbox vcs)) { + $rv->{$key} = $self->{"repo.$repo_path.$key"}; + } + + # of course git is the default VCS + $rv->{vcs} ||= 'git'; + $self->{-cache}->{$repo_path} = $rv; +} + +1; |