index-pack: support multithreaded delta resolving
[gitweb.git] / gitweb / gitweb.perl
index ecd4a39d22768c5ac7a577ae43928d3aea28867f..eaf5f942502dd6593fa44724ea18d0153e725aea 100755 (executable)
@@ -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$,, &&
@@ -817,9 +817,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));
                }
        }
 }
@@ -1131,8 +1131,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 {
@@ -2399,7 +2401,7 @@ sub get_feed_info {
        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);
+       return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
 
        my $branch;
        # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
@@ -2773,7 +2775,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) {
@@ -2985,6 +2987,10 @@ sub search_projects_list {
        return @$projlist
                unless ($tagfilter || $searchtext);
 
+       # searching projects require filling to be run before it;
+       fill_project_list_info($projlist,
+                              $tagfilter  ? 'ctags' : (),
+                              $searchtext ? ('path', 'descr') : ());
        my @projects;
  PROJECT:
        foreach my $pr (@$projlist) {
@@ -3742,7 +3748,7 @@ sub get_page_title {
 
        unless (defined $project) {
                if (defined $project_filter) {
-                       $title .= " - " . to_utf8($project_filter);
+                       $title .= " - projects in '" . esc_path($project_filter) . "'";
                }
                return $title;
        }
@@ -3839,12 +3845,27 @@ sub print_header_links {
        }
 }
 
+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 = @_;
 
        print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
        if (defined $project) {
-               print $cgi->a({-href => href(action=>"summary")}, esc_html($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}) {
@@ -3857,6 +3878,8 @@ sub print_nav_breadcrumbs {
                        print " / $opts{-action_extra}";
                }
                print "\n";
+       } elsif (defined $project_filter) {
+               print_nav_breadcrumbs_path(split '/', $project_filter);
        }
 }
 
@@ -3887,7 +3910,7 @@ sub print_search_form {
                               -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
              $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
              " search:\n",
-             $cgi->textfield(-name => "s", -value => $searchtext) . "\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) .
@@ -5141,35 +5164,98 @@ sub git_patchset_body {
 
 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
 
-# fills project list info (age, description, owner, category, forks)
+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->startform(-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
+# 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 = shift;
+       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 ($show_ctags) {
+               if ($show_ctags &&
+                   project_info_needs_filling($pr, $filter_set->('ctags'))) {
                        $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
                }
-               if ($projects_list_group_categories && !defined $pr->{'category'}) {
+               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);
@@ -5298,19 +5384,20 @@ sub git_project_list_body {
 
        my $check_forks = gitweb_check_feature('forks');
        my $show_ctags  = gitweb_check_feature('ctags');
-       my $tagfilter = $show_ctags ? $cgi->param('by_tag') : undef;
+       my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef;
        $check_forks = undef
                if ($tagfilter || $searchtext);
 
        # filtering out forks before filling info allows to do less work
        @projects = filter_forks_from_projects_list(\@projects)
                if ($check_forks);
-       @projects = fill_project_list_info(\@projects);
-       # searching projects require filling to be run before it
+       # search_projects_list pre-fills required info
        @projects = search_projects_list(\@projects,
                                         'searchtext' => $searchtext,
                                         'tagfilter'  => $tagfilter)
                if ($tagfilter || $searchtext);
+       # fill the rest
+       @projects = fill_project_list_info(\@projects);
 
        $order ||= $default_projects_order;
        $from = 0 unless defined $from;
@@ -5586,7 +5673,7 @@ sub git_tags_body {
 
 sub git_heads_body {
        # uses global variable $project
-       my ($headlist, $head, $from, $to, $extra) = @_;
+       my ($headlist, $head_at, $from, $to, $extra) = @_;
        $from = 0 unless defined $from;
        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
 
@@ -5595,7 +5682,7 @@ sub git_heads_body {
        for (my $i = $from; $i <= $to; $i++) {
                my $entry = $headlist->[$i];
                my %ref = %$entry;
-               my $curr = $ref{'id'} eq $head;
+               my $curr = defined $head_at && $ref{'id'} eq $head_at;
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
                } else {
@@ -5868,9 +5955,10 @@ sub git_search_files {
        my $alternate = 1;
        my $matches = 0;
        my $lastfile = '';
+       my $file_href;
        while (my $line = <$fd>) {
                chomp $line;
-               my ($file, $file_href, $lno, $ltext, $binary);
+               my ($file, $lno, $ltext, $binary);
                last if ($matches++ > 1000);
                if ($line =~ /^Binary file (.+) matches$/) {
                        $file = $1;
@@ -6008,11 +6096,8 @@ sub git_project_list {
                insert_file($home_text);
                print "</div>\n";
        }
-       print $cgi->startform(-method => "get") .
-             "<p class=\"projsearch\">Search:\n" .
-             $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
-             "</p>" .
-             $cgi->end_form() . "\n";
+
+       git_project_search_form($searchtext, $search_use_regexp);
        git_project_list_body(\@list, $order);
        git_footer_html();
 }
@@ -6217,7 +6302,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
        }