Merge branch 'jm/maint-gitweb-filter-forks-fix'
authorJunio C Hamano <gitster@pobox.com>
Wed, 26 Oct 2011 23:16:30 +0000 (16:16 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 26 Oct 2011 23:16:30 +0000 (16:16 -0700)
* jm/maint-gitweb-filter-forks-fix:
gitweb: fix regression when filtering out forks

1  2 
gitweb/gitweb.perl
diff --combined gitweb/gitweb.perl
index 85d64b244dead86132de8a2a980cfbfc27c86494,10cae7263e3757517aa3e3c21d47dd853ff68de3..a95226e089fa22d7041e4374382bb2709da10df1
@@@ -115,14 -115,6 +115,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";
@@@ -194,7 -186,7 +194,7 @@@ our %known_snapshot_formats = 
                'type' => 'application/x-gzip',
                'suffix' => '.tar.gz',
                'format' => 'tar',
 -              'compressor' => ['gzip']},
 +              'compressor' => ['gzip', '-n']},
  
        'tbz2' => {
                'display' => 'tar.bz2',
@@@ -321,10 -313,6 +321,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]},
        # 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];
        # 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];
                '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]},
                '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,
@@@ -649,42 -620,18 +649,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.
@@@ -756,7 -703,6 +756,7 @@@ our @cgi_param_mapping = 
        snapshot_format => "sf",
        extra_options => "opt",
        search_use_regexp => "sr",
 +      ctag => "by_tag",
        # this must be last entry (for manipulation from JavaScript)
        javascript => "js"
  );
@@@ -1517,17 -1463,6 +1517,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;
@@@ -2537,13 -2472,6 +2537,13 @@@ 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 =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
 +              $key = join(".", lc($hi), $mi, lc($lo));
 +      } else {
 +              $key = lc($key);
 +      }
        $key =~ s/^gitweb\.//;
        return if ($key =~ m/\W/);
  
