X-Git-Url: https://git.ladys.computer/Gitweb/blobdiff_plain/724f05d045c6bb1a453d188e3dc152d92a072fa28072fb1a60d933ef1014bb64..a2a9fe5a6916fd20cd2a18e0eb8e663b14a1db26474d7a57aac40860faecfbbe:/gitweb.perl?ds=sidebyside
diff --git a/gitweb.perl b/gitweb.perl
index d0ed1fb..1829f56 100755
--- a/gitweb.perl
+++ b/gitweb.perl
@@ -52,7 +52,7 @@ sub evaluate_uri {
# as base URL.
# Therefore, if we needed to strip PATH_INFO, then we know that we have
# to build the base URL ourselves:
- our $path_info = $ENV{"PATH_INFO"};
+ our $path_info = decode_utf8($ENV{"PATH_INFO"});
if ($path_info) {
if ($my_url =~ s,\Q$path_info\E$,, &&
$my_uri =~ s,\Q$path_info\E$,, &&
@@ -85,6 +85,8 @@ our $home_link_str = "++GITWEB_HOME_LINK_STR++";
our $site_name = "++GITWEB_SITENAME++"
|| ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
+# html snippet to include in the
section of each page
+our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
# filename of html text to include at top of each page
our $site_header = "++GITWEB_SITE_HEADER++";
# html text to include at home page
@@ -116,6 +118,14 @@ our $projects_list = "++GITWEB_LIST++";
# the width (in characters) of the projects list "Description" column
our $projects_list_description_width = 25;
+# group projects by category on the projects list
+# (enabled if this variable evaluates to true)
+our $projects_list_group_categories = 0;
+
+# default category if none specified
+# (leave the empty string for no category)
+our $project_list_default_category = "";
+
# default order of projects list
# valid values are none, project, descr, owner, and age
our $default_projects_order = "project";
@@ -314,6 +324,10 @@ our %feature = (
# Enable text search, which will list the commits which match author,
# committer or commit text to a given string. Enabled by default.
# Project specific override is not supported.
+ #
+ # Note that this controls all search features, which means that if
+ # it is disabled, then 'grep' and 'pickaxe' search would also be
+ # disabled.
'search' => {
'override' => 0,
'default' => [1]},
@@ -321,6 +335,7 @@ our %feature = (
# Enable grep search, which will list the files in currently selected
# tree containing the given string. Enabled by default. This can be
# potentially CPU-intensive, of course.
+ # Note that you need to have 'search' feature enabled too.
# To enable system wide have in $GITWEB_CONFIG
# $feature{'grep'}{'default'} = [1];
@@ -335,6 +350,7 @@ our %feature = (
# Enable the pickaxe search, which will list the commits that modified
# a given string in a file. This can be practical and quite faster
# alternative to 'blame', but still potentially CPU-intensive.
+ # Note that you need to have 'search' feature enabled too.
# To enable system wide have in $GITWEB_CONFIG
# $feature{'pickaxe'}{'default'} = [1];
@@ -413,20 +429,23 @@ our %feature = (
'override' => 0,
'default' => []},
- # Allow gitweb scan project content tags described in ctags/
- # of project repository, and display the popular Web 2.0-ish
- # "tag cloud" near the project list. Note that this is something
- # COMPLETELY different from the normal Git tags.
+ # Allow gitweb scan project content tags of project repository,
+ # and display the popular Web 2.0-ish "tag cloud" near the projects
+ # list. Note that this is something COMPLETELY different from the
+ # normal Git tags.
# gitweb by itself can show existing tags, but it does not handle
- # tagging itself; you need an external application for that.
- # For an example script, check Girocco's cgi/tagproj.cgi.
+ # tagging itself; you need to do it externally, outside gitweb.
+ # The format is described in git_get_project_ctags() subroutine.
# You may want to install the HTML::TagCloud Perl module to get
# a pretty tag cloud instead of just a list of tags.
# To enable system wide have in $GITWEB_CONFIG
- # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
+ # $feature{'ctags'}{'default'} = [1];
# Project specific override is not supported.
+
+ # In the future whether ctags editing is enabled might depend
+ # on the value, but using 1 should always mean no editing of ctags.
'ctags' => {
'override' => 0,
'default' => [0]},
@@ -481,6 +500,18 @@ our %feature = (
'override' => 0,
'default' => [0]},
+ # Enable and configure ability to change common timezone for dates
+ # in gitweb output via JavaScript. Enabled by default.
+ # Project specific override is not supported.
+ 'javascript-timezone' => {
+ 'override' => 0,
+ 'default' => [
+ 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
+ # or undef to turn off this feature
+ 'gitweb_tz', # name of cookie where to store selected timezone
+ 'datetime', # CSS class used to mark up dates for manipulation
+ ]},
+
# Syntax highlighting support. This is based on Daniel Svensson's
# and Sham Chukoury's work in gitweb-xmms2.git.
# It requires the 'highlight' program present in $PATH,
@@ -621,18 +652,42 @@ sub filter_snapshot_fmts {
# if it is true then gitweb config would be run for each request.
our $per_request_config = 1;
-our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM);
-sub evaluate_gitweb_config {
- our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
- our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
+# read and parse gitweb config file given by its parameter.
+# returns true on success, false on recoverable error, allowing
+# to chain this subroutine, using first file that exists.
+# dies on errors during parsing config file, as it is unrecoverable.
+sub read_config_file {
+ my $filename = shift;
+ return unless defined $filename;
# die if there are errors parsing config file
- if (-e $GITWEB_CONFIG) {
- do $GITWEB_CONFIG;
- die $@ if $@;
- } elsif (-e $GITWEB_CONFIG_SYSTEM) {
- do $GITWEB_CONFIG_SYSTEM;
+ 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++";
+
+ # Protect agains duplications of file names, to not read config twice.
+ # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
+ # there possibility of duplication of filename there doesn't matter.
+ $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
+ $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
+
+ # Common system-wide settings for convenience.
+ # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
+ read_config_file($GITWEB_CONFIG_COMMON);
+
+ # Use first config file that exists. This means use the per-instance
+ # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
+ read_config_file($GITWEB_CONFIG) and return;
+ read_config_file($GITWEB_CONFIG_SYSTEM);
}
# Get loadavg of system, to compare against $maxload.
@@ -704,6 +759,9 @@ our @cgi_param_mapping = (
snapshot_format => "sf",
extra_options => "opt",
search_use_regexp => "sr",
+ ctag => "by_tag",
+ diff_style => "ds",
+ project_filter => "pf",
# this must be last entry (for manipulation from JavaScript)
javascript => "js"
);
@@ -760,9 +818,9 @@ sub evaluate_query_params {
while (my ($name, $symbol) = each %cgi_param_mapping) {
if ($symbol eq 'opt') {
- $input_params{$name} = [ $cgi->param($symbol) ];
+ $input_params{$name} = [ map { decode_utf8($_) } $cgi->param($symbol) ];
} else {
- $input_params{$name} = $cgi->param($symbol);
+ $input_params{$name} = decode_utf8($cgi->param($symbol));
}
}
}
@@ -920,7 +978,7 @@ sub evaluate_path_info {
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);
+ $searchtext, $search_regexp, $project_filter);
sub evaluate_and_validate_params {
our $action = $input_params{'action'};
if (defined $action) {
@@ -938,6 +996,13 @@ sub evaluate_and_validate_params {
}
}
+ our $project_filter = $input_params{'project_filter'};
+ if (defined $project_filter) {
+ if (!validate_pathname($project_filter)) {
+ die_error(404, "Invalid project_filter parameter");
+ }
+ }
+
our $file_name = $input_params{'file_name'};
if (defined $file_name) {
if (!validate_pathname($file_name)) {
@@ -1017,7 +1082,16 @@ sub evaluate_and_validate_params {
if (length($searchtext) < 2) {
die_error(403, "At least two characters are required for search parameter");
}
- $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
+ 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;
+ }
}
}
@@ -1067,8 +1141,10 @@ 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 {
@@ -1387,8 +1463,8 @@ sub validate_refname {
sub to_utf8 {
my $str = shift;
return undef unless defined $str;
- if (utf8::valid($str)) {
- utf8::decode($str);
+
+ if (utf8::is_utf8($str) || utf8::decode($str)) {
return $str;
} else {
return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
@@ -1464,6 +1540,17 @@ sub esc_path {
return $str;
}
+# Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
+sub sanitize {
+ my $str = shift;
+
+ return undef unless defined $str;
+
+ $str = to_utf8($str);
+ $str =~ s|([[:cntrl:]])|($1 =~ /[\t\n\r]/ ? $1 : quot_cec($1))|eg;
+ return $str;
+}
+
# Make control characters "printable", using character escape codes (CEC)
sub quot_cec {
my $cntrl = shift;
@@ -1629,6 +1716,7 @@ sub chop_and_escape_str {
my ($str) = @_;
my $chopped = chop_str(@_);
+ $str = to_utf8($str);
if ($chopped eq $str) {
return esc_html($chopped);
} else {
@@ -1637,6 +1725,97 @@ sub chop_and_escape_str {
}
}
+# Highlight selected fragments of string, using given CSS class,
+# and escape HTML. It is assumed that fragments do not overlap.
+# Regions are passed as list of pairs (array references).
+#
+# Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
+# 'foobar'
+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;
+
+ # Don't create empty elements.
+ 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;
+}
+
+# return positions of beginning and end of each match
+sub matchpos_list {
+ my ($str, $regexp) = @_;
+ return unless (defined $str && defined $regexp);
+
+ my @matches;
+ while ($str =~ /$regexp/g) {
+ push @matches, [$-[0], $+[0]];
+ }
+ return @matches;
+}
+
+# highlight match (if any), and escape HTML
+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);
+}
+
+
+# highlight match (if any) of shortened string, and escape HTML
+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;
+
+ # filter matches so that we mark chopped string
+ my $tail = "... "; # see chop_str
+ 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);
+}
+
## ----------------------------------------------------------------------
## functions returning short strings
@@ -2159,93 +2338,119 @@ sub format_diff_cc_simplified {
return $result;
}
-# format patch (diff) line (not to be used for diff headers)
-sub format_diff_line {
- my $line = shift;
- my ($from, $to) = @_;
- my $diff_class = "";
-
- chomp $line;
+sub diff_line_class {
+ my ($line, $from, $to) = @_;
+ # ordinary diff
+ my $num_sign = 1;
+ # combined diff
if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
- # combined diff
- my $prefix = substr($line, 0, scalar @{$from->{'href'}});
- if ($line =~ m/^\@{3}/) {
- $diff_class = " chunk_header";
- } elsif ($line =~ m/^\\/) {
- $diff_class = " incomplete";
- } elsif ($prefix =~ tr/+/+/) {
- $diff_class = " add";
- } elsif ($prefix =~ tr/-/-/) {
- $diff_class = " rem";
- }
- } else {
- # assume ordinary diff
- my $char = substr($line, 0, 1);
- if ($char eq '+') {
- $diff_class = " add";
- } elsif ($char eq '-') {
- $diff_class = " rem";
- } elsif ($char eq '@') {
- $diff_class = " chunk_header";
- } elsif ($char eq "\\") {
- $diff_class = " incomplete";
- }
+ $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" },
+ # classifier for context must come before classifier add/rem,
+ # or we would have to use more complicated regexp, for example
+ # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
+ { 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'});
}
- $line = untabify($line);
- if ($from && $to && $line =~ m/^\@{2} /) {
- my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
- $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
- $from_lines = 0 unless defined $from_lines;
- $to_lines = 0 unless defined $to_lines;
+ # fallback
+ return "";
+}
+
+# assumes that $from and $to are defined and correctly filled,
+# and that $line holds a line of chunk header for unified diff
+sub format_unidiff_chunk_header {
+ my ($line, $from, $to) = @_;
- 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 = "@@ $from_text $to_text @@" .
- "" . esc_html($section, -nbsp=>1) . "";
- return "
\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, $check_forks) = @_;
+ 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) {
- my (@activity) = git_get_last_activity($pr->{'path'});
- unless (@activity) {
- next PROJECT;
+ 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;
}
- ($pr->{'age'}, $pr->{'age_string'}) = @activity;
- if (!defined $pr->{'descr'}) {
+ 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 (!defined $pr->{'owner'}) {
+ if (project_info_needs_filling($pr, $filter_set->('owner'))) {
$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
}
- if ($check_forks) {
- my $pname = $pr->{'path'};
- if (($pname =~ s/\.git$//) &&
- ($pname !~ /\/$/) &&
- (-d "$projectroot/$pname")) {
- $pr->{'forks'} = "-d $projectroot/$pname";
- } else {
- $pr->{'forks'} = 0;
- }
+ 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);
}
- $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
+
push @projects, $pr;
}
return @projects;
}
+sub sort_projects_list {
+ my ($projlist, $order) = @_;
+ my @projects;
+
+ my %order_info = (
+ project => { key => 'path', type => 'str' },
+ descr => { key => 'descr_long', type => 'str' },
+ owner => { key => 'owner', type => 'str' },
+ age => { key => 'age', type => 'num' }
+ );
+ my $oi = $order_info{$order};
+ return @$projlist unless defined $oi;
+ if ($oi->{'type'} eq 'str') {
+ @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist;
+ } else {
+ @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist;
+ }
+
+ return @projects;
+}
+
+# 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'
element, generating 'sort by $name' replay link
# if that order is not selected
sub print_sort_th {
@@ -4810,40 +5443,41 @@ sub git_project_list_rows {
$to = $#$projlist if (!defined $to || $#$projlist < $to);
my $alternate = 1;
- my $tagfilter = $cgi->param('by_tag');
for (my $i = $from; $i <= $to; $i++) {
my $pr = $projlist->[$i];
- next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
- next if $searchtext and not $pr->{'path'} =~ /$searchtext/
- and not $pr->{'descr_long'} =~ /$searchtext/;
- # Weed out forks or non-matching entries of search
- if ($check_forks) {
- my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
- $forkbase="^$forkbase" if $forkbase;
- next if not $searchtext and not $tagfilter and $show_ctags
- and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
- }
-
if ($alternate) {
print "
" .
- $cgi->end_form() . "\n";
+
+ git_project_search_form($searchtext, $search_use_regexp);
git_project_list_body(\@list, $order);
git_footer_html();
}
@@ -5355,7 +6223,9 @@ sub git_forks {
die_error(400, "Unknown order parameter");
}
- my @list = git_get_projects_list($project);
+ my $filter = $project;
+ $filter =~ s/\.git$//;
+ my @list = git_get_projects_list($filter);
if (!@list) {
die_error(404, "No forks found");
}
@@ -5368,7 +6238,10 @@ sub git_forks {
}
sub git_project_index {
- my @projects = git_get_projects_list($project);
+ my @projects = git_get_projects_list($project_filter, $strict_export);
+ if (!@projects) {
+ die_error(404, "No projects found");
+ }
print $cgi->header(
-type => 'text/plain',
@@ -5410,7 +6283,13 @@ sub git_summary {
my $check_forks = gitweb_check_feature('forks');
if ($check_forks) {
- @forklist = git_get_projects_list($project);
+ # 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();
@@ -5421,7 +6300,8 @@ sub git_summary {
"
description
" . esc_html($descr) . "
\n" .
"
owner
" . esc_html($owner) . "
\n";
if (defined $cd{'rfc2822'}) {
- print "
last change
$cd{'rfc2822'}
\n";
+ print "
last change
" .
+ "
".format_timestamp_html(\%cd)."
\n";
}
# use per project git URL list in $projectroot/$project/cloneurl
@@ -5439,13 +6319,14 @@ sub git_summary {
my $show_ctags = gitweb_check_feature('ctags');
if ($show_ctags) {
my $ctags = git_get_project_ctags($project);
- my $cloud = git_populate_project_tagcloud($ctags);
- print "
Content tags: ";
- print "
\n
" unless %$ctags;
- print "";
- print "
\n
" if %$ctags;
- print git_show_project_tagcloud($cloud, 48);
- print "
";
+ if (%$ctags) {
+ # without ability to add tags, don't show if there are none
+ my $cloud = git_populate_project_tagcloud($ctags);
+ print "
" .
+ "
content tags
" .
+ "
".git_show_project_tagcloud($cloud, 48)."
" .
+ "
\n";
+ }
}
print "\n";
@@ -5536,7 +6417,7 @@ sub git_tag {
sub git_blame_common {
my $format = shift || 'porcelain';
- if ($format eq 'porcelain' && $cgi->param('js')) {
+ if ($format eq 'porcelain' && $input_params{'javascript'}) {
$format = 'incremental';
$action = 'blame_incremental'; # for page title etc
}
@@ -5585,7 +6466,9 @@ sub git_blame_common {
-type=>"text/plain", -charset => "utf-8",
-status=> "200 OK");
local $| = 1; # output autoflush
- print while <$fd>;
+ while (my $line = <$fd>) {
+ print to_utf8($line);
+ }
close $fd
or print "ERROR $!\n";
@@ -5873,7 +6756,16 @@ sub git_blob_plain {
# want to be sure not to break that by serving the image as an
# attachment (though Firefox 3 doesn't seem to care).
my $sandbox = $prevent_xss &&
- $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
+ $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
+
+ # serve text/* as text/plain
+ 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,
@@ -5971,7 +6863,8 @@ sub git_blob {
$nr++;
$line = untabify($line);
printf qq!