# Copyright (C) 2015 all contributors
# License: AGPL-3.0+
# 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);
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 [ / head [ / path ] ] ]
# cmd: log | commit | diff | tree | view | blob | 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 $tslash = 0;
my $cmd = shift @extra;
my $vcs_lc = $repo->{vcs};
my $vcs = $VCS{$vcs_lc} or return r404();
my $mod;
my $h;
if (defined $cmd && length $cmd) {
$mod = $CMD{$cmd};
if ($mod) {
$h = 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!) {
$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;
}
$req->{h} = $h;
$req->{-tip} = defined $h ? $h : 'HEAD';
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->{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;