Code

Merge branch 'jn/maint-do-not-match-with-unsanitized-searchtext'
authorJunio C Hamano <gitster@pobox.com>
Thu, 8 Mar 2012 21:04:49 +0000 (13:04 -0800)
committerJunio C Hamano <gitster@pobox.com>
Thu, 8 Mar 2012 21:04:49 +0000 (13:04 -0800)
By Jakub Narebski
* jn/maint-do-not-match-with-unsanitized-searchtext:
  gitweb: Fix fixed string (non-regexp) project search

Conflicts:
gitweb/gitweb.perl

1  2 
gitweb/gitweb.perl

diff --combined gitweb/gitweb.perl
index cd98888a9325889ce1c8e06124c31ea5da2eea87,a7e0d8f6887c7a9cf2671d9d419c8728a3924fcf..a8b5fad26631207fcc17a6339ba735877c0902bd
@@@ -52,7 -52,7 +52,7 @@@ sub evaluate_uri 
        # as base URL.
        # Therefore, if we needed to strip PATH_INFO, then we know that we have
        # to build the base URL ourselves:
 -      our $path_info = $ENV{"PATH_INFO"};
 +      our $path_info = decode_utf8($ENV{"PATH_INFO"});
        if ($path_info) {
                if ($my_url =~ s,\Q$path_info\E$,, &&
                    $my_uri =~ s,\Q$path_info\E$,, &&
@@@ -85,8 -85,6 +85,8 @@@ our $home_link_str = "++GITWEB_HOME_LIN
  our $site_name = "++GITWEB_SITENAME++"
                   || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
  
 +# html snippet to include in the <head> section of each page
 +our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
  # filename of html text to include at top of each page
  our $site_header = "++GITWEB_SITE_HEADER++";
  # html text to include at home page
@@@ -323,10 -321,6 +323,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]},
@@@ -667,25 -661,13 +667,25 @@@ sub read_config_file 
        return;
  }
  
 -our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM);
 +our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
  sub evaluate_gitweb_config {
        our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
        our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
 +      our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
 +
 +      # 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);
  
 -      # use first config file that exists
 -      read_config_file($GITWEB_CONFIG) or
 +      # 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);
  }
  
@@@ -759,8 -741,6 +759,8 @@@ our @cgi_param_mapping = 
        extra_options => "opt",
        search_use_regexp => "sr",
        ctag => "by_tag",
 +      diff_style => "ds",
 +      project_filter => "pf",
        # this must be last entry (for manipulation from JavaScript)
        javascript => "js"
  );
@@@ -817,9 -797,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));
                }
        }
  }
@@@ -977,7 -957,7 +977,7 @@@ sub evaluate_path_info 
  
  our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
       $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
 -     $searchtext, $search_regexp);
 +     $searchtext, $search_regexp, $project_filter);
  sub evaluate_and_validate_params {
        our $action = $input_params{'action'};
        if (defined $action) {
                }
        }
  
 +      our $project_filter = $input_params{'project_filter'};
 +      if (defined $project_filter) {
 +              if (!validate_pathname($project_filter)) {
 +                      die_error(404, "Invalid project_filter parameter");
 +              }
 +      }
 +
        our $file_name = $input_params{'file_name'};
        if (defined $file_name) {
                if (!validate_pathname($file_name)) {
                if (length($searchtext) < 2) {
                        die_error(403, "At least two characters are required for search parameter");
                }
 -              $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
 +              if ($search_use_regexp) {
 +                      $search_regexp = $searchtext;
 +                      if (!eval { qr/$search_regexp/; 1; }) {
 +                              (my $error = $@) =~ s/ at \S+ line \d+.*\n?//;
 +                              die_error(400, "Invalid search regexp '$search_regexp'",
 +                                        esc_html($error));
 +                      }
 +              } else {
 +                      $search_regexp = quotemeta $searchtext;
 +              }
        }
  }
  
