use 5.008;
use strict;
use warnings;
use CGI qw(:standard :escapeHTML -nosticky);
use CGI::Util qw(unescape);
use CGI::Carp qw(fatalsToBrowser set_message);
use Encode;
use Fcntl ':mode';
use File::Find qw();
use File::Basename qw(basename);
use Time::HiRes qw(gettimeofday tv_interval);
binmode STDOUT, ':utf8';
if (!defined($CGI::VERSION) || $CGI::VERSION < 4.08) {
eval 'sub CGI::multi_param { CGI::param(@_) }'
}
our $t0 = [ gettimeofday() ];
our $number_of_git_cmds = 0;
BEGIN {
CGI->compile() if $ENV{'MOD_PERL'};
}
our $version = "++GIT_VERSION++";
our ($my_url, $my_uri, $base_url, $path_info, $home_link);
sub evaluate_uri {
our $cgi;
our $my_url = $cgi->url();
our $my_uri = $cgi->url(-absolute => 1);
our $base_url = $my_url;
our $path_info = decode_utf8($ENV{"PATH_INFO"});
if ($path_info) {
$my_url = unescape($my_url);
$my_uri = unescape($my_uri);
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'};
}
}
our $home_link = $my_uri || "/";
}
our $GIT = "++GIT_BINDIR++/git";
our $projectroot = "++GITWEB_PROJECTROOT++";
our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
our $home_link_str = "++GITWEB_HOME_LINK_STR++";
our @extra_breadcrumbs = ();
our $site_name = "++GITWEB_SITENAME++"
|| ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
our $site_header = "++GITWEB_SITE_HEADER++";
our $home_text = "++GITWEB_HOMETEXT++";
our $site_footer = "++GITWEB_SITE_FOOTER++";
our @stylesheets = ("++GITWEB_CSS++");
our $stylesheet = undef;
our $logo = "++GITWEB_LOGO++";
our $favicon = "++GITWEB_FAVICON++";
our $javascript = "++GITWEB_JS++";
our $logo_url = "http://git-scm.com/";
our $logo_label = "git homepage";
our $projects_list = "++GITWEB_LIST++";
our $projects_list_description_width = 25;
our $projects_list_group_categories = 0;
our $project_list_default_category = "";
our $default_projects_order = "project";
our $export_ok = "++GITWEB_EXPORT_OK++";
our $omit_age_column = 0;
our $omit_owner=0;
our $export_auth_hook = undef;
our $strict_export = "++GITWEB_STRICT_EXPORT++";
our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
our $default_blob_plain_mimetype = 'text/plain';
our $default_text_plain_charset = undef;
our $mimetypes_file = undef;
our $fallback_encoding = 'latin1';
our @diff_opts = ('-M');
our $prevent_xss = 0;
our $highlight_bin = "++HIGHLIGHT_BIN++";
our %known_snapshot_formats = (
'tgz' => {
'display' => 'tar.gz',
'type' => 'application/x-gzip',
'suffix' => '.tar.gz',
'format' => 'tar',
'compressor' => ['gzip', '-n']},
'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'},
);
our %known_snapshot_format_aliases = (
'gzip' => 'tgz',
'bzip2' => 'tbz2',
'xz' => 'txz',
'x-gzip' => undef, 'gz' => undef,
'x-bzip2' => undef, 'bz2' => undef,
'x-zip' => undef, '' => undef,
);
our %avatar_size = (
'default' => 16,
'double' => 32
);
our $maxload = 300;
our %highlight_basename = (
'SConstruct' => 'py', 'Makefile' => 'make',
);
our %highlight_ext = (
(map { $_ => $_ } qw(py rb java css js tex bib xml awk bat ini spec tcl sql)),
(map { $_ => 'c' } qw(c h)),
(map { $_ => 'sh' } qw(sh bash zsh ksh)),
(map { $_ => 'cpp' } qw(cpp cxx c++ cc)),
(map { $_ => 'php' } qw(php php3 php4 php5 phps)),
(map { $_ => 'pl' } qw(pl perl pm)), (map { $_ => 'make'} qw(make mak mk)),
(map { $_ => 'xml' } qw(xml xhtml html htm)),
);
our %feature = (
'blame' => {
'sub' => sub { feature_bool('blame', @_) },
'override' => 0,
'default' => [0]},
'snapshot' => {
'sub' => \&feature_snapshot,
'override' => 0,
'default' => ['tgz']},
'search' => {
'override' => 0,
'default' => [1]},
'grep' => {
'sub' => sub { feature_bool('grep', @_) },
'override' => 0,
'default' => [1]},
'pickaxe' => {
'sub' => sub { feature_bool('pickaxe', @_) },
'override' => 0,
'default' => [1]},
'show-sizes' => {
'sub' => sub { feature_bool('showsizes', @_) },
'override' => 0,
'default' => [1]},
'pathinfo' => {
'override' => 0,
'default' => [0]},
'forks' => {
'override' => 0,
'default' => [0]},
'actions' => {
'override' => 0,
'default' => []},
'ctags' => {
'override' => 0,
'default' => [0]},
'patches' => {
'sub' => \&feature_patches,
'override' => 0,
'default' => [16]},
'avatar' => {
'sub' => \&feature_avatar,
'override' => 0,
'default' => ['']},
'timed' => {
'override' => 0,
'default' => [0]},
'javascript-actions' => {
'override' => 0,
'default' => [0]},
'javascript-timezone' => {
'override' => 0,
'default' => [
'local', 'gitweb_tz', 'datetime', ]},
'highlight' => {
'sub' => sub { feature_bool('highlight', @_) },
'override' => 0,
'default' => [0]},
'remote_heads' => {
'sub' => sub { feature_bool('remote_heads', @_) },
'override' => 0,
'default' => [0]},
'extra-branch-refs' => {
'sub' => \&feature_extra_branch_refs,
'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'}});
our $git_dir; if (!$override || !defined $git_dir) {
return @defaults;
}
if (!defined $sub) {
warn "feature $name is not overridable";
return @defaults;
}
return $sub->(@defaults);
}
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 : @_;
}
sub feature_extra_branch_refs {
my (@branch_refs) = @_;
my $values = git_get_project_config('extrabranchrefs');
if ($values) {
$values = config_to_multi ($values);
@branch_refs = ();
foreach my $value (@{$values}) {
push @branch_refs, split /\s+/, $value;
}
}
return @branch_refs;
}
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)));
}
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;
}
sub filter_and_validate_refs {
my @refs = @_;
my %unique_refs = ();
foreach my $ref (@refs) {
die_error(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format($ref));
$unique_refs{$ref} = 1 if ($ref ne 'heads');
}
return sort keys %unique_refs;
}
our $per_request_config = 1;
sub read_config_file {
my $filename = shift;
return unless defined $filename;
if (-e $filename) {
do $filename;
die $@ if $@;
return 1;
}
return;
}
our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
sub evaluate_gitweb_config {
our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
$GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
$GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
read_config_file($GITWEB_CONFIG_COMMON);
read_config_file($GITWEB_CONFIG) and return;
read_config_file($GITWEB_CONFIG_SYSTEM);
}
sub get_loadavg {
if( -e '/proc/loadavg' ){
open my $fd, '<', '/proc/loadavg'
or return 0;
my @load = split(/\s+/, scalar <$fd>);
close $fd;
return $load[0] || 0;
}
return 0;
}
our $git_version;
sub evaluate_git_version {
our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
$number_of_git_cmds++;
}
sub check_loadavg {
if (defined $maxload && get_loadavg() > $maxload) {
die_error(503, "The load average on the server is too high");
}
}
our %input_params = ();
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",
ctag => "by_tag",
diff_style => "ds",
project_filter => "pf",
javascript => "js"
);
our %cgi_param_mapping = @cgi_param_mapping;
our %actions = (
"blame" => \&git_blame,
"blame_incremental" => \&git_blame_incremental,
"blame_data" => \&git_blame_data,
"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,
"remotes" => \&git_remotes,
"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,
"opml" => \&git_opml,
"project_list" => \&git_project_list,
"project_index" => \&git_project_index,
);
our %allowed_options = (
"--no-merges" => [ qw(rss atom log shortlog history) ],
);
sub evaluate_query_params {
our $cgi;
while (my ($name, $symbol) = each %cgi_param_mapping) {
if ($symbol eq 'opt') {
$input_params{$name} = [ map { decode_utf8($_) } $cgi->multi_param($symbol) ];
} else {
$input_params{$name} = decode_utf8($cgi->param($symbol));
}
}
}
sub evaluate_path_info {
return if defined $input_params{'project'};
return if !$path_info;
$path_info =~ s,^/+,,;
return if !$path_info;
my $project = $path_info;
$project =~ s,/+$,,;
while ($project && !check_head_link("$projectroot/$project")) {
$project =~ s,/*[^/]*$,,;
}
return unless $project;
$input_params{'project'} = $project;
return if $input_params{'action'};
$path_info =~ s,^\Q$project\E/*,,;
my $action = $path_info;
$action =~ s,/.*$,,;
if (exists $actions{$action}) {
$path_info =~ s,^$action/*,,;
$input_params{'action'} = $action;
}
my @wants_base = (
'tree',
'history',
);
my ($parentrefname, $parentpathname, $refname, $pathname) =
($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
if (defined $pathname) {
$pathname =~ s,^/+,,;
if (!$pathname || substr($pathname, -1) eq "/") {
$input_params{'action'} ||= "tree";
$pathname =~ s,/$,,;
} else {
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) {
if (defined $parentrefname) {
$input_params{'action'} ||= "shortlog";
}
if ($input_params{'action'} &&
grep { $_ eq $input_params{'action'} } @wants_base) {
$input_params{'hash_base'} ||= $refname;
} else {
$input_params{'hash'} ||= $refname;
}
}
if (defined $parentrefname) {
if ($parentpathname) {
$parentpathname =~ s,^/+,,;
$parentpathname =~ s,/$,,;
$input_params{'file_parent'} ||= $parentpathname;
} else {
$input_params{'file_parent'} ||= $input_params{'file_name'};
}
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;
}
}
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'}) {
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;
$input_params{'snapshot_format'} = $fmt;
$input_params{'hash'} = $hash;
$known_snapshot_formats{$fmt}{'suffix'} = $sfx;
last;
}
}
}
our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
$hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
$searchtext, $search_regexp, $project_filter);
sub evaluate_and_validate_params {
our $action = $input_params{'action'};
if (defined $action) {
if (!is_valid_action($action)) {
die_error(400, "Invalid action parameter");
}
}
our $project = $input_params{'project'};
if (defined $project) {
if (!is_valid_project($project)) {
undef $project;
die_error(404, "No such project");
}
}
our $project_filter = $input_params{'project_filter'};
if (defined $project_filter) {
if (!is_valid_pathname($project_filter)) {
die_error(404, "Invalid project_filter parameter");
}
}
our $file_name = $input_params{'file_name'};
if (defined $file_name) {
if (!is_valid_pathname($file_name)) {
die_error(400, "Invalid file parameter");
}
}
our $file_parent = $input_params{'file_parent'};
if (defined $file_parent) {
if (!is_valid_pathname($file_parent)) {
die_error(400, "Invalid file parent parameter");
}
}
our $hash = $input_params{'hash'};
if (defined $hash) {
if (!is_valid_refname($hash)) {
die_error(400, "Invalid hash parameter");
}
}
our $hash_parent = $input_params{'hash_parent'};
if (defined $hash_parent) {
if (!is_valid_refname($hash_parent)) {
die_error(400, "Invalid hash parent parameter");
}
}
our $hash_base = $input_params{'hash_base'};
if (defined $hash_base) {
if (!is_valid_refname($hash_base)) {
die_error(400, "Invalid hash base parameter");
}
}
our @extra_options = @{$input_params{'extra_options'}};
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 (!is_valid_refname($hash_parent_base)) {
die_error(400, "Invalid hash parent base parameter");
}
}
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 = undef;
if (defined $searchtext) {
if (length($searchtext) < 2) {
die_error(403, "At least two characters are required for search parameter");
}
if ($search_use_regexp) {
$search_regexp = $searchtext;
if (!eval { qr/$search_regexp/; 1; }) {
(my $error = $@) =~ s/ at \S+ line \d+.*\n?//;
die_error(400, "Invalid search regexp '$search_regexp'",
esc_html($error));
}
} else {
$search_regexp = quotemeta $searchtext;
}
}
}
our $git_dir;
sub evaluate_git_dir {
our $git_dir = "$projectroot/$project" if $project;
}
our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
sub configure_gitweb_features {
our @snapshot_fmts = gitweb_get_feature('snapshot');
@snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
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') {
} else {
$git_avatar = '';
}
our @extra_branch_refs = gitweb_get_feature('extra-branch-refs');
@extra_branch_refs = filter_and_validate_refs (@extra_branch_refs);
}
sub get_branch_refs {
return ('heads', @extra_branch_refs);
}
sub handle_errors_html {
my $msg = shift;
set_message("Error occurred when inside die_error:\n$msg");
die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
}
set_message(\&handle_errors_html);
sub dispatch {
if (!defined $action) {
if (defined $hash) {
$action = git_get_type($hash);
$action or die_error(404, "Object does not exist");
} elsif (defined $hash_base && defined $file_name) {
$action = git_get_type("$hash_base:$file_name");
$action or die_error(404, "File or directory does not exist");
} 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}->();
}
sub reset_timer {
our $t0 = [ gettimeofday() ]
if defined $t0;
our $number_of_git_cmds = 0;
}
our $first_request = 1;
sub run_request {
reset_timer();
evaluate_uri();
if ($first_request) {
evaluate_gitweb_config();
evaluate_git_version();
}
if ($per_request_config) {
if (ref($per_request_config) eq 'CODE') {
$per_request_config->();
} elsif (!$first_request) {
evaluate_gitweb_config();
}
}
check_loadavg();
$projects_list ||= $projectroot;
evaluate_query_params();
evaluate_path_info();
evaluate_and_validate_params();
evaluate_git_dir();
configure_gitweb_features();
dispatch();
}
our $is_last_request = sub { 1 };
our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
our $CGI = 'CGI';
our $cgi;
sub configure_as_fcgi {
require CGI::Fast;
our $CGI = 'CGI::Fast';
my $request_number = 0;
our $is_last_request = sub { ++$request_number > 100 };
}
sub evaluate_argv {
my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
configure_as_fcgi()
if $script_name =~ /\.fcgi$/;
return unless (@ARGV);
require Getopt::Long;
Getopt::Long::GetOptions(
'fastcgi|fcgi|f' => \&configure_as_fcgi,
'nproc|n=i' => sub {
my ($arg, $val) = @_;
return unless eval { require FCGI::ProcManager; 1; };
my $proc_manager = FCGI::ProcManager->new({
n_processes => $val,
});
our $pre_listen_hook = sub { $proc_manager->pm_manage() };
our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
},
);
}
sub run {
evaluate_argv();
$first_request = 1;
$pre_listen_hook->()
if $pre_listen_hook;
REQUEST:
while ($cgi = $CGI->new()) {
$pre_dispatch_hook->()
if $pre_dispatch_hook;
run_request();
$post_dispatch_hook->()
if $post_dispatch_hook;
$first_request = 0;
last REQUEST if ($is_last_request->());
}
DONE_GITWEB:
1;
}
run();
if (defined caller) {
return;
} else {
exit;
}
sub href {
my %params = @_;
my $href = $params{-full} ? $my_url : $my_uri;
$params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
$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 (defined $params{'project'} &&
(exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
$href =~ s,/$,,;
$href .= "/".esc_path_info($params{'project'});
delete $params{'project'};
my $is_snapshot = $params{'action'} eq 'snapshot';
if (defined $params{'action'}) {
$href .= "/".esc_path_info($params{'action'})
unless $params{'action'} eq 'summary';
delete $params{'action'};
}
$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_path_info($params{'hash_parent_base'});
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_path_info($params{'file_parent'});
delete $params{'file_parent'};
}
}
$href .= "..";
delete $params{'hash_parent'};
delete $params{'hash_parent_base'};
} elsif (defined $params{'hash_parent'}) {
$href .= esc_path_info($params{'hash_parent'}). "..";
delete $params{'hash_parent'};
}
$href .= esc_path_info($params{'hash_base'});
if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
$href .= ":/".esc_path_info($params{'file_name'});
delete $params{'file_name'};
}
delete $params{'hash'};
delete $params{'hash_base'};
} elsif (defined $params{'hash'}) {
$href .= esc_path_info($params{'hash'});
delete $params{'hash'};
}
if ($is_snapshot) {
my $fmt = $params{'snapshot_format'};
$fmt ||= $snapshot_fmts[0];
$href .= $known_snapshot_formats{$fmt}{'suffix'};
delete $params{'snapshot_format'};
}
}
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;
$href =~ s/(\s+)$/CGI::escape($1)/e;
if ($params{-anchor}) {
$href .= "#".esc_param($params{-anchor});
}
return $href;
}
sub is_valid_action {
my $input = shift;
return undef unless exists $actions{$input};
return 1;
}
sub is_valid_project {
my $input = shift;
return unless defined $input;
if (!is_valid_pathname($input) ||
!(-d "$projectroot/$input") ||
!check_export_ok("$projectroot/$input") ||
($strict_export && !project_in_list($input))) {
return undef;
} else {
return 1;
}
}
sub is_valid_pathname {
my $input = shift;
return undef unless defined $input;
if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
return undef;
}
if ($input =~ m!\0!) {
return undef;
}
return 1;
}
sub is_valid_ref_format {
my $input = shift;
return undef unless defined $input;
if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
return undef;
}
return 1;
}
sub is_valid_refname {
my $input = shift;
return undef unless defined $input;
if ($input =~ m/^[0-9a-fA-F]{40}$/) {
return 1;
}
is_valid_pathname($input) or return undef;
is_valid_ref_format($input) or return undef;
return 1;
}
sub to_utf8 {
my $str = shift;
return undef unless defined $str;
if (utf8::is_utf8($str) || utf8::decode($str)) {
return $str;
} else {
return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
}
}
sub esc_param {
my $str = shift;
return undef unless defined $str;
$str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
$str =~ s/ /\+/g;
return $str;
}
sub esc_path_info {
my $str = shift;
return undef unless defined $str;
$str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
return $str;
}
sub esc_url {
my $str = shift;
return undef unless defined $str;
$str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
$str =~ s/ /\+/g;
return $str;
}
sub esc_attr {
return esc_html(@_);
}
sub esc_html {
my $str = shift;
my %opts = @_;
return undef unless defined $str;
$str = to_utf8($str);
$str = $cgi->escapeHTML($str);
if ($opts{'-nbsp'}) {
$str =~ s/ / /g;
}
$str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
return $str;
}
sub esc_path {
my $str = shift;
my %opts = @_;
return undef unless defined $str;
$str = to_utf8($str);
$str = $cgi->escapeHTML($str);
if ($opts{'-nbsp'}) {
$str =~ s/ / /g;
}
$str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
return $str;
}
sub sanitize {
my $str = shift;
return undef unless defined $str;
$str = to_utf8($str);
$str =~ s|([[:cntrl:]])|(index("\t\n\r", $1) != -1 ? $1 : quot_cec($1))|eg;
return $str;
}
sub quot_cec {
my $cntrl = shift;
my %opts = @_;
my %es = ( "\t" => '\t', "\n" => '\n', "\r" => '\r', "\f" => '\f', "\b" => '\b', "\a" => '\a', "\e" => '\e', "\013" => '\v', "\000" => '\0', );
my $chr = ( (exists $es{$cntrl})
? $es{$cntrl}
: sprintf('\%2x', ord($cntrl)) );
if ($opts{-nohtml}) {
return $chr;
} else {
return "<span class=\"cntrl\">$chr</span>";
}
}
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>";
}
}
sub unquote {
my $str = shift;
sub unq {
my $seq = shift;
my %es = ( 't' => "\t", 'n' => "\n", 'r' => "\r", 'f' => "\f", 'b' => "\b", 'a' => "\a", 'e' => "\e", 'v' => "\013", );
if ($seq =~ m/^[0-7]{1,3}$/) {
return chr(oct($seq));
} elsif (exists $es{$seq}) {
return $es{$seq};
}
return $seq;
}
if ($str =~ m/^"(.*)"$/) {
$str = $1;
$str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
}
return $str;
}
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);
}
sub chop_str {
my $str = shift;
my $len = shift;
my $add_len = shift || 10;
my $where = shift || 'right';
$str = to_utf8($str);
if ($where eq 'center') {
return $str if ($len + 5 >= length($str)); $len = int($len/2);
} else {
return $str if ($len + 4 >= length($str)); }
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) {
$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) {
$mid = " ... ";
}
return "$left$mid$right";
} else {
$str =~ m/^($endre)(.*)$/;
my $body = $1;
my $tail = $2;
if (length($tail) > 4) {
$tail = "... ";
}
return "$body$tail";
}
}
sub chop_and_escape_str {
my ($str) = @_;
my $chopped = chop_str(@_);
$str = to_utf8($str);
if ($chopped eq $str) {
return esc_html($chopped);
} else {
$str =~ s/[[:cntrl:]]/?/g;
return $cgi->span({-title=>$str}, esc_html($chopped));
}
}
sub esc_html_hl_regions {
my ($str, $css_class, @sel) = @_;
my %opts = grep { ref($_) ne 'ARRAY' } @sel;
@sel = grep { ref($_) eq 'ARRAY' } @sel;
return esc_html($str, %opts) unless @sel;
my $out = '';
my $pos = 0;
for my $s (@sel) {
my ($begin, $end) = @$s;
next if $end <= $begin;
my $escaped = esc_html(substr($str, $begin, $end - $begin),
%opts);
$out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
if ($begin - $pos > 0);
$out .= $cgi->span({-class => $css_class}, $escaped);
$pos = $end;
}
$out .= esc_html(substr($str, $pos), %opts)
if ($pos < length($str));
return $out;
}
sub matchpos_list {
my ($str, $regexp) = @_;
return unless (defined $str && defined $regexp);
my @matches;
while ($str =~ /$regexp/g) {
push @matches, [$-[0], $+[0]];
}
return @matches;
}
sub esc_html_match_hl {
my ($str, $regexp) = @_;
return esc_html($str) unless defined $regexp;
my @matches = matchpos_list($str, $regexp);
return esc_html($str) unless @matches;
return esc_html_hl_regions($str, 'match', @matches);
}
sub esc_html_match_hl_chopped {
my ($str, $chopped, $regexp) = @_;
return esc_html_match_hl($str, $regexp) unless defined $chopped;
my @matches = matchpos_list($str, $regexp);
return esc_html($chopped) unless @matches;
my $tail = "... "; unless ($chopped =~ s/\Q$tail\E$//) {
$tail = '';
}
my $chop_len = length($chopped);
my $tail_len = length($tail);
my @filtered;
for my $m (@matches) {
if ($m->[0] > $chop_len) {
push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
last;
} elsif ($m->[1] > $chop_len) {
push @filtered, [ $m->[0], $chop_len + $tail_len ];
last;
}
push @filtered, $m;
}
return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
}
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";
}
}
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,
};
sub S_ISGITLINK {
my $mode = shift;
return (($mode & S_IFMT) == S_IFGITLINK)
}
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)) {
if ($mode & S_IXUSR) {
return '-rwxr-xr-x';
} else {
return '-rw-r--r--';
};
} else {
return '----------';
}
}
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";
}
}
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";
}
}
sub format_log_line_html {
my $line = shift;
$line = esc_html($line, -nbsp=>1);
$line =~ s{
\b
(
(?<!-) [A-Za-z0-9.-]+
(?!\.) -g[0-9a-fA-F]{7,40}
|
[0-9a-fA-F]{7,40}
)
\b
}{
$cgi->a({-href => href(action=>"object", hash=>$1),
-class => "text"}, $1);
}egx;
return $line;
}
sub format_ref_marker {
my ($refs, $id) = @_;
my $markers = '';
if (defined $refs->{$id}) {
foreach my $ref (@{$refs->{$id}}) {
my ($type, $name) = qw();
my $indirect = ($ref =~ s/\^\{\}$//);
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
)}, esc_html($name));
$markers .= " <span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
$link . "</span>";
}
}
if ($markers) {
return ' <span class="refs">'. $markers . '</span>';
} else {
return "";
}
}
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;
}
}
our %avatar_cache = ();
sub picon_url {
my $email = lc shift;
if (!$avatar_cache{$email}) {
my ($user, $domain) = split('@', $email);
$avatar_cache{$email} =
"//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
"$domain/$user/" .
"users+domains+unknown/up/single";
}
return $avatar_cache{$email};
}
sub gravatar_url {
my $email = lc shift;
my $size = shift;
$avatar_cache{$email} ||=
"//www.gravatar.com/avatar/" .
Digest::MD5::md5_hex($email) . "?s=";
return $avatar_cache{$email} . $size;
}
sub git_get_avatar {
my ($email, %opts) = @_;
my $pre_white = ($opts{-pad_before} ? " " : "");
my $post_white = ($opts{-pad_after} ? " " : "");
$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);
}
if ($url) {
return $pre_white .
"<img width=\"$size\" " .
"class=\"avatar\" " .
"src=\"".esc_url($url)."\" " .
"alt=\"\" " .
"/>" . $post_white;
} else {
return "";
}
}
sub format_search_author {
my ($author, $searchtype, $displaytext) = @_;
my $have_search = gitweb_check_feature('search');
if ($have_search) {
my $performed = "";
if ($searchtype eq 'author') {
$performed = "authored";
} elsif ($searchtype eq 'committer') {
$performed = "committed";
}
return $cgi->a({-href => href(action=>"search", hash=>$hash,
searchtext=>$author,
searchtype=>$searchtype), class=>"list",
title=>"Search for commits $performed by $author"},
$displaytext);
} else {
return $displaytext;
}
}
sub format_author_html {
my $tag = shift;
my $co = shift;
my $author = chop_and_escape_str($co->{'author_name'}, @_);
return "<$tag class=\"author\">" .
format_search_author($co->{'author_name'}, "author",
git_get_avatar($co->{'author_email'}, -pad_after => 1) .
$author) .
"</$tag>";
}
sub format_git_diff_header_line {
my $line = shift;
my $diffinfo = shift;
my ($from, $to) = @_;
if ($diffinfo->{'nparents'}) {
$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 { $line .= 'a/' . esc_path($from->{'file'});
}
$line .= ' ';
if ($to->{'href'}) {
$line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
'b/' . esc_path($to->{'file'}));
} else { $line .= 'b/' . esc_path($to->{'file'});
}
}
return "<div class=\"diff header\">$line</div>\n";
}
sub format_extended_diff_header_line {
my $line = shift;
my $diffinfo = shift;
my ($from, $to) = @_;
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'}));
}
if ($line =~ m/\s(\d{6})$/) {
$line .= '<span class="info"> (' .
file_type_long($1) .
')</span>';
}
if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
$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;
}
$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}/) {
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";
}
sub format_diff_from_to_header {
my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
my $line;
my $result = '';
$line = $from_line;
if (! $diffinfo->{'nparents'}) {
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;
}
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" . "<div class=\"diff nodifferences\">" .
"Simple merge" .
"</div>\n";
return $result;
}
sub diff_line_class {
my ($line, $from, $to) = @_;
my $num_sign = 1;
if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
$num_sign = scalar @{$from->{'href'}};
}
my @diff_line_classifier = (
{ regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
{ regexp => qr/^\\/, class => "incomplete" },
{ regexp => qr/^ {$num_sign}/, class => "ctx" },
{ regexp => qr/^[+ ]{$num_sign}/, class => "add" },
{ regexp => qr/^[- ]{$num_sign}/, class => "rem" },
);
for my $clsfy (@diff_line_classifier) {
return $clsfy->{'class'}
if ($line =~ $clsfy->{'regexp'});
}
return "";
}
sub format_unidiff_chunk_header {
my ($line, $from, $to) = @_;
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 $line;
}
sub format_cc_diff_chunk_header {
my ($line, $from, $to) = @_;
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 $line;
}
sub format_diff_line {
my ($line, $diff_class, $from, $to) = @_;
if (ref($line)) {
$line = $$line;
} else {
chomp $line;
$line = untabify($line);
if ($from && $to && $line =~ m/^\@{2} /) {
$line = format_unidiff_chunk_header($line, $from, $to);
} elsif ($from && $to && $line =~ m/^\@{3}/) {
$line = format_cc_diff_chunk_header($line, $from, $to);
} else {
$line = esc_html($line, -nbsp=>1);
}
}
my $diff_classes = "diff";
$diff_classes .= " $diff_class" if ($diff_class);
$line = "<div class=\"$diff_classes\">$line</div>\n";
return $line;
}
sub format_snapshot_links {
my ($hash) = @_;
my $num_fmts = @snapshot_fmts;
if ($num_fmts > 1) {
return "snapshot (" . join(' ', map
$cgi->a({
-href => href(
action=>"snapshot",
hash=>$hash,
snapshot_format=>$_
)
}, $known_snapshot_formats{$_}{'display'})
, @snapshot_fmts) . ")";
} elsif ($num_fmts == 1) {
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 { return undef;
}
}
sub get_feed_info {
my $format = shift || 'Atom';
my %res = (action => lc($format));
my $matched_ref = 0;
return unless (defined $project);
return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
my $branch = undef;
for my $ref (get_branch_refs()) {
if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
(defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) {
$branch = $1;
$matched_ref = $ref;
last;
}
}
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/$matched_ref/$branch" : undef);
$res{'file_name'} = $file_name;
return %res;
}
sub git_cmd {
$number_of_git_cmds++;
return $GIT, '--git-dir='.$git_dir;
}
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 {
return git_get_full_hash(shift, 'HEAD');
}
sub git_get_full_hash {
return git_get_hash(@_);
}
sub git_get_short_hash {
return git_get_hash(@_, '--short=7');
}
sub git_get_hash {
my ($project, $hash, @options) = @_;
my $o_git_dir = $git_dir;
my $retval = undef;
$git_dir = "$projectroot/$project";
if (open my $fd, '-|', git_cmd(), 'rev-parse',
'--verify', '-q', @options, $hash) {
$retval = <$fd>;
chomp $retval if defined $retval;
close $fd;
}
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) = @_;
return unless defined $git_dir;
# key sanity check
return unless ($key);
# only subsection, if exists, is case sensitive,
# and not lowercased by 'git config -z -l'
if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
$lo =~ s/_//g;
$key = join(".", lc($hi), $mi, lc($lo));
return if ($lo =~ /\W/ || $hi =~ /\W/);
} else {
$key = lc($key);
$key =~ s/_//g;
return if ($key =~ /\W/);
}
$key =~ s/^gitweb\.//;
# 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;
}
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;
if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
close $fd;
return $1;
}
}
close $fd;
return undef;
}
sub git_get_file_or_project_config {
my ($path, $name) = @_;
$git_dir = "$projectroot/$path";
open my $fd, '<', "$git_dir/$name"
or return git_get_project_config($name);
my $conf = <$fd>;
close $fd;
if (defined $conf) {
chomp $conf;
}
return $conf;
}
sub git_get_project_description {
my $path = shift;
return git_get_file_or_project_config($path, 'description');
}
sub git_get_project_category {
my $path = shift;
return git_get_file_or_project_config($path, 'category');
}
sub git_get_project_ctags {
my $project = shift;
my $ctags = {};
$git_dir = "$projectroot/$project";
if (opendir my $dh, "$git_dir/ctags") {
my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
foreach my $tagfile (@files) {
open my $ct, '<', $tagfile
or next;
my $val = <$ct>;
chomp $val if $val;
close $ct;
(my $ctag = $tagfile) =~ s if ($val =~ /^\d+$/) {
$ctags->{$ctag} = $val;
} else {
$ctags->{$ctag} = 1;
}
}
closedir $dh;
} elsif (open my $fh, '<', "$git_dir/ctags") {
while (my $line = <$fh>) {
chomp $line;
$ctags->{$line}++ if $line;
}
close $fh;
} else {
my $taglist = config_to_multi(git_get_project_config('ctag'));
foreach my $tag (@$taglist) {
$ctags->{$tag}++;
}
}
return $ctags;
}
sub git_gather_all_ctags {
my $projects = shift;
my $ctags = {};
foreach my $p (@$projects) {
foreach my $ct (keys %{$p->{'ctags'}}) {
$ctags->{$ct} += $p->{'ctags'}->{$ct};
}
}
return $ctags;
}
sub git_populate_project_tagcloud {
my $ctags = shift;
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;
my $matched = $input_params{'ctag'};
if (eval { require HTML::TagCloud; 1; }) {
$cloud = HTML::TagCloud->new;
foreach my $ctag (sort keys %ctags_lc) {
my $title = esc_html($ctags_lc{$ctag}->{topname});
$title =~ s/ / /g;
$title =~ s/^/ /g;
$title =~ s/$/ /g;
if (defined $matched && $matched eq $ctag) {
$title = qq(<span class="match">$title</span>);
}
$cloud->add($title, href(project=>undef, ctag=>$ctag),
$ctags_lc{$ctag}->{count});
}
} else {
$cloud = {};
foreach my $ctag (keys %ctags_lc) {
my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
if (defined $matched && $matched eq $ctag) {
$title = qq(<span class="match">$title</span>);
}
$cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
$cloud->{$ctag}{ctag} =
$cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title);
}
}
return $cloud;
}
sub git_show_project_tagcloud {
my ($cloud, $count) = @_;
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
'<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
join (', ', map {
$cloud->{$_}->{'ctag'}
} splice(@tags, 0, $count)) .
'</div>';
}
}
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 = shift || '';
my $paranoid = shift;
my @list;
if (-d $projects_list) {
my $dir = $projects_list;
$dir =~ s!/+$!!;
my $pfxlen = length("$dir");
my $pfxdepth = ($dir =~ tr!/!!);
if ($filter && !$paranoid) {
$dir .= "/$filter";
$dir =~ s!/+$!!;
}
File::Find::find({
follow_fast => 1, follow_skip => 2, dangling_symlinks => 0, wanted => sub {
our $project_maxdepth;
our $projectroot;
return if (m!^[/.]$!);
return unless (-d $_);
return unless (-x $_);
if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
$File::Find::prune = 1;
return;
}
my $path = substr($File::Find::name, $pfxlen + 1);
if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
next;
}
if (check_export_ok("$projectroot/$path")) {
push @list, { path => $path };
$File::Find::prune = 1;
}
},
}, "$dir");
} elsif (-f $projects_list) {
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 && $path !~ m!^\Q$filter\E/!) {
next;
}
if (check_export_ok("$projectroot/$path")) {
my $pr = {
path => $path
};
if ($owner) {
$pr->{'owner'} = to_utf8($owner);
}
push @list, $pr;
}
}
close $fd;
}
return @list;
}
sub filter_forks_from_projects_list {
my $projects = shift;
my %trie; foreach my $pr (@$projects) {
my $path = $pr->{'path'};
$path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
next if ($path =~ m!/$!); next unless ($path); next unless (-d "$projectroot/$path"); $pr->{'forks'} = [];
my @dirs = split('/', $path);
my $ref = \%trie;
while (scalar @dirs &&
exists($ref->{$dirs[0]})) {
$ref = $ref->{shift @dirs};
}
foreach my $dir (@dirs) {
$ref = $ref->{$dir} = {};
}
$ref->{''} = $pr if (!exists $ref->{''});
}
my @filtered;
PROJECT:
foreach my $pr (@$projects) {
my $ref = \%trie;
DIR:
foreach my $dir (split('/', $pr->{'path'})) {
if (exists $ref->{''}) {
push @{$ref->{''}{'forks'}}, $pr;
next PROJECT;
}
if (!exists $ref->{$dir}) {
push @filtered, $pr;
next PROJECT;
}
$ref = $ref->{$dir};
}
push @filtered, $pr;
}
return @filtered;
}
sub search_projects_list {
my ($projlist, %opts) = @_;
my $tagfilter = $opts{'tagfilter'};
my $search_re = $opts{'search_regexp'};
return @$projlist
unless ($tagfilter || $search_re);
fill_project_list_info($projlist,
$tagfilter ? 'ctags' : (),
$search_re ? ('path', 'descr') : ());
my @projects;
PROJECT:
foreach my $pr (@$projlist) {
if ($tagfilter) {
next unless ref($pr->{'ctags'}) eq 'HASH';
next unless
grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
}
if ($search_re) {
next unless
$pr->{'path'} =~ /$search_re/ ||
$pr->{'descr_long'} =~ /$search_re/;
}
push @projects, $pr;
}
return @projects;
}
our $gitweb_project_owner = undef;
sub git_get_project_list_from_file {
return if (defined $gitweb_project_owner);
$gitweb_project_owner = {};
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',
map { "refs/$_" } get_branch_refs ()) 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_remotes_list {
my $wanted = shift;
my %remotes = ();
open my $fd, '-|' , git_cmd(), 'remote', '-v';
return unless $fd;
while (my $remote = <$fd>) {
chomp $remote;
$remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
next if $wanted and not $remote eq $wanted;
my ($url, $key) = ($1, $2);
$remotes{$remote} ||= { 'heads' => () };
$remotes{$remote}{$key} = $url;
}
close $fd or return;
return wantarray ? %remotes : \%remotes;
}
sub fill_remote_heads {
my $remotes = shift;
my @heads = map { "remotes/$_" } keys %$remotes;
my @remoteheads = git_get_heads_list(undef, @heads);
foreach my $remote (keys %$remotes) {
$remotes->{$remote}{'heads'} = [ grep {
$_->{'name'} =~ s!^$remote/!!
} @remoteheads ];
}
}
sub git_get_references {
my $type = shift || "";
my %refs;
open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
($type ? ("--", "refs/$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 {
return undef;
}
}
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;
my ($tz_sign, $tz_hour, $tz_min) =
($tz =~ m/^([-+])(\d\d)(\d\d)$/);
$tz_sign = ($tz_sign eq '-' ? -1 : +1);
my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
($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;
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);
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)';
}
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;
}
sub parse_difftree_raw_line {
my $line = shift;
my %res;
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') { ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
} else {
$res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
}
}
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);
}
elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
$res{'commit'} = $1;
}
return wantarray ? %res : \%res;
}
sub parsed_difftree_line {
my $line_or_ref = shift;
if (ref($line_or_ref) eq "HASH") {
return $line_or_ref;
} else {
return parse_difftree_raw_line($line_or_ref);
}
}
sub parse_ls_tree_line {
my $line = shift;
my %opts = @_;
my %res;
if ($opts{'-l'}) {
$line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
$res{'mode'} = $1;
$res{'type'} = $2;
$res{'hash'} = $3;
$res{'size'} = $4;
if ($opts{'-z'}) {
$res{'name'} = $5;
} else {
$res{'name'} = unquote($5);
}
} else {
$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;
}
sub parse_from_to_diffinfo {
my ($diffinfo, $from, $to, @parents) = @_;
if ($diffinfo->{'nparents'}) {
$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") { $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 {
$from->{'file'} = $diffinfo->{'from_file'};
if ($diffinfo->{'status'} ne "A") { $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)) { $to->{'href'} = href(action=>"blob", hash_base=>$hash,
hash=>$diffinfo->{'to_id'},
file_name=>$to->{'file'});
} else {
delete $to->{'href'};
}
}
sub git_get_heads_list {
my ($limit, @classes) = @_;
@classes = get_branch_refs() unless @classes;
my @patterns = map { "refs/$_" } @classes;
my @headslist;
open my $fd, '-|', git_cmd(), 'for-each-ref',
($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
'--format=%(objectname) %(refname) %(subject)%00%(committer)',
@patterns
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;
my $strip_refs = join '|', map { quotemeta } get_branch_refs();
$name =~ s!^refs/($strip_refs|remotes)/!!;
$ref_item{'name'} = $name;
my $ref_dir = (defined $1) ? $1 : '';
if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
$ref_item{'name'} .= ' (' . $ref_dir . ')';
}
$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;
}
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);
}
sub insert_file {
my $filename = shift;
open my $fd, '<', $filename;
print map { to_utf8($_) } <$fd>;
close $fd;
}
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/^ my ($mimetype, @exts) = split(/\s+/);
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!^/!) { $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;
}
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;
}
sub guess_file_syntax {
my ($highlight, $file_name) = @_;
return undef unless ($highlight && defined $file_name);
my $basename = basename($file_name, '.in');
return $highlight_basename{$basename}
if exists $highlight_basename{$basename};
$basename =~ /\.([^.]*)$/;
my $ext = $1 or return undef;
return $highlight_ext{$ext}
if exists $highlight_ext{$ext};
return undef;
}
sub run_highlighter {
my ($fd, $highlight, $syntax) = @_;
return $fd unless ($highlight);
close $fd;
my $syntax_arg = (defined $syntax) ? "--syntax $syntax" : "--force";
open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
quote_command($^X, '-CO', '-MEncode=decode,FB_DEFAULT', '-pse',
'$_ = decode($fe, $_, FB_DEFAULT) if !utf8::decode($_);',
'--', "-fe=$fallback_encoding")." | ".
quote_command($highlight_bin).
" --replace-tabs=8 --fragment $syntax_arg |"
or die_error(500, "Couldn't open file or run syntax highlighter");
return $fd;
}
sub get_page_title {
my $title = to_utf8($site_name);
unless (defined $project) {
if (defined $project_filter) {
$title .= " - projects in '" . esc_path($project_filter) . "'";
}
return $title;
}
$title .= " - " . to_utf8($project);
return $title unless (defined $action);
$title .= "/$action";
return $title unless (defined $file_name);
$title .= " - " . esc_path($file_name);
if ($action eq "tree" && $file_name !~ m|/$|) {
$title .= "/";
}
return $title;
}
sub get_content_type_html {
if (defined $cgi->http('HTTP_ACCEPT') &&
$cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
$cgi->Accept('application/xhtml+xml') != 0) {
return 'application/xhtml+xml';
} else {
return 'text/html';
}
}
sub print_feed_meta {
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' => esc_attr("$project - $href_params{'-title'} - $format feed"),
'-type' => "application/$type+xml"
);
$href_params{'extra_options'} = undef;
$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",
esc_attr($site_name), href(project=>undef, action=>"project_index"));
printf('<link rel="alternate" title="%s projects feeds" '.
'href="%s" type="text/x-opml" />'."\n",
esc_attr($site_name), href(project=>undef, action=>"opml"));
}
}
sub print_header_links {
my $status = shift;
if (defined $stylesheet) {
print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
} else {
foreach my $stylesheet (@stylesheets) {
next unless $stylesheet;
print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
}
}
print_feed_meta()
if ($status eq '200 OK');
if (defined $favicon) {
print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
}
}
sub print_nav_breadcrumbs_path {
my $dirprefix = undef;
while (my $part = shift) {
$dirprefix .= "/" if defined $dirprefix;
$dirprefix .= $part;
print $cgi->a({-href => href(project => undef,
project_filter => $dirprefix,
action => "project_list")},
esc_html($part)) . " / ";
}
}
sub print_nav_breadcrumbs {
my %opts = @_;
for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
print $cgi->a({-href => esc_url($crumb->[1])}, $crumb->[0]) . " / ";
}
if (defined $project) {
my @dirname = split '/', $project;
my $projectbasename = pop @dirname;
print_nav_breadcrumbs_path(@dirname);
print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));
if (defined $action) {
my $action_print = $action ;
if (defined $opts{-action_extra}) {
$action_print = $cgi->a({-href => href(action=>$action)},
$action);
}
print " / $action_print";
}
if (defined $opts{-action_extra}) {
print " / $opts{-action_extra}";
}
print "\n";
} elsif (defined $project_filter) {
print_nav_breadcrumbs_path(split '/', $project_filter);
}
}
sub print_search_form {
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->start_form(-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->a({-href => href(action=>"search_help"),
-title => "search help" }, "?") . " search:\n",
$cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\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_header_html {
my $status = shift || "200 OK";
my $expires = shift;
my %opts = @_;
my $title = get_page_title();
my $content_type = get_content_type_html();
print $cgi->header(-type=>$content_type, -charset => 'utf-8',
-status=> $status, -expires => $expires)
unless ($opts{'-no_http_header'});
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
if ($ENV{'PATH_INFO'}) {
print "<base href=\"".esc_url($base_url)."\" />\n";
}
print_header_links($status);
if (defined $site_html_head_string) {
print to_utf8($site_html_head_string);
}
print "</head>\n" .
"<body>\n";
if (defined $site_header && -f $site_header) {
insert_file($site_header);
}
print "<div class=\"page_header\">\n";
if (defined $logo) {
print $cgi->a({-href => esc_url($logo_url),
-title => $logo_label},
$cgi->img({-src => esc_url($logo),
-width => 72, -height => 27,
-alt => "git",
-class => "logo"}));
}
print_nav_breadcrumbs(%opts);
print "</div>\n";
my $have_search = gitweb_check_feature('search');
if (defined $project && $have_search) {
print_search_form();
}
}
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",
project_filter => $project_filter),
-class => $feed_class}, "OPML") . " ";
print $cgi->a({-href => href(project=>undef, action=>"project_index",
project_filter => $project_filter),
-class => $feed_class}, "TXT") . "\n";
}
print "</div>\n";
if (defined $t0 && gitweb_check_feature('timed')) {
print "<div id=\"generating_info\">\n";
print 'This page took '.
'<span id="generating_time" class="time_span">'.
tv_interval($t0, [ gettimeofday() ]).
' seconds </span>'.
' and '.
'<span id="generating_cmd">'.
$number_of_git_cmds.
'</span> git commands '.
" to generate.\n";
print "</div>\n"; }
if (defined $site_footer && -f $site_footer) {
insert_file($site_footer);
}
print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
if (defined $action &&
$action eq 'blame_incremental') {
print qq!<script type="text/javascript">\n!.
qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
qq! "!. href() .qq!");\n!.
qq!</script>\n!;
} else {
my ($jstimezone, $tz_cookie, $datetime_class) =
gitweb_get_feature('javascript-timezone');
print qq!<script type="text/javascript">\n!.
qq!window.onload = function () {\n!;
if (gitweb_check_feature('javascript-actions')) {
print qq! fixLinks();\n!;
}
if ($jstimezone && $tz_cookie && $datetime_class) {
print qq! var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. qq! onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!;
}
print qq!};\n!.
qq!</script>\n!;
}
print "</body>\n" .
"</html>";
}
sub die_error {
my $status = shift || 500;
my $error = esc_html(shift) || "Internal Server Error";
my $extra = shift;
my %opts = @_;
my %http_responses = (
400 => '400 Bad Request',
403 => '403 Forbidden',
404 => '404 Not Found',
500 => '500 Internal Server Error',
503 => '503 Service Unavailable',
);
git_header_html($http_responses{$status}, undef, %opts);
print <<EOF;
<div class="page_body">
<br /><br />
$status - $error
<br />
EOF
if (defined $extra) {
print "<hr />\n" .
"$extra\n";
}
print "</div>\n";
git_footer_html();
goto DONE_GITWEB
unless ($opts{'-error_handler'});
}
sub git_print_page_nav {
my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
$extra = '' if !defined $extra;
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, 'f' => $git_dir, 'h' => $treehead || '', 'b' => $treebase || '', );
while (@actions) {
my ($label, $link, $pos) = splice(@actions,0,3);
@navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
$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_ref_views {
my ($current) = @_;
my @ref_views = qw{tags heads};
push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
return join " | ", map {
$_ eq $current ? $_ :
$cgi->a({-href => href(action=>$_)}, $_)
} @ref_views
}
sub format_paging_nav {
my ($action, $page, $has_next_link) = @_;
my $paging_nav;
if ($page > 0) {
$paging_nav .=
$cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
" ⋅ " .
$cgi->a({-href => href(-replay=>1, page=>$page-1),
-accesskey => "p", -title => "Alt-p"}, "prev");
} else {
$paging_nav .= "first ⋅ prev";
}
if ($has_next_link) {
$paging_nav .= " ⋅ " .
$cgi->a({-href => href(-replay=>1, page=>$page+1),
-accesskey => "n", -title => "Alt-n"}, "next");
} else {
$paging_nav .= " ⋅ next";
}
return $paging_nav;
}
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 format_repo_url {
my ($name, $url) = @_;
return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
}
sub git_print_section {
my ($div_args, $header_args, $content);
my $arg = shift;
if (ref($arg) eq 'HASH') {
$div_args = $arg;
$arg = shift;
}
if (ref($arg) eq 'ARRAY') {
$header_args = $arg;
$arg = shift;
}
$content = $arg;
print $cgi->start_div($div_args);
git_print_header_div(@$header_args);
if (ref($content) eq 'CODE') {
$content->(@_);
} elsif (ref($content) eq 'SCALAR') {
print esc_html($$content);
} elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
print <$content>;
} elsif (!ref($content) && defined($content)) {
print $content;
}
print $cgi->end_div;
}
sub format_timestamp_html {
my $date = shift;
my $strtime = $date->{'rfc2822'};
my (undef, undef, $datetime_class) =
gitweb_get_feature('javascript-timezone');
if ($datetime_class) {
$strtime = qq!<span class="$datetime_class">$strtime</span>!;
}
my $localtime_format = '(%02d:%02d %s)';
if ($date->{'hour_local'} < 6) {
$localtime_format = '(<span class="atnight">%02d:%02d</span> %s)';
}
$strtime .= ' ' .
sprintf($localtime_format,
$date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
return $strtime;
}
sub git_print_authorship {
my $co = shift;
my %opts = @_;
my $tag = $opts{-tag} || 'div';
my $author = $co->{'author_name'};
my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
print "<$tag class=\"author_date\">" .
format_search_author($author, "author", esc_html($author)) .
" [".format_timestamp_html(\%ad)."]".
git_get_avatar($co->{'author_email'}, -pad_before => 1) .
"</$tag>\n";
}
sub git_print_authorship_rows {
my $co = shift;
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>" .
format_search_author($co->{"${who}_name"}, $who,
esc_html($co->{"${who}_name"})) . " " .
format_search_author($co->{"${who}_email"}, $who,
esc_html("<" . $co->{"${who}_email"} . ">")) .
"</td><td rowspan=\"2\">" .
git_get_avatar($co->{"${who}_email"}, -size => 'double') .
"</td></tr>\n" .
"<tr>" .
"<td></td><td>" .
format_timestamp_html(\%wd) .
"</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'}) {
shift @$log;
}
while (defined $log->[0] && $log->[0] eq "") {
shift @$log;
}
my $skip_blank_line = 0;
foreach my $line (@$log) {
if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
if (! $opts{'-remove_signoff'}) {
print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
$skip_blank_line = 1;
}
next;
}
if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) {
if (! $opts{'-remove_signoff'}) {
print "<span class=\"signoff\">" . esc_html($1) . ": " .
"<a href=\"" . esc_html($2) . "\">" . esc_html($2) . "</a>" .
"</span><br/>\n";
$skip_blank_line = 1;
}
next;
}
if ($line eq "") {
next if ($skip_blank_line);
$skip_blank_line = 1;
} else {
$skip_blank_line = 0;
}
print format_log_line_html($line) . "<br/>\n";
}
if ($opts{'-final_empty_line'}) {
print "<br/>\n" unless $skip_blank_line;
}
}
sub git_get_link_target {
my $hash = shift;
my $link_target;
open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
or return;
{
local $/ = undef;
$link_target = <$fd>;
}
close $fd
or return;
return $link_target;
}
sub normalize_link_target {
my ($link_target, $basedir) = @_;
return if (substr($link_target, 0, 1) eq '/');
my $path;
if ($basedir) {
$path = $basedir . '/' . $link_target;
} else {
$path = $link_target;
}
my @path_parts;
foreach my $part (split('/', $path)) {
next if (!$part || $part eq '.');
if ($part eq '..') {
if (@path_parts) {
pop @path_parts;
} else {
return;
}
} else {
push @path_parts, $part;
}
}
$path = join('/', @path_parts);
return $path;
}
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;
print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
if (exists $t->{'size'}) {
print "<td class=\"size\">$t->{'size'}</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 {
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";
}
}
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;
}
sub is_deleted {
my $diffinfo = shift;
return $diffinfo->{'to_id'} eq ('0' x 40);
}
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 ($ print(($ }
print "</div>\n";
print "<table class=\"" .
(@parents > 1 ? "combined " : "") .
"diff_tree\">\n";
my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
if ($has_header) {
print "<thead><tr>\n" .
"<th></th><th></th>\n"; 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) .
" </th>\n";
}
print "</tr></thead>\n<tbody>\n";
}
my $alternate = 1;
my $patchno = 0;
foreach my $line (@{$difftree}) {
my $diff = parsed_difftree_line($line);
if ($alternate) {
print "<tr class=\"dark\">\n";
} else {
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
if (exists $diff->{'nparents'}) {
fill_from_file_info($diff, @parents)
unless exists $diff->{'from_file'};
if (!is_deleted($diff)) {
print "<td>" .
$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
file_name=>$diff->{'to_file'},
hash_base=>$hash),
-class => "list"}, esc_path($diff->{'to_file'})) .
"</td>\n";
} else {
print "<td>" .
esc_path($diff->{'to_file'}) .
"</td>\n";
}
if ($action eq 'commitdiff') {
$patchno++;
print "<td class=\"link\">" .
$cgi->a({-href => href(-anchor=>"patch$patchno")},
"patch") .
" | " .
"</td>\n";
}
my $has_history = 0;
my $not_deleted = 0;
for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
my $hash_parent = $parents[$i];
my $from_hash = $diff->{'from_id'}[$i];
my $from_path = $diff->{'from_file'}[$i];
my $status = $diff->{'status'}[$i];
$has_history ||= ($status ne 'A');
$not_deleted ||= ($status ne 'D');
if ($status eq 'A') {
print "<td class=\"link\" align=\"right\"> | </td>\n";
} elsif ($status eq 'D') {
print "<td class=\"link\">" .
$cgi->a({-href => href(action=>"blob",
hash_base=>$hash,
hash=>$from_hash,
file_name=>$from_path)},
"blob" . ($i+1)) .
" | </td>\n";
} else {
if ($diff->{'to_id'} eq $from_hash) {
print "<td class=\"link nochange\">";
} else {
print "<td class=\"link\">";
}
print $cgi->a({-href => href(action=>"blobdiff",
hash=>$diff->{'to_id'},
hash_parent=>$from_hash,
hash_base=>$hash,
hash_parent_base=>$hash_parent,
file_name=>$diff->{'to_file'},
file_parent=>$from_path)},
"diff" . ($i+1)) .
" | </td>\n";
}
}
print "<td class=\"link\">";
if ($not_deleted) {
print $cgi->a({-href => href(action=>"blob",
hash=>$diff->{'to_id'},
file_name=>$diff->{'to_file'},
hash_base=>$hash)},
"blob");
print " | " if ($has_history);
}
if ($has_history) {
print $cgi->a({-href => href(action=>"history",
file_name=>$diff->{'to_file'},
hash_base=>$hash)},
"history");
}
print "</td>\n";
print "</tr>\n";
next; }
my ($to_mode_oct, $to_mode_str, $to_file_type);
my ($from_mode_oct, $from_mode_str, $from_file_type);
if ($diff->{'to_mode'} ne ('0' x 6)) {
$to_mode_oct = oct $diff->{'to_mode'};
if (S_ISREG($to_mode_oct)) { $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); }
$to_file_type = file_type($diff->{'to_mode'});
}
if ($diff->{'from_mode'} ne ('0' x 6)) {
$from_mode_oct = oct $diff->{'from_mode'};
if (S_ISREG($from_mode_oct)) { $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); }
$from_file_type = file_type($diff->{'from_mode'});
}
if ($diff->{'status'} eq "A") { my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
$mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
$mode_chng .= "]</span>";
print "<td>";
print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
hash_base=>$hash, file_name=>$diff->{'file'}),
-class => "list"}, esc_path($diff->{'file'}));
print "</td>\n";
print "<td>$mode_chng</td>\n";
print "<td class=\"link\">";
if ($action eq 'commitdiff') {
$patchno++;
print $cgi->a({-href => href(-anchor=>"patch$patchno")},
"patch") .
" | ";
}
print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
hash_base=>$hash, file_name=>$diff->{'file'})},
"blob");
print "</td>\n";
} elsif ($diff->{'status'} eq "D") { my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
print "<td>";
print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
hash_base=>$parent, file_name=>$diff->{'file'}),
-class => "list"}, esc_path($diff->{'file'}));
print "</td>\n";
print "<td>$mode_chng</td>\n";
print "<td class=\"link\">";
if ($action eq 'commitdiff') {
$patchno++;
print $cgi->a({-href => href(-anchor=>"patch$patchno")},
"patch") .
" | ";
}
print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
hash_base=>$parent, file_name=>$diff->{'file'})},
"blob") . " | ";
if ($have_blame) {
print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
file_name=>$diff->{'file'})},
"blame") . " | ";
}
print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
file_name=>$diff->{'file'})},
"history");
print "</td>\n";
} elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { my $mode_chnge = "";
if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
$mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
if ($from_file_type ne $to_file_type) {
$mode_chnge .= " from $from_file_type to $to_file_type";
}
if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
if ($from_mode_str && $to_mode_str) {
$mode_chnge .= " mode: $from_mode_str->$to_mode_str";
} elsif ($to_mode_str) {
$mode_chnge .= " mode: $to_mode_str";
}
}
$mode_chnge .= "]</span>\n";
}
print "<td>";
print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
hash_base=>$hash, file_name=>$diff->{'file'}),
-class => "list"}, esc_path($diff->{'file'}));
print "</td>\n";
print "<td>$mode_chnge</td>\n";
print "<td class=\"link\">";
if ($action eq 'commitdiff') {
$patchno++;
print $cgi->a({-href => href(-anchor=>"patch$patchno")},
"patch") .
" | ";
} elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
print $cgi->a({-href => href(action=>"blobdiff",
hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
hash_base=>$hash, hash_parent_base=>$parent,
file_name=>$diff->{'file'})},
"diff") .
" | ";
}
print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
hash_base=>$hash, file_name=>$diff->{'file'})},
"blob") . " | ";
if ($have_blame) {
print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
file_name=>$diff->{'file'})},
"blame") . " | ";
}
print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
file_name=>$diff->{'file'})},
"history");
print "</td>\n";
} elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { my %status_name = ('R' => 'moved', 'C' => 'copied');
my $nstatus = $status_name{$diff->{'status'}};
my $mode_chng = "";
if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
$mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
}
print "<td>" .
$cgi->a({-href => href(action=>"blob", hash_base=>$hash,
hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
-class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
"<td><span class=\"file_status $nstatus\">[$nstatus from " .
$cgi->a({-href => href(action=>"blob", hash_base=>$parent,
hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
-class => "list"}, esc_path($diff->{'from_file'})) .
" with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
"<td class=\"link\">";
if ($action eq 'commitdiff') {
$patchno++;
print $cgi->a({-href => href(-anchor=>"patch$patchno")},
"patch") .
" | ";
} elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
print $cgi->a({-href => href(action=>"blobdiff",
hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
hash_base=>$hash, hash_parent_base=>$parent,
file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
"diff") .
" | ";
}
print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
hash_base=>$parent, file_name=>$diff->{'to_file'})},
"blob") . " | ";
if ($have_blame) {
print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
file_name=>$diff->{'to_file'})},
"blame") . " | ";
}
print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
file_name=>$diff->{'to_file'})},
"history");
print "</td>\n";
} print "</tr>\n";
}
print "</tbody>" if $has_header;
print "</table>\n";
}
sub print_sidebyside_diff_lines {
my ($ctx, $rem, $add) = @_;
if (@$ctx) {
print join '',
'<div class="chunk_block ctx">',
'<div class="old">',
@$ctx,
'</div>',
'<div class="new">',
@$ctx,
'</div>',
'</div>';
}
if (!@$add) {
print join '',
'<div class="chunk_block rem">',
'<div class="old">',
@$rem,
'</div>',
'</div>';
} elsif (!@$rem) {
print join '',
'<div class="chunk_block add">',
'<div class="new">',
@$add,
'</div>',
'</div>';
} else {
print join '',
'<div class="chunk_block chg">',
'<div class="old">',
@$rem,
'</div>',
'<div class="new">',
@$add,
'</div>',
'</div>';
}
}
sub print_inline_diff_lines {
my ($ctx, $rem, $add) = @_;
print @$ctx, @$rem, @$add;
}
sub format_rem_add_lines_pair {
my ($rem, $add, $num_parents) = @_;
chomp $rem;
chomp $add;
$rem = untabify($rem);
$add = untabify($add);
my @rem = split(//, $rem);
my @add = split(//, $add);
my ($esc_rem, $esc_add);
my ($prefix_len, $suffix_len) = ($num_parents, 0);
my ($prefix_has_nonspace, $suffix_has_nonspace);
my $shorter = (@rem < @add) ? @rem : @add;
while ($prefix_len < $shorter) {
last if ($rem[$prefix_len] ne $add[$prefix_len]);
$prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
$prefix_len++;
}
while ($prefix_len + $suffix_len < $shorter) {
last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
$suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
$suffix_len++;
}
if ($prefix_has_nonspace || $suffix_has_nonspace) {
$esc_rem = esc_html_hl_regions($rem, 'marked',
[$prefix_len, @rem - $suffix_len], -nbsp=>1);
$esc_add = esc_html_hl_regions($add, 'marked',
[$prefix_len, @add - $suffix_len], -nbsp=>1);
} else {
$esc_rem = esc_html($rem, -nbsp=>1);
$esc_add = esc_html($add, -nbsp=>1);
}
return format_diff_line(\$esc_rem, 'rem'),
format_diff_line(\$esc_add, 'add');
}
sub format_ctx_rem_add_lines {
my ($ctx, $rem, $add, $num_parents) = @_;
my (@new_ctx, @new_rem, @new_add);
my $can_highlight = 0;
my $is_combined = ($num_parents > 1);
if (@$add > 0 && @$add == @$rem) {
$can_highlight = 1;
if ($is_combined) {
for (my $i = 0; $i < @$add; $i++) {
my $prefix_rem = substr($rem->[$i], 0, $num_parents);
my $prefix_add = substr($add->[$i], 0, $num_parents);
$prefix_rem =~ s/-/+/g;
if ($prefix_rem ne $prefix_add) {
$can_highlight = 0;
last;
}
}
}
}
if ($can_highlight) {
for (my $i = 0; $i < @$add; $i++) {
my ($line_rem, $line_add) = format_rem_add_lines_pair(
$rem->[$i], $add->[$i], $num_parents);
push @new_rem, $line_rem;
push @new_add, $line_add;
}
} else {
@new_rem = map { format_diff_line($_, 'rem') } @$rem;
@new_add = map { format_diff_line($_, 'add') } @$add;
}
@new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
return (\@new_ctx, \@new_rem, \@new_add);
}
sub print_diff_lines {
my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
my $is_combined = $num_parents > 1;
($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
$num_parents);
if ($diff_style eq 'sidebyside' && !$is_combined) {
print_sidebyside_diff_lines($ctx, $rem, $add);
} else {
print_inline_diff_lines($ctx, $rem, $add);
}
}
sub print_diff_chunk {
my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
my (@ctx, @rem, @add);
my $prev_class = '';
return unless @chunk;
for (my $i = 1; $i < @chunk; $i++) {
if ($chunk[$i][0] eq 'incomplete') {
$chunk[$i][0] = $chunk[$i-1][0];
}
}
push @chunk, ["", ""];
foreach my $line_info (@chunk) {
my ($class, $line) = @$line_info;
if ($class && $class eq 'chunk_header') {
print format_diff_line($line, $class, $from, $to);
next;
}
if (!$class || ((@rem || @add) && $class eq 'ctx') ||
(@rem && @add && $class ne $prev_class)) {
print_diff_lines(\@ctx, \@rem, \@add,
$diff_style, $num_parents);
@ctx = @rem = @add = ();
}
last unless $line;
if ($class eq 'rem') {
push @rem, $line;
} elsif ($class eq 'add') {
push @add, $line;
}
if ($class eq 'ctx') {
push @ctx, $line;
}
$prev_class = $class;
}
}
sub git_patchset_body {
my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
my ($hash_parent) = $hash_parents[0];
my $is_combined = (@hash_parents > 1);
my $patch_idx = 0;
my $patch_number = 0;
my $patch_line;
my $diffinfo;
my $to_name;
my (%from, %to);
my @chunk;
print "<div class=\"patchset\">\n";
while ($patch_line = <$fd>) {
chomp $patch_line;
last if ($patch_line =~ m/^diff /);
}
PATCH:
while ($patch_line) {
if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
# $1 is from_name, which we do not use
$to_name = unquote($2);
$to_name =~ s!^b/!!;
} elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
# $1 is 'cc' or 'combined', which we do not use
$to_name = unquote($2);
} else {
$to_name = undef;
}
# check if current patch belong to current raw line
# and parse raw git-diff line if needed
if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
# this is continuation of a split patch
print "<div class=\"patch cont\">\n";
} else {
# advance raw git-diff output if needed
$patch_idx++ if defined $diffinfo;
# read and prepare patch information
$diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
# compact combined diff output can have some patches skipped
# find which patch (using pathname of result) we are at now;
if ($is_combined) {
while ($to_name ne $diffinfo->{'to_file'}) {
print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
format_diff_cc_simplified($diffinfo, @hash_parents) .
"</div>\n"; # class="patch"
$patch_idx++;
$patch_number++;
last if $patch_idx > $#$difftree;
$diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
}
}
# modifies %from, %to hashes
parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
# this is first patch for raw difftree line with $patch_idx index
# we index @$difftree array from 0, but number patches from 1
print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
}
# git diff header
#assert($patch_line =~ m/^diff /) if DEBUG;
#assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
$patch_number++;
# print "git diff" header
print format_git_diff_header_line($patch_line, $diffinfo,
\%from, \%to);
# print extended diff header
print "<div class=\"diff extended_header\">\n";
EXTENDED_HEADER:
while ($patch_line = <$fd>) {
chomp $patch_line;
last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
print format_extended_diff_header_line($patch_line, $diffinfo,
\%from, \%to);
}
print "</div>\n"; # class="diff extended_header"
# from-file/to-file diff header
if (! $patch_line) {
print "</div>\n"; # class="patch"
last PATCH;
}
next PATCH if ($patch_line =~ m/^diff /);
#assert($patch_line =~ m/^---/) if DEBUG;
my $last_patch_line = $patch_line;
$patch_line = <$fd>;
chomp $patch_line;
#assert($patch_line =~ m/^\+\+\+/) if DEBUG;
print format_diff_from_to_header($last_patch_line, $patch_line,
$diffinfo, \%from, \%to,
@hash_parents);
# the patch itself
LINE:
while ($patch_line = <$fd>) {
chomp $patch_line;
next PATCH if ($patch_line =~ m/^diff /);
my $class = diff_line_class($patch_line, \%from, \%to);
if ($class eq 'chunk_header') {
print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
@chunk = ();
}
push @chunk, [ $class, $patch_line ];
}
} continue {
if (@chunk) {
print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
@chunk = ();
}
print "</div>\n"; # class="patch"
}
# for compact combined (--cc) format, with chunk and patch simplification
# the patchset might be empty, but there might be unprocessed raw lines
for (++$patch_idx if $patch_number > 0;
$patch_idx < @$difftree;
++$patch_idx) {
# read and prepare patch information
$diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
# generate anchor for "patch" links in difftree / whatchanged part
print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
format_diff_cc_simplified($diffinfo, @hash_parents) .
"</div>\n"; # class="patch"
$patch_number++;
}
if ($patch_number == 0) {
if (@hash_parents > 1) {
print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
} else {
print "<div class=\"diff nodifferences\">No differences found</div>\n";
}
}
print "</div>\n"; # class="patchset"
}
# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
sub git_project_search_form {
my ($searchtext, $search_use_regexp) = @_;
my $limit = '';
if ($project_filter) {
$limit = " in '$project_filter/'";
}
print "<div class=\"projsearch\">\n";
print $cgi->start_form(-method => 'get', -action => $my_uri) .
$cgi->hidden(-name => 'a', -value => 'project_list') . "\n";
print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n"
if (defined $project_filter);
print $cgi->textfield(-name => 's', -value => $searchtext,
-title => "Search project by name and description$limit",
-size => 60) . "\n" .
"<span title=\"Extended regular expression\">" .
$cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
-checked => $search_use_regexp) .
"</span>\n" .
$cgi->submit(-name => 'btnS', -value => 'Search') .
$cgi->end_form() . "\n" .
$cgi->a({-href => href(project => undef, searchtext => undef,
project_filter => $project_filter)},
esc_html("List all projects$limit")) . "<br />\n";
print "</div>\n";
}
# entry for given @keys needs filling if at least one of keys in list
# is not present in %$project_info
sub project_info_needs_filling {
my ($project_info, @keys) = @_;
# return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
foreach my $key (@keys) {
if (!exists $project_info->{$key}) {
return 1;
}
}
return;
}
# fills project list info (age, description, owner, category, forks, etc.)
# for each project in the list, removing invalid projects from
# returned list, or fill only specified info.
#
# Invalid projects are removed from the returned list if and only if you
# ask 'age' or 'age_string' to be filled, because they are the only fields
# that run unconditionally git command that requires repository, and
# therefore do always check if project repository is invalid.
#
# USAGE:
# * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
# ensures that 'descr_long' and 'ctags' fields are filled
# * @project_list = fill_project_list_info(\@project_list)
# ensures that all fields are filled (and invalid projects removed)
#
# NOTE: modifies $projlist, but does not remove entries from it
sub fill_project_list_info {
my ($projlist, @wanted_keys) = @_;
my @projects;
my $filter_set = sub { return @_; };
if (@wanted_keys) {
my %wanted_keys = map { $_ => 1 } @wanted_keys;
$filter_set = sub { return grep { $wanted_keys{$_} } @_; };
}
my $show_ctags = gitweb_check_feature('ctags');
PROJECT:
foreach my $pr (@$projlist) {
if (project_info_needs_filling($pr, $filter_set->('age', 'age_string'))) {
my (@activity) = git_get_last_activity($pr->{'path'});
unless (@activity) {
next PROJECT;
}
($pr->{'age'}, $pr->{'age_string'}) = @activity;
}
if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) {
my $descr = git_get_project_description($pr->{'path'}) || "";
$descr = to_utf8($descr);
$pr->{'descr_long'} = $descr;
$pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
}
if (project_info_needs_filling($pr, $filter_set->('owner'))) {
$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
}
if ($show_ctags &&
project_info_needs_filling($pr, $filter_set->('ctags'))) {
$pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
}
if ($projects_list_group_categories &&
project_info_needs_filling($pr, $filter_set->('category'))) {
my $cat = git_get_project_category($pr->{'path'}) ||
$project_list_default_category;
$pr->{'category'} = to_utf8($cat);
}
push @projects, $pr;
}
return @projects;
}
sub sort_projects_list {
my ($projlist, $order) = @_;
sub order_str {
my $key = shift;
return sub { $a->{$key} cmp $b->{$key} };
}
sub order_num_then_undef {
my $key = shift;
return sub {
defined $a->{$key} ?
(defined $b->{$key} ? $a->{$key} <=> $b->{$key} : -1) :
(defined $b->{$key} ? 1 : 0)
};
}
my %orderings = (
project => order_str('path'),
descr => order_str('descr_long'),
owner => order_str('owner'),
age => order_num_then_undef('age'),
);
my $ordering = $orderings{$order};
return defined $ordering ? sort $ordering @$projlist : @$projlist;
}
# returns a hash of categories, containing the list of project
# belonging to each category
sub build_projlist_by_category {
my ($projlist, $from, $to) = @_;
my %categories;
$from = 0 unless defined $from;
$to = $#$projlist if (!defined $to || $#$projlist < $to);
for (my $i = $from; $i <= $to; $i++) {
my $pr = $projlist->[$i];
push @{$categories{ $pr->{'category'} }}, $pr;
}
return wantarray ? %categories : \%categories;
}
# print 'sort by' <th> element, generating 'sort by $name' replay link
# if that order is not selected
sub print_sort_th {
print format_sort_th(@_);
}
sub format_sort_th {
my ($name, $order, $header) = @_;
my $sort_th = "";
$header ||= ucfirst($name);
if ($order eq $name) {
$sort_th .= "<th>$header</th>\n";
} else {
$sort_th .= "<th>" .
$cgi->a({-href => href(-replay=>1, order=>$name),
-class => "header"}, $header) .
"</th>\n";
}
return $sort_th;
}
sub git_project_list_rows {
my ($projlist, $from, $to, $check_forks) = @_;
$from = 0 unless defined $from;
$to = $#$projlist if (!defined $to || $#$projlist < $to);
my $alternate = 1;
for (my $i = $from; $i <= $to; $i++) {
my $pr = $projlist->[$i];
if ($alternate) {
print "<tr class=\"dark\">\n";
} else {
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
if ($check_forks) {
print "<td>";
if ($pr->{'forks'}) {
my $nforks = scalar @{$pr->{'forks'}};
if ($nforks > 0) {
print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
-title => "$nforks forks"}, "+");
} else {
print $cgi->span({-title => "$nforks forks"}, "+");
}
}
print "</td>\n";
}
print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
-class => "list"},
esc_html_match_hl($pr->{'path'}, $search_regexp)) .
"</td>\n" .
"<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
-class => "list",
-title => $pr->{'descr_long'}},
$search_regexp
? esc_html_match_hl_chopped($pr->{'descr_long'},
$pr->{'descr'}, $search_regexp)
: esc_html($pr->{'descr'})) .
"</td>\n";
unless ($omit_owner) {
print "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
}
unless ($omit_age_column) {
print "<td class=\"". age_class($pr->{'age'}) . "\">" .
(defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n";
}
print"<td class=\"link\">" .
$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
$cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
$cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
$cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
"</td>\n" .
"</tr>\n";
}
}
sub git_project_list_body {
# actually uses global variable $project
my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
my @projects = @$projlist;
my $check_forks = gitweb_check_feature('forks');
my $show_ctags = gitweb_check_feature('ctags');
my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef;
$check_forks = undef
if ($tagfilter || $search_regexp);
# filtering out forks before filling info allows to do less work
@projects = filter_forks_from_projects_list(\@projects)
if ($check_forks);
# search_projects_list pre-fills required info
@projects = search_projects_list(\@projects,
'search_regexp' => $search_regexp,
'tagfilter' => $tagfilter)
if ($tagfilter || $search_regexp);
# fill the rest
my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
push @all_fields, ('age', 'age_string') unless($omit_age_column);
push @all_fields, 'owner' unless($omit_owner);
@projects = fill_project_list_info(\@projects, @all_fields);
$order ||= $default_projects_order;
$from = 0 unless defined $from;
$to = $#projects if (!defined $to || $#projects < $to);
# short circuit
if ($from > $to) {
print "<center>\n".
"<b>No such projects found</b><br />\n".
"Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
"</center>\n<br />\n";
return;
}
@projects = sort_projects_list(\@projects, $order);
if ($show_ctags) {
my $ctags = git_gather_all_ctags(\@projects);
my $cloud = git_populate_project_tagcloud($ctags);
print git_show_project_tagcloud($cloud, 64);
}
print "<table class=\"project_list\">\n";
unless ($no_header) {
print "<tr>\n";
if ($check_forks) {
print "<th></th>\n";
}
print_sort_th('project', $order, 'Project');
print_sort_th('descr', $order, 'Description');
print_sort_th('owner', $order, 'Owner') unless $omit_owner;
print_sort_th('age', $order, 'Last Change') unless $omit_age_column;
print "<th></th>\n" . # for links
"</tr>\n";
}
if ($projects_list_group_categories) {
# only display categories with projects in the $from-$to window
@projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
my %categories = build_projlist_by_category(\@projects, $from, $to);
foreach my $cat (sort keys %categories) {
unless ($cat eq "") {
print "<tr>\n";
if ($check_forks) {
print "<td></td>\n";
}
print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
print "</tr>\n";
}
git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
}
} else {
git_project_list_rows(\@projects, $from, $to, $check_forks);
}
if (defined $extra) {
print "<tr>\n";
if ($check_forks) {
print "<td></td>\n";
}
print "<td colspan=\"5\">$extra</td>\n" .
"</tr>\n";
}
print "</table>\n";
}
sub git_log_body {
# uses global variable $project
my ($commitlist, $from, $to, $refs, $extra) = @_;
$from = 0 unless defined $from;
$to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
for (my $i = 0; $i <= $to; $i++) {
my %co = %{$commitlist->[$i]};
next if !%co;
my $commit = $co{'id'};
my $ref = format_ref_marker($refs, $commit);
git_print_header_div('commit',
"<span class=\"age\">$co{'age_string'}</span>" .
esc_html($co{'title'}) . $ref,
$commit);
print "<div class=\"title_text\">\n" .
"<div class=\"log_link\">\n" .
$cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
" | " .
$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
" | " .
$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
"<br/>\n" .
"</div>\n";
git_print_authorship(\%co, -tag => 'span');
print "<br/>\n</div>\n";
print "<div class=\"log_body\">\n";
git_print_log($co{'comment'}, -final_empty_line=> 1);
print "</div>\n";
}
if ($extra) {
print "<div class=\"page_nav\">\n";
print "$extra\n";
print "</div>\n";
}
}
sub git_shortlog_body {
# uses global variable $project
my ($commitlist, $from, $to, $refs, $extra) = @_;
$from = 0 unless defined $from;
$to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
print "<table class=\"shortlog\">\n";
my $alternate = 1;
for (my $i = $from; $i <= $to; $i++) {
my %co = %{$commitlist->[$i]};
my $commit = $co{'id'};
my $ref = format_ref_marker($refs, $commit);
if ($alternate) {
print "<tr class=\"dark\">\n";
} else {
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
# git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
format_author_html('td', \%co, 10) . "<td>";
print format_subject_html($co{'title'}, $co{'title_short'},
href(action=>"commit", hash=>$commit), $ref);
print "</td>\n" .
"<td class=\"link\">" .
$cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
my $snapshot_links = format_snapshot_links($commit);
if (defined $snapshot_links) {
print " | " . $snapshot_links;
}
print "</td>\n" .
"</tr>\n";
}
if (defined $extra) {
print "<tr>\n" .
"<td colspan=\"4\">$extra</td>\n" .
"</tr>\n";
}
print "</table>\n";
}
sub git_history_body {
# Warning: assumes constant type (blob or tree) during history
my ($commitlist, $from, $to, $refs, $extra,
$file_name, $file_hash, $ftype) = @_;
$from = 0 unless defined $from;
$to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
print "<table class=\"history\">\n";
my $alternate = 1;
for (my $i = $from; $i <= $to; $i++) {
my %co = %{$commitlist->[$i]};
if (!%co) {
next;
}
my $commit = $co{'id'};
my $ref = format_ref_marker($refs, $commit);
if ($alternate) {
print "<tr class=\"dark\">\n";
} else {
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
# shortlog: format_author_html('td', \%co, 10)
format_author_html('td', \%co, 15, 3) . "<td>";
# originally git_history used chop_str($co{'title'}, 50)
print format_subject_html($co{'title'}, $co{'title_short'},
href(action=>"commit", hash=>$commit), $ref);
print "</td>\n" .
"<td class=\"link\">" .
$cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
if ($ftype eq 'blob') {
print " | " .
$cgi->a({-href => href(action=>"blob_plain", hash_base=>$commit, file_name=>$file_name)}, "raw");
my $blob_current = $file_hash;
my $blob_parent = git_get_hash_by_path($commit, $file_name);
if (defined $blob_current && defined $blob_parent &&
$blob_current ne $blob_parent) {
print " | " .
$cgi->a({-href => href(action=>"blobdiff",
hash=>$blob_current, hash_parent=>$blob_parent,
hash_base=>$hash_base, hash_parent_base=>$commit,
file_name=>$file_name)},
"diff to current");
}
}
print "</td>\n" .
"</tr>\n";
}
if (defined $extra) {
print "<tr>\n" .
"<td colspan=\"4\">$extra</td>\n" .
"</tr>\n";
}
print "</table>\n";
}
sub git_tags_body {
# uses global variable $project
my ($taglist, $from, $to, $extra) = @_;
$from = 0 unless defined $from;
$to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
print "<table class=\"tags\">\n";
my $alternate = 1;
for (my $i = $from; $i <= $to; $i++) {
my $entry = $taglist->[$i];
my %tag = %$entry;
my $comment = $tag{'subject'};
my $comment_short;
if (defined $comment) {
$comment_short = chop_str($comment, 30, 5);
}
if ($alternate) {
print "<tr class=\"dark\">\n";
} else {
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
if (defined $tag{'age'}) {
print "<td><i>$tag{'age'}</i></td>\n";
} else {
print "<td></td>\n";
}
print "<td>" .
$cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
-class => "list name"}, esc_html($tag{'name'})) .
"</td>\n" .
"<td>";
if (defined $comment) {
print format_subject_html($comment, $comment_short,
href(action=>"tag", hash=>$tag{'id'}));
}
print "</td>\n" .
"<td class=\"selflink\">";
if ($tag{'type'} eq "tag") {
print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
} else {
print " ";
}
print "</td>\n" .
"<td class=\"link\">" . " | " .
$cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
if ($tag{'reftype'} eq "commit") {
print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
" | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
} elsif ($tag{'reftype'} eq "blob") {
print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
}
print "</td>\n" .
"</tr>";
}
if (defined $extra) {
print "<tr>\n" .
"<td colspan=\"5\">$extra</td>\n" .
"</tr>\n";
}
print "</table>\n";
}
sub git_heads_body {
# uses global variable $project
my ($headlist, $head_at, $from, $to, $extra) = @_;
$from = 0 unless defined $from;
$to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
print "<table class=\"heads\">\n";
my $alternate = 1;
for (my $i = $from; $i <= $to; $i++) {
my $entry = $headlist->[$i];
my %ref = %$entry;
my $curr = defined $head_at && $ref{'id'} eq $head_at;
if ($alternate) {
print "<tr class=\"dark\">\n";
} else {
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
print "<td><i>$ref{'age'}</i></td>\n" .
($curr ? "<td class=\"current_head\">" : "<td>") .
$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
-class => "list name"},esc_html($ref{'name'})) .
"</td>\n" .
"<td class=\"link\">" .
$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
$cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
$cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
"</td>\n" .
"</tr>";
}
if (defined $extra) {
print "<tr>\n" .
"<td colspan=\"3\">$extra</td>\n" .
"</tr>\n";
}
print "</table>\n";
}
# Display a single remote block
sub git_remote_block {
my ($remote, $rdata, $limit, $head) = @_;
my $heads = $rdata->{'heads'};
my $fetch = $rdata->{'fetch'};
my $push = $rdata->{'push'};
my $urls_table = "<table class=\"projects_list\">\n" ;
if (defined $fetch) {
if ($fetch eq $push) {
$urls_table .= format_repo_url("URL", $fetch);
} else {
$urls_table .= format_repo_url("Fetch URL", $fetch);
$urls_table .= format_repo_url("Push URL", $push) if defined $push;
}
} elsif (defined $push) {
$urls_table .= format_repo_url("Push URL", $push);
} else {
$urls_table .= format_repo_url("", "No remote URL");
}
$urls_table .= "</table>\n";
my $dots;
if (defined $limit && $limit < @$heads) {
$dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
}
print $urls_table;
git_heads_body($heads, $head, 0, $limit, $dots);
}
# Display a list of remote names with the respective fetch and push URLs
sub git_remotes_list {
my ($remotedata, $limit) = @_;
print "<table class=\"heads\">\n";
my $alternate = 1;
my @remotes = sort keys %$remotedata;
my $limited = $limit && $limit < @remotes;
$#remotes = $limit - 1 if $limited;
while (my $remote = shift @remotes) {
my $rdata = $remotedata->{$remote};
my $fetch = $rdata->{'fetch'};
my $push = $rdata->{'push'};
if ($alternate) {
print "<tr class=\"dark\">\n";
} else {
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
print "<td>" .
$cgi->a({-href=> href(action=>'remotes', hash=>$remote),
-class=> "list name"},esc_html($remote)) .
"</td>";
print "<td class=\"link\">" .
(defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
" | " .
(defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
"</td>";
print "</tr>\n";
}
if ($limited) {
print "<tr>\n" .
"<td colspan=\"3\">" .
$cgi->a({-href => href(action=>"remotes")}, "...") .
"</td>\n" . "</tr>\n";
}
print "</table>";
}
# Display remote heads grouped by remote, unless there are too many
# remotes, in which case we only display the remote names
sub git_remotes_body {
my ($remotedata, $limit, $head) = @_;
if ($limit and $limit < keys %$remotedata) {
git_remotes_list($remotedata, $limit);
} else {
fill_remote_heads($remotedata);
while (my ($remote, $rdata) = each %$remotedata) {
git_print_section({-class=>"remote", -id=>$remote},
["remotes", $remote, $remote], sub {
git_remote_block($remote, $rdata, $limit, $head);
});
}
}
}
sub git_search_message {
my %co = @_;
my $greptype;
if ($searchtype eq 'commit') {
$greptype = "--grep=";
} elsif ($searchtype eq 'author') {
$greptype = "--author=";
} elsif ($searchtype eq 'committer') {
$greptype = "--committer=";
}
$greptype .= $searchtext;
my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
$greptype, '--regexp-ignore-case',
$search_use_regexp ? '--extended-regexp' : '--fixed-strings');
my $paging_nav = '';
if ($page > 0) {
$paging_nav .=
$cgi->a({-href => href(-replay=>1, page=>undef)},
"first") .
" ⋅ " .
$cgi->a({-href => href(-replay=>1, page=>$page-1),
-accesskey => "p", -title => "Alt-p"}, "prev");
} else {
$paging_nav .= "first ⋅ prev";
}
my $next_link = '';
if ($#commitlist >= 100) {
$next_link =
$cgi->a({-href => href(-replay=>1, page=>$page+1),
-accesskey => "n", -title => "Alt-n"}, "next");
$paging_nav .= " ⋅ $next_link";
} else {
$paging_nav .= " ⋅ next";
}
git_header_html();
git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
git_print_header_div('commit', esc_html($co{'title'}), $hash);
if ($page == 0 && !@commitlist) {
print "<p>No match.</p>\n";
} else {
git_search_grep_body(\@commitlist, 0, 99, $next_link);
}
git_footer_html();
}
sub git_search_changes {
my %co = @_;
local $/ = "\n";
open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
'--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
($search_use_regexp ? '--pickaxe-regex' : ())
or die_error(500, "Open git-log failed");
git_header_html();
git_print_page_nav('','', $hash,$co{'tree'},$hash);
git_print_header_div('commit', esc_html($co{'title'}), $hash);
print "<table class=\"pickaxe search\">\n";
my $alternate = 1;
undef %co;
my @files;
while (my $line = <$fd>) {
chomp $line;
next unless $line;
my %set = parse_difftree_raw_line($line);
if (defined $set{'commit'}) {
# finish previous commit
if (%co) {
print "</td>\n" .
"<td class=\"link\">" .
$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
"commit") .
" | " .
$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
hash_base=>$co{'id'})},
"tree") .
"</td>\n" .
"</tr>\n";
}
if ($alternate) {
print "<tr class=\"dark\">\n";
} else {
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
%co = parse_commit($set{'commit'});
my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
"<td><i>$author</i></td>\n" .
"<td>" .
$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
-class => "list subject"},
chop_and_escape_str($co{'title'}, 50) . "<br/>");
} elsif (defined $set{'to_id'}) {
next if ($set{'to_id'} =~ m/^0{40}$/);
print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
-class => "list"},
"<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
"<br/>\n";
}
}
close $fd;
# finish last commit (warning: repetition!)
if (%co) {
print "</td>\n" .
"<td class=\"link\">" .
$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
"commit") .
" | " .
$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
hash_base=>$co{'id'})},
"tree") .
"</td>\n" .
"</tr>\n";
}
print "</table>\n";
git_footer_html();
}
sub git_search_files {
my %co = @_;
local $/ = "\n";
open my $fd, "-|", git_cmd(), 'grep', '-n', '-z',
$search_use_regexp ? ('-E', '-i') : '-F',
$searchtext, $co{'tree'}
or die_error(500, "Open git-grep failed");
git_header_html();
git_print_page_nav('','', $hash,$co{'tree'},$hash);
git_print_header_div('commit', esc_html($co{'title'}), $hash);
print "<table class=\"grep_search\">\n";
my $alternate = 1;
my $matches = 0;
my $lastfile = '';
my $file_href;
while (my $line = <$fd>) {
chomp $line;
my ($file, $lno, $ltext, $binary);
last if ($matches++ > 1000);
if ($line =~ /^Binary file (.+) matches$/) {
$file = $1;
$binary = 1;
} else {
($file, $lno, $ltext) = split(/\0/, $line, 3);
$file =~ s/^$co{'tree'}://;
}
if ($file ne $lastfile) {
$lastfile and print "</td></tr>\n";
if ($alternate++) {
print "<tr class=\"dark\">\n";
} else {
print "<tr class=\"light\">\n";
}
$file_href = href(action=>"blob", hash_base=>$co{'id'},
file_name=>$file);
print "<td class=\"list\">".
$cgi->a({-href => $file_href, -class => "list"}, esc_path($file));
print "</td><td>\n";
$lastfile = $file;
}
if ($binary) {
print "<div class=\"binary\">Binary file</div>\n";
} else {
$ltext = untabify($ltext);
if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
$ltext = esc_html($1, -nbsp=>1);
$ltext .= '<span class="match">';
$ltext .= esc_html($2, -nbsp=>1);
$ltext .= '</span>';
$ltext .= esc_html($3, -nbsp=>1);
} else {
$ltext = esc_html($ltext, -nbsp=>1);
}
print "<div class=\"pre\">" .
$cgi->a({-href => $file_href.'#l'.$lno,
-class => "linenr"}, sprintf('%4i', $lno)) .
' ' . $ltext . "</div>\n";
}
}
if ($lastfile) {
print "</td></tr>\n";
if ($matches > 1000) {
print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
}
} else {
print "<div class=\"diff nodifferences\">No matches found</div>\n";
}
close $fd;
print "</table>\n";
git_footer_html();
}
sub git_search_grep_body {
my ($commitlist, $from, $to, $extra) = @_;
$from = 0 unless defined $from;
$to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
print "<table class=\"commit_search\">\n";
my $alternate = 1;
for (my $i = $from; $i <= $to; $i++) {
my %co = %{$commitlist->[$i]};
if (!%co) {
next;
}
my $commit = $co{'id'};
if ($alternate) {
print "<tr class=\"dark\">\n";
} else {
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
format_author_html('td', \%co, 15, 5) .
"<td>" .
$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
-class => "list subject"},
chop_and_escape_str($co{'title'}, 50) . "<br/>");
my $comment = $co{'comment'};
foreach my $line (@$comment) {
if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
my ($lead, $match, $trail) = ($1, $2, $3);
$match = chop_str($match, 70, 5, 'center');
my $contextlen = int((80 - length($match))/2);
$contextlen = 30 if ($contextlen > 30);
$lead = chop_str($lead, $contextlen, 10, 'left');
$trail = chop_str($trail, $contextlen, 10, 'right');
$lead = esc_html($lead);
$match = esc_html($match);
$trail = esc_html($trail);
print "$lead<span class=\"match\">$match</span>$trail<br />";
}
}
print "</td>\n" .
"<td class=\"link\">" .
$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
" | " .
$cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
" | " .
$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
print "</td>\n" .
"</tr>\n";
}
if (defined $extra) {
print "<tr>\n" .
"<td colspan=\"3\">$extra</td>\n" .
"</tr>\n";
}
print "</table>\n";
}
## ======================================================================
## ======================================================================
## actions
sub git_project_list {
my $order = $input_params{'order'};
if (defined $order && $order !~ m/none|project|descr|owner|age/) {
die_error(400, "Unknown order parameter");
}
my @list = git_get_projects_list($project_filter, $strict_export);
if (!@list) {
die_error(404, "No projects found");
}
git_header_html();
if (defined $home_text && -f $home_text) {
print "<div class=\"index_include\">\n";
insert_file($home_text);
print "</div>\n";
}
git_project_search_form($searchtext, $search_use_regexp);
git_project_list_body(\@list, $order);
git_footer_html();
}
sub git_forks {
my $order = $input_params{'order'};
if (defined $order && $order !~ m/none|project|descr|owner|age/) {
die_error(400, "Unknown order parameter");
}
my $filter = $project;
$filter =~ s/\.git$//;
my @list = git_get_projects_list($filter);
if (!@list) {
die_error(404, "No forks found");
}
git_header_html();
git_print_page_nav('','');
git_print_header_div('summary', "$project forks");
git_project_list_body(\@list, $order);
git_footer_html();
}
sub git_project_index {
my @projects = git_get_projects_list($project_filter, $strict_export);
if (!@projects) {
die_error(404, "No projects found");
}
print $cgi->header(
-type => 'text/plain',
-charset => 'utf-8',
-content_disposition => 'inline; filename="index.aux"');
foreach my $pr (@projects) {
if (!exists $pr->{'owner'}) {
$pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
}
my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
# quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
$path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
$owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
$path =~ s/ /\+/g;
$owner =~ s/ /\+/g;
print "$path $owner\n";
}
}
sub git_summary {
my $descr = git_get_project_description($project) || "none";
my %co = parse_commit("HEAD");
my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
my $head = $co{'id'};
my $remote_heads = gitweb_check_feature('remote_heads');
my $owner = git_get_project_owner($project);
my $refs = git_get_references();
# These get_*_list functions return one more to allow us to see if
# there are more ...
my @taglist = git_get_tags_list(16);
my @headlist = git_get_heads_list(16);
my %remotedata = $remote_heads ? git_get_remotes_list() : ();
my @forklist;
my $check_forks = gitweb_check_feature('forks');
if ($check_forks) {
# find forks of a project
my $filter = $project;
$filter =~ s/\.git$//;
@forklist = git_get_projects_list($filter);
# filter out forks of forks
@forklist = filter_forks_from_projects_list(\@forklist)
if (@forklist);
}
git_header_html();
git_print_page_nav('summary','', $head);
print "<div class=\"title\"> </div>\n";
print "<table class=\"projects_list\">\n" .
"<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n";
if ($owner and not $omit_owner) {
print "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
}
if (defined $cd{'rfc2822'}) {
print "<tr id=\"metadata_lchange\"><td>last change</td>" .
"<td>".format_timestamp_html(\%cd)."</td></tr>\n";
}
# use per project git URL list in $projectroot/$project/cloneurl
# or make project git URL from git base URL and project name
my $url_tag = "URL";
my @url_list = git_get_project_url_list($project);
@url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
foreach my $git_url (@url_list) {
next unless $git_url;
print format_repo_url($url_tag, $git_url);
$url_tag = "";
}
# Tag cloud
my $show_ctags = gitweb_check_feature('ctags');
if ($show_ctags) {
my $ctags = git_get_project_ctags($project);
if (%$ctags) {
# without ability to add tags, don't show if there are none
my $cloud = git_populate_project_tagcloud($ctags);
print "<tr id=\"metadata_ctags\">" .
"<td>content tags</td>" .
"<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
"</tr>\n";
}
}
print "</table>\n";
# If XSS prevention is on, we don't include README.html.
# TODO: Allow a readme in some safe format.
if (!$prevent_xss && -s "$projectroot/$project/README.html") {
print "<div class=\"title\">readme</div>\n" .
"<div class=\"readme\">\n";
insert_file("$projectroot/$project/README.html");
print "\n</div>\n"; # class="readme"
}
# we need to request one more than 16 (0..15) to check if
# those 16 are all
my @commitlist = $head ? parse_commits($head, 17) : ();
if (@commitlist) {
git_print_header_div('shortlog');
git_shortlog_body(\@commitlist, 0, 15, $refs,
$#commitlist <= 15 ? undef :
$cgi->a({-href => href(action=>"shortlog")}, "..."));
}
if (@taglist) {
git_print_header_div('tags');
git_tags_body(\@taglist, 0, 15,
$#taglist <= 15 ? undef :
$cgi->a({-href => href(action=>"tags")}, "..."));
}
if (@headlist) {
git_print_header_div('heads');
git_heads_body(\@headlist, $head, 0, 15,
$#headlist <= 15 ? undef :
$cgi->a({-href => href(action=>"heads")}, "..."));
}
if (%remotedata) {
git_print_header_div('remotes');
git_remotes_body(\%remotedata, 15, $head);
}
if (@forklist) {
git_print_header_div('forks');
git_project_list_body(\@forklist, 'age', 0, 15,
$#forklist <= 15 ? undef :
$cgi->a({-href => href(action=>"forks")}, "..."),
'no_header');
}
git_footer_html();
}
sub git_tag {
my %tag = parse_tag($hash);
if (! %tag) {
die_error(404, "Unknown tag object");
}
my $head = git_get_head_hash($project);
git_header_html();
git_print_page_nav('','', $head,undef,$head);
git_print_header_div('commit', esc_html($tag{'name'}), $hash);
print "<div class=\"title_text\">\n" .
"<table class=\"object_header\">\n" .
"<tr>\n" .
"<td>object</td>\n" .
"<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
$tag{'object'}) . "</td>\n" .
"<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
$tag{'type'}) . "</td>\n" .
"</tr>\n";
if (defined($tag{'author'})) {
git_print_authorship_rows(\%tag, 'author');
}
print "</table>\n\n" .
"</div>\n";
print "<div class=\"page_body\">";
my $comment = $tag{'comment'};
foreach my $line (@$comment) {
chomp $line;
print esc_html($line, -nbsp=>1) . "<br/>\n";
}
print "</div>\n";
git_footer_html();
}
sub git_blame_common {
my $format = shift || 'porcelain';
if ($format eq 'porcelain' && $input_params{'javascript'}) {
$format = 'incremental';
$action = 'blame_incremental'; # for page title etc
}
# permissions
gitweb_check_feature('blame')
or die_error(403, "Blame view not allowed");
# error checking
die_error(400, "No file name given") unless $file_name;
$hash_base ||= git_get_head_hash($project);
die_error(404, "Couldn't find base commit") unless $hash_base;
my %co = parse_commit($hash_base)
or die_error(404, "Commit not found");
my $ftype = "blob";
if (!defined $hash) {
$hash = git_get_hash_by_path($hash_base, $file_name, "blob")
or die_error(404, "Error looking up file");
} else {
$ftype = git_get_type($hash);
if ($ftype !~ "blob") {
die_error(400, "Object is not a blob");
}
}
my $fd;
if ($format eq 'incremental') {
# get file contents (as base)
open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
or die_error(500, "Open git-cat-file failed");
} elsif ($format eq 'data') {
# run git-blame --incremental
open $fd, "-|", git_cmd(), "blame", "--incremental",
$hash_base, "--", $file_name
or die_error(500, "Open git-blame --incremental failed");
} else {
# run git-blame --porcelain
open $fd, "-|", git_cmd(), "blame", '-p',
$hash_base, '--', $file_name
or die_error(500, "Open git-blame --porcelain failed");
}
binmode $fd, ':utf8';
# incremental blame data returns early
if ($format eq 'data') {
print $cgi->header(
-type=>"text/plain", -charset => "utf-8",
-status=> "200 OK");
local $| = 1; # output autoflush
while (my $line = <$fd>) {
print to_utf8($line);
}
close $fd
or print "ERROR $!\n";
print 'END';
if (defined $t0 && gitweb_check_feature('timed')) {
print ' '.
tv_interval($t0, [ gettimeofday() ]).
' '.$number_of_git_cmds;
}
print "\n";
return;
}
# page header
git_header_html();
my $formats_nav =
$cgi->a({-href => href(action=>"blob", -replay=>1)},
"blob") .
" | ";
if ($format eq 'incremental') {
$formats_nav .=
$cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
"blame") . " (non-incremental)";
} else {
$formats_nav .=
$cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
"blame") . " (incremental)";
}
$formats_nav .=
" | " .
$cgi->a({-href => href(action=>"history", -replay=>1)},
"history") .
" | " .
$cgi->a({-href => href(action=>$action, file_name=>$file_name)},
"HEAD");
git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
git_print_page_path($file_name, $ftype, $hash_base);
# page body
if ($format eq 'incremental') {
print "<noscript>\n<div class=\"error\"><center><b>\n".
"This page requires JavaScript to run.\n Use ".
$cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
'this page').
" instead.\n".
"</b></center></div>\n</noscript>\n";
print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
}
print qq!<div class="page_body">\n!;
print qq!<div id="progress_info">... / ...</div>\n!
if ($format eq 'incremental');
print qq!<table id="blame_table" class="blame" width="100%">\n!.
#qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
qq!<thead>\n!.
qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
qq!</thead>\n!.
qq!<tbody>\n!;
my @rev_color = qw(light dark);
my $num_colors = scalar(@rev_color);
my $current_color = 0;
if ($format eq 'incremental') {
my $color_class = $rev_color[$current_color];
#contents of a file
my $linenr = 0;
LINE:
while (my $line = <$fd>) {
chomp $line;
$linenr++;
print qq!<tr id="l$linenr" class="$color_class">!.
qq!<td class="sha1"><a href=""> </a></td>!.
qq!<td class="linenr">!.
qq!<a class="linenr" href="">$linenr</a></td>!;
print qq!<td class="pre">! . esc_html($line) . "</td>\n";
print qq!</tr>\n!;
}
} else { # porcelain, i.e. ordinary blame
my %metainfo = (); # saves information about commits
# blame data
LINE:
while (my $line = <$fd>) {
chomp $line;
# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
# no <lines in group> for subsequent lines in group of lines
my ($full_rev, $orig_lineno, $lineno, $group_size) =
($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
if (!exists $metainfo{$full_rev}) {
$metainfo{$full_rev} = { 'nprevious' => 0 };
}
my $meta = $metainfo{$full_rev};
my $data;
while ($data = <$fd>) {
chomp $data;
last if ($data =~ s/^\t//); # contents of line
if ($data =~ /^(\S+)(?: (.*))?$/) {
$meta->{$1} = $2 unless exists $meta->{$1};
}
if ($data =~ /^previous /) {
$meta->{'nprevious'}++;
}
}
my $short_rev = substr($full_rev, 0, 8);
my $author = $meta->{'author'};
my %date =
parse_date($meta->{'author-time'}, $meta->{'author-tz'});
my $date = $date{'iso-tz'};
if ($group_size) {
$current_color = ($current_color + 1) % $num_colors;
}
my $tr_class = $rev_color[$current_color];
$tr_class .= ' boundary' if (exists $meta->{'boundary'});
$tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
$tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
if ($group_size) {
print "<td class=\"sha1\"";
print " title=\"". esc_html($author) . ", $date\"";
print " rowspan=\"$group_size\"" if ($group_size > 1);
print ">";
print $cgi->a({-href => href(action=>"commit",
hash=>$full_rev,
file_name=>$file_name)},
esc_html($short_rev));
if ($group_size >= 2) {
my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
if (@author_initials) {
print "<br />" .
esc_html(join('', @author_initials));
# or join('.', ...)
}
}
print "</td>\n";
}
# 'previous' <sha1 of parent commit> <filename at commit>
if (exists $meta->{'previous'} &&
$meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
$meta->{'parent'} = $1;
$meta->{'file_parent'} = unquote($2);
}
my $linenr_commit =
exists($meta->{'parent'}) ?
$meta->{'parent'} : $full_rev;
my $linenr_filename =
exists($meta->{'file_parent'}) ?
$meta->{'file_parent'} : unquote($meta->{'filename'});
my $blamed = href(action => 'blame',
file_name => $linenr_filename,
hash_base => $linenr_commit);
print "<td class=\"linenr\">";
print $cgi->a({ -href => "$blamed#l$orig_lineno",
-class => "linenr" },
esc_html($lineno));
print "</td>";
print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
print "</tr>\n";
} # end while
}
# footer
print "</tbody>\n".
"</table>\n"; # class="blame"
print "</div>\n"; # class="blame_body"
close $fd
or print "Reading blob failed\n";
git_footer_html();
}
sub git_blame {
git_blame_common();
}
sub git_blame_incremental {
git_blame_common('incremental');
}
sub git_blame_data {
git_blame_common('data');
}
sub git_tags {
my $head = git_get_head_hash($project);
git_header_html();
git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
git_print_header_div('summary', $project);
my @tagslist = git_get_tags_list();
if (@tagslist) {
git_tags_body(\@tagslist);
}
git_footer_html();
}
sub git_heads {
my $head = git_get_head_hash($project);
git_header_html();
git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
git_print_header_div('summary', $project);
my @headslist = git_get_heads_list();
if (@headslist) {
git_heads_body(\@headslist, $head);
}
git_footer_html();
}
# used both for single remote view and for list of all the remotes
sub git_remotes {
gitweb_check_feature('remote_heads')
or die_error(403, "Remote heads view is disabled");
my $head = git_get_head_hash($project);
my $remote = $input_params{'hash'};
my $remotedata = git_get_remotes_list($remote);
die_error(500, "Unable to get remote information") unless defined $remotedata;
unless (%$remotedata) {
die_error(404, defined $remote ?
"Remote $remote not found" :
"No remotes found");
}
git_header_html(undef, undef, -action_extra => $remote);
git_print_page_nav('', '', $head, undef, $head,
format_ref_views($remote ? '' : 'remotes'));
fill_remote_heads($remotedata);
if (defined $remote) {
git_print_header_div('remotes', "$remote remote for $project");
git_remote_block($remote, $remotedata->{$remote}, undef, $head);
} else {
git_print_header_div('summary', "$project remotes");
git_remotes_body($remotedata, undef, $head);
}
git_footer_html();
}
sub git_blob_plain {
my $type = shift;
my $expires;
if (!defined $hash) {
if (defined $file_name) {
my $base = $hash_base || git_get_head_hash($project);
$hash = git_get_hash_by_path($base, $file_name, "blob")
or die_error(404, "Cannot find file");
} else {
die_error(400, "No file name defined");
}
} elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
# blobs defined by non-textual hash id's can be cached
$expires = "+1d";
}
open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
or die_error(500, "Open git-cat-file blob '$hash' failed");
$type = blob_contenttype($fd, $file_name, $type);
my $save_as = "$hash";
if (defined $file_name) {
$save_as = $file_name;
} elsif ($type =~ m/^text\//) {
$save_as .= '.txt';
}
my $sandbox = $prevent_xss &&
$type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
if ($prevent_xss &&
($type =~ m!^text/[a-z]+\b(.*)$! ||
($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
my $rest = $1;
$rest = defined $rest ? $rest : '';
$type = "text/plain$rest";
}
print $cgi->header(
-type => $type,
-expires => $expires,
-content_disposition =>
($sandbox ? 'attachment' : 'inline')
. '; filename="' . $save_as . '"');
local $/ = undef;
binmode STDOUT, ':raw';
print <$fd>;
binmode STDOUT, ':utf8'; close $fd;
}
sub git_blob {
my $expires;
if (!defined $hash) {
if (defined $file_name) {
my $base = $hash_base || git_get_head_hash($project);
$hash = git_get_hash_by_path($base, $file_name, "blob")
or die_error(404, "Cannot find file");
} else {
die_error(400, "No file name defined");
}
} elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
$expires = "+1d";
}
my $have_blame = gitweb_check_feature('blame');
open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
or die_error(500, "Couldn't cat $file_name, $hash");
my $mimetype = blob_mimetype($fd, $file_name);
if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
close $fd;
return git_blob_plain($mimetype);
}
$have_blame &&= ($mimetype =~ m!^text/!);
my $highlight = gitweb_check_feature('highlight');
my $syntax = guess_file_syntax($highlight, $file_name);
$fd = run_highlighter($fd, $highlight, $syntax);
git_header_html(undef, $expires);
my $formats_nav = '';
if (defined $hash_base && (my %co = parse_commit($hash_base))) {
if (defined $file_name) {
if ($have_blame) {
$formats_nav .=
$cgi->a({-href => href(action=>"blame", -replay=>1)},
"blame") .
" | ";
}
$formats_nav .=
$cgi->a({-href => href(action=>"history", -replay=>1)},
"history") .
" | " .
$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
"raw") .
" | " .
$cgi->a({-href => href(action=>"blob",
hash_base=>"HEAD", file_name=>$file_name)},
"HEAD");
} else {
$formats_nav .=
$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
"raw");
}
git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
} else {
print "<div class=\"page_nav\">\n" .
"<br/><br/></div>\n" .
"<div class=\"title\">".esc_html($hash)."</div>\n";
}
git_print_page_path($file_name, "blob", $hash_base);
print "<div class=\"page_body\">\n";
if ($mimetype =~ m!^image/!) {
print qq!<img class="blob" type="!.esc_attr($mimetype).qq!"!;
if ($file_name) {
print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
}
print qq! src="! .
href(action=>"blob_plain", hash=>$hash,
hash_base=>$hash_base, file_name=>$file_name) .
qq!" />\n!;
} else {
my $nr;
while (my $line = <$fd>) {
chomp $line;
$nr++;
$line = untabify($line);
printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
$nr, esc_attr(href(-replay => 1)), $nr, $nr,
$highlight ? sanitize($line) : esc_html($line, -nbsp=>1);
}
}
close $fd
or print "Reading blob failed.\n";
print "</div>";
git_footer_html();
}
sub git_tree {
if (!defined $hash_base) {
$hash_base = "HEAD";
}
if (!defined $hash) {
if (defined $file_name) {
$hash = git_get_hash_by_path($hash_base, $file_name, "tree");
} else {
$hash = $hash_base;
}
}
die_error(404, "No such tree") unless defined($hash);
my $show_sizes = gitweb_check_feature('show-sizes');
my $have_blame = gitweb_check_feature('blame');
my @entries = ();
{
local $/ = "\0";
open my $fd, "-|", git_cmd(), "ls-tree", '-z',
($show_sizes ? '-l' : ()), @extra_options, $hash
or die_error(500, "Open git-ls-tree failed");
@entries = map { chomp; $_ } <$fd>;
close $fd
or die_error(404, "Reading tree failed");
}
my $refs = git_get_references();
my $ref = format_ref_marker($refs, $hash_base);
git_header_html();
my $basedir = '';
if (defined $hash_base && (my %co = parse_commit($hash_base))) {
my @views_nav = ();
if (defined $file_name) {
push @views_nav,
$cgi->a({-href => href(action=>"history", -replay=>1)},
"history"),
$cgi->a({-href => href(action=>"tree",
hash_base=>"HEAD", file_name=>$file_name)},
"HEAD"),
}
my $snapshot_links = format_snapshot_links($hash);
if (defined $snapshot_links) {
push @views_nav, $snapshot_links;
}
git_print_page_nav('tree','', $hash_base, undef, undef,
join(' | ', @views_nav));
git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
} else {
undef $hash_base;
print "<div class=\"page_nav\">\n";
print "<br/><br/></div>\n";
print "<div class=\"title\">".esc_html($hash)."</div>\n";
}
if (defined $file_name) {
$basedir = $file_name;
if ($basedir ne '' && substr($basedir, -1) ne '/') {
$basedir .= '/';
}
git_print_page_path($file_name, 'tree', $hash_base);
}
print "<div class=\"page_body\">\n";
print "<table class=\"tree\">\n";
my $alternate = 1;
if (defined $hash_base &&
defined $file_name && $file_name =~ m![^/]+$!) {
if ($alternate) {
print "<tr class=\"dark\">\n";
} else {
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
my $up = $file_name;
$up =~ s!/?[^/]+$!!;
undef $up unless $up;
print '<td class="mode">' . mode_str('040000') . "</td>\n";
print '<td class="size"> </td>'."\n" if $show_sizes;
print '<td class="list">';
print $cgi->a({-href => href(action=>"tree",
hash_base=>$hash_base,
file_name=>$up)},
"..");
print "</td>\n";
print "<td class=\"link\"></td>\n";
print "</tr>\n";
}
foreach my $line (@entries) {
my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
if ($alternate) {
print "<tr class=\"dark\">\n";
} else {
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
print "</tr>\n";
}
print "</table>\n" .
"</div>";
git_footer_html();
}
sub sanitize_for_filename {
my $name = shift;
$name =~ s!/!-!g;
$name =~ s/[^[:alnum:]_.-]//g;
return $name;
}
sub snapshot_name {
my ($project, $hash) = @_;
my $name = to_utf8($project);
$name =~ s,([^/])/*\.git$,$1,;
$name = sanitize_for_filename(basename($name));
my $ver = $hash;
if ($hash =~ /^[0-9a-fA-F]+$/) {
my $full_hash = git_get_full_hash($project, $hash);
if ($full_hash =~ /^$hash/ && length($hash) > 7) {
$ver = git_get_short_hash($project, $hash);
}
} elsif ($hash =~ m!^refs/tags/(.*)$!) {
$ver = $1;
} else {
my $strip_refs = join '|', map { quotemeta } get_branch_refs();
if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
my $ref_dir = (defined $1) ? $1 : '';
$ver = $2;
$ref_dir = sanitize_for_filename($ref_dir);
if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
$ver = $ref_dir . '-' . $ver;
}
}
$ver .= '-' . git_get_short_hash($project, $hash);
}
$ver =~ s!/!.!g;
$ver =~ s/[^[:alnum:]_.-]//g;
$name = "$name-$ver";
return wantarray ? ($name, $name) : $name;
}
sub exit_if_unmodified_since {
my ($latest_epoch) = @_;
our $cgi;
my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
if (defined $if_modified) {
my $since;
if (eval { require HTTP::Date; 1; }) {
$since = HTTP::Date::str2time($if_modified);
} elsif (eval { require Time::ParseDate; 1; }) {
$since = Time::ParseDate::parsedate($if_modified, GMT => 1);
}
if (defined $since && $latest_epoch <= $since) {
my %latest_date = parse_date($latest_epoch);
print $cgi->header(
-last_modified => $latest_date{'rfc2822'},
-status => '304 Not Modified');
goto DONE_GITWEB;
}
}
}
sub git_snapshot {
my $format = $input_params{'snapshot_format'};
if (!@snapshot_fmts) {
die_error(403, "Snapshots not allowed");
}
$format ||= $snapshot_fmts[0];
if ($format !~ m/^[a-z0-9]+$/) {
die_error(400, "Invalid snapshot format parameter");
} elsif (!exists($known_snapshot_formats{$format})) {
die_error(400, "Unknown snapshot format");
} elsif ($known_snapshot_formats{$format}{'disabled'}) {
die_error(403, "Snapshot format not allowed");
} elsif (!grep($_ eq $format, @snapshot_fmts)) {
die_error(403, "Unsupported snapshot format");
}
my $type = git_get_type("$hash^{}");
if (!$type) {
die_error(404, 'Object does not exist');
} elsif ($type eq 'blob') {
die_error(400, 'Object is not a tree-ish');
}
my ($name, $prefix) = snapshot_name($project, $hash);
my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
my %co = parse_commit($hash);
exit_if_unmodified_since($co{'committer_epoch'}) if %co;
my $cmd = quote_command(
git_cmd(), 'archive',
"--format=$known_snapshot_formats{$format}{'format'}",
"--prefix=$prefix/", $hash);
if (exists $known_snapshot_formats{$format}{'compressor'}) {
$cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
}
$filename =~ s/(["\\])/\\$1/g;
my %latest_date;
if (%co) {
%latest_date = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
}
print $cgi->header(
-type => $known_snapshot_formats{$format}{'type'},
-content_disposition => 'inline; filename="' . $filename . '"',
%co ? (-last_modified => $latest_date{'rfc2822'}) : (),
-status => '200 OK');
open my $fd, "-|", $cmd
or die_error(500, "Execute git-archive failed");
binmode STDOUT, ':raw';
print <$fd>;
binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
close $fd;
}
sub git_log_generic {
my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
my $head = git_get_head_hash($project);
if (!defined $base) {
$base = $head;
}
if (!defined $page) {
$page = 0;
}
my $refs = git_get_references();
my $commit_hash = $base;
if (defined $parent) {
$commit_hash = "$parent..$base";
}
my @commitlist =
parse_commits($commit_hash, 101, (100 * $page),
defined $file_name ? ($file_name, "--full-history") : ());
my $ftype;
if (!defined $file_hash && defined $file_name) {
# some commits could have deleted file in question,
# and not have it in tree, but one of them has to have it
for (my $i = 0; $i < @commitlist; $i++) {
$file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
last if defined $file_hash;
}
}
if (defined $file_hash) {
$ftype = git_get_type($file_hash);
}
if (defined $file_name && !defined $ftype) {
die_error(500, "Unknown type of object");
}
my %co;
if (defined $file_name) {
%co = parse_commit($base)
or die_error(404, "Unknown commit object");
}
my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
my $next_link = '';
if ($#commitlist >= 100) {
$next_link =
$cgi->a({-href => href(-replay=>1, page=>$page+1),
-accesskey => "n", -title => "Alt-n"}, "next");
}
my $patch_max = gitweb_get_feature('patches');
if ($patch_max && !defined $file_name) {
if ($patch_max < 0 || @commitlist <= $patch_max) {
$paging_nav .= " ⋅ " .
$cgi->a({-href => href(action=>"patches", -replay=>1)},
"patches");
}
}
git_header_html();
git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
if (defined $file_name) {
git_print_header_div('commit', esc_html($co{'title'}), $base);
} else {
git_print_header_div('summary', $project)
}
git_print_page_path($file_name, $ftype, $hash_base)
if (defined $file_name);
$body_subr->(\@commitlist, 0, 99, $refs, $next_link,
$file_name, $file_hash, $ftype);
git_footer_html();
}
sub git_log {
git_log_generic('log', \&git_log_body,
$hash, $hash_parent);
}
sub git_commit {
$hash ||= $hash_base || "HEAD";
my %co = parse_commit($hash)
or die_error(404, "Unknown commit object");
my $parent = $co{'parent'};
my $parents = $co{'parents'}; # listref
# we need to prepare $formats_nav before any parameter munging
my $formats_nav;
if (!defined $parent) {
# --root commitdiff
$formats_nav .= '(initial)';
} elsif (@$parents == 1) {
# single parent commit
$formats_nav .=
'(parent: ' .
$cgi->a({-href => href(action=>"commit",
hash=>$parent)},
esc_html(substr($parent, 0, 7))) .
')';
} else {
# merge commit
$formats_nav .=
'(merge: ' .
join(' ', map {
$cgi->a({-href => href(action=>"commit",
hash=>$_)},
esc_html(substr($_, 0, 7)));
} @$parents ) .
')';
}
if (gitweb_check_feature('patches') && @$parents <= 1) {
$formats_nav .= " | " .
$cgi->a({-href => href(action=>"patch", -replay=>1)},
"patch");
}
if (!defined $parent) {
$parent = "--root";
}
my @difftree;
open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
@diff_opts,
(@$parents <= 1 ? $parent : '-c'),
$hash, "--"
or die_error(500, "Open git-diff-tree failed");
@difftree = map { chomp; $_ } <$fd>;
close $fd or die_error(404, "Reading git-diff-tree failed");
# non-textual hash id's can be cached
my $expires;
if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
$expires = "+1d";
}
my $refs = git_get_references();
my $ref = format_ref_marker($refs, $co{'id'});
git_header_html(undef, $expires);
git_print_page_nav('commit', '',
$hash, $co{'tree'}, $hash,
$formats_nav);
if (defined $co{'parent'}) {
git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
} else {
git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
}
print "<div class=\"title_text\">\n" .
"<table class=\"object_header\">\n";
git_print_authorship_rows(\%co);
print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
print "<tr>" .
"<td>tree</td>" .
"<td class=\"sha1\">" .
$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
class => "list"}, $co{'tree'}) .
"</td>" .
"<td class=\"link\">" .
$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
"tree");
my $snapshot_links = format_snapshot_links($hash);
if (defined $snapshot_links) {
print " | " . $snapshot_links;
}
print "</td>" .
"</tr>\n";
foreach my $par (@$parents) {
print "<tr>" .
"<td>parent</td>" .
"<td class=\"sha1\">" .
$cgi->a({-href => href(action=>"commit", hash=>$par),
class => "list"}, $par) .
"</td>" .
"<td class=\"link\">" .
$cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
" | " .
$cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
"</td>" .
"</tr>\n";
}
print "</table>".
"</div>\n";
print "<div class=\"page_body\">\n";
git_print_log($co{'comment'});
print "</div>\n";
git_difftree_body(\@difftree, $hash, @$parents);
git_footer_html();
}
sub git_object {
# object is defined by:
# - hash or hash_base alone
# - hash_base and file_name
my $type;
# - hash or hash_base alone
if ($hash || ($hash_base && !defined $file_name)) {
my $object_id = $hash || $hash_base;
open my $fd, "-|", quote_command(
git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
or die_error(404, "Object does not exist");
$type = <$fd>;
defined $type && chomp $type;
close $fd
or die_error(404, "Object does not exist");
# - hash_base and file_name
} elsif ($hash_base && defined $file_name) {
$file_name =~ s,/+$,,;
system(git_cmd(), "cat-file", '-e', $hash_base) == 0
or die_error(404, "Base object does not exist");
# here errors should not happen
open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
or die_error(500, "Open git-ls-tree failed");
my $line = <$fd>;
close $fd;
#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
die_error(404, "File or directory for given base does not exist");
}
$type = $2;
$hash = $3;
} else {
die_error(400, "Not enough information to find object");
}
print $cgi->redirect(-uri => href(action=>$type, -full=>1,
hash=>$hash, hash_base=>$hash_base,
file_name=>$file_name),
-status => '302 Found');
}
sub git_blobdiff {
my $format = shift || 'html';
my $diff_style = $input_params{'diff_style'} || 'inline';
my $fd;
my @difftree;
my %diffinfo;
my $expires;
# preparing $fd and %diffinfo for git_patchset_body
# new style URI
if (defined $hash_base && defined $hash_parent_base) {
if (defined $file_name) {
# read raw output
open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
$hash_parent_base, $hash_base,
"--", (defined $file_parent ? $file_parent : ()), $file_name
or die_error(500, "Open git-diff-tree failed");
@difftree = map { chomp; $_ } <$fd>;
close $fd
or die_error(404, "Reading git-diff-tree failed");
@difftree
or die_error(404, "Blob diff not found");
} elsif (defined $hash &&
$hash =~ /[0-9a-fA-F]{40}/) {
# try to find filename from $hash
# read filtered raw output
open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
$hash_parent_base, $hash_base, "--"
or die_error(500, "Open git-diff-tree failed");
@difftree =
# ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
# $hash == to_id
grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
map { chomp; $_ } <$fd>;
close $fd
or die_error(404, "Reading git-diff-tree failed");
@difftree
or die_error(404, "Blob diff not found");
} else {
die_error(400, "Missing one of the blob diff parameters");
}
if (@difftree > 1) {
die_error(400, "Ambiguous blob diff specification");
}
%diffinfo = parse_difftree_raw_line($difftree[0]);
$file_parent ||= $diffinfo{'from_file'} || $file_name;
$file_name ||= $diffinfo{'to_file'};
$hash_parent ||= $diffinfo{'from_id'};
$hash ||= $diffinfo{'to_id'};
# non-textual hash id's can be cached
if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
$hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
$expires = '+1d';
}
# open patch output
open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
'-p', ($format eq 'html' ? "--full-index" : ()),
$hash_parent_base, $hash_base,
"--", (defined $file_parent ? $file_parent : ()), $file_name
or die_error(500, "Open git-diff-tree failed");
}
# old/legacy style URI -- not generated anymore since 1.4.3.
if (!%diffinfo) {
die_error('404 Not Found', "Missing one of the blob diff parameters")
}
# header
if ($format eq 'html') {
my $formats_nav =
$cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
"raw");
$formats_nav .= diff_style_nav($diff_style);
git_header_html(undef, $expires);
if (defined $hash_base && (my %co = parse_commit($hash_base))) {
git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
} else {
print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
}
if (defined $file_name) {
git_print_page_path($file_name, "blob", $hash_base);
} else {
print "<div class=\"page_path\"></div>\n";
}
} elsif ($format eq 'plain') {
print $cgi->header(
-type => 'text/plain',
-charset => 'utf-8',
-expires => $expires,
-content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
print "X-Git-Url: " . $cgi->self_url() . "\n\n";
} else {
die_error(400, "Unknown blobdiff format");
}
# patch
if ($format eq 'html') {
print "<div class=\"page_body\">\n";
git_patchset_body($fd, $diff_style,
[ \%diffinfo ], $hash_base, $hash_parent_base);
close $fd;
print "</div>\n"; # class="page_body"
git_footer_html();
} else {
while (my $line = <$fd>) {
$line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
$line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
print $line;
last if $line =~ m!^\+\+\+!;
}
local $/ = undef;
print <$fd>;
close $fd;
}
}
sub git_blobdiff_plain {
git_blobdiff('plain');
}
# assumes that it is added as later part of already existing navigation,
# so it returns "| foo | bar" rather than just "foo | bar"
sub diff_style_nav {
my ($diff_style, $is_combined) = @_;
$diff_style ||= 'inline';
return "" if ($is_combined);
my @styles = (inline => 'inline', 'sidebyside' => 'side by side');
my %styles = @styles;
@styles =
@styles[ map { $_ * 2 } 0..$#styles/2 ];
return join '',
map { " | ".$_ }
map {
$_ eq $diff_style ? $styles{$_} :
$cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_})
} @styles;
}
sub git_commitdiff {
my %params = @_;
my $format = $params{-format} || 'html';
my $diff_style = $input_params{'diff_style'} || 'inline';
my ($patch_max) = gitweb_get_feature('patches');
if ($format eq 'patch') {
die_error(403, "Patch view not allowed") unless $patch_max;
}
$hash ||= $hash_base || "HEAD";
my %co = parse_commit($hash)
or die_error(404, "Unknown commit object");
# choose format for commitdiff for merge
if (! defined $hash_parent && @{$co{'parents'}} > 1) {
$hash_parent = '--cc';
}
# we need to prepare $formats_nav before almost any parameter munging
my $formats_nav;
if ($format eq 'html') {
$formats_nav =
$cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
"raw");
if ($patch_max && @{$co{'parents'}} <= 1) {
$formats_nav .= " | " .
$cgi->a({-href => href(action=>"patch", -replay=>1)},
"patch");
}
$formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
if (defined $hash_parent &&
$hash_parent ne '-c' && $hash_parent ne '--cc') {
# commitdiff with two commits given
my $hash_parent_short = $hash_parent;
if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
$hash_parent_short = substr($hash_parent, 0, 7);
}
$formats_nav .=
' (from';
for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
if ($co{'parents'}[$i] eq $hash_parent) {
$formats_nav .= ' parent ' . ($i+1);
last;
}
}
$formats_nav .= ': ' .
$cgi->a({-href => href(-replay=>1,
hash=>$hash_parent, hash_base=>undef)},
esc_html($hash_parent_short)) .
')';
} elsif (!$co{'parent'}) {
# --root commitdiff
$formats_nav .= ' (initial)';
} elsif (scalar @{$co{'parents'}} == 1) {
# single parent commit
$formats_nav .=
' (parent: ' .
$cgi->a({-href => href(-replay=>1,
hash=>$co{'parent'}, hash_base=>undef)},
esc_html(substr($co{'parent'}, 0, 7))) .
')';
} else {
# merge commit
if ($hash_parent eq '--cc') {
$formats_nav .= ' | ' .
$cgi->a({-href => href(-replay=>1,
hash=>$hash, hash_parent=>'-c')},
'combined');
} else { # $hash_parent eq '-c'
$formats_nav .= ' | ' .
$cgi->a({-href => href(-replay=>1,
hash=>$hash, hash_parent=>'--cc')},
'compact');
}
$formats_nav .=
' (merge: ' .
join(' ', map {
$cgi->a({-href => href(-replay=>1,
hash=>$_, hash_base=>undef)},
esc_html(substr($_, 0, 7)));
} @{$co{'parents'}} ) .
')';
}
}
my $hash_parent_param = $hash_parent;
if (!defined $hash_parent_param) {
# --cc for multiple parents, --root for parentless
$hash_parent_param =
@{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
}
# read commitdiff
my $fd;
my @difftree;
if ($format eq 'html') {
open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
"--no-commit-id", "--patch-with-raw", "--full-index",
$hash_parent_param, $hash, "--"
or die_error(500, "Open git-diff-tree failed");
while (my $line = <$fd>) {
chomp $line;
# empty line ends raw part of diff-tree output
last unless $line;
push @difftree, scalar parse_difftree_raw_line($line);
}
} elsif ($format eq 'plain') {
open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
'-p', $hash_parent_param, $hash, "--"
or die_error(500, "Open git-diff-tree failed");
} elsif ($format eq 'patch') {
# For commit ranges, we limit the output to the number of
# patches specified in the 'patches' feature.
# For single commits, we limit the output to a single patch,
# diverging from the git-format-patch default.
my @commit_spec = ();
if ($hash_parent) {
if ($patch_max > 0) {
push @commit_spec, "-$patch_max";
}
push @commit_spec, '-n', "$hash_parent..$hash";
} else {
if ($params{-single}) {
push @commit_spec, '-1';
} else {
if ($patch_max > 0) {
push @commit_spec, "-$patch_max";
}
push @commit_spec, "-n";
}
push @commit_spec, '--root', $hash;
}
open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
'--encoding=utf8', '--stdout', @commit_spec
or die_error(500, "Open git-format-patch failed");
} else {
die_error(400, "Unknown commitdiff format");
}
# non-textual hash id's can be cached
my $expires;
if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
$expires = "+1d";
}
# write commit message
if ($format eq 'html') {
my $refs = git_get_references();
my $ref = format_ref_marker($refs, $co{'id'});
git_header_html(undef, $expires);
git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
print "<div class=\"title_text\">\n" .
"<table class=\"object_header\">\n";
git_print_authorship_rows(\%co);
print "</table>".
"</div>\n";
print "<div class=\"page_body\">\n";
if (@{$co{'comment'}} > 1) {
print "<div class=\"log\">\n";
git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
print "</div>\n"; # class="log"
}
} elsif ($format eq 'plain') {
my $refs = git_get_references("tags");
my $tagname = git_get_rev_name_tags($hash);
my $filename = basename($project) . "-$hash.patch";
print $cgi->header(
-type => 'text/plain',
-charset => 'utf-8',
-expires => $expires,
-content_disposition => 'inline; filename="' . "$filename" . '"');
my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
print "From: " . to_utf8($co{'author'}) . "\n";
print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
print "Subject: " . to_utf8($co{'title'}) . "\n";
print "X-Git-Tag: $tagname\n" if $tagname;
print "X-Git-Url: " . $cgi->self_url() . "\n\n";
foreach my $line (@{$co{'comment'}}) {
print to_utf8($line) . "\n";
}
print "---\n\n";
} elsif ($format eq 'patch') {
my $filename = basename($project) . "-$hash.patch";
print $cgi->header(
-type => 'text/plain',
-charset => 'utf-8',
-expires => $expires,
-content_disposition => 'inline; filename="' . "$filename" . '"');
}
# write patch
if ($format eq 'html') {
my $use_parents = !defined $hash_parent ||
$hash_parent eq '-c' || $hash_parent eq '--cc';
git_difftree_body(\@difftree, $hash,
$use_parents ? @{$co{'parents'}} : $hash_parent);
print "<br/>\n";
git_patchset_body($fd, $diff_style,
\@difftree, $hash,
$use_parents ? @{$co{'parents'}} : $hash_parent);
close $fd;
print "</div>\n"; # class="page_body"
git_footer_html();
} elsif ($format eq 'plain') {
local $/ = undef;
print <$fd>;
close $fd
or print "Reading git-diff-tree failed\n";
} elsif ($format eq 'patch') {
local $/ = undef;
print <$fd>;
close $fd
or print "Reading git-format-patch failed\n";
}
}
sub git_commitdiff_plain {
git_commitdiff(-format => 'plain');
}
# format-patch-style patches
sub git_patch {
git_commitdiff(-format => 'patch', -single => 1);
}
sub git_patches {
git_commitdiff(-format => 'patch');
}
sub git_history {
git_log_generic('history', \&git_history_body,
$hash_base, $hash_parent_base,
$file_name, $hash);
}
sub git_search {
$searchtype ||= 'commit';
# check if appropriate features are enabled
gitweb_check_feature('search')
or die_error(403, "Search is disabled");
if ($searchtype eq 'pickaxe') {
# pickaxe may take all resources of your box and run for several minutes
# with every query - so decide by yourself how public you make this feature
gitweb_check_feature('pickaxe')
or die_error(403, "Pickaxe search is disabled");
}
if ($searchtype eq 'grep') {
# grep search might be potentially CPU-intensive, too
gitweb_check_feature('grep')
or die_error(403, "Grep search is disabled");
}
if (!defined $searchtext) {
die_error(400, "Text field is empty");
}
if (!defined $hash) {
$hash = git_get_head_hash($project);
}
my %co = parse_commit($hash);
if (!%co) {
die_error(404, "Unknown commit object");
}
if (!defined $page) {
$page = 0;
}
if ($searchtype eq 'commit' ||
$searchtype eq 'author' ||
$searchtype eq 'committer') {
git_search_message(%co);
} elsif ($searchtype eq 'pickaxe') {
git_search_changes(%co);
} elsif ($searchtype eq 'grep') {
git_search_files(%co);
} else {
die_error(400, "Unknown search type");
}
}
sub git_search_help {
git_header_html();
git_print_page_nav('','', $hash,$hash,$hash);
print <<EOT;
<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
the pattern entered is recognized as the POSIX extended
<a href="https://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
insensitive).</p>
<dl>
<dt><b>commit</b></dt>
<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
EOT
my $have_grep = gitweb_check_feature('grep');
if ($have_grep) {
print <<EOT;
<dt><b>grep</b></dt>
<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
a different one) are searched for the given pattern. On large trees, this search can take
a while and put some strain on the server, so please use it with some consideration. Note that
due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
case-sensitive.</dd>
EOT
}
print <<EOT;
<dt><b>author</b></dt>
<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
<dt><b>committer</b></dt>
<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
EOT
my $have_pickaxe = gitweb_check_feature('pickaxe');
if ($have_pickaxe) {
print <<EOT;
<dt><b>pickaxe</b></dt>
<dd>All commits that caused the string to appear or disappear from any file (changes that
added, removed or "modified" the string) will be listed. This search can take a while and
takes a lot of strain on the server, so please use it wisely. Note that since you may be
interested even in changes just changing the case as well, this search is case sensitive.</dd>
EOT
}
print "</dl>\n";
git_footer_html();
}
sub git_shortlog {
git_log_generic('shortlog', \&git_shortlog_body,
$hash, $hash_parent);
}
## ......................................................................
## feeds (RSS, Atom; OPML)
sub git_feed {
my $format = shift || 'atom';
my $have_blame = gitweb_check_feature('blame');
# Atom: http://www.atomenabled.org/developers/syndication/
# RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
if ($format ne 'rss' && $format ne 'atom') {
die_error(400, "Unknown web feed format");
}
# log/feed of current (HEAD) branch, log of given branch, history of file/directory
my $head = $hash || 'HEAD';
my @commitlist = parse_commits($head, 150, 0, $file_name);
my %latest_commit;
my %latest_date;
my $content_type = "application/$format+xml";
if (defined $cgi->http('HTTP_ACCEPT') &&
$cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
# browser (feed reader) prefers text/xml
$content_type = 'text/xml';
}
if (defined($commitlist[0])) {
%latest_commit = %{$commitlist[0]};
my $latest_epoch = $latest_commit{'committer_epoch'};
exit_if_unmodified_since($latest_epoch);
%latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'});
}
print $cgi->header(
-type => $content_type,
-charset => 'utf-8',
%latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (),
-status => '200 OK');
# Optimization: skip generating the body if client asks only
# for Last-Modified date.
return if ($cgi->request_method() eq 'HEAD');
# header variables
my $title = "$site_name - $project/$action";
my $feed_type = 'log';
if (defined $hash) {
$title .= " - '$hash'";
$feed_type = 'branch log';
if (defined $file_name) {
$title .= " :: $file_name";
$feed_type = 'history';
}
} elsif (defined $file_name) {
$title .= " - $file_name";
$feed_type = 'history';
}
$title .= " $feed_type";
$title = esc_html($title);
my $descr = git_get_project_description($project);
if (defined $descr) {
$descr = esc_html($descr);
} else {
$descr = "$project " .
($format eq 'rss' ? 'RSS' : 'Atom') .
" feed";
}
my $owner = git_get_project_owner($project);
$owner = esc_html($owner);
#header
my $alt_url;
if (defined $file_name) {
$alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
} elsif (defined $hash) {
$alt_url = href(-full=>1, action=>"log", hash=>$hash);
} else {
$alt_url = href(-full=>1, action=>"summary");
}
print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
if ($format eq 'rss') {
print <<XML;
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
XML
print "<title>$title</title>\n" .
"<link>$alt_url</link>\n" .
"<description>$descr</description>\n" .
"<language>en</language>\n" .
# project owner is responsible for 'editorial' content
"<managingEditor>$owner</managingEditor>\n";
if (defined $logo || defined $favicon) {
# prefer the logo to the favicon, since RSS
# doesn't allow both
my $img = esc_url($logo || $favicon);
print "<image>\n" .
"<url>$img</url>\n" .
"<title>$title</title>\n" .
"<link>$alt_url</link>\n" .
"</image>\n";
}
if (%latest_date) {
print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
}
print "<generator>gitweb v.$version/$git_version</generator>\n";
} elsif ($format eq 'atom') {
print <<XML;
<feed xmlns="http://www.w3.org/2005/Atom">
XML
print "<title>$title</title>\n" .
"<subtitle>$descr</subtitle>\n" .
'<link rel="alternate" type="text/html" href="' .
$alt_url . '" />' . "\n" .
'<link rel="self" type="' . $content_type . '" href="' .
$cgi->self_url() . '" />' . "\n" .
"<id>" . href(-full=>1) . "</id>\n" .
# use project owner for feed author
"<author><name>$owner</name></author>\n";
if (defined $favicon) {
print "<icon>" . esc_url($favicon) . "</icon>\n";
}
if (defined $logo) {
# not twice as wide as tall: 72 x 27 pixels
print "<logo>" . esc_url($logo) . "</logo>\n";
}
if (! %latest_date) {
# dummy date to keep the feed valid until commits trickle in:
print "<updated>1970-01-01T00:00:00Z</updated>\n";
} else {
print "<updated>$latest_date{'iso-8601'}</updated>\n";
}
print "<generator version='$version/$git_version'>gitweb</generator>\n";
}
# contents
for (my $i = 0; $i <= $#commitlist; $i++) {
my %co = %{$commitlist[$i]};
my $commit = $co{'id'};
# we read 150, we always show 30 and the ones more recent than 48 hours
if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
last;
}
my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
# get list of changed files
open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
$co{'parent'} || "--root",
$co{'id'}, "--", (defined $file_name ? $file_name : ())
or next;
my @difftree = map { chomp; $_ } <$fd>;
close $fd
or next;
# print element (entry, item)
my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
if ($format eq 'rss') {
print "<item>\n" .
"<title>" . esc_html($co{'title'}) . "</title>\n" .
"<author>" . esc_html($co{'author'}) . "</author>\n" .
"<pubDate>$cd{'rfc2822'}</pubDate>\n" .
"<guid isPermaLink=\"true\">$co_url</guid>\n" .
"<link>$co_url</link>\n" .
"<description>" . esc_html($co{'title'}) . "</description>\n" .
"<content:encoded>" .
"<![CDATA[\n";
} elsif ($format eq 'atom') {
print "<entry>\n" .
"<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
"<updated>$cd{'iso-8601'}</updated>\n" .
"<author>\n" .
" <name>" . esc_html($co{'author_name'}) . "</name>\n";
if ($co{'author_email'}) {
print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
}
print "</author>\n" .
# use committer for contributor
"<contributor>\n" .
" <name>" . esc_html($co{'committer_name'}) . "</name>\n";
if ($co{'committer_email'}) {
print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
}
print "</contributor>\n" .
"<published>$cd{'iso-8601'}</published>\n" .
"<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
"<id>$co_url</id>\n" .
"<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
"<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
}
my $comment = $co{'comment'};
print "<pre>\n";
foreach my $line (@$comment) {
$line = esc_html($line);
print "$line\n";
}
print "</pre><ul>\n";
foreach my $difftree_line (@difftree) {
my %difftree = parse_difftree_raw_line($difftree_line);
next if !$difftree{'from_id'};
my $file = $difftree{'file'} || $difftree{'to_file'};
print "<li>" .
"[" .
$cgi->a({-href => href(-full=>1, action=>"blobdiff",
hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
file_name=>$file, file_parent=>$difftree{'from_file'}),
-title => "diff"}, 'D');
if ($have_blame) {
print $cgi->a({-href => href(-full=>1, action=>"blame",
file_name=>$file, hash_base=>$commit),
-title => "blame"}, 'B');
}
# if this is not a feed of a file history
if (!defined $file_name || $file_name ne $file) {
print $cgi->a({-href => href(-full=>1, action=>"history",
file_name=>$file, hash=>$commit),
-title => "history"}, 'H');
}
$file = esc_path($file);
print "] ".
"$file</li>\n";
}
if ($format eq 'rss') {
print "</ul>]]>\n" .
"</content:encoded>\n" .
"</item>\n";
} elsif ($format eq 'atom') {
print "</ul>\n</div>\n" .
"</content>\n" .
"</entry>\n";
}
}
# end of feed
if ($format eq 'rss') {
print "</channel>\n</rss>\n";
} elsif ($format eq 'atom') {
print "</feed>\n";
}
}
sub git_rss {
git_feed('rss');
}
sub git_atom {
git_feed('atom');
}
sub git_opml {
my @list = git_get_projects_list($project_filter, $strict_export);
if (!@list) {
die_error(404, "No projects found");
}
print $cgi->header(
-type => 'text/xml',
-charset => 'utf-8',
-content_disposition => 'inline; filename="opml.xml"');
my $title = esc_html($site_name);
my $filter = " within subdirectory ";
if (defined $project_filter) {
$filter .= esc_html($project_filter);
} else {
$filter = "";
}
print <<XML;
<?xml version="1.0" encoding="utf-8"?>
<opml version="1.0">
<head>
<title>$title OPML Export$filter</title>
</head>
<body>
<outline text="git RSS feeds">
XML
foreach my $pr (@list) {
my %proj = %$pr;
my $head = git_get_head_hash($proj{'path'});
if (!defined $head) {
next;
}
$git_dir = "$projectroot/$proj{'path'}";
my %co = parse_commit($head);
if (!%co) {
next;
}
my $path = esc_html(chop_str($proj{'path'}, 25, 5));
my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
}
print <<XML;
</outline>
</body>
</opml>
XML
}