@@@ -2630,94 -2558,37 +2630,94 @@@ sub git_get_path_by_hash 
  ## ......................................................................
  ## git utility functions, directly accessing git repository
  
 -sub git_get_project_description {
 -      my $path = shift;
 +# get the value of config variable either from file named as the variable
 +# itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
 +# configuration variable in the repository config file.
 +sub git_get_file_or_project_config {
 +      my ($path, $name) = @_;
  
        $git_dir = "$projectroot/$path";
 -      open my $fd, '<', "$git_dir/description"
 -              or return git_get_project_config('description');
 -      my $descr = <$fd>;
 +      open my $fd, '<', "$git_dir/$name"
 +              or return git_get_project_config($name);
 +      my $conf = <$fd>;
        close $fd;
 -      if (defined $descr) {
 -              chomp $descr;
 +      if (defined $conf) {
 +              chomp $conf;
        }
 -      return $descr;
 +      return $conf;
  }
  
 -sub git_get_project_ctags {
 +sub git_get_project_description {
        my $path = shift;
 +      return git_get_file_or_project_config($path, 'description');
 +}
 +
 +sub git_get_project_category {
 +      my $path = shift;
 +      return git_get_file_or_project_config($path, 'category');
 +}
 +
 +
 +# supported formats:
 +# * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
 +#   - if its contents is a number, use it as tag weight,
 +#   - otherwise add a tag with weight 1
 +# * $GIT_DIR/ctags file, each line is a tag (with weight 1)
 +#   the same value multiple times increases tag weight
 +# * `gitweb.ctag' multi-valued repo config variable
 +sub git_get_project_ctags {
 +      my $project = shift;
        my $ctags = {};
  
 -      $git_dir = "$projectroot/$path";
 -      opendir my $dh, "$git_dir/ctags"
 -              or return $ctags;
 -      foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
 -              open my $ct, '<', $_ or next;
 -              my $val = <$ct>;
 -              chomp $val;
 -              close $ct;
 -              my $ctag = $_; $ctag =~ s#.*/##;
 -              $ctags->{$ctag} = $val;
 +      $git_dir = "$projectroot/$project";
 +      if (opendir my $dh, "$git_dir/ctags") {
 +              my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
 +              foreach my $tagfile (@files) {
 +                      open my $ct, '<', $tagfile
 +                              or next;
 +                      my $val = <$ct>;
 +                      chomp $val if $val;
 +                      close $ct;
 +
 +                      (my $ctag = $tagfile) =~ s#.*/##;
 +                      if ($val =~ /^\d+$/) {
 +                              $ctags->{$ctag} = $val;
 +                      } else {
 +                              $ctags->{$ctag} = 1;
 +                      }
 +              }
 +              closedir $dh;
 +
 +      } elsif (open my $fh, '<', "$git_dir/ctags") {
 +              while (my $line = <$fh>) {
 +                      chomp $line;
 +                      $ctags->{$line}++ if $line;
 +              }
 +              close $fh;
 +
 +      } else {
 +              my $taglist = config_to_multi(git_get_project_config('ctag'));
 +              foreach my $tag (@$taglist) {
 +                      $ctags->{$tag}++;
 +              }
        }
 -      closedir $dh;
 -      $ctags;
 +
 +      return $ctags;
 +}
 +
 +# return hash, where keys are content tags ('ctags'),
 +# and values are sum of weights of given tag in every project
 +sub git_gather_all_ctags {
 +      my $projects = shift;
 +      my $ctags = {};
 +
 +      foreach my $p (@$projects) {
 +              foreach my $ct (keys %{$p->{'ctags'}}) {
 +                      $ctags->{$ct} += $p->{'ctags'}->{$ct};
 +              }
 +      }
 +
 +      return $ctags;
  }
  
  sub git_populate_project_tagcloud {
        }
  
        my $cloud;
 +      my $matched = $cgi->param('by_tag');
        if (eval { require HTML::TagCloud; 1; }) {
                $cloud = HTML::TagCloud->new;
 -              foreach (sort keys %ctags_lc) {
 +              foreach my $ctag (sort keys %ctags_lc) {
                        # Pad the title with spaces so that the cloud looks
                        # less crammed.
 -                      my $title = $ctags_lc{$_}->{topname};
 +                      my $title = esc_html($ctags_lc{$ctag}->{topname});
                        $title =~ s/ /&nbsp;/g;
                        $title =~ s/^/&nbsp;/g;
                        $title =~ s/$/&nbsp;/g;
 -                      $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
 +                      if (defined $matched && $matched eq $ctag) {
 +                              $title = qq(<span class="match">$title</span>);
 +                      }
 +                      $cloud->add($title, href(project=>undef, ctag=>$ctag),
 +                                  $ctags_lc{$ctag}->{count});
                }
        } else {
 -              $cloud = \%ctags_lc;
 +              $cloud = {};
 +              foreach my $ctag (keys %ctags_lc) {
 +                      my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
 +                      if (defined $matched && $matched eq $ctag) {
 +                              $title = qq(<span class="match">$title</span>);
 +                      }
 +                      $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
 +                      $cloud->{$ctag}{ctag} =
 +                              $cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title);
 +              }
        }
 -      $cloud;
 +      return $cloud;
  }
  
  sub git_show_project_tagcloud {
        my ($cloud, $count) = @_;
 -      print STDERR ref($cloud)."..\n";
        if (ref $cloud eq 'HTML::TagCloud') {
                return $cloud->html_and_css($count);
        } else {
 -              my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
 -              return '<p align="center">' . join (', ', map {
 -                      $cgi->a({-href=>"$home_link?by_tag=$_"}, $cloud->{$_}->{topname})
 -              } splice(@tags, 0, $count)) . '</p>';
 +              my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
 +              return
 +                      '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
 +                      join (', ', map {
 +                              $cloud->{$_}->{'ctag'}
 +                      } splice(@tags, 0, $count)) .
 +                      '</div>';
        }
  }
  
@@@ -2886,7 -2741,7 +2886,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
@@@ -3596,9 -3451,12 +3596,9 @@@ sub mimetype_guess_file 
        open(my $mh, '<', $mimemap) or return undef;
        while (<$mh>) {
                next if m/^#/; # skip comments
 -              my ($mimetype, $exts) = split(/\t+/);
 -              if (defined $exts) {
 -                      my @exts = split(/\s+/, $exts);
 -                      foreach my $ext (@exts) {
 -                              $mimemap{$ext} = $mimetype;
 -                      }
 +              my ($mimetype, @exts) = split(/\s+/);
 +              foreach my $ext (@exts) {
 +                      $mimemap{$ext} = $mimetype;
                }
        }
        close($mh);
@@@ -3714,20 -3572,6 +3714,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();
        }
  }
  
 +sub print_header_links {
 +      my $status = shift;
 +
 +      # print out each stylesheet that exist, providing backwards capability
 +      # for those people who defined $stylesheet in a config file
 +      if (defined $stylesheet) {
 +              print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
 +      } else {
 +              foreach my $stylesheet (@stylesheets) {
 +                      next unless $stylesheet;
 +                      print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
 +              }
 +      }
 +      print_feed_meta()
 +              if ($status eq '200 OK');
 +      if (defined $favicon) {
 +              print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
 +      }
 +}
 +
 +sub print_nav_breadcrumbs {
 +      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));
 +              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";
 +      }
 +}
 +
 +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->startform(-method => "get", -action => $action) .
 +            "<div class=\"search\">\n" .
 +            (!$use_pathinfo &&
 +            $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
 +            $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
 +            $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
 +            $cgi->popup_menu(-name => 'st', -default => 'commit',
 +                             -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
 +            $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
 +            " search:\n",
 +            $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
 +            "<span title=\"Extended regular expression\">" .
 +            $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
 +                           -checked => $search_use_regexp) .
 +            "</span>" .
 +            "</div>" .
 +            $cgi->end_form() . "\n";
 +}
 +
  sub git_header_html {
        my $status = shift || "200 OK";
        my $expires = shift;
        my %opts = @_;
  
        my $title = get_page_title();
 -      my $content_type;
 -      # 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) {
 -              $content_type = 'application/xhtml+xml';
 -      } else {
 -              $content_type = 'text/html';
 -      }
 +      my $content_type = get_content_type_html();
        print $cgi->header(-type=>$content_type, -charset => 'utf-8',
                           -status=> $status, -expires => $expires)
                unless ($opts{'-no_http_header'});
        if ($ENV{'PATH_INFO'}) {
                print "<base href=\"".esc_url($base_url)."\" />\n";
        }
 -      # print out each stylesheet that exist, providing backwards capability
 -      # for those people who defined $stylesheet in a config file
 -      if (defined $stylesheet) {
 -              print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
 -      } else {
 -              foreach my $stylesheet (@stylesheets) {
 -                      next unless $stylesheet;
 -                      print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
 -              }
 -      }
 -      print_feed_meta()
 -              if ($status eq '200 OK');
 -      if (defined $favicon) {
 -              print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
 -      }
 -
 +      print_header_links($status);
        print "</head>\n" .
              "<body>\n";
  
                                         -alt => "git",
                                         -class => "logo"}));
        }
 -      print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
 -      if (defined $project) {
 -              print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
 -              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";
 -      }
 +      print_nav_breadcrumbs(%opts);
        print "</div>\n";
  
        my $have_search = gitweb_check_feature('search');
        if (defined $project && $have_search) {
 -              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->startform(-method => "get", -action => $action) .
 -                    "<div class=\"search\">\n" .
 -                    (!$use_pathinfo &&
 -                    $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
 -                    $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
 -                    $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
 -                    $cgi->popup_menu(-name => 'st', -default => 'commit',
 -                                     -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
 -                    $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
 -                    " search:\n",
 -                    $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
 -                    "<span title=\"Extended regular expression\">" .
 -                    $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
 -                                   -checked => $search_use_regexp) .
 -                    "</span>" .
 -                    "</div>" .
 -                    $cgi->end_form() . "\n";
 +              print_search_form();
        }
  }
  