@@@ -1140,10 -1104,8 +1140,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 {
@@@ -1462,8 -1424,8 +1462,8 @@@ sub validate_refname 
  sub to_utf8 {
        my $str = shift;
        return undef unless defined $str;
 -      if (utf8::valid($str)) {
 -              utf8::decode($str);
 +
 +      if (utf8::is_utf8($str) || utf8::decode($str)) {
                return $str;
        } else {
                return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
@@@ -1539,17 -1501,6 +1539,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;
@@@ -1715,7 -1666,6 +1715,7 @@@ sub chop_and_escape_str 
        my ($str) = @_;
  
        my $chopped = chop_str(@_);
 +      $str = to_utf8($str);
        if ($chopped eq $str) {
                return esc_html($chopped);
        } else {
        }
  }
  
 +# Highlight selected fragments of string, using given CSS class,
 +# and escape HTML.  It is assumed that fragments do not overlap.
 +# Regions are passed as list of pairs (array references).
 +#
 +# Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
 +# '<span class="mark">foo</span>bar'
 +sub esc_html_hl_regions {
 +      my ($str, $css_class, @sel) = @_;
 +      return esc_html($str) unless @sel;
 +
 +      my $out = '';
 +      my $pos = 0;
 +
 +      for my $s (@sel) {
 +              $out .= esc_html(substr($str, $pos, $s->[0] - $pos))
 +                      if ($s->[0] - $pos > 0);
 +              $out .= $cgi->span({-class => $css_class},
 +                                 esc_html(substr($str, $s->[0], $s->[1] - $s->[0])));
 +
 +              $pos = $s->[1];
 +      }
 +      $out .= esc_html(substr($str, $pos))
 +              if ($pos < length($str));
 +
 +      return $out;
 +}
 +
 +# return positions of beginning and end of each match
 +sub matchpos_list {
 +      my ($str, $regexp) = @_;
 +      return unless (defined $str && defined $regexp);
 +
 +      my @matches;
 +      while ($str =~ /$regexp/g) {
 +              push @matches, [$-[0], $+[0]];
 +      }
 +      return @matches;
 +}
 +
 +# highlight match (if any), and escape HTML
 +sub esc_html_match_hl {
 +      my ($str, $regexp) = @_;
 +      return esc_html($str) unless defined $regexp;
 +
 +      my @matches = matchpos_list($str, $regexp);
 +      return esc_html($str) unless @matches;
 +
 +      return esc_html_hl_regions($str, 'match', @matches);
 +}
 +
 +
 +# highlight match (if any) of shortened string, and escape HTML
 +sub esc_html_match_hl_chopped {
 +      my ($str, $chopped, $regexp) = @_;
 +      return esc_html_match_hl($str, $regexp) unless defined $chopped;
 +
 +      my @matches = matchpos_list($str, $regexp);
 +      return esc_html($chopped) unless @matches;
 +
 +      # filter matches so that we mark chopped string
 +      my $tail = "... "; # see chop_str
 +      unless ($chopped =~ s/\Q$tail\E$//) {
 +              $tail = '';
 +      }
 +      my $chop_len = length($chopped);
 +      my $tail_len = length($tail);
 +      my @filtered;
 +
 +      for my $m (@matches) {
 +              if ($m->[0] > $chop_len) {
 +                      push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
 +                      last;
 +              } elsif ($m->[1] > $chop_len) {
 +                      push @filtered, [ $m->[0], $chop_len + $tail_len ];
 +                      last;
 +              }
 +              push @filtered, $m;
 +      }
 +
 +      return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
 +}
 +
  ## ----------------------------------------------------------------------
  ## functions returning short strings
  
@@@ -2328,119 -2196,93 +2328,119 @@@ sub format_diff_cc_simplified 
        return $result;
  }
  
 -# format patch (diff) line (not to be used for diff headers)
 -sub format_diff_line {
 -      my $line = shift;
 -      my ($from, $to) = @_;
 -      my $diff_class = "";
 -
 -      chomp $line;
 +sub diff_line_class {
 +      my ($line, $from, $to) = @_;
  
 +      # ordinary diff
 +      my $num_sign = 1;
 +      # combined diff
        if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
 -              # combined diff
 -              my $prefix = substr($line, 0, scalar @{$from->{'href'}});
 -              if ($line =~ m/^\@{3}/) {
 -                      $diff_class = " chunk_header";
 -              } elsif ($line =~ m/^\\/) {
 -                      $diff_class = " incomplete";
 -              } elsif ($prefix =~ tr/+/+/) {
 -                      $diff_class = " add";
 -              } elsif ($prefix =~ tr/-/-/) {
 -                      $diff_class = " rem";
 -              }
 -      } else {
 -              # assume ordinary diff
 -              my $char = substr($line, 0, 1);
 -              if ($char eq '+') {
 -                      $diff_class = " add";
 -              } elsif ($char eq '-') {
 -                      $diff_class = " rem";
 -              } elsif ($char eq '@') {
 -                      $diff_class = " chunk_header";
 -              } elsif ($char eq "\\") {
 -                      $diff_class = " incomplete";
 -              }
 +              $num_sign = scalar @{$from->{'href'}};
 +      }
 +
 +      my @diff_line_classifier = (
 +              { regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
 +              { regexp => qr/^\\/,               class => "incomplete"  },
 +              { regexp => qr/^ {$num_sign}/,     class => "ctx" },
 +              # classifier for context must come before classifier add/rem,
 +              # or we would have to use more complicated regexp, for example
 +              # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
 +              { regexp => qr/^[+ ]{$num_sign}/,   class => "add" },
 +              { regexp => qr/^[- ]{$num_sign}/,   class => "rem" },
 +      );
 +      for my $clsfy (@diff_line_classifier) {
 +              return $clsfy->{'class'}
 +                      if ($line =~ $clsfy->{'regexp'});
        }
 -      $line = untabify($line);
 -      if ($from && $to && $line =~ m/^\@{2} /) {
 -              my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
 -                      $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
  
 -              $from_lines = 0 unless defined $from_lines;
 -              $to_lines   = 0 unless defined $to_lines;
 +      # fallback
 +      return "";
 +}
  
 -              if ($from->{'href'}) {
 -                      $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
 -                                           -class=>"list"}, $from_text);
 -              }
 -              if ($to->{'href'}) {
 -                      $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
 -                                           -class=>"list"}, $to_text);
 -              }
 -              $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
 -                      "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
 -              return "<div class=\"diff$diff_class\">$line</div>\n";
 -      } elsif ($from && $to && $line =~ m/^\@{3}/) {
 -              my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
 -              my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
 +# assumes that $from and $to are defined and correctly filled,
 +# and that $line holds a line of chunk header for unified diff
 +sub format_unidiff_chunk_header {
 +      my ($line, $from, $to) = @_;
  
 -              @from_text = split(' ', $ranges);
 -              for (my $i = 0; $i < @from_text; ++$i) {
 -                      ($from_start[$i], $from_nlines[$i]) =
 -                              (split(',', substr($from_text[$i], 1)), 0);
 -              }
 +      my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
 +              $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
  
 -              $to_text   = pop @from_text;
 -              $to_start  = pop @from_start;
 -              $to_nlines = pop @from_nlines;
 +      $from_lines = 0 unless defined $from_lines;
 +      $to_lines   = 0 unless defined $to_lines;
  
 -              $line = "<span class=\"chunk_info\">$prefix ";
 -              for (my $i = 0; $i < @from_text; ++$i) {
 -                      if ($from->{'href'}[$i]) {
 -                              $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
 -                                                -class=>"list"}, $from_text[$i]);
 -                      } else {
 -                              $line .= $from_text[$i];
 -                      }
 -                      $line .= " ";
 -              }
 -              if ($to->{'href'}) {
 -                      $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
 -                                        -class=>"list"}, $to_text);
 +      if ($from->{'href'}) {
 +              $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
 +                                   -class=>"list"}, $from_text);
 +      }
 +      if ($to->{'href'}) {
 +              $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
 +                                   -class=>"list"}, $to_text);
 +      }
 +      $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
 +              "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
 +      return $line;
 +}
 +
 +# assumes that $from and $to are defined and correctly filled,
 +# and that $line holds a line of chunk header for combined diff
 +sub format_cc_diff_chunk_header {
 +      my ($line, $from, $to) = @_;
 +
 +      my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
 +      my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
 +
 +      @from_text = split(' ', $ranges);
 +      for (my $i = 0; $i < @from_text; ++$i) {
 +              ($from_start[$i], $from_nlines[$i]) =
 +                      (split(',', substr($from_text[$i], 1)), 0);
 +      }
 +
 +      $to_text   = pop @from_text;
 +      $to_start  = pop @from_start;
 +      $to_nlines = pop @from_nlines;
 +
 +      $line = "<span class=\"chunk_info\">$prefix ";
 +      for (my $i = 0; $i < @from_text; ++$i) {
 +              if ($from->{'href'}[$i]) {
 +                      $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
 +                                        -class=>"list"}, $from_text[$i]);
                } else {
 -                      $line .= $to_text;
 +                      $line .= $from_text[$i];
                }
 -              $line .= " $prefix</span>" .
 -                       "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
 -              return "<div class=\"diff$diff_class\">$line</div>\n";
 +              $line .= " ";
 +      }
 +      if ($to->{'href'}) {
 +              $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
 +                                -class=>"list"}, $to_text);
 +      } else {
 +              $line .= $to_text;
 +      }
 +      $line .= " $prefix</span>" .
 +               "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
 +      return $line;
 +}
 +
 +# process patch (diff) line (not to be used for diff headers),
 +# returning class and HTML-formatted (but not wrapped) line
 +sub process_diff_line {
 +      my $line = shift;
 +      my ($from, $to) = @_;
 +
 +      my $diff_class = diff_line_class($line, $from, $to);
 +
 +      chomp $line;
 +      $line = untabify($line);
 +
 +      if ($from && $to && $line =~ m/^\@{2} /) {
 +              $line = format_unidiff_chunk_header($line, $from, $to);
 +              return $diff_class, $line;
 +
 +      } elsif ($from && $to && $line =~ m/^\@{3}/) {
 +              $line = format_cc_diff_chunk_header($line, $from, $to);
 +              return $diff_class, $line;
 +
        }
 -      return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
 +      return $diff_class, esc_html($line, -nbsp=>1);
  }
  
  # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
