about summary refs log tree commit homepage
path: root/lib/PublicInbox/Repobrowse.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/PublicInbox/Repobrowse.pm')
-rw-r--r--lib/PublicInbox/Repobrowse.pm166
1 files changed, 166 insertions, 0 deletions
diff --git a/lib/PublicInbox/Repobrowse.pm b/lib/PublicInbox/Repobrowse.pm
new file mode 100644
index 00000000..87e12278
--- /dev/null
+++ b/lib/PublicInbox/Repobrowse.pm
@@ -0,0 +1,166 @@
+# 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 Tree Patch Blob Plain Tag Atom
+        Diff Snapshot);
+my %VCS = (git => 'Git');
+my %LOADED;
+
+sub new {
+        my ($class, $rconfig) = @_;
+        $rconfig ||= PublicInbox::RepoConfig->new;
+        bless { rconfig => $rconfig }, $class;
+}
+
+# simple response for errors
+sub r { [ $_[0], ['Content-Type' => 'text/plain'], [ join(' ', @_, "\n") ] ] }
+
+sub base_url ($) {
+        my ($env) = @_;
+        my $scheme = $env->{'psgi.url_scheme'} || 'http';
+        my $host = $env->{HTTP_HOST};
+        my $base = "$scheme://";
+        if (defined $host) {
+                $base .= $host;
+        } else {
+                $base .= $env->{SERVER_NAME};
+                my $port = $env->{SERVER_PORT} || 80;
+                if (($scheme eq 'http' && $port != 80) ||
+                                ($scheme eq 'https' && $port != 443)) {
+                        $base.= ":$port";
+                }
+        }
+        $base .= $env->{SCRIPT_NAME};
+}
+
+# Remove trailing slash in URLs which regular humans are likely to read
+# in an attempt to improve cache hit ratios.  Do not redirect
+# plain|patch|blob|fallback endpoints since those could be using
+# automated tools which may not follow redirects automatically
+# (e.g. curl does not follow 301 unless given "-L")
+my %NO_TSLASH = map { $_ => 1 } qw(Log Commit Tree Summary Tag);
+sub no_tslash {
+        my ($env) = @_;
+        my $base = base_url($env);
+        my $uri = $env->{REQUEST_URI};
+        my $qs = '';
+        if ($uri =~ s/(\?.+)\z//) {
+                $qs = $1;
+        }
+        if ($uri !~ s!/+\z!!) {
+                warn "W: buggy redirect? base=$base request_uri=$uri\n";
+        }
+        my $url = $base . $uri . $qs;
+        [ 301,
+          [ Location => $url, 'Content-Type' => 'text/plain' ],
+          [ "Redirecting to $url\n" ] ]
+}
+
+sub root_index {
+        my ($self) = @_;
+        my $mod = load_once('PublicInbox::RepoRoot');
+        $mod->new->call($self->{rconfig}); # RepoRoot::call
+}
+
+sub call {
+        my ($self, $env) = @_;
+        my $method = $env->{REQUEST_METHOD};
+        return r(405, 'Method Not Allowed') if ($method !~ /\AGET|HEAD|POST\z/);
+
+        # URL syntax: / repo [ / cmd [ / path ] ]
+        # cmd: log | commit | diff | tree | view | blob | snapshot
+        # repo and path (@extra) may both contain '/'
+        my $path_info = uri_unescape($env->{PATH_INFO});
+        my (undef, $repo_path, @extra) = split(m{/+}, $path_info, -1);
+
+        return $self->root_index($self) unless length($repo_path);
+
+        my $rconfig = $self->{rconfig}; # RepoConfig
+        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,
+                extra => \@extra, # path
+                rconfig => $rconfig,
+                env => $env,
+        };
+        my $tslash = 0;
+        my $cmd = shift @extra;
+        my $vcs_lc = $repo_info->{vcs};
+        my $vcs = $VCS{$vcs_lc} or return r404();
+        my $mod;
+        if (defined $cmd && length $cmd) {
+                $mod = $CMD{$cmd};
+                unless ($mod) {
+                        unshift @extra, $cmd;
+                        $mod = 'Fallback';
+                }
+                $req->{relcmd} = '../' x scalar(@extra);
+        } else {
+                $mod = 'Summary';
+                $cmd = 'summary';
+                if ($path_info =~ m!/\z!) {
+                        $tslash = $path_info =~ tr!/!!;
+                } else {
+                        my @x = split('/', $repo_path);
+                        $req->{relcmd} = @x > 1 ? "./$x[-1]/" : "/$x[-1]/";
+                }
+        }
+        while (@extra && $extra[-1] eq '') {
+                pop @extra;
+                ++$tslash;
+        }
+
+        return no_tslash($env) if ($tslash && $NO_TSLASH{$mod});
+
+        $req->{tslash} = $tslash;
+        $mod = load_once("PublicInbox::Repo$vcs$mod");
+        $vcs = load_once("PublicInbox::$vcs");
+
+        # $repo_info->{git} ||= PublicInbox::Git->new(...)
+        $repo_info->{$vcs_lc} ||= $vcs->new($repo_info->{path});
+
+        $req->{expath} = join('/', @extra);
+        my $rv = eval { $mod->new->call($cmd, $req) }; # RepoBase::call
+        $rv || r404();
+}
+
+sub r404 { r(404, 'Not Found') }
+
+sub load_once {
+        my ($mod) = @_;
+
+        return $mod if $LOADED{$mod};
+        eval "require $mod";
+        $LOADED{$mod} = 1 unless $@;
+        $mod;
+}
+
+1;