blob: c77cd0341d98f6a38af55c6ed6f09704abf4e967 [file] [log] [blame]
#!/usr/bin/perl
# gitweb - simple web interface to track changes in git repositories
#
# (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
# (C) 2005, Christian Gierke
#
# This program is licensed under the GPLv2
use strict;
use warnings;
use CGI qw(:standard :escapeHTML -nosticky);
use CGI::Util qw(unescape);
use CGI::Carp qw(fatalsToBrowser);
use Encode;
use Fcntl ':mode';
use File::Find qw();
use File::Basename qw(basename);
binmode STDOUT, ':utf8';
BEGIN {
CGI->compile() if $ENV{'MOD_PERL'};
}
our $cgi = new CGI;
our $version = "++GIT_VERSION++";
our $my_url = $cgi->url();
our $my_uri = $cgi->url(-absolute => 1);
# Base URL for relative URLs in gitweb ($logo, $favicon, ...),
# needed and used only for URLs with nonempty PATH_INFO
our $base_url = $my_url;
# When the script is used as DirectoryIndex, the URL does not contain the name
# of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
# have to do it ourselves. We make $path_info global because it's also used
# later on.
#
# Another issue with the script being the DirectoryIndex is that the resulting
# $my_url data is not the full script URL: this is good, because we want
# generated links to keep implying the script name if it wasn't explicitly
# indicated in the URL we're handling, but it means that $my_url cannot be used
# as base URL.
# Therefore, if we needed to strip PATH_INFO, then we know that we have
# to build the base URL ourselves:
our $path_info = $ENV{"PATH_INFO"};
if ($path_info) {
if ($my_url =~ s,\Q$path_info\E$,, &&
$my_uri =~ s,\Q$path_info\E$,, &&
defined $ENV{'SCRIPT_NAME'}) {
$base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
}
}
# core git executable to use
# this can just be "git" if your webserver has a sensible PATH
our $GIT = "++GIT_BINDIR++/git";
# absolute fs-path which will be prepended to the project path
#our $projectroot = "/pub/scm";
our $projectroot = "++GITWEB_PROJECTROOT++";
# fs traversing limit for getting project list
# the number is relative to the projectroot
our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
# target of the home link on top of all pages
our $home_link = $my_uri || "/";
# string of the home link on top of all pages
our $home_link_str = "++GITWEB_HOME_LINK_STR++";
# name of your site or organization to appear in page titles
# replace this with something more descriptive for clearer bookmarks
our $site_name = "++GITWEB_SITENAME++"
|| ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
# filename of html text to include at top of each page
our $site_header = "++GITWEB_SITE_HEADER++";
# html text to include at home page
our $home_text = "++GITWEB_HOMETEXT++";
# filename of html text to include at bottom of each page
our $site_footer = "++GITWEB_SITE_FOOTER++";
# URI of stylesheets
our @stylesheets = ("++GITWEB_CSS++");
# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
our $stylesheet = undef;
# URI of GIT logo (72x27 size)
our $logo = "++GITWEB_LOGO++";
# URI of GIT favicon, assumed to be image/png type
our $favicon = "++GITWEB_FAVICON++";
# URI and label (title) of GIT logo link
#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
#our $logo_label = "git documentation";
our $logo_url = "http://git-scm.com/";
our $logo_label = "git homepage";
# source of projects list
our $projects_list = "++GITWEB_LIST++";
# the width (in characters) of the projects list "Description" column
our $projects_list_description_width = 25;
# default order of projects list
# valid values are none, project, descr, owner, and age
our $default_projects_order = "project";
# show repository only if this file exists
# (only effective if this variable evaluates to true)
our $export_ok = "++GITWEB_EXPORT_OK++";
# show repository only if this subroutine returns true
# when given the path to the project, for example:
# sub { return -e "$_[0]/git-daemon-export-ok"; }
our $export_auth_hook = undef;
# only allow viewing of repositories also shown on the overview page
our $strict_export = "++GITWEB_STRICT_EXPORT++";
# list of git base URLs used for URL to where fetch project from,
# i.e. full URL is "$git_base_url/$project"
our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
# default blob_plain mimetype and default charset for text/plain blob
our $default_blob_plain_mimetype = 'text/plain';
our $default_text_plain_charset = undef;
# file to use for guessing MIME types before trying /etc/mime.types
# (relative to the current git repository)
our $mimetypes_file = undef;
# assume this charset if line contains non-UTF-8 characters;
# it should be valid encoding (see Encoding::Supported(3pm) for list),
# for which encoding all byte sequences are valid, for example
# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
# could be even 'utf-8' for the old behavior)
our $fallback_encoding = 'latin1';
# rename detection options for git-diff and git-diff-tree
# - default is '-M', with the cost proportional to
# (number of removed files) * (number of new files).
# - more costly is '-C' (which implies '-M'), with the cost proportional to
# (number of changed files + number of removed files) * (number of new files)
# - even more costly is '-C', '--find-copies-harder' with cost
# (number of files in the original tree) * (number of new files)
# - one might want to include '-B' option, e.g. '-B', '-M'
our @diff_opts = ('-M'); # taken from git_commit
# Disables features that would allow repository owners to inject script into
# the gitweb domain.
our $prevent_xss = 0;
# information about snapshot formats that gitweb is capable of serving
our %known_snapshot_formats = (
# name => {
# 'display' => display name,
# 'type' => mime type,
# 'suffix' => filename suffix,
# 'format' => --format for git-archive,
# 'compressor' => [compressor command and arguments]
# (array reference, optional)
# 'disabled' => boolean (optional)}
#
'tgz' => {
'display' => 'tar.gz',
'type' => 'application/x-gzip',
'suffix' => '.tar.gz',
'format' => 'tar',
'compressor' => ['gzip']},
'tbz2' => {
'display' => 'tar.bz2',
'type' => 'application/x-bzip2',
'suffix' => '.tar.bz2',
'format' => 'tar',
'compressor' => ['bzip2']},
'txz' => {
'display' => 'tar.xz',
'type' => 'application/x-xz',
'suffix' => '.tar.xz',
'format' => 'tar',
'compressor' => ['xz'],
'disabled' => 1},
'zip' => {
'display' => 'zip',
'type' => 'application/x-zip',
'suffix' => '.zip',
'format' => 'zip'},
);
# Aliases so we understand old gitweb.snapshot values in repository
# configuration.
our %known_snapshot_format_aliases = (
'gzip' => 'tgz',
'bzip2' => 'tbz2',
'xz' => 'txz',
# backward compatibility: legacy gitweb config support
'x-gzip' => undef, 'gz' => undef,
'x-bzip2' => undef, 'bz2' => undef,
'x-zip' => undef, '' => undef,
);
# Pixel sizes for icons and avatars. If the default font sizes or lineheights
# are changed, it may be appropriate to change these values too via
# $GITWEB_CONFIG.
our %avatar_size = (
'default' => 16,
'double' => 32
);
# You define site-wide feature defaults here; override them with
# $GITWEB_CONFIG as necessary.
our %feature = (
# feature => {
# 'sub' => feature-sub (subroutine),
# 'override' => allow-override (boolean),
# 'default' => [ default options...] (array reference)}
#
# if feature is overridable (it means that allow-override has true value),
# then feature-sub will be called with default options as parameters;
# return value of feature-sub indicates if to enable specified feature
#
# if there is no 'sub' key (no feature-sub), then feature cannot be
# overriden
#
# use gitweb_get_feature(<feature>) to retrieve the <feature> value
# (an array) or gitweb_check_feature(<feature>) to check if <feature>
# is enabled
# Enable the 'blame' blob view, showing the last commit that modified
# each line in the file. This can be very CPU-intensive.
# To enable system wide have in $GITWEB_CONFIG
# $feature{'blame'}{'default'} = [1];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'blame'}{'override'} = 1;
# and in project config gitweb.blame = 0|1;
'blame' => {
'sub' => sub { feature_bool('blame', @_) },
'override' => 0,
'default' => [0]},
# Enable the 'snapshot' link, providing a compressed archive of any
# tree. This can potentially generate high traffic if you have large
# project.
# Value is a list of formats defined in %known_snapshot_formats that
# you wish to offer.
# To disable system wide have in $GITWEB_CONFIG
# $feature{'snapshot'}{'default'} = [];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'snapshot'}{'override'} = 1;
# and in project config, a comma-separated list of formats or "none"
# to disable. Example: gitweb.snapshot = tbz2,zip;
'snapshot' => {
'sub' => \&feature_snapshot,
'override' => 0,
'default' => ['tgz']},
# Enable text search, which will list the commits which match author,
# committer or commit text to a given string. Enabled by default.
# Project specific override is not supported.
'search' => {
'override' => 0,
'default' => [1]},
# Enable grep search, which will list the files in currently selected
# tree containing the given string. Enabled by default. This can be
# potentially CPU-intensive, of course.
# To enable system wide have in $GITWEB_CONFIG
# $feature{'grep'}{'default'} = [1];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'grep'}{'override'} = 1;
# and in project config gitweb.grep = 0|1;
'grep' => {
'sub' => sub { feature_bool('grep', @_) },
'override' => 0,
'default' => [1]},
# Enable the pickaxe search, which will list the commits that modified
# a given string in a file. This can be practical and quite faster
# alternative to 'blame', but still potentially CPU-intensive.
# To enable system wide have in $GITWEB_CONFIG
# $feature{'pickaxe'}{'default'} = [1];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'pickaxe'}{'override'} = 1;
# and in project config gitweb.pickaxe = 0|1;
'pickaxe' => {
'sub' => sub { feature_bool('pickaxe', @_) },
'override' => 0,
'default' => [1]},
# Make gitweb use an alternative format of the URLs which can be
# more readable and natural-looking: project name is embedded
# directly in the path and the query string contains other
# auxiliary information. All gitweb installations recognize
# URL in either format; this configures in which formats gitweb
# generates links.
# To enable system wide have in $GITWEB_CONFIG
# $feature{'pathinfo'}{'default'} = [1];
# Project specific override is not supported.
# Note that you will need to change the default location of CSS,
# favicon, logo and possibly other files to an absolute URL. Also,
# if gitweb.cgi serves as your indexfile, you will need to force
# $my_uri to contain the script name in your $GITWEB_CONFIG.
'pathinfo' => {
'override' => 0,
'default' => [0]},
# Make gitweb consider projects in project root subdirectories
# to be forks of existing projects. Given project $projname.git,
# projects matching $projname/*.git will not be shown in the main
# projects list, instead a '+' mark will be added to $projname
# there and a 'forks' view will be enabled for the project, listing
# all the forks. If project list is taken from a file, forks have
# to be listed after the main project.
# To enable system wide have in $GITWEB_CONFIG
# $feature{'forks'}{'default'} = [1];
# Project specific override is not supported.
'forks' => {
'override' => 0,
'default' => [0]},
# Insert custom links to the action bar of all project pages.
# This enables you mainly to link to third-party scripts integrating
# into gitweb; e.g. git-browser for graphical history representation
# or custom web-based repository administration interface.
# The 'default' value consists of a list of triplets in the form
# (label, link, position) where position is the label after which
# to insert the link and link is a format string where %n expands
# to the project name, %f to the project path within the filesystem,
# %h to the current hash (h gitweb parameter) and %b to the current
# hash base (hb gitweb parameter); %% expands to %.
# To enable system wide have in $GITWEB_CONFIG e.g.
# $feature{'actions'}{'default'} = [('graphiclog',
# '/git-browser/by-commit.html?r=%n', 'summary')];
# Project specific override is not supported.
'actions' => {
'override' => 0,
'default' => []},
# Allow gitweb scan project content tags described in ctags/
# of project repository, and display the popular Web 2.0-ish
# "tag cloud" near the project list. Note that this is something
# COMPLETELY different from the normal Git tags.
# gitweb by itself can show existing tags, but it does not handle
# tagging itself; you need an external application for that.
# For an example script, check Girocco's cgi/tagproj.cgi.
# You may want to install the HTML::TagCloud Perl module to get
# a pretty tag cloud instead of just a list of tags.
# To enable system wide have in $GITWEB_CONFIG
# $feature{'ctags'}{'default'} = ['path_to_tag_script'];
# Project specific override is not supported.
'ctags' => {
'override' => 0,
'default' => [0]},
# The maximum number of patches in a patchset generated in patch
# view. Set this to 0 or undef to disable patch view, or to a
# negative number to remove any limit.
# To disable system wide have in $GITWEB_CONFIG
# $feature{'patches'}{'default'} = [0];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'patches'}{'override'} = 1;
# and in project config gitweb.patches = 0|n;
# where n is the maximum number of patches allowed in a patchset.
'patches' => {
'sub' => \&feature_patches,
'override' => 0,
'default' => [16]},
# Avatar support. When this feature is enabled, views such as
# shortlog or commit will display an avatar associated with
# the email of the committer(s) and/or author(s).
# Currently available providers are gravatar and picon.
# If an unknown provider is specified, the feature is disabled.
# Gravatar depends on Digest::MD5.
# Picon currently relies on the indiana.edu database.
# To enable system wide have in $GITWEB_CONFIG
# $feature{'avatar'}{'default'} = ['<provider>'];
# where <provider> is either gravatar or picon.
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'avatar'}{'override'} = 1;
# and in project config gitweb.avatar = <provider>;
'avatar' => {
'sub' => \&feature_avatar,
'override' => 0,
'default' => ['']},
);
sub gitweb_get_feature {
my ($name) = @_;
return unless exists $feature{$name};
my ($sub, $override, @defaults) = (
$feature{$name}{'sub'},
$feature{$name}{'override'},
@{$feature{$name}{'default'}});
if (!$override) { return @defaults; }
if (!defined $sub) {
warn "feature $name is not overridable";
return @defaults;
}
return $sub->(@defaults);
}
# A wrapper to check if a given feature is enabled.
# With this, you can say
#
# my $bool_feat = gitweb_check_feature('bool_feat');
# gitweb_check_feature('bool_feat') or somecode;
#
# instead of
#
# my ($bool_feat) = gitweb_get_feature('bool_feat');
# (gitweb_get_feature('bool_feat'))[0] or somecode;
#
sub gitweb_check_feature {
return (gitweb_get_feature(@_))[0];
}
sub feature_bool {
my $key = shift;
my ($val) = git_get_project_config($key, '--bool');
if (!defined $val) {
return ($_[0]);
} elsif ($val eq 'true') {
return (1);
} elsif ($val eq 'false') {
return (0);
}
}
sub feature_snapshot {
my (@fmts) = @_;
my ($val) = git_get_project_config('snapshot');
if ($val) {
@fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
}
return @fmts;
}
sub feature_patches {
my @val = (git_get_project_config('patches', '--int'));
if (@val) {
return @val;
}
return ($_[0]);
}
sub feature_avatar {
my @val = (git_get_project_config('avatar'));
return @val ? @val : @_;
}
# checking HEAD file with -e is fragile if the repository was
# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
# and then pruned.
sub check_head_link {
my ($dir) = @_;
my $headfile = "$dir/HEAD";
return ((-e $headfile) ||
(-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
}
sub check_export_ok {
my ($dir) = @_;
return (check_head_link($dir) &&
(!$export_ok || -e "$dir/$export_ok") &&
(!$export_auth_hook || $export_auth_hook->($dir)));
}
# process alternate names for backward compatibility
# filter out unsupported (unknown) snapshot formats
sub filter_snapshot_fmts {
my @fmts = @_;
@fmts = map {
exists $known_snapshot_format_aliases{$_} ?
$known_snapshot_format_aliases{$_} : $_} @fmts;
@fmts = grep {
exists $known_snapshot_formats{$_} &&
!$known_snapshot_formats{$_}{'disabled'}} @fmts;
}
our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
if (-e $GITWEB_CONFIG) {
do $GITWEB_CONFIG;
} else {
our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
}
# version of the core git binary
our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
$projects_list ||= $projectroot;
# ======================================================================
# input validation and dispatch
# input parameters can be collected from a variety of sources (presently, CGI
# and PATH_INFO), so we define an %input_params hash that collects them all
# together during validation: this allows subsequent uses (e.g. href()) to be
# agnostic of the parameter origin
our %input_params = ();
# input parameters are stored with the long parameter name as key. This will
# also be used in the href subroutine to convert parameters to their CGI
# equivalent, and since the href() usage is the most frequent one, we store
# the name -> CGI key mapping here, instead of the reverse.
#
# XXX: Warning: If you touch this, check the search form for updating,
# too.
our @cgi_param_mapping = (
project => "p",
action => "a",
file_name => "f",
file_parent => "fp",
hash => "h",
hash_parent => "hp",
hash_base => "hb",
hash_parent_base => "hpb",
page => "pg",
order => "o",
searchtext => "s",
searchtype => "st",
snapshot_format => "sf",
extra_options => "opt",
search_use_regexp => "sr",
);
our %cgi_param_mapping = @cgi_param_mapping;
# we will also need to know the possible actions, for validation
our %actions = (
"blame" => \&git_blame,
"blobdiff" => \&git_blobdiff,
"blobdiff_plain" => \&git_blobdiff_plain,
"blob" => \&git_blob,
"blob_plain" => \&git_blob_plain,
"commitdiff" => \&git_commitdiff,
"commitdiff_plain" => \&git_commitdiff_plain,
"commit" => \&git_commit,
"forks" => \&git_forks,
"heads" => \&git_heads,
"history" => \&git_history,
"log" => \&git_log,
"patch" => \&git_patch,
"patches" => \&git_patches,
"rss" => \&git_rss,
"atom" => \&git_atom,
"search" => \&git_search,
"search_help" => \&git_search_help,
"shortlog" => \&git_shortlog,
"summary" => \&git_summary,
"tag" => \&git_tag,
"tags" => \&git_tags,
"tree" => \&git_tree,
"snapshot" => \&git_snapshot,
"object" => \&git_object,
# those below don't need $project
"opml" => \&git_opml,
"project_list" => \&git_project_list,
"project_index" => \&git_project_index,
);
# finally, we have the hash of allowed extra_options for the commands that
# allow them
our %allowed_options = (
"--no-merges" => [ qw(rss atom log shortlog history) ],
);
# fill %input_params with the CGI parameters. All values except for 'opt'
# should be single values, but opt can be an array. We should probably
# build an array of parameters that can be multi-valued, but since for the time
# being it's only this one, we just single it out
while (my ($name, $symbol) = each %cgi_param_mapping) {
if ($symbol eq 'opt') {
$input_params{$name} = [ $cgi->param($symbol) ];
} else {
$input_params{$name} = $cgi->param($symbol);
}
}
# now read PATH_INFO and update the parameter list for missing parameters
sub evaluate_path_info {
return if defined $input_params{'project'};
return if !$path_info;
$path_info =~ s,^/+,,;
return if !$path_info;
# find which part of PATH_INFO is project
my $project = $path_info;
$project =~ s,/+$,,;
while ($project && !check_head_link("$projectroot/$project")) {
$project =~ s,/*[^/]*$,,;
}
return unless $project;
$input_params{'project'} = $project;
# do not change any parameters if an action is given using the query string
return if $input_params{'action'};
$path_info =~ s,^\Q$project\E/*,,;
# next, check if we have an action
my $action = $path_info;
$action =~ s,/.*$,,;
if (exists $actions{$action}) {
$path_info =~ s,^$action/*,,;
$input_params{'action'} = $action;
}
# list of actions that want hash_base instead of hash, but can have no
# pathname (f) parameter
my @wants_base = (
'tree',
'history',
);
# we want to catch
# [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
my ($parentrefname, $parentpathname, $refname, $pathname) =
($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
# first, analyze the 'current' part
if (defined $pathname) {
# we got "branch:filename" or "branch:dir/"
# we could use git_get_type(branch:pathname), but:
# - it needs $git_dir
# - it does a git() call
# - the convention of terminating directories with a slash
# makes it superfluous
# - embedding the action in the PATH_INFO would make it even
# more superfluous
$pathname =~ s,^/+,,;
if (!$pathname || substr($pathname, -1) eq "/") {
$input_params{'action'} ||= "tree";
$pathname =~ s,/$,,;
} else {
# the default action depends on whether we had parent info
# or not
if ($parentrefname) {
$input_params{'action'} ||= "blobdiff_plain";
} else {
$input_params{'action'} ||= "blob_plain";
}
}
$input_params{'hash_base'} ||= $refname;
$input_params{'file_name'} ||= $pathname;
} elsif (defined $refname) {
# we got "branch". In this case we have to choose if we have to
# set hash or hash_base.
#
# Most of the actions without a pathname only want hash to be
# set, except for the ones specified in @wants_base that want
# hash_base instead. It should also be noted that hand-crafted
# links having 'history' as an action and no pathname or hash
# set will fail, but that happens regardless of PATH_INFO.
$input_params{'action'} ||= "shortlog";
if (grep { $_ eq $input_params{'action'} } @wants_base) {
$input_params{'hash_base'} ||= $refname;
} else {
$input_params{'hash'} ||= $refname;
}
}
# next, handle the 'parent' part, if present
if (defined $parentrefname) {
# a missing pathspec defaults to the 'current' filename, allowing e.g.
# someproject/blobdiff/oldrev..newrev:/filename
if ($parentpathname) {
$parentpathname =~ s,^/+,,;
$parentpathname =~ s,/$,,;
$input_params{'file_parent'} ||= $parentpathname;
} else {
$input_params{'file_parent'} ||= $input_params{'file_name'};
}
# we assume that hash_parent_base is wanted if a path was specified,
# or if the action wants hash_base instead of hash
if (defined $input_params{'file_parent'} ||
grep { $_ eq $input_params{'action'} } @wants_base) {
$input_params{'hash_parent_base'} ||= $parentrefname;
} else {
$input_params{'hash_parent'} ||= $parentrefname;
}
}
# for the snapshot action, we allow URLs in the form
# $project/snapshot/$hash.ext
# where .ext determines the snapshot and gets removed from the
# passed $refname to provide the $hash.
#
# To be able to tell that $refname includes the format extension, we
# require the following two conditions to be satisfied:
# - the hash input parameter MUST have been set from the $refname part
# of the URL (i.e. they must be equal)
# - the snapshot format MUST NOT have been defined already (e.g. from
# CGI parameter sf)
# It's also useless to try any matching unless $refname has a dot,
# so we check for that too
if (defined $input_params{'action'} &&
$input_params{'action'} eq 'snapshot' &&
defined $refname && index($refname, '.') != -1 &&
$refname eq $input_params{'hash'} &&
!defined $input_params{'snapshot_format'}) {
# We loop over the known snapshot formats, checking for
# extensions. Allowed extensions are both the defined suffix
# (which includes the initial dot already) and the snapshot
# format key itself, with a prepended dot
while (my ($fmt, $opt) = each %known_snapshot_formats) {
my $hash = $refname;
unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
next;
}
my $sfx = $1;
# a valid suffix was found, so set the snapshot format
# and reset the hash parameter
$input_params{'snapshot_format'} = $fmt;
$input_params{'hash'} = $hash;
# we also set the format suffix to the one requested
# in the URL: this way a request for e.g. .tgz returns
# a .tgz instead of a .tar.gz
$known_snapshot_formats{$fmt}{'suffix'} = $sfx;
last;
}
}
}
evaluate_path_info();
our $action = $input_params{'action'};
if (defined $action) {
if (!validate_action($action)) {
die_error(400, "Invalid action parameter");
}
}
# parameters which are pathnames
our $project = $input_params{'project'};
if (defined $project) {
if (!validate_project($project)) {
undef $project;
die_error(404, "No such project");
}
}
our $file_name = $input_params{'file_name'};
if (defined $file_name) {
if (!validate_pathname($file_name)) {
die_error(400, "Invalid file parameter");
}
}
our $file_parent = $input_params{'file_parent'};
if (defined $file_parent) {
if (!validate_pathname($file_parent)) {
die_error(400, "Invalid file parent parameter");
}
}
# parameters which are refnames
our $hash = $input_params{'hash'};
if (defined $hash) {
if (!validate_refname($hash)) {
die_error(400, "Invalid hash parameter");
}
}
our $hash_parent = $input_params{'hash_parent'};
if (defined $hash_parent) {
if (!validate_refname($hash_parent)) {
die_error(400, "Invalid hash parent parameter");
}
}
our $hash_base = $input_params{'hash_base'};
if (defined $hash_base) {
if (!validate_refname($hash_base)) {
die_error(400, "Invalid hash base parameter");
}
}
our @extra_options = @{$input_params{'extra_options'}};
# @extra_options is always defined, since it can only be (currently) set from
# CGI, and $cgi->param() returns the empty array in array context if the param
# is not set
foreach my $opt (@extra_options) {
if (not exists $allowed_options{$opt}) {
die_error(400, "Invalid option parameter");
}
if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
die_error(400, "Invalid option parameter for this action");
}
}
our $hash_parent_base = $input_params{'hash_parent_base'};
if (defined $hash_parent_base) {
if (!validate_refname($hash_parent_base)) {
die_error(400, "Invalid hash parent base parameter");
}
}
# other parameters
our $page = $input_params{'page'};
if (defined $page) {
if ($page =~ m/[^0-9]/) {
die_error(400, "Invalid page parameter");
}
}
our $searchtype = $input_params{'searchtype'};
if (defined $searchtype) {
if ($searchtype =~ m/[^a-z]/) {
die_error(400, "Invalid searchtype parameter");
}
}
our $search_use_regexp = $input_params{'search_use_regexp'};
our $searchtext = $input_params{'searchtext'};
our $search_regexp;
if (defined $searchtext) {
if (length($searchtext) < 2) {
die_error(403, "At least two characters are required for search parameter");
}
$search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
}
# path to the current git repository
our $git_dir;
$git_dir = "$projectroot/$project" if $project;
# list of supported snapshot formats
our @snapshot_fmts = gitweb_get_feature('snapshot');
@snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
# check that the avatar feature is set to a known provider name,
# and for each provider check if the dependencies are satisfied.
# if the provider name is invalid or the dependencies are not met,
# reset $git_avatar to the empty string.
our ($git_avatar) = gitweb_get_feature('avatar');
if ($git_avatar eq 'gravatar') {
$git_avatar = '' unless (eval { require Digest::MD5; 1; });
} elsif ($git_avatar eq 'picon') {
# no dependencies
} else {
$git_avatar = '';
}
# dispatch
if (!defined $action) {
if (defined $hash) {
$action = git_get_type($hash);
} elsif (defined $hash_base && defined $file_name) {
$action = git_get_type("$hash_base:$file_name");
} elsif (defined $project) {
$action = 'summary';
} else {
$action = 'project_list';
}
}
if (!defined($actions{$action})) {
die_error(400, "Unknown action");
}
if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
!$project) {
die_error(400, "Project needed");
}
$actions{$action}->();
exit;
## ======================================================================
## action links
sub href {
my %params = @_;
# default is to use -absolute url() i.e. $my_uri
my $href = $params{-full} ? $my_url : $my_uri;
$params{'project'} = $project unless exists $params{'project'};
if ($params{-replay}) {
while (my ($name, $symbol) = each %cgi_param_mapping) {
if (!exists $params{$name}) {
$params{$name} = $input_params{$name};
}
}
}
my $use_pathinfo = gitweb_check_feature('pathinfo');
if ($use_pathinfo and defined $params{'project'}) {
# try to put as many parameters as possible in PATH_INFO:
# - project name
# - action
# - hash_parent or hash_parent_base:/file_parent
# - hash or hash_base:/filename
# - the snapshot_format as an appropriate suffix
# When the script is the root DirectoryIndex for the domain,
# $href here would be something like http://gitweb.example.com/
# Thus, we strip any trailing / from $href, to spare us double
# slashes in the final URL
$href =~ s,/$,,;
# Then add the project name, if present
$href .= "/".esc_url($params{'project'});
delete $params{'project'};
# since we destructively absorb parameters, we keep this
# boolean that remembers if we're handling a snapshot
my $is_snapshot = $params{'action'} eq 'snapshot';
# Summary just uses the project path URL, any other action is
# added to the URL
if (defined $params{'action'}) {
$href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
delete $params{'action'};
}
# Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
# stripping nonexistent or useless pieces
$href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
|| $params{'hash_parent'} || $params{'hash'});
if (defined $params{'hash_base'}) {
if (defined $params{'hash_parent_base'}) {
$href .= esc_url($params{'hash_parent_base'});
# skip the file_parent if it's the same as the file_name
if (defined $params{'file_parent'}) {
if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
delete $params{'file_parent'};
} elsif ($params{'file_parent'} !~ /\.\./) {
$href .= ":/".esc_url($params{'file_parent'});
delete $params{'file_parent'};
}
}
$href .= "..";
delete $params{'hash_parent'};
delete $params{'hash_parent_base'};
} elsif (defined $params{'hash_parent'}) {
$href .= esc_url($params{'hash_parent'}). "..";
delete $params{'hash_parent'};
}
$href .= esc_url($params{'hash_base'});
if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
$href .= ":/".esc_url($params{'file_name'});
delete $params{'file_name'};
}
delete $params{'hash'};
delete $params{'hash_base'};
} elsif (defined $params{'hash'}) {
$href .= esc_url($params{'hash'});
delete $params{'hash'};
}
# If the action was a snapshot, we can absorb the
# snapshot_format parameter too
if ($is_snapshot) {
my $fmt = $params{'snapshot_format'};
# snapshot_format should always be defined when href()
# is called, but just in case some code forgets, we
# fall back to the default
$fmt ||= $snapshot_fmts[0];
$href .= $known_snapshot_formats{$fmt}{'suffix'};
delete $params{'snapshot_format'};
}
}
# now encode the parameters explicitly
my @result = ();
for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
if (defined $params{$name}) {
if (ref($params{$name}) eq "ARRAY") {
foreach my $par (@{$params{$name}}) {
push @result, $symbol . "=" . esc_param($par);
}
} else {
push @result, $symbol . "=" . esc_param($params{$name});
}
}
}
$href .= "?" . join(';', @result) if scalar @result;
return $href;
}
## ======================================================================
## validation, quoting/unquoting and escaping
sub validate_action {
my $input = shift || return undef;
return undef unless exists $actions{$input};
return $input;
}
sub validate_project {
my $input = shift || return undef;
if (!validate_pathname($input) ||
!(-d "$projectroot/$input") ||
!check_export_ok("$projectroot/$input") ||
($strict_export && !project_in_list($input))) {
return undef;
} else {
return $input;
}
}
sub validate_pathname {
my $input = shift || return undef;
# no '.' or '..' as elements of path, i.e. no '.' nor '..'
# at the beginning, at the end, and between slashes.
# also this catches doubled slashes
if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
return undef;
}
# no null characters
if ($input =~ m!\0!) {
return undef;
}
return $input;
}
sub validate_refname {
my $input = shift || return undef;
# textual hashes are O.K.
if ($input =~ m/^[0-9a-fA-F]{40}$/) {
return $input;
}
# it must be correct pathname
$input = validate_pathname($input)
or return undef;
# restrictions on ref name according to git-check-ref-format
if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
return undef;
}
return $input;
}
# decode sequences of octets in utf8 into Perl's internal form,
# which is utf-8 with utf8 flag set if needed. gitweb writes out
# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
sub to_utf8 {
my $str = shift;
if (utf8::valid($str)) {
utf8::decode($str);
return $str;
} else {
return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
}
}
# quote unsafe chars, but keep the slash, even when it's not
# correct, but quoted slashes look too horrible in bookmarks
sub esc_param {
my $str = shift;
$str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
$str =~ s/ /\+/g;
return $str;
}
# quote unsafe chars in whole URL, so some charactrs cannot be quoted
sub esc_url {
my $str = shift;
$str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
$str =~ s/\+/%2B/g;
$str =~ s/ /\+/g;
return $str;
}
# replace invalid utf8 character with SUBSTITUTION sequence
sub esc_html {
my $str = shift;
my %opts = @_;
$str = to_utf8($str);
$str = $cgi->escapeHTML($str);
if ($opts{'-nbsp'}) {
$str =~ s/ /&nbsp;/g;
}
$str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
return $str;
}
# quote control characters and escape filename to HTML
sub esc_path {
my $str = shift;
my %opts = @_;
$str = to_utf8($str);
$str = $cgi->escapeHTML($str);
if ($opts{'-nbsp'}) {
$str =~ s/ /&nbsp;/g;
}
$str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
return $str;
}
# Make control characters "printable", using character escape codes (CEC)
sub quot_cec {
my $cntrl = shift;
my %opts = @_;
my %es = ( # character escape codes, aka escape sequences
"\t" => '\t', # tab (HT)
"\n" => '\n', # line feed (LF)
"\r" => '\r', # carrige return (CR)
"\f" => '\f', # form feed (FF)
"\b" => '\b', # backspace (BS)
"\a" => '\a', # alarm (bell) (BEL)
"\e" => '\e', # escape (ESC)
"\013" => '\v', # vertical tab (VT)
"\000" => '\0', # nul character (NUL)
);
my $chr = ( (exists $es{$cntrl})
? $es{$cntrl}
: sprintf('\%2x', ord($cntrl)) );
if ($opts{-nohtml}) {
return $chr;
} else {
return "<span class=\"cntrl\">$chr</span>";
}
}
# Alternatively use unicode control pictures codepoints,
# Unicode "printable representation" (PR)
sub quot_upr {
my $cntrl = shift;
my %opts = @_;
my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
if ($opts{-nohtml}) {
return $chr;
} else {
return "<span class=\"cntrl\">$chr</span>";
}
}
# git may return quoted and escaped filenames
sub unquote {
my $str = shift;
sub unq {
my $seq = shift;
my %es = ( # character escape codes, aka escape sequences
't' => "\t", # tab (HT, TAB)
'n' => "\n", # newline (NL)
'r' => "\r", # return (CR)
'f' => "\f", # form feed (FF)
'b' => "\b", # backspace (BS)
'a' => "\a", # alarm (bell) (BEL)
'e' => "\e", # escape (ESC)
'v' => "\013", # vertical tab (VT)
);
if ($seq =~ m/^[0-7]{1,3}$/) {
# octal char sequence
return chr(oct($seq));
} elsif (exists $es{$seq}) {
# C escape sequence, aka character escape code
return $es{$seq};
}
# quoted ordinary character
return $seq;
}
if ($str =~ m/^"(.*)"$/) {
# needs unquoting
$str = $1;
$str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
}
return $str;
}
# escape tabs (convert tabs to spaces)
sub untabify {
my $line = shift;
while ((my $pos = index($line, "\t")) != -1) {
if (my $count = (8 - ($pos % 8))) {
my $spaces = ' ' x $count;
$line =~ s/\t/$spaces/;
}
}
return $line;
}
sub project_in_list {
my $project = shift;
my @list = git_get_projects_list();
return @list && scalar(grep { $_->{'path'} eq $project } @list);
}
## ----------------------------------------------------------------------
## HTML aware string manipulation
# Try to chop given string on a word boundary between position
# $len and $len+$add_len. If there is no word boundary there,
# chop at $len+$add_len. Do not chop if chopped part plus ellipsis
# (marking chopped part) would be longer than given string.
sub chop_str {
my $str = shift;
my $len = shift;
my $add_len = shift || 10;
my $where = shift || 'right'; # 'left' | 'center' | 'right'
# Make sure perl knows it is utf8 encoded so we don't
# cut in the middle of a utf8 multibyte char.
$str = to_utf8($str);
# allow only $len chars, but don't cut a word if it would fit in $add_len
# if it doesn't fit, cut it if it's still longer than the dots we would add
# remove chopped character entities entirely
# when chopping in the middle, distribute $len into left and right part
# return early if chopping wouldn't make string shorter
if ($where eq 'center') {
return $str if ($len + 5 >= length($str)); # filler is length 5
$len = int($len/2);
} else {
return $str if ($len + 4 >= length($str)); # filler is length 4
}
# regexps: ending and beginning with word part up to $add_len
my $endre = qr/.{$len}\w{0,$add_len}/;
my $begre = qr/\w{0,$add_len}.{$len}/;
if ($where eq 'left') {
$str =~ m/^(.*?)($begre)$/;
my ($lead, $body) = ($1, $2);
if (length($lead) > 4) {
$body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
$lead = " ...";
}
return "$lead$body";
} elsif ($where eq 'center') {
$str =~ m/^($endre)(.*)$/;
my ($left, $str) = ($1, $2);
$str =~ m/^(.*?)($begre)$/;
my ($mid, $right) = ($1, $2);
if (length($mid) > 5) {
$left =~ s/&[^;]*$//;
$right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
$mid = " ... ";
}
return "$left$mid$right";
} else {
$str =~ m/^($endre)(.*)$/;
my $body = $1;
my $tail = $2;
if (length($tail) > 4) {
$body =~ s/&[^;]*$//;
$tail = "... ";
}
return "$body$tail";
}
}
# takes the same arguments as chop_str, but also wraps a <span> around the
# result with a title attribute if it does get chopped. Additionally, the
# string is HTML-escaped.
sub chop_and_escape_str {
my ($str) = @_;
my $chopped = chop_str(@_);
if ($chopped eq $str) {
return esc_html($chopped);
} else {
$str =~ s/[[:cntrl:]]/?/g;
return $cgi->span({-title=>$str}, esc_html($chopped));
}
}
## ----------------------------------------------------------------------
## functions returning short strings
# CSS class for given age value (in seconds)
sub age_class {
my $age = shift;
if (!defined $age) {
return "noage";
} elsif ($age < 60*60*2) {
return "age0";
} elsif ($age < 60*60*24*2) {
return "age1";
} else {
return "age2";
}
}
# convert age in seconds to "nn units ago" string
sub age_string {
my $age = shift;
my $age_str;
if ($age > 60*60*24*365*2) {
$age_str = (int $age/60/60/24/365);
$age_str .= " years ago";
} elsif ($age > 60*60*24*(365/12)*2) {
$age_str = int $age/60/60/24/(365/12);
$age_str .= " months ago";
} elsif ($age > 60*60*24*7*2) {
$age_str = int $age/60/60/24/7;
$age_str .= " weeks ago";
} elsif ($age > 60*60*24*2) {
$age_str = int $age/60/60/24;
$age_str .= " days ago";
} elsif ($age > 60*60*2) {
$age_str = int $age/60/60;
$age_str .= " hours ago";
} elsif ($age > 60*2) {
$age_str = int $age/60;
$age_str .= " min ago";
} elsif ($age > 2) {
$age_str = int $age;
$age_str .= " sec ago";
} else {
$age_str .= " right now";
}
return $age_str;
}
use constant {
S_IFINVALID => 0030000,
S_IFGITLINK => 0160000,
};
# submodule/subproject, a commit object reference
sub S_ISGITLINK {
my $mode = shift;
return (($mode & S_IFMT) == S_IFGITLINK)
}
# convert file mode in octal to symbolic file mode string
sub mode_str {
my $mode = oct shift;
if (S_ISGITLINK($mode)) {
return 'm---------';
} elsif (S_ISDIR($mode & S_IFMT)) {
return 'drwxr-xr-x';
} elsif (S_ISLNK($mode)) {
return 'lrwxrwxrwx';
} elsif (S_ISREG($mode)) {
# git cares only about the executable bit
if ($mode & S_IXUSR) {
return '-rwxr-xr-x';
} else {
return '-rw-r--r--';
};
} else {
return '----------';
}
}
# convert file mode in octal to file type string
sub file_type {
my $mode = shift;
if ($mode !~ m/^[0-7]+$/) {
return $mode;
} else {
$mode = oct $mode;
}
if (S_ISGITLINK($mode)) {
return "submodule";
} elsif (S_ISDIR($mode & S_IFMT)) {
return "directory";
} elsif (S_ISLNK($mode)) {
return "symlink";
} elsif (S_ISREG($mode)) {
return "file";
} else {
return "unknown";
}
}
# convert file mode in octal to file type description string
sub file_type_long {
my $mode = shift;
if ($mode !~ m/^[0-7]+$/) {
return $mode;
} else {
$mode = oct $mode;
}
if (S_ISGITLINK($mode)) {
return "submodule";
} elsif (S_ISDIR($mode & S_IFMT)) {
return "directory";
} elsif (S_ISLNK($mode)) {
return "symlink";
} elsif (S_ISREG($mode)) {
if ($mode & S_IXUSR) {
return "executable";
} else {
return "file";
};
} else {
return "unknown";
}
}
## ----------------------------------------------------------------------
## functions returning short HTML fragments, or transforming HTML fragments
## which don't belong to other sections
# format line of commit message.
sub format_log_line_html {
my $line = shift;
$line = esc_html($line, -nbsp=>1);
$line =~ s{\b([0-9a-fA-F]{8,40})\b}{
$cgi->a({-href => href(action=>"object", hash=>$1),
-class => "text"}, $1);
}eg;
return $line;
}
# format marker of refs pointing to given object
# the destination action is chosen based on object type and current context:
# - for annotated tags, we choose the tag view unless it's the current view
# already, in which case we go to shortlog view
# - for other refs, we keep the current view if we're in history, shortlog or
# log view, and select shortlog otherwise
sub format_ref_marker {
my ($refs, $id) = @_;
my $markers = '';
if (defined $refs->{$id}) {
foreach my $ref (@{$refs->{$id}}) {
# this code exploits the fact that non-lightweight tags are the
# only indirect objects, and that they are the only objects for which
# we want to use tag instead of shortlog as action
my ($type, $name) = qw();
my $indirect = ($ref =~ s/\^\{\}$//);
# e.g. tags/v2.6.11 or heads/next
if ($ref =~ m!^(.*?)s?/(.*)$!) {
$type = $1;
$name = $2;
} else {
$type = "ref";
$name = $ref;
}
my $class = $type;
$class .= " indirect" if $indirect;
my $dest_action = "shortlog";
if ($indirect) {
$dest_action = "tag" unless $action eq "tag";
} elsif ($action =~ /^(history|(short)?log)$/) {
$dest_action = $action;
}
my $dest = "";
$dest .= "refs/" unless $ref =~ m!^refs/!;
$dest .= $ref;
my $link = $cgi->a({
-href => href(
action=>$dest_action,
hash=>$dest
)}, $name);
$markers .= " <span class=\"$class\" title=\"$ref\">" .
$link . "</span>";
}
}
if ($markers) {
return ' <span class="refs">'. $markers . '</span>';
} else {
return "";
}
}
# format, perhaps shortened and with markers, title line
sub format_subject_html {
my ($long, $short, $href, $extra) = @_;
$extra = '' unless defined($extra);
if (length($short) < length($long)) {
$long =~ s/[[:cntrl:]]/?/g;
return $cgi->a({-href => $href, -class => "list subject",
-title => to_utf8($long)},
esc_html($short)) . $extra;
} else {
return $cgi->a({-href => $href, -class => "list subject"},
esc_html($long)) . $extra;
}
}
# Rather than recomputing the url for an email multiple times, we cache it
# after the first hit. This gives a visible benefit in views where the avatar
# for the same email is used repeatedly (e.g. shortlog).
# The cache is shared by all avatar engines (currently gravatar only), which
# are free to use it as preferred. Since only one avatar engine is used for any
# given page, there's no risk for cache conflicts.
our %avatar_cache = ();
# Compute the picon url for a given email, by using the picon search service over at
# http://www.cs.indiana.edu/picons/search.html
sub picon_url {
my $email = lc shift;
if (!$avatar_cache{$email}) {
my ($user, $domain) = split('@', $email);
$avatar_cache{$email} =
"http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
"$domain/$user/" .
"users+domains+unknown/up/single";
}
return $avatar_cache{$email};
}
# Compute the gravatar url for a given email, if it's not in the cache already.
# Gravatar stores only the part of the URL before the size, since that's the
# one computationally more expensive. This also allows reuse of the cache for
# different sizes (for this particular engine).
sub gravatar_url {
my $email = lc shift;
my $size = shift;
$avatar_cache{$email} ||=
"http://www.gravatar.com/avatar/" .
Digest::MD5::md5_hex($email) . "?s=";
return $avatar_cache{$email} . $size;
}
# Insert an avatar for the given $email at the given $size if the feature
# is enabled.
sub git_get_avatar {
my ($email, %opts) = @_;
my $pre_white = ($opts{-pad_before} ? "&nbsp;" : "");
my $post_white = ($opts{-pad_after} ? "&nbsp;" : "");
$opts{-size} ||= 'default';
my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
my $url = "";
if ($git_avatar eq 'gravatar') {
$url = gravatar_url($email, $size);
} elsif ($git_avatar eq 'picon') {
$url = picon_url($email);
}
# Other providers can be added by extending the if chain, defining $url
# as needed. If no variant puts something in $url, we assume avatars
# are completely disabled/unavailable.
if ($url) {
return $pre_white .
"<img width=\"$size\" " .
"class=\"avatar\" " .
"src=\"$url\" " .
"alt=\"\" " .
"/>" . $post_white;
} else {
return "";
}
}
# format the author name of the given commit with the given tag
# the author name is chopped and escaped according to the other
# optional parameters (see chop_str).
sub format_author_html {
my $tag = shift;
my $co = shift;
my $author = chop_and_escape_str($co->{'author_name'}, @_);
return "<$tag class=\"author\">" .
git_get_avatar($co->{'author_email'}, -pad_after => 1) .
$author . "</$tag>";
}
# format git diff header line, i.e. "diff --(git|combined|cc) ..."
sub format_git_diff_header_line {
my $line = shift;
my $diffinfo = shift;
my ($from, $to) = @_;
if ($diffinfo->{'nparents'}) {
# combined diff
$line =~ s!^(diff (.*?) )"?.*$!$1!;
if ($to->{'href'}) {
$line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
esc_path($to->{'file'}));
} else { # file was deleted (no href)
$line .= esc_path($to->{'file'});
}
} else {
# "ordinary" diff
$line =~ s!^(diff (.*?) )"?a/.*$!$1!;
if ($from->{'href'}) {
$line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
'a/' . esc_path($from->{'file'}));
} else { # file was added (no href)
$line .= 'a/' . esc_path($from->{'file'});
}
$line .= ' ';
if ($to->{'href'}) {
$line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
'b/' . esc_path($to->{'file'}));
} else { # file was deleted
$line .= 'b/' . esc_path($to->{'file'});
}
}
return "<div class=\"diff header\">$line</div>\n";
}
# format extended diff header line, before patch itself
sub format_extended_diff_header_line {
my $line = shift;
my $diffinfo = shift;
my ($from, $to) = @_;
# match <path>
if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
$line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
esc_path($from->{'file'}));
}
if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
$line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
esc_path($to->{'file'}));
}
# match single <mode>
if ($line =~ m/\s(\d{6})$/) {
$line .= '<span class="info"> (' .
file_type_long($1) .
')</span>';
}
# match <hash>
if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
# can match only for combined diff
$line = 'index ';
for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
if ($from->{'href'}[$i]) {
$line .= $cgi->a({-href=>$from->{'href'}[$i],
-class=>"hash"},
substr($diffinfo->{'from_id'}[$i],0,7));
} else {
$line .= '0' x 7;
}
# separator
$line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
}
$line .= '..';
if ($to->{'href'}) {
$line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
substr($diffinfo->{'to_id'},0,7));
} else {
$line .= '0' x 7;
}
} elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
# can match only for ordinary diff
my ($from_link, $to_link);
if ($from->{'href'}) {
$from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
substr($diffinfo->{'from_id'},0,7));
} else {
$from_link = '0' x 7;
}
if ($to->{'href'}) {
$to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
substr($diffinfo->{'to_id'},0,7));
} else {
$to_link = '0' x 7;
}
my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
$line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
}
return $line . "<br/>\n";
}
# format from-file/to-file diff header
sub format_diff_from_to_header {
my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
my $line;
my $result = '';
$line = $from_line;
#assert($line =~ m/^---/) if DEBUG;
# no extra formatting for "^--- /dev/null"
if (! $diffinfo->{'nparents'}) {
# ordinary (single parent) diff
if ($line =~ m!^--- "?a/!) {
if ($from->{'href'}) {
$line = '--- a/' .
$cgi->a({-href=>$from->{'href'}, -class=>"path"},
esc_path($from->{'file'}));
} else {
$line = '--- a/' .
esc_path($from->{'file'});
}
}
$result .= qq!<div class="diff from_file">$line</div>\n!;
} else {
# combined diff (merge commit)
for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
if ($from->{'href'}[$i]) {
$line = '--- ' .
$cgi->a({-href=>href(action=>"blobdiff",
hash_parent=>$diffinfo->{'from_id'}[$i],
hash_parent_base=>$parents[$i],
file_parent=>$from->{'file'}[$i],
hash=>$diffinfo->{'to_id'},
hash_base=>$hash,
file_name=>$to->{'file'}),
-class=>"path",
-title=>"diff" . ($i+1)},
$i+1) .
'/' .
$cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
esc_path($from->{'file'}[$i]));
} else {
$line = '--- /dev/null';
}
$result .= qq!<div class="diff from_file">$line</div>\n!;
}
}
$line = $to_line;
#assert($line =~ m/^\+\+\+/) if DEBUG;
# no extra formatting for "^+++ /dev/null"
if ($line =~ m!^\+\+\+ "?b/!) {
if ($to->{'href'}) {
$line = '+++ b/' .
$cgi->a({-href=>$to->{'href'}, -class=>"path"},
esc_path($to->{'file'}));
} else {
$line = '+++ b/' .
esc_path($to->{'file'});
}
}
$result .= qq!<div class="diff to_file">$line</div>\n!;
return $result;
}
# create note for patch simplified by combined diff
sub format_diff_cc_simplified {
my ($diffinfo, @parents) = @_;
my $result = '';
$result .= "<div class=\"diff header\">" .
"diff --cc ";
if (!is_deleted($diffinfo)) {
$result .= $cgi->a({-href => href(action=>"blob",
hash_base=>$hash,
hash=>$diffinfo->{'to_id'},
file_name=>$diffinfo->{'to_file'}),
-class => "path"},
esc_path($diffinfo->{'to_file'}));
} else {
$result .= esc_path($diffinfo->{'to_file'});
}
$result .= "</div>\n" . # class="diff header"
"<div class=\"diff nodifferences\">" .
"Simple merge" .
"</div>\n"; # class="diff nodifferences"
return $result;
}
# format patch (diff) line (not to be used for diff headers)
sub format_diff_line {
my $line = shift;
my ($from, $to) = @_;
my $diff_class = "";
chomp $line;
if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
# combined diff
my $prefix = substr($line, 0, scalar @{$from->{'href'}});
if ($line =~ m/^\@{3}/) {
$diff_class = " chunk_header";
} elsif ($line =~ m/^\\/) {
$diff_class = " incomplete";
} elsif ($prefix =~ tr/+/+/) {
$diff_class = " add";
} elsif ($prefix =~ tr/-/-/) {
$diff_class = " rem";
}
} else {
# assume ordinary diff
my $char = substr($line, 0, 1);
if ($char eq '+') {
$diff_class = " add";
} elsif ($char eq '-') {
$diff_class = " rem";
} elsif ($char eq '@') {
$diff_class = " chunk_header";
} elsif ($char eq "\\") {
$diff_class = " incomplete";
}
}
$line = untabify($line);
if ($from && $to && $line =~ m/^\@{2} /) {
my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
$line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
$from_lines = 0 unless defined $from_lines;
$to_lines = 0 unless defined $to_lines;
if ($from->{'href'}) {
$from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
-class=>"list"}, $from_text);
}
if ($to->{'href'}) {
$to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
-class=>"list"}, $to_text);
}
$line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
"<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
return "<div class=\"diff$diff_class\">$line</div>\n";
} elsif ($from && $to && $line =~ m/^\@{3}/) {
my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
@from_text = split(' ', $ranges);
for (my $i = 0; $i < @from_text; ++$i) {
($from_start[$i], $from_nlines[$i]) =
(split(',', substr($from_text[$i], 1)), 0);
}
$to_text = pop @from_text;
$to_start = pop @from_start;
$to_nlines = pop @from_nlines;
$line = "<span class=\"chunk_info\">$prefix ";
for (my $i = 0; $i < @from_text; ++$i) {
if ($from->{'href'}[$i]) {
$line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
-class=>"list"}, $from_text[$i]);
} else {
$line .= $from_text[$i];
}
$line .= " ";
}
if ($to->{'href'}) {
$line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
-class=>"list"}, $to_text);
} else {
$line .= $to_text;
}
$line .= " $prefix</span>" .
"<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
return "<div class=\"diff$diff_class\">$line</div>\n";
}
return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
}
# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
# linked. Pass the hash of the tree/commit to snapshot.
sub format_snapshot_links {
my ($hash) = @_;
my $num_fmts = @snapshot_fmts;
if ($num_fmts > 1) {
# A parenthesized list of links bearing format names.
# e.g. "snapshot (_tar.gz_ _zip_)"
return "snapshot (" . join(' ', map
$cgi->a({
-href => href(
action=>"snapshot",
hash=>$hash,
snapshot_format=>$_
)
}, $known_snapshot_formats{$_}{'display'})
, @snapshot_fmts) . ")";
} elsif ($num_fmts == 1) {
# A single "snapshot" link whose tooltip bears the format name.
# i.e. "_snapshot_"
my ($fmt) = @snapshot_fmts;
return
$cgi->a({
-href => href(
action=>"snapshot",
hash=>$hash,
snapshot_format=>$fmt
),
-title => "in format: $known_snapshot_formats{$fmt}{'display'}"
}, "snapshot");
} else { # $num_fmts == 0
return undef;
}
}
## ......................................................................
## functions returning values to be passed, perhaps after some
## transformation, to other functions; e.g. returning arguments to href()
# returns hash to be passed to href to generate gitweb URL
# in -title key it returns description of link
sub get_feed_info {
my $format = shift || 'Atom';
my %res = (action => lc($format));
# feed links are possible only for project views
return unless (defined $project);
# some views should link to OPML, or to generic project feed,
# or don't have specific feed yet (so they should use generic)
return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
my $branch;
# branches refs uses 'refs/heads/' prefix (fullname) to differentiate
# from tag links; this also makes possible to detect branch links
if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
(defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
$branch = $1;
}
# find log type for feed description (title)
my $type = 'log';
if (defined $file_name) {
$type = "history of $file_name";
$type .= "/" if ($action eq 'tree');
$type .= " on '$branch'" if (defined $branch);
} else {
$type = "log of $branch" if (defined $branch);
}
$res{-title} = $type;
$res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
$res{'file_name'} = $file_name;
return %res;
}
## ----------------------------------------------------------------------
## git utility subroutines, invoking git commands
# returns path to the core git executable and the --git-dir parameter as list
sub git_cmd {
return $GIT, '--git-dir='.$git_dir;
}
# quote the given arguments for passing them to the shell
# quote_command("command", "arg 1", "arg with ' and ! characters")
# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
# Try to avoid using this function wherever possible.
sub quote_command {
return join(' ',
map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
}
# get HEAD ref of given project as hash
sub git_get_head_hash {
my $project = shift;
my $o_git_dir = $git_dir;
my $retval = undef;
$git_dir = "$projectroot/$project";
if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
my $head = <$fd>;
close $fd;
if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
$retval = $1;
}
}
if (defined $o_git_dir) {
$git_dir = $o_git_dir;
}
return $retval;
}
# get type of given object
sub git_get_type {
my $hash = shift;
open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
my $type = <$fd>;
close $fd or return;
chomp $type;
return $type;
}
# repository configuration
our $config_file = '';
our %config;
# store multiple values for single key as anonymous array reference
# single values stored directly in the hash, not as [ <value> ]
sub hash_set_multi {
my ($hash, $key, $value) = @_;
if (!exists $hash->{$key}) {
$hash->{$key} = $value;
} elsif (!ref $hash->{$key}) {
$hash->{$key} = [ $hash->{$key}, $value ];
} else {
push @{$hash->{$key}}, $value;
}
}
# return hash of git project configuration
# optionally limited to some section, e.g. 'gitweb'
sub git_parse_project_config {
my $section_regexp = shift;
my %config;
local $/ = "\0";
open my $fh, "-|", git_cmd(), "config", '-z', '-l',
or return;
while (my $keyval = <$fh>) {
chomp $keyval;
my ($key, $value) = split(/\n/, $keyval, 2);
hash_set_multi(\%config, $key, $value)
if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
}
close $fh;
return %config;
}
# convert config value to boolean: 'true' or 'false'
# no value, number > 0, 'true' and 'yes' values are true
# rest of values are treated as false (never as error)
sub config_to_bool {
my $val = shift;
return 1 if !defined $val; # section.key
# strip leading and trailing whitespace
$val =~ s/^\s+//;
$val =~ s/\s+$//;
return (($val =~ /^\d+$/ && $val) || # section.key = 1
($val =~ /^(?:true|yes)$/i)); # section.key = true
}
# convert config value to simple decimal number
# an optional value suffix of 'k', 'm', or 'g' will cause the value
# to be multiplied by 1024, 1048576, or 1073741824
sub config_to_int {
my $val = shift;
# strip leading and trailing whitespace
$val =~ s/^\s+//;
$val =~ s/\s+$//;
if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
$unit = lc($unit);
# unknown unit is treated as 1
return $num * ($unit eq 'g' ? 1073741824 :
$unit eq 'm' ? 1048576 :
$unit eq 'k' ? 1024 : 1);
}
return $val;
}
# convert config value to array reference, if needed
sub config_to_multi {
my $val = shift;
return ref($val) ? $val : (defined($val) ? [ $val ] : []);
}
sub git_get_project_config {
my ($key, $type) = @_;
# key sanity check
return unless ($key);
$key =~ s/^gitweb\.//;
return if ($key =~ m/\W/);
# type sanity check
if (defined $type) {
$type =~ s/^--//;
$type = undef
unless ($type eq 'bool' || $type eq 'int');
}
# get config
if (!defined $config_file ||
$config_file ne "$git_dir/config") {
%config = git_parse_project_config('gitweb');
$config_file = "$git_dir/config";
}
# check if config variable (key) exists
return unless exists $config{"gitweb.$key"};
# ensure given type
if (!defined $type) {
return $config{"gitweb.$key"};
} elsif ($type eq 'bool') {
# backward compatibility: 'git config --bool' returns true/false
return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
} elsif ($type eq 'int') {
return config_to_int($config{"gitweb.$key"});
}
return $config{"gitweb.$key"};
}
# get hash of given path at given ref
sub git_get_hash_by_path {
my $base = shift;
my $path = shift || return undef;
my $type = shift;
$path =~ s,/+$,,;
open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
or die_error(500, "Open git-ls-tree failed");
my $line = <$fd>;
close $fd or return undef;
if (!defined $line) {
# there is no tree or hash given by $path at $base
return undef;
}
#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
$line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
if (defined $type && $type ne $2) {
# type doesn't match
return undef;
}
return $3;
}
# get path of entry with given hash at given tree-ish (ref)
# used to get 'from' filename for combined diff (merge commit) for renames
sub git_get_path_by_hash {
my $base = shift || return;
my $hash = shift || return;
local $/ = "\0";
open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
or return undef;
while (my $line = <$fd>) {
chomp $line;
#'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
#'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
close $fd;
return $1;
}
}
close $fd;
return undef;
}
## ......................................................................
## git utility functions, directly accessing git repository
sub git_get_project_description {
my $path = shift;
$git_dir = "$projectroot/$path";
open my $fd, '<', "$git_dir/description"
or return git_get_project_config('description');
my $descr = <$fd>;
close $fd;
if (defined $descr) {
chomp $descr;
}
return $descr;
}
sub git_get_project_ctags {
my $path = shift;
my $ctags = {};
$git_dir = "$projectroot/$path";
opendir my $dh, "$git_dir/ctags"
or return $ctags;
foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
open my $ct, '<', $_ or next;
my $val = <$ct>;
chomp $val;
close $ct;
my $ctag = $_; $ctag =~ s#.*/##;
$ctags->{$ctag} = $val;
}
closedir $dh;
$ctags;
}
sub git_populate_project_tagcloud {
my $ctags = shift;
# First, merge different-cased tags; tags vote on casing
my %ctags_lc;
foreach (keys %$ctags) {
$ctags_lc{lc $_}->{count} += $ctags->{$_};
if (not $ctags_lc{lc $_}->{topcount}
or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
$ctags_lc{lc $_}->{topcount} = $ctags->{$_};
$ctags_lc{lc $_}->{topname} = $_;
}
}
my $cloud;
if (eval { require HTML::TagCloud; 1; }) {
$cloud = HTML::TagCloud->new;
foreach (sort keys %ctags_lc) {
# Pad the title with spaces so that the cloud looks
# less crammed.
my $title = $ctags_lc{$_}->{topname};
$title =~ s/ /&nbsp;/g;
$title =~ s/^/&nbsp;/g;
$title =~ s/$/&nbsp;/g;
$cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
}
} else {
$cloud = \%ctags_lc;
}
$cloud;
}
sub git_show_project_tagcloud {
my ($cloud, $count) = @_;
print STDERR ref($cloud)."..\n";
if (ref $cloud eq 'HTML::TagCloud') {
return $cloud->html_and_css($count);
} else {
my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
return '<p align="center">' . join (', ', map {
"<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
} splice(@tags, 0, $count)) . '</p>';
}
}
sub git_get_project_url_list {
my $path = shift;
$git_dir = "$projectroot/$path";
open my $fd, '<', "$git_dir/cloneurl"
or return wantarray ?
@{ config_to_multi(git_get_project_config('url')) } :
config_to_multi(git_get_project_config('url'));
my @git_project_url_list = map { chomp; $_ } <$fd>;
close $fd;
return wantarray ? @git_project_url_list : \@git_project_url_list;
}
sub git_get_projects_list {
my ($filter) = @_;
my @list;
$filter ||= '';
$filter =~ s/\.git$//;
my $check_forks = gitweb_check_feature('forks');
if (-d $projects_list) {
# search in directory
my $dir = $projects_list . ($filter ? "/$filter" : '');
# remove the trailing "/"
$dir =~ s!/+$!!;
my $pfxlen = length("$dir");
my $pfxdepth = ($dir =~ tr!/!!);
File::Find::find({
follow_fast => 1, # follow symbolic links
follow_skip => 2, # ignore duplicates
dangling_symlinks => 0, # ignore dangling symlinks, silently
wanted => sub {
# skip project-list toplevel, if we get it.
return if (m!^[/.]$!);
# only directories can be git repositories
return unless (-d $_);
# don't traverse too deep (Find is super slow on os x)
if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
$File::Find::prune = 1;
return;
}
my $subdir = substr($File::Find::name, $pfxlen + 1);
# we check related file in $projectroot
my $path = ($filter ? "$filter/" : '') . $subdir;
if (check_export_ok("$projectroot/$path")) {
push @list, { path => $path };
$File::Find::prune = 1;
}
},
}, "$dir");
} elsif (-f $projects_list) {
# read from file(url-encoded):
# 'git%2Fgit.git Linus+Torvalds'
# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
my %paths;
open my $fd, '<', $projects_list or return;
PROJECT:
while (my $line = <$fd>) {
chomp $line;
my ($path, $owner) = split ' ', $line;
$path = unescape($path);
$owner = unescape($owner);
if (!defined $path) {
next;
}
if ($filter ne '') {
# looking for forks;
my $pfx = substr($path, 0, length($filter));
if ($pfx ne $filter) {
next PROJECT;
}
my $sfx = substr($path, length($filter));
if ($sfx !~ /^\/.*\.git$/) {
next PROJECT;
}
} elsif ($check_forks) {
PATH:
foreach my $filter (keys %paths) {
# looking for forks;
my $pfx = substr($path, 0, length($filter));
if ($pfx ne $filter) {
next PATH;
}
my $sfx = substr($path, length($filter));
if ($sfx !~ /^\/.*\.git$/) {
next PATH;
}
# is a fork, don't include it in
# the list
next PROJECT;
}
}
if (check_export_ok("$projectroot/$path")) {
my $pr = {
path => $path,
owner => to_utf8($owner),
};
push @list, $pr;
(my $forks_path = $path) =~ s/\.git$//;
$paths{$forks_path}++;
}
}
close $fd;
}
return @list;
}
our $gitweb_project_owner = undef;
sub git_get_project_list_from_file {
return if (defined $gitweb_project_owner);
$gitweb_project_owner = {};
# read from file (url-encoded):
# 'git%2Fgit.git Linus+Torvalds'
# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
if (-f $projects_list) {
open(my $fd, '<', $projects_list);
while (my $line = <$fd>) {
chomp $line;
my ($pr, $ow) = split ' ', $line;
$pr = unescape($pr);
$ow = unescape($ow);
$gitweb_project_owner->{$pr} = to_utf8($ow);
}
close $fd;
}
}
sub git_get_project_owner {
my $project = shift;
my $owner;
return undef unless $project;
$git_dir = "$projectroot/$project";
if (!defined $gitweb_project_owner) {
git_get_project_list_from_file();
}
if (exists $gitweb_project_owner->{$project}) {
$owner = $gitweb_project_owner->{$project};
}
if (!defined $owner){
$owner = git_get_project_config('owner');
}
if (!defined $owner) {
$owner = get_file_owner("$git_dir");
}
return $owner;
}
sub git_get_last_activity {
my ($path) = @_;
my $fd;
$git_dir = "$projectroot/$path";
open($fd, "-|", git_cmd(), 'for-each-ref',
'--format=%(committer)',
'--sort=-committerdate',
'--count=1',
'refs/heads') or return;
my $most_recent = <$fd>;
close $fd or return;
if (defined $most_recent &&
$most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
my $timestamp = $1;
my $age = time - $timestamp;
return ($age, age_string($age));
}
return (undef, undef);
}
sub git_get_references {
my $type = shift || "";
my %refs;
# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
# c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
or return;
while (my $line = <$fd>) {
chomp $line;
if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
if (defined $refs{$1}) {
push @{$refs{$1}}, $2;
} else {
$refs{$1} = [ $2 ];
}
}
}
close $fd or return;
return \%refs;
}
sub git_get_rev_name_tags {
my $hash = shift || return undef;
open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
or return;
my $name_rev = <$fd>;
close $fd;
if ($name_rev =~ m|^$hash tags/(.*)$|) {
return $1;
} else {
# catches also '$hash undefined' output
return undef;
}
}
## ----------------------------------------------------------------------
## parse to hash functions
sub parse_date {
my $epoch = shift;
my $tz = shift || "-0000";
my %date;
my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
$date{'hour'} = $hour;
$date{'minute'} = $min;
$date{'mday'} = $mday;
$date{'day'} = $days[$wday];
$date{'month'} = $months[$mon];
$date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
$days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
$date{'mday-time'} = sprintf "%d %s %02d:%02d",
$mday, $months[$mon], $hour ,$min;
$date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
$tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
my $local = $epoch + ((int $1 + ($2/60)) * 3600);
($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
$date{'hour_local'} = $hour;
$date{'minute_local'} = $min;
$date{'tz_local'} = $tz;
$date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
1900+$year, $mon+1, $mday,
$hour, $min, $sec, $tz);
return %date;
}
sub parse_tag {
my $tag_id = shift;
my %tag;
my @comment;
open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
$tag{'id'} = $tag_id;
while (my $line = <$fd>) {
chomp $line;
if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
$tag{'object'} = $1;
} elsif ($line =~ m/^type (.+)$/) {
$tag{'type'} = $1;
} elsif ($line =~ m/^tag (.+)$/) {
$tag{'name'} = $1;
} elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
$tag{'author'} = $1;
$tag{'author_epoch'} = $2;
$tag{'author_tz'} = $3;
if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
$tag{'author_name'} = $1;
$tag{'author_email'} = $2;
} else {
$tag{'author_name'} = $tag{'author'};
}
} elsif ($line =~ m/--BEGIN/) {
push @comment, $line;
last;
} elsif ($line eq "") {
last;
}
}
push @comment, <$fd>;
$tag{'comment'} = \@comment;
close $fd or return;
if (!defined $tag{'name'}) {
return
};
return %tag
}
sub parse_commit_text {
my ($commit_text, $withparents) = @_;
my @commit_lines = split '\n', $commit_text;
my %co;
pop @commit_lines; # Remove '\0'
if (! @commit_lines) {
return;
}
my $header = shift @commit_lines;
if ($header !~ m/^[0-9a-fA-F]{40}/) {
return;
}
($co{'id'}, my @parents) = split ' ', $header;
while (my $line = shift @commit_lines) {
last if $line eq "\n";
if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
$co{'tree'} = $1;
} elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
push @parents, $1;
} elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
$co{'author'} = to_utf8($1);
$co{'author_epoch'} = $2;
$co{'author_tz'} = $3;
if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
$co{'author_name'} = $1;
$co{'author_email'} = $2;
} else {
$co{'author_name'} = $co{'author'};
}
} elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
$co{'committer'} = to_utf8($1);
$co{'committer_epoch'} = $2;
$co{'committer_tz'} = $3;
if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
$co{'committer_name'} = $1;
$co{'committer_email'} = $2;
} else {
$co{'committer_name'} = $co{'committer'};
}
}
}
if (!defined $co{'tree'}) {
return;
};
$co{'parents'} = \@parents;
$co{'parent'} = $parents[0];
foreach my $title (@commit_lines) {
$title =~ s/^ //;
if ($title ne "") {
$co{'title'} = chop_str($title, 80, 5);
# remove leading stuff of merges to make the interesting part visible
if (length($title) > 50) {
$title =~ s/^Automatic //;
$title =~ s/^merge (of|with) /Merge ... /i;
if (length($title) > 50) {
$title =~ s/(http|rsync):\/\///;
}
if (length($title) > 50) {
$title =~ s/(master|www|rsync)\.//;
}
if (length($title) > 50) {
$title =~ s/kernel.org:?//;
}
if (length($title) > 50) {
$title =~ s/\/pub\/scm//;
}
}
$co{'title_short'} = chop_str($title, 50, 5);
last;
}
}
if (! defined $co{'title'} || $co{'title'} eq "") {
$co{'title'} = $co{'title_short'} = '(no commit message)';
}
# remove added spaces
foreach my $line (@commit_lines) {
$line =~ s/^ //;
}
$co{'comment'} = \@commit_lines;
my $age = time - $co{'committer_epoch'};
$co{'age'} = $age;
$co{'age_string'} = age_string($age);
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
if ($age > 60*60*24*7*2) {
$co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
$co{'age_string_age'} = $co{'age_string'};
} else {
$co{'age_string_date'} = $co{'age_string'};
$co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
}
return %co;
}
sub parse_commit {
my ($commit_id) = @_;
my %co;
local $/ = "\0";
open my $fd, "-|", git_cmd(), "rev-list",
"--parents",
"--header",
"--max-count=1",
$commit_id,
"--",
or die_error(500, "Open git-rev-list failed");
%co = parse_commit_text(<$fd>, 1);
close $fd;
return %co;
}
sub parse_commits {
my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
my @cos;
$maxcount ||= 1;
$skip ||= 0;
local $/ = "\0";
open my $fd, "-|", git_cmd(), "rev-list",
"--header",
@args,
("--max-count=" . $maxcount),
("--skip=" . $skip),
@extra_options,
$commit_id,
"--",
($filename ? ($filename) : ())
or die_error(500, "Open git-rev-list failed");
while (my $line = <$fd>) {
my %co = parse_commit_text($line);
push @cos, \%co;
}
close $fd;
return wantarray ? @cos : \@cos;
}
# parse line of git-diff-tree "raw" output
sub parse_difftree_raw_line {
my $line = shift;
my %res;
# ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
# ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
$res{'from_mode'} = $1;
$res{'to_mode'} = $2;
$res{'from_id'} = $3;
$res{'to_id'} = $4;
$res{'status'} = $5;
$res{'similarity'} = $6;
if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
} else {
$res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
}
}
# '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
# combined diff (for merge commit)
elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
$res{'nparents'} = length($1);
$res{'from_mode'} = [ split(' ', $2) ];
$res{'to_mode'} = pop @{$res{'from_mode'}};
$res{'from_id'} = [ split(' ', $3) ];
$res{'to_id'} = pop @{$res{'from_id'}};
$res{'status'} = [ split('', $4) ];
$res{'to_file'} = unquote($5);
}
# 'c512b523472485aef4fff9e57b229d9d243c967f'
elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
$res{'commit'} = $1;
}
return wantarray ? %res : \%res;
}
# wrapper: return parsed line of git-diff-tree "raw" output
# (the argument might be raw line, or parsed info)
sub parsed_difftree_line {
my $line_or_ref = shift;
if (ref($line_or_ref) eq "HASH") {
# pre-parsed (or generated by hand)
return $line_or_ref;
} else {
return parse_difftree_raw_line($line_or_ref);
}
}
# parse line of git-ls-tree output
sub parse_ls_tree_line {
my $line = shift;
my %opts = @_;
my %res;
#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
$line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
$res{'mode'} = $1;
$res{'type'} = $2;
$res{'hash'} = $3;
if ($opts{'-z'}) {
$res{'name'} = $4;
} else {
$res{'name'} = unquote($4);
}
return wantarray ? %res : \%res;
}
# generates _two_ hashes, references to which are passed as 2 and 3 argument
sub parse_from_to_diffinfo {
my ($diffinfo, $from, $to, @parents) = @_;
if ($diffinfo->{'nparents'}) {
# combined diff
$from->{'file'} = [];
$from->{'href'} = [];
fill_from_file_info($diffinfo, @parents)
unless exists $diffinfo->{'from_file'};
for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
$from->{'file'}[$i] =
defined $diffinfo->{'from_file'}[$i] ?
$diffinfo->{'from_file'}[$i] :
$diffinfo->{'to_file'};
if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
$from->{'href'}[$i] = href(action=>"blob",
hash_base=>$parents[$i],
hash=>$diffinfo->{'from_id'}[$i],
file_name=>$from->{'file'}[$i]);
} else {
$from->{'href'}[$i] = undef;
}
}
} else {
# ordinary (not combined) diff
$from->{'file'} = $diffinfo->{'from_file'};
if ($diffinfo->{'status'} ne "A") { # not new (added) file
$from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
hash=>$diffinfo->{'from_id'},
file_name=>$from->{'file'});
} else {
delete $from->{'href'};
}
}
$to->{'file'} = $diffinfo->{'to_file'};
if (!is_deleted($diffinfo)) { # file exists in result
$to->{'href'} = href(action=>"blob", hash_base=>$hash,
hash=>$diffinfo->{'to_id'},
file_name=>$to->{'file'});
} else {
delete $to->{'href'};
}
}
## ......................................................................
## parse to array of hashes functions
sub git_get_heads_list {
my $limit = shift;
my @headslist;
open my $fd, '-|', git_cmd(), 'for-each-ref',
($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
'--format=%(objectname) %(refname) %(subject)%00%(committer)',
'refs/heads'
or return;
while (my $line = <$fd>) {
my %ref_item;
chomp $line;
my ($refinfo, $committerinfo) = split(/\0/, $line);
my ($hash, $name, $title) = split(' ', $refinfo, 3);
my ($committer, $epoch, $tz) =
($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
$ref_item{'fullname'} = $name;
$name =~ s!^refs/heads/!!;
$ref_item{'name'} = $name;
$ref_item{'id'} = $hash;
$ref_item{'title'} = $title || '(no commit message)';
$ref_item{'epoch'} = $epoch;
if ($epoch) {
$ref_item{'age'} = age_string(time - $ref_item{'epoch'});
} else {
$ref_item{'age'} = "unknown";
}
push @headslist, \%ref_item;
}
close $fd;
return wantarray ? @headslist : \@headslist;
}
sub git_get_tags_list {
my $limit = shift;
my @tagslist;
open my $fd, '-|', git_cmd(), 'for-each-ref',
($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
'--format=%(objectname) %(objecttype) %(refname) '.
'%(*objectname) %(*objecttype) %(subject)%00%(creator)',
'refs/tags'
or return;
while (my $line = <$fd>) {
my %ref_item;
chomp $line;
my ($refinfo, $creatorinfo) = split(/\0/, $line);
my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
my ($creator, $epoch, $tz) =
($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
$ref_item{'fullname'} = $name;
$name =~ s!^refs/tags/!!;
$ref_item{'type'} = $type;
$ref_item{'id'} = $id;
$ref_item{'name'} = $name;
if ($type eq "tag") {
$ref_item{'subject'} = $title;
$ref_item{'reftype'} = $reftype;
$ref_item{'refid'} = $refid;
} else {
$ref_item{'reftype'} = $type;
$ref_item{'refid'} = $id;
}
if ($type eq "tag" || $type eq "commit") {
$ref_item{'epoch'} = $epoch;
if ($epoch) {
$ref_item{'age'} = age_string(time - $ref_item{'epoch'});
} else {
$ref_item{'age'} = "unknown";
}
}
push @tagslist, \%ref_item;
}
close $fd;
return wantarray ? @tagslist : \@tagslist;
}
## ----------------------------------------------------------------------
## filesystem-related functions
sub get_file_owner {
my $path = shift;
my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
if (!defined $gcos) {
return undef;
}
my $owner = $gcos;
$owner =~ s/[,;].*$//;
return to_utf8($owner);
}
# assume that file exists
sub insert_file {
my $filename = shift;
open my $fd, '<', $filename;
print map { to_utf8($_) } <$fd>;
close $fd;
}
## ......................................................................
## mimetype related functions
sub mimetype_guess_file {
my $filename = shift;
my $mimemap = shift;
-r $mimemap or return undef;
my %mimemap;
open(my $mh, '<', $mimemap) or return undef;
while (<$mh>) {
next if m/^#/; # skip comments
my ($mimetype, $exts) = split(/\t+/);
if (defined $exts) {
my @exts = split(/\s+/, $exts);
foreach my $ext (@exts) {
$mimemap{$ext} = $mimetype;
}
}
}
close($mh);
$filename =~ /\.([^.]*)$/;
return $mimemap{$1};
}
sub mimetype_guess {
my $filename = shift;
my $mime;
$filename =~ /\./ or return undef;
if ($mimetypes_file) {
my $file = $mimetypes_file;
if ($file !~ m!^/!) { # if it is relative path
# it is relative to project
$file = "$projectroot/$project/$file";
}
$mime = mimetype_guess_file($filename, $file);
}
$mime ||= mimetype_guess_file($filename, '/etc/mime.types');
return $mime;
}
sub blob_mimetype {
my $fd = shift;
my $filename = shift;
if ($filename) {
my $mime = mimetype_guess($filename);
$mime and return $mime;
}
# just in case
return $default_blob_plain_mimetype unless $fd;
if (-T $fd) {
return 'text/plain';
} elsif (! $filename) {
return 'application/octet-stream';
} elsif ($filename =~ m/\.png$/i) {
return 'image/png';
} elsif ($filename =~ m/\.gif$/i) {
return 'image/gif';
} elsif ($filename =~ m/\.jpe?g$/i) {
return 'image/jpeg';
} else {
return 'application/octet-stream';
}
}
sub blob_contenttype {
my ($fd, $file_name, $type) = @_;
$type ||= blob_mimetype($fd, $file_name);
if ($type eq 'text/plain' && defined $default_text_plain_charset) {
$type .= "; charset=$default_text_plain_charset";
}
return $type;
}
## ======================================================================
## functions printing HTML: header, footer, error page
sub git_header_html {
my $status = shift || "200 OK";
my $expires = shift;
my $title = "$site_name";
if (defined $project) {
$title .= " - " . to_utf8($project);
if (defined $action) {
$title .= "/$action";
if (defined $file_name) {
$title .= " - " . esc_path($file_name);
if ($action eq "tree" && $file_name !~ m|/$|) {
$title .= "/";
}
}
}
}
my $content_type;
# require explicit support from the UA if we are to send the page as
# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
# we have to do this because MSIE sometimes globs '*/*', pretending to
# support xhtml+xml but choking when it gets what it asked for.
if (defined $cgi->http('HTTP_ACCEPT') &&
$cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
$cgi->Accept('application/xhtml+xml') != 0) {
$content_type = 'application/xhtml+xml';
} else {
$content_type = 'text/html';
}
print $cgi->header(-type=>$content_type, -charset => 'utf-8',
-status=> $status, -expires => $expires);
my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
print <<EOF;
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
<!-- git core binaries version $git_version -->
<head>
<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
<meta name="robots" content="index, nofollow"/>
<title>$title</title>
EOF
# the stylesheet, favicon etc urls won't work correctly with path_info
# unless we set the appropriate base URL
if ($ENV{'PATH_INFO'}) {
print "<base href=\"".esc_url($base_url)."\" />\n";
}
# print out each stylesheet that exist, providing backwards capability
# for those people who defined $stylesheet in a config file
if (defined $stylesheet) {
print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
} else {
foreach my $stylesheet (@stylesheets) {
next unless $stylesheet;
print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
}
}
if (defined $project) {
my %href_params = get_feed_info();
if (!exists $href_params{'-title'}) {
$href_params{'-title'} = 'log';
}
foreach my $format qw(RSS Atom) {
my $type = lc($format);
my %link_attr = (
'-rel' => 'alternate',
'-title' => "$project - $href_params{'-title'} - $format feed",
'-type' => "application/$type+xml"
);
$href_params{'action'} = $type;
$link_attr{'-href'} = href(%href_params);
print "<link ".
"rel=\"$link_attr{'-rel'}\" ".
"title=\"$link_attr{'-title'}\" ".
"href=\"$link_attr{'-href'}\" ".
"type=\"$link_attr{'-type'}\" ".
"/>\n";
$href_params{'extra_options'} = '--no-merges';
$link_attr{'-href'} = href(%href_params);
$link_attr{'-title'} .= ' (no merges)';
print "<link ".
"rel=\"$link_attr{'-rel'}\" ".
"title=\"$link_attr{'-title'}\" ".
"href=\"$link_attr{'-href'}\" ".
"type=\"$link_attr{'-type'}\" ".
"/>\n";
}
} else {
printf('<link rel="alternate" title="%s projects list" '.
'href="%s" type="text/plain; charset=utf-8" />'."\n",
$site_name, href(project=>undef, action=>"project_index"));
printf('<link rel="alternate" title="%s projects feeds" '.
'href="%s" type="text/x-opml" />'."\n",
$site_name, href(project=>undef, action=>"opml"));
}
if (defined $favicon) {
print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
}
print "</head>\n" .
"<body>\n";
if (-f $site_header) {
insert_file($site_header);
}
print "<div class=\"page_header\">\n" .
$cgi->a({-href => esc_url($logo_url),
-title => $logo_label},
qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
if (defined $project) {
print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
if (defined $action) {
print " / $action";
}
print "\n";
}
print "</div>\n";
my $have_search = gitweb_check_feature('search');
if (defined $project && $have_search) {
if (!defined $searchtext) {
$searchtext = "";
}
my $search_hash;
if (defined $hash_base) {
$search_hash = $hash_base;
} elsif (defined $hash) {
$search_hash = $hash;
} else {
$search_hash = "HEAD";
}
my $action = $my_uri;
my $use_pathinfo = gitweb_check_feature('pathinfo');
if ($use_pathinfo) {
$action .= "/".esc_url($project);
}
print $cgi->startform(-method => "get", -action => $action) .
"<div class=\"search\">\n" .
(!$use_pathinfo &&
$cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
$cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
$cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
$cgi->popup_menu(-name => 'st', -default => 'commit',
-values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
$cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
" search:\n",
$cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
"<span title=\"Extended regular expression\">" .
$cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
-checked => $search_use_regexp) .
"</span>" .
"</div>" .
$cgi->end_form() . "\n";
}
}
sub git_footer_html {
my $feed_class = 'rss_logo';
print "<div class=\"page_footer\">\n";
if (defined $project) {
my $descr = git_get_project_description($project);
if (defined $descr) {
print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
}
my %href_params = get_feed_info();
if (!%href_params) {
$feed_class .= ' generic';
}
$href_params{'-title'} ||= 'log';
foreach my $format qw(RSS Atom) {
$href_params{'action'} = lc($format);
print $cgi->a({-href => href(%href_params),
-title => "$href_params{'-title'} $format feed",
-class => $feed_class}, $format)."\n";
}
} else {
print $cgi->a({-href => href(project=>undef, action=>"opml"),
-class => $feed_class}, "OPML") . " ";
print $cgi->a({-href => href(project=>undef, action=>"project_index"),
-class => $feed_class}, "TXT") . "\n";
}
print "</div>\n"; # class="page_footer"
if (-f $site_footer) {
insert_file($site_footer);
}
print "</body>\n" .
"</html>";
}
# die_error(<http_status_code>, <error_message>)
# Example: die_error(404, 'Hash not found')
# By convention, use the following status codes (as defined in RFC 2616):
# 400: Invalid or missing CGI parameters, or
# requested object exists but has wrong type.
# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
# this server or project.
# 404: Requested object/revision/project doesn't exist.
# 500: The server isn't configured properly, or
# an internal error occurred (e.g. failed assertions caused by bugs), or
# an unknown error occurred (e.g. the git binary died unexpectedly).
sub die_error {
my $status = shift || 500;
my $error = shift || "Internal server error";
my %http_responses = (400 => '400 Bad Request',
403 => '403 Forbidden',
404 => '404 Not Found',
500 => '500 Internal Server Error');
git_header_html($http_responses{$status});
print <<EOF;
<div class="page_body">
<br /><br />
$status - $error
<br />
</div>
EOF
git_footer_html();
exit;
}
## ----------------------------------------------------------------------
## functions printing or outputting HTML: navigation
sub git_print_page_nav {
my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
$extra = '' if !defined $extra; # pager or formats
my @navs = qw(summary shortlog log commit commitdiff tree);
if ($suppress) {
@navs = grep { $_ ne $suppress } @navs;
}
my %arg = map { $_ => {action=>$_} } @navs;
if (defined $head) {
for (qw(commit commitdiff)) {
$arg{$_}{'hash'} = $head;
}
if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
for (qw(shortlog log)) {
$arg{$_}{'hash'} = $head;
}
}
}
$arg{'tree'}{'hash'} = $treehead if defined $treehead;
$arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
my @actions = gitweb_get_feature('actions');
my %repl = (
'%' => '%',
'n' => $project, # project name
'f' => $git_dir, # project path within filesystem
'h' => $treehead || '', # current hash ('h' parameter)
'b' => $treebase || '', # hash base ('hb' parameter)
);
while (@actions) {
my ($label, $link, $pos) = splice(@actions,0,3);
# insert
@navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
# munch munch
$link =~ s/%([%nfhb])/$repl{$1}/g;
$arg{$label}{'_href'} = $link;
}
print "<div class=\"page_nav\">\n" .
(join " | ",
map { $_ eq $current ?
$_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
} @navs);
print "<br/>\n$extra<br/>\n" .
"</div>\n";
}
sub format_paging_nav {
my ($action, $hash, $head, $page, $has_next_link) = @_;
my $paging_nav;
if ($hash ne $head || $page) {
$paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
} else {
$paging_nav .= "HEAD";
}
if ($page > 0) {
$paging_nav .= " &sdot; " .
$cgi->a({-href => href(-replay=>1, page=>$page-1),
-accesskey => "p", -title => "Alt-p"}, "prev");
} else {
$paging_nav .= " &sdot; prev";
}
if ($has_next_link) {
$paging_nav .= " &sdot; " .
$cgi->a({-href => href(-replay=>1, page=>$page+1),
-accesskey => "n", -title => "Alt-n"}, "next");
} else {
$paging_nav .= " &sdot; next";
}
return $paging_nav;
}
## ......................................................................
## functions printing or outputting HTML: div
sub git_print_header_div {
my ($action, $title, $hash, $hash_base) = @_;
my %args = ();
$args{'action'} = $action;
$args{'hash'} = $hash if $hash;
$args{'hash_base'} = $hash_base if $hash_base;
print "<div class=\"header\">\n" .
$cgi->a({-href => href(%args), -class => "title"},
$title ? $title : $action) .
"\n</div>\n";
}
sub print_local_time {
my %date = @_;
if ($date{'hour_local'} < 6) {
printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
$date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
} else {
printf(" (%02d:%02d %s)",
$date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
}
}
# Outputs the author name and date in long form
sub git_print_authorship {
my $co = shift;
my %opts = @_;
my $tag = $opts{-tag} || 'div';
my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
print "<$tag class=\"author_date\">" .
esc_html($co->{'author_name'}) .
" [$ad{'rfc2822'}";
print_local_time(%ad) if ($opts{-localtime});
print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
. "</$tag>\n";
}
# Outputs table rows containing the full author or committer information,
# in the format expected for 'commit' view (& similia).
# Parameters are a commit hash reference, followed by the list of people
# to output information for. If the list is empty it defalts to both
# author and committer.
sub git_print_authorship_rows {
my $co = shift;
# too bad we can't use @people = @_ || ('author', 'committer')
my @people = @_;
@people = ('author', 'committer') unless @people;
foreach my $who (@people) {
my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
print "<tr><td>$who</td><td>" . esc_html($co->{$who}) . "</td>" .
"<td rowspan=\"2\">" .
git_get_avatar($co->{"${who}_email"}, -size => 'double') .
"</td></tr>\n" .
"<tr>" .
"<td></td><td> $wd{'rfc2822'}";
print_local_time(%wd);
print "</td>" .
"</tr>\n";
}
}
sub git_print_page_path {
my $name = shift;
my $type = shift;
my $hb = shift;
print "<div class=\"page_path\">";
print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
-title => 'tree root'}, to_utf8("[$project]"));
print " / ";
if (defined $name) {
my @dirname = split '/', $name;
my $basename = pop @dirname;
my $fullname = '';
foreach my $dir (@dirname) {
$fullname .= ($fullname ? '/' : '') . $dir;
print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
hash_base=>$hb),
-title => $fullname}, esc_path($dir));
print " / ";
}
if (defined $type && $type eq 'blob') {
print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
hash_base=>$hb),
-title => $name}, esc_path($basename));
} elsif (defined $type && $type eq 'tree') {
print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
hash_base=>$hb),
-title => $name}, esc_path($basename));
print " / ";
} else {
print esc_path($basename);
}
}
print "<br/></div>\n";
}
sub git_print_log {
my $log = shift;
my %opts = @_;
if ($opts{'-remove_title'}) {
# remove title, i.e. first line of log
shift @$log;
}
# remove leading empty lines
while (defined $log->[0] && $log->[0] eq "") {
shift @$log;
}
# print log
my $signoff = 0;
my $empty = 0;
foreach my $line (@$log) {
if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
$signoff = 1;
$empty = 0;
if (! $opts{'-remove_signoff'}) {
print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
next;
} else {
# remove signoff lines
next;
}
} else {
$signoff = 0;
}
# print only one empty line
# do not print empty line after signoff
if ($line eq "") {
next if ($empty || $signoff);
$empty = 1;
} else {
$empty = 0;
}
print format_log_line_html($line) . "<br/>\n";
}
if ($opts{'-final_empty_line'}) {
# end with single empty line
print "<br/>\n" unless $empty;
}
}
# return link target (what link points to)
sub git_get_link_target {
my $hash = shift;
my $link_target;
# read link
open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
or return;
{
local $/ = undef;
$link_target = <$fd>;
}
close $fd
or return;
return $link_target;
}
# given link target, and the directory (basedir) the link is in,
# return target of link relative to top directory (top tree);
# return undef if it is not possible (including absolute links).
sub normalize_link_target {
my ($link_target, $basedir) = @_;
# absolute symlinks (beginning with '/') cannot be normalized
return if (substr($link_target, 0, 1) eq '/');
# normalize link target to path from top (root) tree (dir)
my $path;
if ($basedir) {
$path = $basedir . '/' . $link_target;
} else {
# we are in top (root) tree (dir)
$path = $link_target;
}
# remove //, /./, and /../
my @path_parts;
foreach my $part (split('/', $path)) {
# discard '.' and ''
next if (!$part || $part eq '.');
# handle '..'
if ($part eq '..') {
if (@path_parts) {
pop @path_parts;
} else {
# link leads outside repository (outside top dir)
return;
}
} else {
push @path_parts, $part;
}
}
$path = join('/', @path_parts);
return $path;
}
# print tree entry (row of git_tree), but without encompassing <tr> element
sub git_print_tree_entry {
my ($t, $basedir, $hash_base, $have_blame) = @_;
my %base_key = ();
$base_key{'hash_base'} = $hash_base if defined $hash_base;
# The format of a table row is: mode list link. Where mode is
# the mode of the entry, list is the name of the entry, an href,
# and link is the action links of the entry.
print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
if ($t->{'type'} eq "blob") {
print "<td class=\"list\">" .
$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
file_name=>"$basedir$t->{'name'}", %base_key),
-class => "list"}, esc_path($t->{'name'}));
if (S_ISLNK(oct $t->{'mode'})) {
my $link_target = git_get_link_target($t->{'hash'});
if ($link_target) {
my $norm_target = normalize_link_target($link_target, $basedir);
if (defined $norm_target) {
print " -> " .
$cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
file_name=>$norm_target),
-title => $norm_target}, esc_path($link_target));
} else {
print " -> " . esc_path($link_target);
}
}
}
print "</td>\n";
print "<td class=\"link\">";
print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
file_name=>"$basedir$t->{'name'}", %base_key)},
"blob");
if ($have_blame) {
print " | " .
$cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
file_name=>"$basedir$t->{'name'}", %base_key)},
"blame");
}
if (defined $hash_base) {
print " | " .
$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
"history");
}
print " | " .
$cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
file_name=>"$basedir$t->{'name'}")},
"raw");
print "</td>\n";
} elsif ($t->{'type'} eq "tree") {
print "<td class=\"list\">";
print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
file_name=>"$basedir$t->{'name'}", %base_key)},
esc_path($t->{'name'}));
print "</td>\n";
print "<td class=\"link\">";
print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
file_name=>"$basedir$t->{'name'}", %base_key)},
"tree");
if (defined $hash_base) {
print " | " .
$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
file_name=>"$basedir$t->{'name'}")},
"history");
}
print "</td>\n";
} else {
# unknown object: we can only present history for it
# (this includes 'commit' object, i.e. submodule support)
print "<td class=\"list\">" .
esc_path($t->{'name'}) .
"</td>\n";
print "<td class=\"link\">";
if (defined $hash_base) {
print $cgi->a({-href => href(action=>"history",
hash_base=>$hash_base,
file_name=>"$basedir$t->{'name'}")},
"history");
}
print "</td>\n";
}
}
## ......................................................................
## functions printing large fragments of HTML
# get pre-image filenames for merge (combined) diff
sub fill_from_file_info {
my ($diff, @parents) = @_;
$diff->{'from_file'} = [ ];
$diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
if ($diff->{'status'}[$i] eq 'R' ||
$diff->{'status'}[$i] eq 'C') {
$diff->{'from_file'}[$i] =
git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
}
}
return $diff;
}
# is current raw difftree line of file deletion
sub is_deleted {
my $diffinfo = shift;
return $diffinfo->{'to_id'} eq ('0' x 40);
}
# does patch correspond to [previous] difftree raw line
# $diffinfo - hashref of parsed raw diff format
# $patchinfo - hashref of parsed patch diff format
# (the same keys as in $diffinfo)
sub is_patch_split {
my ($diffinfo, $patchinfo) = @_;
return defined $diffinfo && defined $patchinfo
&& $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
}
sub git_difftree_body {
my ($difftree, $hash, @parents) = @_;
my ($parent) = $parents[0];
my $have_blame = gitweb_check_feature('blame');
print "<div class=\"list_head\">\n";
if ($#{$difftree} > 10) {
print(($#{$difftree} + 1) . " files changed:\n");
}
print "</div>\n";
print "<table class=\"" .
(@parents > 1 ? "combined " : "") .
"diff_tree\">\n";
# header only for combined diff in 'commitdiff' view
my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
if ($has_header) {
# table header
print "<thead><tr>\n" .
"<th></th><th></th>\n"; # filename, patchN link
for (my $i = 0; $i < @parents; $i++) {
my $par = $parents[$i];
print "<th>" .
$cgi->a({-href => href(action=>"commitdiff",
hash=>$hash, hash_parent=>$par),
-title => 'commitdiff to parent number ' .
($i+1) . ': ' . substr($par,0,7)},
$i+1) .
"&nbsp;</th>\n";
}
print "</tr></thead>\n<tbody>\n";