@@@ -2492,7 -2334,7 +2492,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
@@@ -2668,13 -2510,6 +2668,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/);
  
@@@ -2866,7 -2701,7 +2866,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) {
@@@ -2928,18 -2763,19 +2928,18 @@@ sub git_get_project_url_list 
  
  sub git_get_projects_list {
        my $filter = shift || '';
 +      my $paranoid = shift;
        my @list;
  
 -      $filter =~ s/\.git$//;
 -
        if (-d $projects_list) {
                # search in directory
                my $dir = $projects_list;
                # remove the trailing "/"
                $dir =~ s!/+$!!;
 -              my $pfxlen = length("$projects_list");
 -              my $pfxdepth = ($projects_list =~ tr!/!!);
 +              my $pfxlen = length("$dir");
 +              my $pfxdepth = ($dir =~ tr!/!!);
                # when filtering, search only given subdirectory
 -              if ($filter) {
 +              if ($filter && !$paranoid) {
                        $dir .= "/$filter";
                        $dir =~ s!/+$!!;
                }
                                }
  
                                my $path = substr($File::Find::name, $pfxlen + 1);
 +                              # paranoidly only filter here
 +                              if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
 +                                      next;
 +                              }
                                # we check related file in $projectroot
                                if (check_export_ok("$projectroot/$path")) {
                                        push @list, { path => $path };
@@@ -3020,7 -2852,7 +3020,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
  sub search_projects_list {
        my ($projlist, %opts) = @_;
        my $tagfilter  = $opts{'tagfilter'};
-       my $searchtext = $opts{'searchtext'};
+       my $search_re = $opts{'search_regexp'};
  
        return @$projlist
-               unless ($tagfilter || $searchtext);
+               unless ($tagfilter || $search_re);
  
-                              $searchtext ? ('path', 'descr') : ());
 +      # searching projects require filling to be run before it;
 +      fill_project_list_info($projlist,
 +                             $tagfilter  ? 'ctags' : (),
++                             $search_re ? ('path', 'descr') : ());
        my @projects;
   PROJECT:
        foreach my $pr (@$projlist) {
                                grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
                }
  
-               if ($searchtext) {
+               if ($search_re) {
                        next unless
-                               $pr->{'path'} =~ /$searchtext/ ||
-                               $pr->{'descr_long'} =~ /$searchtext/;
+                               $pr->{'path'} =~ /$search_re/ ||
+                               $pr->{'descr_long'} =~ /$search_re/;
                }
  
                push @projects, $pr;
@@@ -3734,9 -3562,12 +3734,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);
@@@ -3837,12 -3668,7 +3837,12 @@@ sub run_highlighter 
  sub get_page_title {
        my $title = to_utf8($site_name);
  
 -      return $title unless (defined $project);
 +      unless (defined $project) {
 +              if (defined $project_filter) {
 +                      $title .= " - projects in '" . esc_path($project_filter) . "'";
 +              }
 +              return $title;
 +      }
        $title .= " - " . to_utf8($project);
  
        return $title unless (defined $action);
        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_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) {
 +              my @dirname = split '/', $project;
 +              my $projectbasename = pop @dirname;
 +              print_nav_breadcrumbs_path(@dirname);
 +              print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));
 +              if (defined $action) {
 +                      my $action_print = $action ;
 +                      if (defined $opts{-action_extra}) {
 +                              $action_print = $cgi->a({-href => href(action=>$action)},
 +                                      $action);
 +                      }
 +                      print " / $action_print";
 +              }
 +              if (defined $opts{-action_extra}) {
 +                      print " / $opts{-action_extra}";
 +              }
 +              print "\n";
 +      } elsif (defined $project_filter) {
 +              print_nav_breadcrumbs_path(split '/', $project_filter);
 +      }
 +}
 +
 +sub print_search_form {
 +      if (!defined $searchtext) {
 +              $searchtext = "";
 +      }
 +      my $search_hash;
 +      if (defined $hash_base) {
 +              $search_hash = $hash_base;
 +      } elsif (defined $hash) {
 +              $search_hash = $hash;
 +      } else {
 +              $search_hash = "HEAD";
 +      }
 +      my $action = $my_uri;
 +      my $use_pathinfo = gitweb_check_feature('pathinfo');
 +      if ($use_pathinfo) {
 +              $action .= "/".esc_url($project);
 +      }
 +      print $cgi->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, -override => 1) . "\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);
 +
 +      if (defined $site_html_head_string) {
 +              print to_utf8($site_html_head_string);
        }
  
        print "</head>\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();
        }
  }
  
