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.pm167
1 files changed, 167 insertions, 0 deletions
diff --git a/lib/PublicInbox/Repobrowse.pm b/lib/PublicInbox/Repobrowse.pm
new file mode 100644
index 00000000..03960e2b
--- /dev/null
+++ b/lib/PublicInbox/Repobrowse.pm
@@ -0,0 +1,167 @@
+# 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 PublicInbox::RepoConfig;
+
+my %CMD = map { lc($_) => $_ } qw(Log Commit Src Patch Raw 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};
+}
+
+# return a PSGI response if the URL is ambiguous with
+# extra slashes or has a trailing slash
+sub disambiguate_uri {
+        my ($env) = @_;
+        my $redirect;
+        my $uri = $env->{REQUEST_URI};
+        my $qs = '';
+        $uri =~ s!\A([^:]+://)!!;
+        my $scheme = $1 || '';
+        if ($uri =~ s!/(\?.+)?\z!!) { # no trailing slashes
+                $qs = $1 if defined $1;
+                $redirect = 1;
+        }
+        if ($uri =~ s!//+!/!g) { # no redundant slashes
+                $redirect = 1;
+        }
+        return unless $redirect;
+        $uri = ($scheme ? $scheme : base_url($env)) . $uri . $qs;
+        [ 301,
+          [ 'Location', $uri, 'Content-Type', 'text/plain' ],
+          [ "Redirecting to $uri\n" ] ]
+}
+
+sub root_index {
+        my ($self) = @_;
+        my $mod = load_once('PublicInbox::RepoRoot');
+        $mod->new->call($self->{rconfig}); # RepoRoot::call
+}
+
+# PSGI entry point
+sub call {
+        my ($self, $env) = @_;
+        my $method = $env->{REQUEST_METHOD};
+        return r(405, 'Method Not Allowed') if ($method !~ /\AGET|HEAD|POST\z/);
+        if (my $res = disambiguate_uri($env)) {
+                return $res;
+        }
+
+        # URL syntax: / repo [ / cmd [ / head [ / path ] ] ]
+        # cmd: log | commit | diff | src | raw | snapshot
+        # repo and path (@extra) may both contain '/'
+        my $path_info = $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;
+        until ($repo = $rconfig->lookup($repo_path)) {
+                my $p = shift @extra or last;
+                $repo_path .= "/$p";
+        }
+        return r404() unless $repo;
+
+        my $req = {
+                -repo => $repo,
+                extra => \@extra, # path
+                rconfig => $rconfig,
+                env => $env,
+        };
+        my $cmd = shift @extra;
+        my $vcs_lc = $repo->{vcs};
+        my $vcs = $VCS{$vcs_lc} or return r404();
+        my $mod;
+        my $tip;
+        if (defined $cmd && length $cmd) {
+                $mod = $CMD{$cmd};
+                if ($mod) {
+                        $tip = shift @extra if @extra;
+                } else {
+                        unshift @extra, $cmd;
+                        $mod = 'Fallback';
+                }
+                $req->{relcmd} = '../' x (scalar(@extra) + 1);
+        } else {
+                $mod = 'Summary';
+                $cmd = 'summary';
+                if ($path_info =~ m!/\z!) {
+                        $path_info =~ tr!/!!;
+                } else {
+                        my @x = split('/', $repo_path);
+                        $req->{relcmd} = @x > 1 ? "./$x[-1]/" : "/$x[-1]/";
+                }
+        }
+        while (@extra && $extra[-1] eq '') {
+                pop @extra;
+        }
+        $req->{tip} = $tip;
+        $mod = load_once("PublicInbox::Repo$vcs$mod");
+        $vcs = load_once("PublicInbox::$vcs");
+
+        # $repo->{git} ||= PublicInbox::Git->new(...)
+        $repo->{$vcs_lc} ||= $vcs->new($repo->{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;