@@@ -3960,20 -3800,9 +3960,20 @@@ sub git_footer_html 
                      qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
                      qq!           "!. href() .qq!");\n!.
                      qq!</script>\n!;
 -      } elsif (gitweb_check_feature('javascript-actions')) {
 +      } else {
 +              my ($jstimezone, $tz_cookie, $datetime_class) =
 +                      gitweb_get_feature('javascript-timezone');
 +
                print qq!<script type="text/javascript">\n!.
 -                    qq!window.onload = fixLinks;\n!.
 +                    qq!window.onload = function () {\n!;
 +              if (gitweb_check_feature('javascript-actions')) {
 +                      print qq!       fixLinks();\n!;
 +              }
 +              if ($jstimezone && $tz_cookie && $datetime_class) {
 +                      print qq!       var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. # in days
 +                            qq!       onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!;
 +              }
 +              print qq!};\n!.
                      qq!</script>\n!;
        }
  
@@@ -4177,25 -4006,22 +4177,25 @@@ sub git_print_section 
        print $cgi->end_div;
  }
  
 -sub print_local_time {
 -      print format_local_time(@_);
 -}
 +sub format_timestamp_html {
 +      my $date = shift;
 +      my $strtime = $date->{'rfc2822'};
  
 -sub format_local_time {
 -      my $localtime = '';
 -      my %date = @_;
 -      if ($date{'hour_local'} < 6) {
 -              $localtime .= sprintf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
 -                      $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
 -      } else {
 -              $localtime .= sprintf(" (%02d:%02d %s)",
 -                      $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
 +      my (undef, undef, $datetime_class) =
 +              gitweb_get_feature('javascript-timezone');
 +      if ($datetime_class) {
 +              $strtime = qq!<span class="$datetime_class">$strtime</span>!;
        }
  
 -      return $localtime;
 +      my $localtime_format = '(%02d:%02d %s)';
 +      if ($date->{'hour_local'} < 6) {
 +              $localtime_format = '(<span class="atnight">%02d:%02d</span> %s)';
 +      }
 +      $strtime .= ' ' .
 +                  sprintf($localtime_format,
 +                          $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
 +
 +      return $strtime;
  }
  
  # Outputs the author name and date in long form
@@@ -4208,9 -4034,10 +4208,9 @@@ sub git_print_authorship 
        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
        print "<$tag class=\"author_date\">" .
              format_search_author($author, "author", esc_html($author)) .
 -            " [$ad{'rfc2822'}";
 -      print_local_time(%ad) if ($opts{-localtime});
 -      print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
 -                . "</$tag>\n";
 +            " [".format_timestamp_html(\%ad)."]".
 +            git_get_avatar($co->{'author_email'}, -pad_before => 1) .
 +            "</$tag>\n";
  }
  
  # Outputs table rows containing the full author or committer information,
@@@ -4227,16 -4054,16 +4227,16 @@@ sub git_print_authorship_rows 
                my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
                print "<tr><td>$who</td><td>" .
                      format_search_author($co->{"${who}_name"}, $who,
 -                             esc_html($co->{"${who}_name"})) . " " .
 +                                         esc_html($co->{"${who}_name"})) . " " .
                      format_search_author($co->{"${who}_email"}, $who,
 -                             esc_html("<" . $co->{"${who}_email"} . ">")) .
 +                                         esc_html("<" . $co->{"${who}_email"} . ">")) .
                      "</td><td rowspan=\"2\">" .
                      git_get_avatar($co->{"${who}_email"}, -size => 'double') .
                      "</td></tr>\n" .
                      "<tr>" .
 -                    "<td></td><td> $wd{'rfc2822'}";
 -              print_local_time(%wd);
 -              print "</td>" .
 +                    "<td></td><td>" .
 +                    format_timestamp_html(\%wd) .
 +                    "</td>" .
                      "</tr>\n";
        }
  }