@@@ -4093,11 -3879,9 +4093,11 @@@ sub git_footer_html 
                }
  
        } else {
 -              print $cgi->a({-href => href(project=>undef, action=>"opml"),
 +              print $cgi->a({-href => href(project=>undef, action=>"opml",
 +                                           project_filter => $project_filter),
                              -class => $feed_class}, "OPML") . " ";
 -              print $cgi->a({-href => href(project=>undef, action=>"project_index"),
 +              print $cgi->a({-href => href(project=>undef, action=>"project_index",
 +                                           project_filter => $project_filter),
                              -class => $feed_class}, "TXT") . "\n";
        }
        print "</div>\n"; # class="page_footer"
@@@ -4993,97 -4777,8 +4993,97 @@@ sub git_difftree_body 
        print "</table>\n";
  }
  
 +sub print_sidebyside_diff_chunk {
 +      my @chunk = @_;
 +      my (@ctx, @rem, @add);
 +
 +      return unless @chunk;
 +
 +      # incomplete last line might be among removed or added lines,
 +      # or both, or among context lines: find which
 +      for (my $i = 1; $i < @chunk; $i++) {
 +              if ($chunk[$i][0] eq 'incomplete') {
 +                      $chunk[$i][0] = $chunk[$i-1][0];
 +              }
 +      }
 +
 +      # guardian
 +      push @chunk, ["", ""];
 +
 +      foreach my $line_info (@chunk) {
 +              my ($class, $line) = @$line_info;
 +
 +              # print chunk headers
 +              if ($class && $class eq 'chunk_header') {
 +                      print $line;
 +                      next;
 +              }
 +
 +              ## print from accumulator when type of class of lines change
 +              # empty contents block on start rem/add block, or end of chunk
 +              if (@ctx && (!$class || $class eq 'rem' || $class eq 'add')) {
 +                      print join '',
 +                              '<div class="chunk_block ctx">',
 +                                      '<div class="old">',
 +                                      @ctx,
 +                                      '</div>',
 +                                      '<div class="new">',
 +                                      @ctx,
 +                                      '</div>',
 +                              '</div>';
 +                      @ctx = ();
 +              }
 +              # empty add/rem block on start context block, or end of chunk
 +              if ((@rem || @add) && (!$class || $class eq 'ctx')) {
 +                      if (!@add) {
 +                              # pure removal
 +                              print join '',
 +                                      '<div class="chunk_block rem">',
 +                                              '<div class="old">',
 +                                              @rem,
 +                                              '</div>',
 +                                      '</div>';
 +                      } elsif (!@rem) {
 +                              # pure addition
 +                              print join '',
 +                                      '<div class="chunk_block add">',
 +                                              '<div class="new">',
 +                                              @add,
 +                                              '</div>',
 +                                      '</div>';
 +                      } else {
 +                              # assume that it is change
 +                              print join '',
 +                                      '<div class="chunk_block chg">',
 +                                              '<div class="old">',
 +                                              @rem,
 +                                              '</div>',
 +                                              '<div class="new">',
 +                                              @add,
 +                                              '</div>',
 +                                      '</div>';
 +                      }
 +                      @rem = @add = ();
 +              }
 +
 +              ## adding lines to accumulator
 +              # guardian value
 +              last unless $line;
 +              # rem, add or change
 +              if ($class eq 'rem') {
 +                      push @rem, $line;
 +              } elsif ($class eq 'add') {
 +                      push @add, $line;
 +              }
 +              # context line
 +              if ($class eq 'ctx') {
 +                      push @ctx, $line;
 +              }
 +      }
 +}
 +
  sub git_patchset_body {
 -      my ($fd, $difftree, $hash, @hash_parents) = @_;
 +      my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
        my ($hash_parent) = $hash_parents[0];
  
        my $is_combined = (@hash_parents > 1);
        my $diffinfo;
        my $to_name;
        my (%from, %to);
 +      my @chunk; # for side-by-side diff
  
        print "<div class=\"patchset\">\n";
  
  
                        next PATCH if ($patch_line =~ m/^diff /);
  
 -                      print format_diff_line($patch_line, \%from, \%to);
 +                      my ($class, $line) = process_diff_line($patch_line, \%from, \%to);
 +                      my $diff_classes = "diff";
 +                      $diff_classes .= " $class" if ($class);
 +                      $line = "<div class=\"$diff_classes\">$line</div>\n";
 +
 +                      if ($diff_style eq 'sidebyside' && !$is_combined) {
 +                              if ($class eq 'chunk_header') {
 +                                      print_sidebyside_diff_chunk(@chunk);
 +                                      @chunk = ( [ $class, $line ] );
 +                              } else {
 +                                      push @chunk, [ $class, $line ];
 +                              }
 +                      } else {
 +                              # default 'inline' style and unknown styles
 +                              print $line;
 +                      }
                }
  
        } continue {
 +              if (@chunk) {
 +                      print_sidebyside_diff_chunk(@chunk);
 +                      @chunk = ();
 +              }
                print "</div>\n"; # class="patch"
        }
  
  
  # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
  
 -# 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);
@@@ -5450,17 -5062,10 +5450,17 @@@ sub git_project_list_rows 
                        print "</td>\n";
                }
                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
 -                                      -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
 +                                      -class => "list"},
 +                                     esc_html_match_hl($pr->{'path'}, $search_regexp)) .
 +                    "</td>\n" .
                      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
 -                                      -class => "list", -title => $pr->{'descr_long'}},
 -                                      esc_html($pr->{'descr'})) . "</td>\n" .
 +                                      -class => "list",
 +                                      -title => $pr->{'descr_long'}},
 +                                      $search_regexp
 +                                      ? esc_html_match_hl_chopped($pr->{'descr_long'},
 +                                                                  $pr->{'descr'}, $search_regexp)
 +                                      : 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" .
