Imported Upstream version 2.0.1
[platform/upstream/git.git] / gitweb / gitweb.perl
index c6bafe6..a9f57d6 100755 (executable)
@@ -85,6 +85,9 @@ 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++"
@@ -545,6 +548,20 @@ our %feature = (
                '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 {
@@ -623,6 +640,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.
@@ -653,6 +685,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.
@@ -683,7 +727,7 @@ sub evaluate_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.
+       # 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);
@@ -991,7 +1035,7 @@ our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_bas
 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");
                }
        }
@@ -999,7 +1043,7 @@ 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");
                }
@@ -1007,21 +1051,21 @@ sub evaluate_and_validate_params {
 
        our $project_filter = $input_params{'project_filter'};
        if (defined $project_filter) {
-               if (!validate_pathname($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");
                }
        }
@@ -1029,21 +1073,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");
                }
        }
@@ -1063,7 +1107,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");
                }
        }
@@ -1086,7 +1130,7 @@ 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");
@@ -1110,7 +1154,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');
@@ -1128,6 +1172,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 <message>' is Internal Server Error
@@ -1136,7 +1187,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_
@@ -1415,28 +1466,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!(^|/)(|\.|\.\.)(/|$)!) {
@@ -1446,24 +1500,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,
@@ -2068,7 +2131,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";
        }
@@ -2083,7 +2146,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;
 }
@@ -2512,6 +2575,7 @@ 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);
@@ -2519,12 +2583,17 @@ sub get_feed_info {
        # or don't have specific feed yet (so they should use generic)
        return if (!$action || $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;
+       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';
@@ -2537,7 +2606,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;
@@ -3190,7 +3259,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 &&
@@ -3641,7 +3710,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;
 
@@ -3659,9 +3728,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;
@@ -3982,7 +4058,9 @@ sub print_nav_breadcrumbs_path {
 sub print_nav_breadcrumbs {
        my %opts = @_;
 
-       print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
+       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;
@@ -4030,8 +4108,8 @@ sub print_search_form {
              $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
              $cgi->popup_menu(-name => 'st', -default => 'commit',
                               -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
-             $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
-             " search:\n",
+             " " . $cgi->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',
@@ -6463,7 +6541,7 @@ sub git_summary {
        print "<div class=\"title\">&nbsp;</div>\n";
        print "<table class=\"projects_list\">\n" .
              "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n";
-        unless ($omit_owner) {
+        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'}) {
@@ -6626,6 +6704,7 @@ sub git_blame_common {
                        $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') {
@@ -7015,7 +7094,7 @@ sub git_blob {
        git_print_page_path($file_name, "blob", $hash_base);
        print "<div class=\"page_body\">\n";
        if ($mimetype =~ m!^image/!) {
-               print qq!<img type="!.esc_attr($mimetype).qq!"!;
+               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!"!;
                }
@@ -7150,6 +7229,15 @@ sub git_tree {
        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) = @_;
 
@@ -7157,9 +7245,7 @@ sub snapshot_name {
        # path/to/project/.git -> project
        my $name = to_utf8($project);
        $name =~ s,([^/])/*\.git$,$1,;
-       $name = basename($name);
-       # sanitize name
-       $name =~ s/[[:cntrl:]]/?/g;
+       $name = sanitize_for_filename(basename($name));
 
        my $ver = $hash;
        if ($hash =~ /^[0-9a-fA-F]+$/) {
@@ -7173,13 +7259,25 @@ sub snapshot_name {
                $ver = $1;
        } else {
                # branches and other need shortened SHA-1 hash
-               if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
-                       $ver = $1;
+               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);
+                       # for refs neither in heads nor remotes we want to
+                       # add a ref dir to archive name
+                       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);
        }
+       # special case of sanitization for filename - we change
+       # slashes to dots instead of dashes
        # in case of hierarchical branch names
        $ver =~ s!/!.!g;
+       $ver =~ s/[^[:alnum:]_.-]//g;
 
        # name = project-version_string
        $name = "$name-$ver";
@@ -7485,7 +7583,7 @@ sub git_object {
                system(git_cmd(), "cat-file", '-e', $hash_base) == 0
                        or die_error(404, "Base object does not exist");
 
-               # here errors should not hapen
+               # 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>;