@@@ -4979,9 -4806,8 +4979,9 @@@ sub git_patchset_body 
  
  # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
  
 -# fills project list info (age, description, owner, forks) for each
 -# project in the list, removing invalid projects from returned list
 +# fills project list info (age, description, owner, category, forks)
 +# for each project in the list, removing invalid projects from
 +# returned list
  # NOTE: modifies $projlist, but does not remove entries from it
  sub fill_project_list_info {
        my $projlist = shift;
                if ($show_ctags) {
                        $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
                }
 +              if ($projects_list_group_categories && !defined $pr->{'category'}) {
 +                      my $cat = git_get_project_category($pr->{'path'}) ||
 +                                                         $project_list_default_category;
 +                      $pr->{'category'} = to_utf8($cat);
 +              }
 +
                push @projects, $pr;
        }
  
@@@ -5040,23 -4860,6 +5040,23 @@@ sub sort_projects_list 
        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' <th> element, generating 'sort by $name' replay link
  # if that order is not selected
  sub print_sort_th {
@@@ -5080,55 -4883,6 +5080,55 @@@ sub format_sort_th 
        return $sort_th;
  }
  
 +sub git_project_list_rows {
 +      my ($projlist, $from, $to, $check_forks) = @_;
 +
 +      $from = 0 unless defined $from;
 +      $to = $#$projlist if (!defined $to || $#$projlist < $to);
 +
 +      my $alternate = 1;
 +      for (my $i = $from; $i <= $to; $i++) {
 +              my $pr = $projlist->[$i];
 +
 +              if ($alternate) {
 +                      print "<tr class=\"dark\">\n";
 +              } else {
 +                      print "<tr class=\"light\">\n";
 +              }
 +              $alternate ^= 1;
 +
 +              if ($check_forks) {
 +                      print "<td>";
 +                      if ($pr->{'forks'}) {
 +                              my $nforks = scalar @{$pr->{'forks'}};
 +                              if ($nforks > 0) {
 +                                      print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
 +                                                     -title => "$nforks forks"}, "+");
 +                              } else {
 +                                      print $cgi->span({-title => "$nforks forks"}, "+");
 +                              }
 +                      }
 +                      print "</td>\n";
 +              }
 +              print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
 +                                      -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
 +                    "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
 +                                      -class => "list", -title => $pr->{'descr_long'}},
 +                                      esc_html($pr->{'descr'})) . "</td>\n" .
 +                    "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
 +              print "<td class=\"". age_class($pr->{'age'}) . "\">" .
 +                    (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
 +                    "<td class=\"link\">" .
 +                    $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
 +                    $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
 +                    $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
 +                    $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
 +                    ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
 +                    "</td>\n" .
 +                    "</tr>\n";
 +      }
 +}
 +
  sub git_project_list_body {
        # actually uses global variable $project
        my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
        @projects = sort_projects_list(\@projects, $order);
  
        if ($show_ctags) {
 -              my %ctags;
 -              foreach my $p (@projects) {
 -                      foreach my $ct (keys %{$p->{'ctags'}}) {
 -                              $ctags{$ct} += $p->{'ctags'}->{$ct};
 -                      }
 -              }
 -              my $cloud = git_populate_project_tagcloud(\%ctags);
 +              my $ctags = git_gather_all_ctags(\@projects);
 +              my $cloud = git_populate_project_tagcloud($ctags);
                print git_show_project_tagcloud($cloud, 64);
        }
  
                print "<th></th>\n" . # for links
                      "</tr>\n";
        }
 -      my $alternate = 1;
 -      for (my $i = $from; $i <= $to; $i++) {
 -              my $pr = $projects[$i];
  
 -              if ($alternate) {
 -                      print "<tr class=\"dark\">\n";
 -              } else {
 -                      print "<tr class=\"light\">\n";
 -              }
 -              $alternate ^= 1;
 -
 -              if ($check_forks) {
 -                      print "<td>";
 -                      if ($pr->{'forks'}) {
 -                              my $nforks = scalar @{$pr->{'forks'}};
 -                              if ($nforks > 0) {
 -                                      print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
 -                                                     -title => "$nforks forks"}, "+");
 -                              } else {
 -                                      print $cgi->span({-title => "$nforks forks"}, "+");
 +      if ($projects_list_group_categories) {
 +              # only display categories with projects in the $from-$to window
 +              @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
 +              my %categories = build_projlist_by_category(\@projects, $from, $to);
 +              foreach my $cat (sort keys %categories) {
 +                      unless ($cat eq "") {
 +                              print "<tr>\n";
 +                              if ($check_forks) {
 +                                      print "<td></td>\n";
                                }
 +                              print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
 +                              print "</tr>\n";
                        }
 -                      print "</td>\n";
 +
 +                      git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
                }
 -              print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
 -                                      -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
 -                    "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
 -                                      -class => "list", -title => $pr->{'descr_long'}},
 -                                      esc_html($pr->{'descr'})) . "</td>\n" .
 -                    "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
 -              print "<td class=\"". age_class($pr->{'age'}) . "\">" .
 -                    (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
 -                    "<td class=\"link\">" .
 -                    $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
 -                    $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
 -                    $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
 -                    $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
 -                    ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
 -                    "</td>\n" .
 -                    "</tr>\n";
 +      } else {
 +              git_project_list_rows(\@projects, $from, $to, $check_forks);
        }
 +
        if (defined $extra) {
                print "<tr>\n";
                if ($check_forks) {
@@@ -5555,216 -5334,6 +5555,216 @@@ sub git_remotes_body 
        }
  }
  
 +sub git_search_message {
 +      my %co = @_;
 +
 +      my $greptype;
 +      if ($searchtype eq 'commit') {
 +              $greptype = "--grep=";
 +      } elsif ($searchtype eq 'author') {
 +              $greptype = "--author=";
 +      } elsif ($searchtype eq 'committer') {
 +              $greptype = "--committer=";
 +      }
 +      $greptype .= $searchtext;
 +      my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
 +                                     $greptype, '--regexp-ignore-case',
 +                                     $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
 +
 +      my $paging_nav = '';
 +      if ($page > 0) {
 +              $paging_nav .=
 +                      $cgi->a({-href => href(-replay=>1, page=>undef)},
 +                              "first") .
 +                      " &sdot; " .
 +                      $cgi->a({-href => href(-replay=>1, page=>$page-1),
 +                               -accesskey => "p", -title => "Alt-p"}, "prev");
 +      } else {
 +              $paging_nav .= "first &sdot; prev";
 +      }
 +      my $next_link = '';
 +      if ($#commitlist >= 100) {
 +              $next_link =
 +                      $cgi->a({-href => href(-replay=>1, page=>$page+1),
 +                               -accesskey => "n", -title => "Alt-n"}, "next");
 +              $paging_nav .= " &sdot; $next_link";
 +      } else {
 +              $paging_nav .= " &sdot; next";
 +      }
 +
 +      git_header_html();
 +
 +      git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
 +      git_print_header_div('commit', esc_html($co{'title'}), $hash);
 +      if ($page == 0 && !@commitlist) {
 +              print "<p>No match.</p>\n";
 +      } else {
 +              git_search_grep_body(\@commitlist, 0, 99, $next_link);
 +      }
 +
 +      git_footer_html();
 +}
 +
 +sub git_search_changes {
 +      my %co = @_;
 +
 +      local $/ = "\n";
 +      open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
 +              '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
 +              ($search_use_regexp ? '--pickaxe-regex' : ())
 +                      or die_error(500, "Open git-log failed");
 +
 +      git_header_html();
 +
 +      git_print_page_nav('','', $hash,$co{'tree'},$hash);
 +      git_print_header_div('commit', esc_html($co{'title'}), $hash);
 +
 +      print "<table class=\"pickaxe search\">\n";
 +      my $alternate = 1;
 +      undef %co;
 +      my @files;
 +      while (my $line = <$fd>) {
 +              chomp $line;
 +              next unless $line;
 +
 +              my %set = parse_difftree_raw_line($line);
 +              if (defined $set{'commit'}) {
 +                      # finish previous commit
 +                      if (%co) {
 +                              print "</td>\n" .
 +                                    "<td class=\"link\">" .
 +                                    $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
 +                                            "commit") .
 +                                    " | " .
 +                                    $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
 +                                                           hash_base=>$co{'id'})},
 +                                            "tree") .
 +                                    "</td>\n" .
 +                                    "</tr>\n";
 +                      }
 +
 +                      if ($alternate) {
 +                              print "<tr class=\"dark\">\n";
 +                      } else {
 +                              print "<tr class=\"light\">\n";
 +                      }
 +                      $alternate ^= 1;
 +                      %co = parse_commit($set{'commit'});
 +                      my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
 +                      print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
 +                            "<td><i>$author</i></td>\n" .
 +                            "<td>" .
 +                            $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
 +                                    -class => "list subject"},
 +                                    chop_and_escape_str($co{'title'}, 50) . "<br/>");
 +              } elsif (defined $set{'to_id'}) {
 +                      next if ($set{'to_id'} =~ m/^0{40}$/);
 +
 +                      print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
 +                                                   hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
 +                                    -class => "list"},
 +                                    "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
 +                            "<br/>\n";
 +              }
 +      }
 +      close $fd;
 +
 +      # finish last commit (warning: repetition!)
 +      if (%co) {
 +              print "</td>\n" .
 +                    "<td class=\"link\">" .
 +                    $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
 +                            "commit") .
 +                    " | " .
 +                    $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
 +                                           hash_base=>$co{'id'})},
 +                            "tree") .
 +                    "</td>\n" .
 +                    "</tr>\n";
 +      }
 +
 +      print "</table>\n";
 +
 +      git_footer_html();
 +}
 +
 +sub git_search_files {
 +      my %co = @_;
 +
 +      local $/ = "\n";
 +      open my $fd, "-|", git_cmd(), 'grep', '-n',
 +              $search_use_regexp ? ('-E', '-i') : '-F',
 +              $searchtext, $co{'tree'}
 +                      or die_error(500, "Open git-grep failed");
 +
 +      git_header_html();
 +
 +      git_print_page_nav('','', $hash,$co{'tree'},$hash);
 +      git_print_header_div('commit', esc_html($co{'title'}), $hash);
 +
 +      print "<table class=\"grep_search\">\n";
 +      my $alternate = 1;
 +      my $matches = 0;
 +      my $lastfile = '';
 +      while (my $line = <$fd>) {
 +              chomp $line;
 +              my ($file, $lno, $ltext, $binary);
 +              last if ($matches++ > 1000);
 +              if ($line =~ /^Binary file (.+) matches$/) {
 +                      $file = $1;
 +                      $binary = 1;
 +              } else {
 +                      (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
 +              }
 +              if ($file ne $lastfile) {
 +                      $lastfile and print "</td></tr>\n";
 +                      if ($alternate++) {
 +                              print "<tr class=\"dark\">\n";
 +                      } else {
 +                              print "<tr class=\"light\">\n";
 +                      }
 +                      print "<td class=\"list\">".
 +                              $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
 +                                                     file_name=>"$file"),
 +                                      -class => "list"}, esc_path($file));
 +                      print "</td><td>\n";
 +                      $lastfile = $file;
 +              }
 +              if ($binary) {
 +                      print "<div class=\"binary\">Binary file</div>\n";
 +              } else {
 +                      $ltext = untabify($ltext);
 +                      if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
 +                              $ltext = esc_html($1, -nbsp=>1);
 +                              $ltext .= '<span class="match">';
 +                              $ltext .= esc_html($2, -nbsp=>1);
 +                              $ltext .= '</span>';
 +                              $ltext .= esc_html($3, -nbsp=>1);
 +                      } else {
 +                              $ltext = esc_html($ltext, -nbsp=>1);
 +                      }
 +                      print "<div class=\"pre\">" .
 +                              $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
 +                                                     file_name=>"$file").'#l'.$lno,
 +                                      -class => "linenr"}, sprintf('%4i', $lno))
 +                              . ' ' .  $ltext . "</div>\n";
 +              }
 +      }
 +      if ($lastfile) {
 +              print "</td></tr>\n";
 +              if ($matches > 1000) {
 +                      print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
 +              }
 +      } else {
 +              print "<div class=\"diff nodifferences\">No matches found</div>\n";
 +      }
 +      close $fd;
 +
 +      print "</table>\n";
 +
 +      git_footer_html();
 +}
 +
  sub git_search_grep_body {
        my ($commitlist, $from, $to, $extra) = @_;
        $from = 0 unless defined $from;
@@@ -5934,8 -5503,7 +5934,8 @@@ sub git_summary 
              "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
              "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
        if (defined $cd{'rfc2822'}) {
 -              print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
 +              print "<tr id=\"metadata_lchange\"><td>last change</td>" .
 +                    "<td>".format_timestamp_html(\%cd)."</td></tr>\n";
        }
  
        # use per project git URL list in $projectroot/$project/cloneurl
        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 "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
 -              print "</td>\n<td>" unless %$ctags;
 -              print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
 -              print "</td>\n<td>" if %$ctags;
 -              print git_show_project_tagcloud($cloud, 48);
 -              print "</td></tr>";
 +              if (%$ctags) {
 +                      # without ability to add tags, don't show if there are none
 +                      my $cloud = git_populate_project_tagcloud($ctags);
 +                      print "<tr id=\"metadata_ctags\">" .
 +                            "<td>content tags</td>" .
 +                            "<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
 +                            "</tr>\n";
 +              }
        }
  
        print "</table>\n";