@@@ -5482,20 -5087,19 +5482,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);
+               if ($tagfilter || $search_regexp);
  
        # 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,
+                                        'search_regexp' => $search_regexp,
                                         'tagfilter'  => $tagfilter)
-               if ($tagfilter || $searchtext);
+               if ($tagfilter || $search_regexp);
 +      # fill the rest
 +      @projects = fill_project_list_info(\@projects);
  
        $order ||= $default_projects_order;
        $from = 0 unless defined $from;
@@@ -5771,7 -5375,7 +5771,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);
  
        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 {
@@@ -5902,217 -5506,6 +5902,217 @@@ 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', '-z',
 +              $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 = '';
 +      my $file_href;
 +      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 {
 +                      ($file, $lno, $ltext) = split(/\0/, $line, 3);
 +                      $file =~ s/^$co{'tree'}://;
 +              }
 +              if ($file ne $lastfile) {
 +                      $lastfile and print "</td></tr>\n";
 +                      if ($alternate++) {
 +                              print "<tr class=\"dark\">\n";
 +                      } else {
 +                              print "<tr class=\"light\">\n";
 +                      }
 +                      $file_href = href(action=>"blob", hash_base=>$co{'id'},
 +                                        file_name=>$file);
 +                      print "<td class=\"list\">".
 +                              $cgi->a({-href => $file_href, -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 => $file_href.'#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;
@@@ -6183,7 -5576,7 +6183,7 @@@ sub git_project_list 
                die_error(400, "Unknown order parameter");
        }
  
 -      my @list = git_get_projects_list();
 +      my @list = git_get_projects_list($project_filter, $strict_export);
        if (!@list) {
                die_error(404, "No projects found");
        }
                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();
  }
@@@ -6206,9 -5602,7 +6206,9 @@@ sub git_forks 
                die_error(400, "Unknown order parameter");
        }
  
 -      my @list = git_get_projects_list($project);
 +      my $filter = $project;
 +      $filter =~ s/\.git$//;
 +      my @list = git_get_projects_list($filter);
        if (!@list) {
                die_error(404, "No forks found");
        }
  }
  
  sub git_project_index {
 -      my @projects = git_get_projects_list();
 +      my @projects = git_get_projects_list($project_filter, $strict_export);
        if (!@projects) {
                die_error(404, "No projects found");
        }
@@@ -6267,9 -5661,7 +6267,9 @@@ sub git_summary 
  
        if ($check_forks) {
                # find forks of a project
 -              @forklist = git_get_projects_list($project);
 +              my $filter = $project;
 +              $filter =~ s/\.git$//;
 +              @forklist = git_get_projects_list($filter);
                # filter out forks of forks
                @forklist = filter_forks_from_projects_list(\@forklist)
                        if (@forklist);
@@@ -6400,7 -5792,7 +6400,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
        }
                        -type=>"text/plain", -charset => "utf-8",
                        -status=> "200 OK");
                local $| = 1; # output autoflush
 -              print while <$fd>;
 +              while (my $line = <$fd>) {
 +                      print to_utf8($line);
 +              }
                close $fd
                        or print "ERROR $!\n";
  
