X-Git-Url: https://git.ladys.computer/Gitweb/blobdiff_plain/19ffc2f06610e83269ad9ad795929d84e0fd61d0d0d454233e46582138872495..4830c61dad195176acb9f21ea6846cefbd75091a17d7d12d747f43551b07448f:/gitweb.perl
diff --git a/gitweb.perl b/gitweb.perl
index 38dc890..61ee84c 100755
--- a/gitweb.perl
+++ b/gitweb.perl
@@ -20,6 +20,10 @@ 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;
@@ -52,8 +56,13 @@ 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) {
+ # $path_info has already been URL-decoded by the web server, but
+ # $my_url and $my_uri have not. URL-decode them so we can properly
+ # strip $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'}) {
@@ -80,11 +89,16 @@ our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
# string of the home link on top of all pages
our $home_link_str = "++GITWEB_HOME_LINK_STR++";
+# extra breadcrumbs preceding the home link
+our @extra_breadcrumbs = ();
+
# name of your site or organization to appear in page titles
# replace this with something more descriptive for clearer bookmarks
our $site_name = "++GITWEB_SITENAME++"
|| ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
+# 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
@@ -132,6 +146,12 @@ our $default_projects_order = "project";
# (only effective if this variable evaluates to true)
our $export_ok = "++GITWEB_EXPORT_OK++";
+# don't generate age column on the projects list page
+our $omit_age_column = 0;
+
+# don't generate information about owners of repositories
+our $omit_owner=0;
+
# show repository only if this subroutine returns true
# when given the path to the project, for example:
# sub { return -e "$_[0]/git-daemon-export-ok"; }
@@ -258,16 +278,15 @@ our %highlight_basename = (
our %highlight_ext = (
# main extensions, defining name of syntax;
# see files in /usr/share/highlight/langDefs/ directory
- map { $_ => $_ }
- qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl sql make),
+ (map { $_ => $_ } qw(py rb java css js tex bib xml awk bat ini spec tcl sql)),
# alternate extensions, see /etc/highlight/filetypes.conf
- 'h' => 'c',
- map { $_ => 'sh' } qw(bash zsh ksh),
- map { $_ => 'cpp' } qw(cxx c++ cc),
- map { $_ => 'php' } qw(php3 php4 php5 phps),
- map { $_ => 'pl' } qw(perl pm), # perhaps also 'cgi'
- map { $_ => 'make'} qw(mak mk),
- map { $_ => 'xml' } qw(xhtml html htm),
+ (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)), # perhaps also 'cgi'
+ (map { $_ => 'make'} qw(make mak mk)),
+ (map { $_ => 'xml' } qw(xml xhtml html htm)),
);
# You define site-wide feature defaults here; override them with
@@ -529,11 +548,25 @@ our %feature = (
# $feature{'remote_heads'}{'default'} = [1];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'remote_heads'}{'override'} = 1;
- # and in project config gitweb.remote_heads = 0|1;
+ # and in project config gitweb.remoteheads = 0|1;
'remote_heads' => {
'sub' => sub { feature_bool('remote_heads', @_) },
'override' => 0,
'default' => [0]},
+
+ # Enable showing branches under other refs in addition to heads
+
+ # To set system wide extra branch refs have in $GITWEB_CONFIG
+ # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice'];
+ # To have project specific config enable override in $GITWEB_CONFIG
+ # $feature{'extra-branch-refs'}{'override'} = 1;
+ # and in project config gitweb.extrabranchrefs = dirs of choice
+ # Every directory is separated with whitespace.
+
+ 'extra-branch-refs' => {
+ 'sub' => \&feature_extra_branch_refs,
+ 'override' => 0,
+ 'default' => []},
);
sub gitweb_get_feature {
@@ -612,6 +645,21 @@ sub feature_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;
+}
+
# checking HEAD file with -e is fragile if the repository was
# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
# and then pruned.
@@ -642,6 +690,18 @@ sub filter_snapshot_fmts {
!$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));
+ # 'heads' are added implicitly in get_branch_refs().
+ $unique_refs{$ref} = 1 if ($ref ne 'heads');
+ }
+ return sort keys %unique_refs;
+}
+
# If it is set to code reference, it is code that it is to be run once per
# request, allowing updating configurations that change with each request,
# while running other code in config file only once.
@@ -666,13 +726,25 @@ sub read_config_file {
return;
}
-our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM);
+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++";
- # use first config file that exists
- read_config_file($GITWEB_CONFIG) or
+ # Protect against 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);
}
@@ -746,6 +818,8 @@ our @cgi_param_mapping = (
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"
);
@@ -802,9 +876,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->multi_param($symbol) ];
} else {
- $input_params{$name} = $cgi->param($symbol);
+ $input_params{$name} = decode_utf8($cgi->param($symbol));
}
}
}
@@ -962,11 +1036,11 @@ 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) {
- if (!validate_action($action)) {
+ if (!is_valid_action($action)) {
die_error(400, "Invalid action parameter");
}
}
@@ -974,22 +1048,29 @@ sub evaluate_and_validate_params {
# parameters which are pathnames
our $project = $input_params{'project'};
if (defined $project) {
- if (!validate_project($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 (!validate_pathname($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 (!validate_pathname($file_parent)) {
+ if (!is_valid_pathname($file_parent)) {
die_error(400, "Invalid file parent parameter");
}
}
@@ -997,21 +1078,21 @@ sub evaluate_and_validate_params {
# parameters which are refnames
our $hash = $input_params{'hash'};
if (defined $hash) {
- if (!validate_refname($hash)) {
+ if (!is_valid_refname($hash)) {
die_error(400, "Invalid hash parameter");
}
}
our $hash_parent = $input_params{'hash_parent'};
if (defined $hash_parent) {
- if (!validate_refname($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 (!validate_refname($hash_base)) {
+ if (!is_valid_refname($hash_base)) {
die_error(400, "Invalid hash base parameter");
}
}
@@ -1031,7 +1112,7 @@ sub evaluate_and_validate_params {
our $hash_parent_base = $input_params{'hash_parent_base'};
if (defined $hash_parent_base) {
- if (!validate_refname($hash_parent_base)) {
+ if (!is_valid_refname($hash_parent_base)) {
die_error(400, "Invalid hash parent base parameter");
}
}
@@ -1054,12 +1135,21 @@ sub evaluate_and_validate_params {
our $search_use_regexp = $input_params{'search_use_regexp'};
our $searchtext = $input_params{'searchtext'};
- our $search_regexp;
+ our $search_regexp = undef;
if (defined $searchtext) {
if (length($searchtext) < 2) {
die_error(403, "At least two characters are required for search parameter");
}
- $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
+ 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;
+ }
}
}
@@ -1069,7 +1159,7 @@ sub evaluate_git_dir {
our $git_dir = "$projectroot/$project" if $project;
}
-our (@snapshot_fmts, $git_avatar);
+our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
sub configure_gitweb_features {
# list of supported snapshot formats
our @snapshot_fmts = gitweb_get_feature('snapshot');
@@ -1087,6 +1177,13 @@ sub configure_gitweb_features {
} 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);
}
# custom error handler: 'die ' is Internal Server Error
@@ -1095,7 +1192,7 @@ sub handle_errors_html {
# to avoid infinite loop where error occurs in die_error,
# change handler to default handler, disabling handle_errors_html
- set_message("Error occured when inside die_error:\n$msg");
+ set_message("Error occurred when inside die_error:\n$msg");
# you cannot jump out of die_error when called as error handler;
# the subroutine set via CGI::Carp::set_message is called _after_
@@ -1109,8 +1206,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 {
@@ -1372,28 +1471,31 @@ sub href {
## ======================================================================
## validation, quoting/unquoting and escaping
-sub validate_action {
- my $input = shift || return undef;
+sub is_valid_action {
+ my $input = shift;
return undef unless exists $actions{$input};
- return $input;
+ return 1;
}
-sub validate_project {
- my $input = shift || return undef;
- if (!validate_pathname($input) ||
+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 $input;
+ return 1;
}
}
-sub validate_pathname {
- my $input = shift || return undef;
+sub is_valid_pathname {
+ my $input = shift;
- # no '.' or '..' as elements of path, i.e. no '.' nor '..'
+ return undef unless defined $input;
+ # no '.' or '..' as elements of path, i.e. no '.' or '..'
# at the beginning, at the end, and between slashes.
# also this catches doubled slashes
if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
@@ -1403,24 +1505,33 @@ sub validate_pathname {
if ($input =~ m!\0!) {
return undef;
}
- return $input;
+ return 1;
}
-sub validate_refname {
- my $input = shift || return undef;
+sub is_valid_ref_format {
+ my $input = shift;
- # textual hashes are O.K.
- if ($input =~ m/^[0-9a-fA-F]{40}$/) {
- return $input;
- }
- # it must be correct pathname
- $input = validate_pathname($input)
- or return undef;
+ return undef unless defined $input;
# restrictions on ref name according to git-check-ref-format
if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
return undef;
}
- return $input;
+ return 1;
+}
+
+sub is_valid_refname {
+ my $input = shift;
+
+ return undef unless defined $input;
+ # textual hashes are O.K.
+ if ($input =~ m/^[0-9a-fA-F]{40}$/) {
+ return 1;
+ }
+ # it must be correct pathname
+ is_valid_pathname($input) or return undef;
+ # check git-check-ref-format restrictions
+ is_valid_ref_format($input) or return undef;
+ return 1;
}
# decode sequences of octets in utf8 into Perl's internal form,
@@ -1429,8 +1540,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);
@@ -1506,6 +1617,17 @@ sub esc_path {
return $str;
}
+# Sanitize for use in XHTML + application/xml+xhtml (valid XML 1.0)
+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;
+}
+
# Make control characters "printable", using character escape codes (CEC)
sub quot_cec {
my $cntrl = shift;
@@ -1671,6 +1793,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 {
@@ -1679,6 +1802,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
@@ -1877,7 +2091,7 @@ sub format_ref_marker {
-href => href(
action=>$dest_action,
hash=>$dest
- )}, $name);
+ )}, esc_html($name));
$markers .= " " .
$link . "";
@@ -1922,7 +2136,7 @@ sub picon_url {
if (!$avatar_cache{$email}) {
my ($user, $domain) = split('@', $email);
$avatar_cache{$email} =
- "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
+ "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
"$domain/$user/" .
"users+domains+unknown/up/single";
}
@@ -1937,7 +2151,7 @@ sub gravatar_url {
my $email = lc shift;
my $size = shift;
$avatar_cache{$email} ||=
- "http://www.gravatar.com/avatar/" .
+ "//www.gravatar.com/avatar/" .
Digest::MD5::md5_hex($email) . "?s=";
return $avatar_cache{$email} . $size;
}
@@ -2201,93 +2415,125 @@ 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";
- }
- }
- $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;
+ $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'});
+ }
- 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 "
$line
\n";
- } elsif ($from && $to && $line =~ m/^\@{3}/) {
- my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
- my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
+ # fallback
+ return "";
+}
- @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);
- }
+# 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) = @_;
- $to_text = pop @from_text;
- $to_start = pop @from_start;
- $to_nlines = pop @from_nlines;
+ my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
+ $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
- $line = "$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 .= " ";
+ $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 = "@@ $from_text $to_text @@" .
+ "" . esc_html($section, -nbsp=>1) . "";
+ return $line;
+}
+
+# assumes that $from and $to are defined and correctly filled,
+# and that $line holds a line of chunk header for combined diff
+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 = "$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];
}
- if ($to->{'href'}) {
- $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
- -class=>"list"}, $to_text);
+ $line .= " ";
+ }
+ if ($to->{'href'}) {
+ $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
+ -class=>"list"}, $to_text);
+ } else {
+ $line .= $to_text;
+ }
+ $line .= " $prefix" .
+ "" . esc_html($section, -nbsp=>1) . "";
+ return $line;
+}
+
+# process patch (diff) line (not to be used for diff headers),
+# returning HTML-formatted (but not wrapped) line.
+# If the line is passed as a reference, it is treated as HTML and not
+# esc_html()'ed.
+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 .= $to_text;
+ $line = esc_html($line, -nbsp=>1);
}
- $line .= " $prefix" .
- "" . esc_html($section, -nbsp=>1) . "";
- return "
$line
\n";
}
- return "
" . esc_html($line, -nbsp=>1) . "
\n";
+
+ my $diff_classes = "diff";
+ $diff_classes .= " $diff_class" if ($diff_class);
+ $line = "
$line
\n";
+
+ return $line;
}
# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
@@ -2334,19 +2580,25 @@ sub format_snapshot_links {
sub get_feed_info {
my $format = shift || 'Atom';
my %res = (action => lc($format));
+ my $matched_ref = 0;
# feed links are possible only for project views
return unless (defined $project);
# some views should link to OPML, or to generic project feed,
# or don't have specific feed yet (so they should use generic)
- return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
-
- my $branch;
- # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
- # from tag links; this also makes possible to detect branch links
- if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
- (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
- $branch = $1;
+ return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
+
+ my $branch = undef;
+ # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
+ # (fullname) to differentiate from tag links; this also makes
+ # possible to detect branch links
+ 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;
+ }
}
# find log type for feed description (title)
my $type = 'log';
@@ -2359,7 +2611,7 @@ sub get_feed_info {
}
$res{-title} = $type;
- $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
+ $res{'hash'} = (defined $branch ? "refs/$matched_ref/$branch" : undef);
$res{'file_name'} = $file_name;
return %res;
@@ -2515,8 +2767,18 @@ sub git_get_project_config {
# 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\.//;
- return if ($key =~ m/\W/);
# type sanity check
if (defined $type) {
@@ -2706,7 +2968,7 @@ sub git_populate_project_tagcloud {
}
my $cloud;
- my $matched = $cgi->param('by_tag');
+ my $matched = $input_params{'ctag'};
if (eval { require HTML::TagCloud; 1; }) {
$cloud = HTML::TagCloud->new;
foreach my $ctag (sort keys %ctags_lc) {
@@ -2768,19 +3030,18 @@ sub git_get_project_url_list {
sub git_get_projects_list {
my $filter = shift || '';
+ my $paranoid = shift;
my @list;
- $filter =~ s/\.git$//;
-
if (-d $projects_list) {
# search in directory
my $dir = $projects_list;
# remove the trailing "/"
$dir =~ s!/+$!!;
- my $pfxlen = length("$projects_list");
- my $pfxdepth = ($projects_list =~ tr!/!!);
+ my $pfxlen = length("$dir");
+ my $pfxdepth = ($dir =~ tr!/!!);
# when filtering, search only given subdirectory
- if ($filter) {
+ if ($filter && !$paranoid) {
$dir .= "/$filter";
$dir =~ s!/+$!!;
}
@@ -2805,6 +3066,10 @@ sub git_get_projects_list {
}
my $path = substr($File::Find::name, $pfxlen + 1);
+ # paranoidly only filter here
+ if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
+ next;
+ }
# we check related file in $projectroot
if (check_export_ok("$projectroot/$path")) {
push @list, { path => $path };
@@ -2834,9 +3099,11 @@ sub git_get_projects_list {
}
if (check_export_ok("$projectroot/$path")) {
my $pr = {
- path => $path,
- owner => to_utf8($owner),
+ path => $path
};
+ if ($owner) {
+ $pr->{'owner'} = to_utf8($owner);
+ }
push @list, $pr;
}
}
@@ -2857,7 +3124,7 @@ sub filter_forks_from_projects_list {
$path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
next unless ($path); # skip '.git' repository: tests, git-instaweb
- next unless (-d $path); # containing directory exists
+ next unless (-d "$projectroot/$path"); # containing directory exists
$pr->{'forks'} = []; # there can be 0 or more forks of project
# add to trie
@@ -2910,11 +3177,15 @@ sub filter_forks_from_projects_list {
sub search_projects_list {
my ($projlist, %opts) = @_;
my $tagfilter = $opts{'tagfilter'};
- my $searchtext = $opts{'searchtext'};
+ my $search_re = $opts{'search_regexp'};
return @$projlist
- unless ($tagfilter || $searchtext);
+ unless ($tagfilter || $search_re);
+ # searching projects require filling to be run before it;
+ fill_project_list_info($projlist,
+ $tagfilter ? 'ctags' : (),
+ $search_re ? ('path', 'descr') : ());
my @projects;
PROJECT:
foreach my $pr (@$projlist) {
@@ -2925,10 +3196,10 @@ sub search_projects_list {
grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
}
- if ($searchtext) {
+ if ($search_re) {
next unless
- $pr->{'path'} =~ /$searchtext/ ||
- $pr->{'descr_long'} =~ /$searchtext/;
+ $pr->{'path'} =~ /$search_re/ ||
+ $pr->{'descr_long'} =~ /$search_re/;
}
push @projects, $pr;
@@ -2993,7 +3264,7 @@ sub git_get_last_activity {
'--format=%(committer)',
'--sort=-committerdate',
'--count=1',
- 'refs/heads') or return;
+ map { "refs/$_" } get_branch_refs ()) or return;
my $most_recent = <$fd>;
close $fd or return;
if (defined $most_recent &&
@@ -3444,7 +3715,7 @@ sub parse_from_to_diffinfo {
sub git_get_heads_list {
my ($limit, @classes) = @_;
- @classes = ('heads') unless @classes;
+ @classes = get_branch_refs() unless @classes;
my @patterns = map { "refs/$_" } @classes;
my @headslist;
@@ -3462,9 +3733,16 @@ sub git_get_heads_list {
my ($committer, $epoch, $tz) =
($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
$ref_item{'fullname'} = $name;
- $name =~ s!^refs/(?:head|remote)s/!!;
+ my $strip_refs = join '|', map { quotemeta } get_branch_refs();
+ $name =~ s!^refs/($strip_refs|remotes)/!!;
+ $ref_item{'name'} = $name;
+ # for refs neither in 'heads' nor 'remotes' we want to
+ # show their ref dir
+ 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{'name'} = $name;
$ref_item{'id'} = $hash;
$ref_item{'title'} = $title || '(no commit message)';
$ref_item{'epoch'} = $epoch;
@@ -3636,7 +3914,7 @@ sub blob_contenttype {
# guess file syntax for syntax highlighting; return undef if no highlighting
# the name of syntax can (in the future) depend on syntax highlighter used
sub guess_file_syntax {
- my ($highlight, $mimetype, $file_name) = @_;
+ my ($highlight, $file_name) = @_;
return undef unless ($highlight && defined $file_name);
my $basename = basename($file_name, '.in');
return $highlight_basename{$basename}
@@ -3654,12 +3932,16 @@ sub guess_file_syntax {
# or return original FD if no highlighting
sub run_highlighter {
my ($fd, $highlight, $syntax) = @_;
- return $fd unless ($highlight && defined $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 $syntax |"
+ " --replace-tabs=8 --fragment $syntax_arg |"
or die_error(500, "Couldn't open file or run syntax highlighter");
return $fd;
}
@@ -3670,7 +3952,12 @@ sub run_highlighter {
sub get_page_title {
my $title = to_utf8($site_name);
- return $title unless (defined $project);
+ 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);
@@ -3685,6 +3972,20 @@ sub get_page_title {
return $title;
}
+sub get_content_type_html {
+ # require explicit support from the UA if we are to send the page as
+ # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
+ # we have to do this because MSIE sometimes globs '*/*', pretending to
+ # support xhtml+xml but choking when it gets what it asked for.
+ if (defined $cgi->http('HTTP_ACCEPT') &&
+ $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
+ $cgi->Accept('application/xhtml+xml') != 0) {
+ return 'application/xhtml+xml';
+ } else {
+ return 'text/html';
+ }
+}
+
sub print_feed_meta {
if (defined $project) {
my %href_params = get_feed_info();
@@ -3700,6 +4001,7 @@ sub print_feed_meta {
'-type' => "application/$type+xml"
);
+ $href_params{'extra_options'} = undef;
$href_params{'action'} = $type;
$link_attr{'-href'} = href(%href_params);
print "'."\n";
+ } else {
+ foreach my $stylesheet (@stylesheets) {
+ next unless $stylesheet;
+ print ''."\n";
+ }
+ }
+ print_feed_meta()
+ if ($status eq '200 OK');
+ if (defined $favicon) {
+ print qq(\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) .
+ "