@@@ -6388,16 -5955,7 +6388,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,
@@@ -6495,8 -6053,7 +6495,8 @@@ sub git_blob 
                        $nr++;
                        $line = untabify($line);
                        printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
 -                             $nr, esc_attr(href(-replay => 1)), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1);
 +                             $nr, esc_attr(href(-replay => 1)), $nr, $nr,
 +                             $syntax ? sanitize($line) : esc_html($line, -nbsp=>1);
                }
        }
        close $fd
@@@ -7321,23 -6878,7 +7321,23 @@@ sub git_history 
  }
  
  sub git_search {
 -      gitweb_check_feature('search') or die_error(403, "Search is disabled");
 +      $searchtype ||= 'commit';
 +
 +      # check if appropriate features are enabled
 +      gitweb_check_feature('search')
 +              or die_error(403, "Search is disabled");
 +      if ($searchtype eq 'pickaxe') {
 +              # pickaxe may take all resources of your box and run for several minutes
 +              # with every query - so decide by yourself how public you make this feature
 +              gitweb_check_feature('pickaxe')
 +                      or die_error(403, "Pickaxe search is disabled");
 +      }
 +      if ($searchtype eq 'grep') {
 +              # grep search might be potentially CPU-intensive, too
 +              gitweb_check_feature('grep')
 +                      or die_error(403, "Grep search is disabled");
 +      }
 +
        if (!defined $searchtext) {
                die_error(400, "Text field is empty");
        }
                $page = 0;
        }
  
 -      $searchtype ||= 'commit';
 -      if ($searchtype eq 'pickaxe') {
 -              # pickaxe may take all resources of your box and run for several minutes
 -              # with every query - so decide by yourself how public you make this feature
 -              gitweb_check_feature('pickaxe')
 -                  or die_error(403, "Pickaxe is disabled");
 -      }
 -      if ($searchtype eq 'grep') {
 -              gitweb_check_feature('grep')
 -                  or die_error(403, "Grep is disabled");
 -      }
 -
 -      git_header_html();
 -
 -      if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
 -              my $greptype;
 -              if ($searchtype eq 'commit') {
 -                      $greptype = "--grep=";
 -              } elsif ($searchtype eq 'author') {
 -                      $greptype = "--author=";
 -              } elsif ($searchtype eq 'committer') {
 -                      $greptype = "--committer=";
 -              }
 -              $greptype .= $searchtext;
 -              my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
 -                                             $greptype, '--regexp-ignore-case',
 -                                             $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
 -
 -              my $paging_nav = '';
 -              if ($page > 0) {
 -                      $paging_nav .=
 -                              $cgi->a({-href => href(action=>"search", hash=>$hash,
 -                                                     searchtext=>$searchtext,
 -                                                     searchtype=>$searchtype)},
 -                                      "first");
 -                      $paging_nav .= " &sdot; " .
 -                              $cgi->a({-href => href(-replay=>1, page=>$page-1),
 -                                       -accesskey => "p", -title => "Alt-p"}, "prev");
 -              } else {
 -                      $paging_nav .= "first";
 -                      $paging_nav .= " &sdot; prev";
 -              }
 -              my $next_link = '';
 -              if ($#commitlist >= 100) {
 -                      $next_link =
 -                              $cgi->a({-href => href(-replay=>1, page=>$page+1),
 -                                       -accesskey => "n", -title => "Alt-n"}, "next");
 -                      $paging_nav .= " &sdot; $next_link";
 -              } else {
 -                      $paging_nav .= " &sdot; next";
 -              }
 -
 -              git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
 -              git_print_header_div('commit', esc_html($co{'title'}), $hash);
 -              if ($page == 0 && !@commitlist) {
 -                      print "<p>No match.</p>\n";
 -              } else {
 -                      git_search_grep_body(\@commitlist, 0, 99, $next_link);
 -              }
 -      }
 -
 -      if ($searchtype eq 'pickaxe') {
 -              git_print_page_nav('','', $hash,$co{'tree'},$hash);
 -              git_print_header_div('commit', esc_html($co{'title'}), $hash);
 -
 -              print "<table class=\"pickaxe search\">\n";
 -              my $alternate = 1;
 -              local $/ = "\n";
 -              open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
 -                      '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
 -                      ($search_use_regexp ? '--pickaxe-regex' : ());
 -              undef %co;
 -              my @files;
 -              while (my $line = <$fd>) {
 -                      chomp $line;
 -                      next unless $line;
 -
 -                      my %set = parse_difftree_raw_line($line);
 -                      if (defined $set{'commit'}) {
 -                              # finish previous commit
 -                              if (%co) {
 -                                      print "</td>\n" .
 -                                            "<td class=\"link\">" .
 -                                            $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
 -                                            " | " .
 -                                            $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
 -                                      print "</td>\n" .
 -                                            "</tr>\n";
 -                              }
 -
 -                              if ($alternate) {
 -                                      print "<tr class=\"dark\">\n";
 -                              } else {
 -                                      print "<tr class=\"light\">\n";
 -                              }
 -                              $alternate ^= 1;
 -                              %co = parse_commit($set{'commit'});
 -                              my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
 -                              print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
 -                                    "<td><i>$author</i></td>\n" .
 -                                    "<td>" .
 -                                    $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
 -                                            -class => "list subject"},
 -                                            chop_and_escape_str($co{'title'}, 50) . "<br/>");
 -                      } elsif (defined $set{'to_id'}) {
 -                              next if ($set{'to_id'} =~ m/^0{40}$/);
 -
 -                              print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
 -                                                           hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
 -                                            -class => "list"},
 -                                            "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
 -                                    "<br/>\n";
 -                      }
 -              }
 -              close $fd;
 -
 -              # finish last commit (warning: repetition!)
 -              if (%co) {
 -                      print "</td>\n" .
 -                            "<td class=\"link\">" .
 -                            $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
 -                            " | " .
 -                            $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
 -                      print "</td>\n" .
 -                            "</tr>\n";
 -              }
 -
 -              print "</table>\n";
 -      }
 -
 -      if ($searchtype eq 'grep') {
 -              git_print_page_nav('','', $hash,$co{'tree'},$hash);
 -              git_print_header_div('commit', esc_html($co{'title'}), $hash);
 -
 -              print "<table class=\"grep_search\">\n";
 -              my $alternate = 1;
 -              my $matches = 0;
 -              local $/ = "\n";
 -              open my $fd, "-|", git_cmd(), 'grep', '-n',
 -                      $search_use_regexp ? ('-E', '-i') : '-F',
 -                      $searchtext, $co{'tree'};
 -              my $lastfile = '';
 -              while (my $line = <$fd>) {
 -                      chomp $line;
 -                      my ($file, $lno, $ltext, $binary);
 -                      last if ($matches++ > 1000);
 -                      if ($line =~ /^Binary file (.+) matches$/) {
 -                              $file = $1;
 -                              $binary = 1;
 -                      } else {
 -                              (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
 -                      }
 -                      if ($file ne $lastfile) {
 -                              $lastfile and print "</td></tr>\n";
 -                              if ($alternate++) {
 -                                      print "<tr class=\"dark\">\n";
 -                              } else {
 -                                      print "<tr class=\"light\">\n";
 -                              }
 -                              print "<td class=\"list\">".
 -                                      $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
 -                                                             file_name=>"$file"),
 -                                              -class => "list"}, esc_path($file));
 -                              print "</td><td>\n";
 -                              $lastfile = $file;
 -                      }
 -                      if ($binary) {
 -                              print "<div class=\"binary\">Binary file</div>\n";
 -                      } else {
 -                              $ltext = untabify($ltext);
 -                              if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
 -                                      $ltext = esc_html($1, -nbsp=>1);
 -                                      $ltext .= '<span class="match">';
 -                                      $ltext .= esc_html($2, -nbsp=>1);
 -                                      $ltext .= '</span>';
 -                                      $ltext .= esc_html($3, -nbsp=>1);
 -                              } else {
 -                                      $ltext = esc_html($ltext, -nbsp=>1);
 -                              }
 -                              print "<div class=\"pre\">" .
 -                                      $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
 -                                                             file_name=>"$file").'#l'.$lno,
 -                                              -class => "linenr"}, sprintf('%4i', $lno))
 -                                      . ' ' .  $ltext . "</div>\n";
 -                      }
 -              }
 -              if ($lastfile) {
 -                      print "</td></tr>\n";
 -                      if ($matches > 1000) {
 -                              print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
 -                      }
 -              } else {
 -                      print "<div class=\"diff nodifferences\">No matches found</div>\n";
 -              }
 -              close $fd;
 -
 -              print "</table>\n";
 +      if ($searchtype eq 'commit' ||
 +          $searchtype eq 'author' ||
 +          $searchtype eq 'committer') {
 +              git_search_message(%co);
 +      } elsif ($searchtype eq 'pickaxe') {
 +              git_search_changes(%co);
 +      } elsif ($searchtype eq 'grep') {
 +              git_search_files(%co);
 +      } else {
 +              die_error(400, "Unknown search type");
        }
 -      git_footer_html();
  }
  
  sub git_search_help {