@@@ -6846,8 -6236,7 +6846,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
@@@ -7293,7 -6682,6 +7293,7 @@@ sub git_object 
  
  sub git_blobdiff {
        my $format = shift || 'html';
 +      my $diff_style = $input_params{'diff_style'} || 'inline';
  
        my $fd;
        my @difftree;
                my $formats_nav =
                        $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
                                "raw");
 +              $formats_nav .= diff_style_nav($diff_style);
                git_header_html(undef, $expires);
                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
                        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
        if ($format eq 'html') {
                print "<div class=\"page_body\">\n";
  
 -              git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
 +              git_patchset_body($fd, $diff_style,
 +                                [ \%diffinfo ], $hash_base, $hash_parent_base);
                close $fd;
  
                print "</div>\n"; # class="page_body"
@@@ -7430,31 -6816,9 +7430,31 @@@ sub git_blobdiff_plain 
        git_blobdiff('plain');
  }
  
 +# assumes that it is added as later part of already existing navigation,
 +# so it returns "| foo | bar" rather than just "foo | bar"
 +sub diff_style_nav {
 +      my ($diff_style, $is_combined) = @_;
 +      $diff_style ||= 'inline';
 +
 +      return "" if ($is_combined);
 +
 +      my @styles = (inline => 'inline', 'sidebyside' => 'side by side');
 +      my %styles = @styles;
 +      @styles =
 +              @styles[ map { $_ * 2 } 0..$#styles/2 ];
 +
 +      return join '',
 +              map { " | ".$_ }
 +              map {
 +                      $_ eq $diff_style ? $styles{$_} :
 +                      $cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_})
 +              } @styles;
 +}
 +
  sub git_commitdiff {
        my %params = @_;
        my $format = $params{-format} || 'html';
 +      my $diff_style = $input_params{'diff_style'} || 'inline';
  
        my ($patch_max) = gitweb_get_feature('patches');
        if ($format eq 'patch') {
                                $cgi->a({-href => href(action=>"patch", -replay=>1)},
                                        "patch");
                }
 +              $formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
  
                if (defined $hash_parent &&
                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
                                }
                        }
                        $formats_nav .= ': ' .
 -                              $cgi->a({-href => href(action=>"commitdiff",
 -                                                     hash=>$hash_parent)},
 +                              $cgi->a({-href => href(-replay=>1,
 +                                                     hash=>$hash_parent, hash_base=>undef)},
                                        esc_html($hash_parent_short)) .
                                ')';
                } elsif (!$co{'parent'}) {
                        # single parent commit
                        $formats_nav .=
                                ' (parent: ' .
 -                              $cgi->a({-href => href(action=>"commitdiff",
 -                                                     hash=>$co{'parent'})},
 +                              $cgi->a({-href => href(-replay=>1,
 +                                                     hash=>$co{'parent'}, hash_base=>undef)},
                                        esc_html(substr($co{'parent'}, 0, 7))) .
                                ')';
                } else {
                        # merge commit
                        if ($hash_parent eq '--cc') {
                                $formats_nav .= ' | ' .
 -                                      $cgi->a({-href => href(action=>"commitdiff",
 +                                      $cgi->a({-href => href(-replay=>1,
                                                               hash=>$hash, hash_parent=>'-c')},
                                                'combined');
                        } else { # $hash_parent eq '-c'
                                $formats_nav .= ' | ' .
 -                                      $cgi->a({-href => href(action=>"commitdiff",
 +                                      $cgi->a({-href => href(-replay=>1,
                                                               hash=>$hash, hash_parent=>'--cc')},
                                                'compact');
                        }
                        $formats_nav .=
                                ' (merge: ' .
                                join(' ', map {
 -                                      $cgi->a({-href => href(action=>"commitdiff",
 -                                                             hash=>$_)},
 +                                      $cgi->a({-href => href(-replay=>1,
 +                                                             hash=>$_, hash_base=>undef)},
                                                esc_html(substr($_, 0, 7)));
                                } @{$co{'parents'}} ) .
                                ')';
                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
                print "<br/>\n";
  
 -              git_patchset_body($fd, \@difftree, $hash,
 +              git_patchset_body($fd, $diff_style,
 +                                \@difftree, $hash,
                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
                close $fd;
                print "</div>\n"; # class="page_body"
@@@ -7699,23 -7061,7 +7699,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 {
@@@ -8060,7 -7594,7 +8060,7 @@@ sub git_atom 
  }
  
  sub git_opml {
 -      my @list = git_get_projects_list();
 +      my @list = git_get_projects_list($project_filter, $strict_export);
        if (!@list) {
                die_error(404, "No projects found");
        }
                -charset => 'utf-8',
                -content_disposition => 'inline; filename="opml.xml"');
  
 +      my $title = esc_html($site_name);
 +      my $filter = " within subdirectory ";
 +      if (defined $project_filter) {
 +              $filter .= esc_html($project_filter);
 +      } else {
 +              $filter = "";
 +      }
        print <<XML;
  <?xml version="1.0" encoding="utf-8"?>
  <opml version="1.0">
  <head>
 -  <title>$site_name OPML Export</title>
 +  <title>$title OPML Export$filter</title>
  </head>
  <body>
  <outline text="git RSS feeds">