Merge branch 'os/gitweb-highlight-uncaptured' into maint
authorJunio C Hamano <gitster@pobox.com>
Sat, 12 Jan 2013 00:48:30 +0000 (16:48 -0800)
committerJunio C Hamano <gitster@pobox.com>
Sat, 12 Jan 2013 00:48:30 +0000 (16:48 -0800)
"gitweb", when sorting by age to show repositories with new
activities first, used to sort repositories with absolutely nothing
in it early, which was not very useful.

* os/gitweb-highlight-uncaptured:
gitweb: fix error in sanitize when highlight is enabled

1  2 
gitweb/gitweb.perl
diff --combined gitweb/gitweb.perl
index 656b324fb7f82c7f65c71075f57e909843c50cce,8d34d55884233765ec2bd0cd495c92f8ab5b064e..c6bafe6ead815ece2e820d303e442270749c5c4b
@@@ -54,11 -54,6 +54,11 @@@ sub evaluate_uri 
        # to build the base URL ourselves:
        our $path_info = decode_utf8($ENV{"PATH_INFO"});
        if ($path_info) {
 +              # $path_info has already been URL-decoded by the web server, but
 +              # $my_url and $my_uri have not. URL-decode them so we can properly
 +              # strip $path_info.
 +              $my_url = unescape($my_url);
 +              $my_uri = unescape($my_uri);
                if ($my_url =~ s,\Q$path_info\E$,, &&
                    $my_uri =~ s,\Q$path_info\E$,, &&
                    defined $ENV{'SCRIPT_NAME'}) {
@@@ -138,12 -133,6 +138,12 @@@ our $default_projects_order = "project"
  # (only effective if this variable evaluates to true)
  our $export_ok = "++GITWEB_EXPORT_OK++";
  
 +# don't generate age column on the projects list page
 +our $omit_age_column = 0;
 +
 +# don't generate information about owners of repositories
 +our $omit_owner=0;
 +
  # show repository only if this subroutine returns true
  # when given the path to the project, for example:
  #    sub { return -e "$_[0]/git-daemon-export-ok"; }
@@@ -270,15 -259,16 +270,15 @@@ our %highlight_basename = 
  our %highlight_ext = (
        # main extensions, defining name of syntax;
        # see files in /usr/share/highlight/langDefs/ directory
 -      map { $_ => $_ }
 -              qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl sql make),
 +      (map { $_ => $_ } qw(py rb java css js tex bib xml awk bat ini spec tcl sql)),
        # alternate extensions, see /etc/highlight/filetypes.conf
 -      'h' => 'c',
 -      map { $_ => 'sh'  } qw(bash zsh ksh),
 -      map { $_ => 'cpp' } qw(cxx c++ cc),
 -      map { $_ => 'php' } qw(php3 php4 php5 phps),
 -      map { $_ => 'pl'  } qw(perl pm), # perhaps also 'cgi'
 -      map { $_ => 'make'} qw(mak mk),
 -      map { $_ => 'xml' } qw(xhtml html htm),
 +      (map { $_ => 'c'   } qw(c h)),
 +      (map { $_ => 'sh'  } qw(sh bash zsh ksh)),
 +      (map { $_ => 'cpp' } qw(cpp cxx c++ cc)),
 +      (map { $_ => 'php' } qw(php php3 php4 php5 phps)),
 +      (map { $_ => 'pl'  } qw(pl perl pm)), # perhaps also 'cgi'
 +      (map { $_ => 'make'} qw(make mak mk)),
 +      (map { $_ => 'xml' } qw(xml xhtml html htm)),
  );
  
  # You define site-wide feature defaults here; override them with
@@@ -540,7 -530,7 +540,7 @@@ our %feature = 
        # $feature{'remote_heads'}{'default'} = [1];
        # To have project specific config enable override in $GITWEB_CONFIG
        # $feature{'remote_heads'}{'override'} = 1;
 -      # and in project config gitweb.remote_heads = 0|1;
 +      # and in project config gitweb.remoteheads = 0|1;
        'remote_heads' => {
                'sub' => sub { feature_bool('remote_heads', @_) },
                'override' => 0,
@@@ -770,7 -760,6 +770,7 @@@ our @cgi_param_mapping = 
        search_use_regexp => "sr",
        ctag => "by_tag",
        diff_style => "ds",
 +      project_filter => "pf",
        # this must be last entry (for manipulation from JavaScript)
        javascript => "js"
  );
@@@ -987,7 -976,7 +987,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)) {
@@@ -1556,7 -1538,7 +1556,7 @@@ sub sanitize 
        return undef unless defined $str;
  
        $str = to_utf8($str);
-       $str =~ s|([[:cntrl:]])|($1 =~ /[\t\n\r]/ ? $1 : quot_cec($1))|eg;
+       $str =~ s|([[:cntrl:]])|(index("\t\n\r", $1) != -1 ? $1 : quot_cec($1))|eg;
        return $str;
  }
  
@@@ -1734,97 -1716,6 +1734,97 @@@ sub chop_and_escape_str 
        }
  }
  
 +# Highlight selected fragments of string, using given CSS class,
 +# and escape HTML.  It is assumed that fragments do not overlap.
 +# Regions are passed as list of pairs (array references).
 +#
 +# Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
 +# '<span class="mark">foo</span>bar'
 +sub esc_html_hl_regions {
 +      my ($str, $css_class, @sel) = @_;
 +      my %opts = grep { ref($_) ne 'ARRAY' } @sel;
 +      @sel     = grep { ref($_) eq 'ARRAY' } @sel;
 +      return esc_html($str, %opts) unless @sel;
 +
 +      my $out = '';
 +      my $pos = 0;
 +
 +      for my $s (@sel) {
 +              my ($begin, $end) = @$s;
 +
 +              # Don't create empty <span> elements.
 +              next if $end <= $begin;
 +
 +              my $escaped = esc_html(substr($str, $begin, $end - $begin),
 +                                     %opts);
 +
 +              $out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
 +                      if ($begin - $pos > 0);
 +              $out .= $cgi->span({-class => $css_class}, $escaped);
 +
 +              $pos = $end;
 +      }
 +      $out .= esc_html(substr($str, $pos), %opts)
 +              if ($pos < length($str));
 +
 +      return $out;
 +}
 +
 +# return positions of beginning and end of each match
 +sub matchpos_list {
 +      my ($str, $regexp) = @_;
 +      return unless (defined $str && defined $regexp);
 +
 +      my @matches;
 +      while ($str =~ /$regexp/g) {
 +              push @matches, [$-[0], $+[0]];
 +      }
 +      return @matches;
 +}
 +
 +# highlight match (if any), and escape HTML
 +sub esc_html_match_hl {
 +      my ($str, $regexp) = @_;
 +      return esc_html($str) unless defined $regexp;
 +
 +      my @matches = matchpos_list($str, $regexp);
 +      return esc_html($str) unless @matches;
 +
 +      return esc_html_hl_regions($str, 'match', @matches);
 +}
 +
 +
 +# highlight match (if any) of shortened string, and escape HTML
 +sub esc_html_match_hl_chopped {
 +      my ($str, $chopped, $regexp) = @_;
 +      return esc_html_match_hl($str, $regexp) unless defined $chopped;
 +
 +      my @matches = matchpos_list($str, $regexp);
 +      return esc_html($chopped) unless @matches;
 +
 +      # filter matches so that we mark chopped string
 +      my $tail = "... "; # see chop_str
 +      unless ($chopped =~ s/\Q$tail\E$//) {
 +              $tail = '';
 +      }
 +      my $chop_len = length($chopped);
 +      my $tail_len = length($tail);
 +      my @filtered;
 +
 +      for my $m (@matches) {
 +              if ($m->[0] > $chop_len) {
 +                      push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
 +                      last;
 +              } elsif ($m->[1] > $chop_len) {
 +                      push @filtered, [ $m->[0], $chop_len + $tail_len ];
 +                      last;
 +              }
 +              push @filtered, $m;
 +      }
 +
 +      return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
 +}
 +
  ## ----------------------------------------------------------------------
  ## functions returning short strings
  
@@@ -2440,32 -2331,26 +2440,32 @@@ sub format_cc_diff_chunk_header 
  }
  
  # 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);
 +# returning HTML-formatted (but not wrapped) line.
 +# If the line is passed as a reference, it is treated as HTML and not
 +# esc_html()'ed.
 +sub format_diff_line {
 +      my ($line, $diff_class, $from, $to) = @_;
 +
 +      if (ref($line)) {
 +              $line = $$line;
 +      } else {
 +              chomp $line;
 +              $line = untabify($line);
  
 -      if ($from && $to && $line =~ m/^\@{2} /) {
 -              $line = format_unidiff_chunk_header($line, $from, $to);
 -              return $diff_class, $line;
 +              if ($from && $to && $line =~ m/^\@{2} /) {
 +                      $line = format_unidiff_chunk_header($line, $from, $to);
 +              } elsif ($from && $to && $line =~ m/^\@{3}/) {
 +                      $line = format_cc_diff_chunk_header($line, $from, $to);
 +              } else {
 +                      $line = esc_html($line, -nbsp=>1);
 +              }
 +      }
  
 -      } elsif ($from && $to && $line =~ m/^\@{3}/) {
 -              $line = format_cc_diff_chunk_header($line, $from, $to);
 -              return $diff_class, $line;
 +      my $diff_classes = "diff";
 +      $diff_classes .= " $diff_class" if ($diff_class);
 +      $line = "<div class=\"$diff_classes\">$line</div>\n";
  
 -      }
 -      return $diff_class, esc_html($line, -nbsp=>1);
 +      return $line;
  }
  
  # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
@@@ -2696,15 -2581,12 +2696,15 @@@ sub git_get_project_config 
        # only subsection, if exists, is case sensitive,
        # and not lowercased by 'git config -z -l'
        if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
 +              $lo =~ s/_//g;
                $key = join(".", lc($hi), $mi, lc($lo));
 +              return if ($lo =~ /\W/ || $hi =~ /\W/);
        } else {
                $key = lc($key);
 +              $key =~ s/_//g;
 +              return if ($key =~ /\W/);
        }
        $key =~ s/^gitweb\.//;
 -      return if ($key =~ m/\W/);
  
        # type sanity check
        if (defined $type) {
@@@ -2956,9 -2838,10 +2956,9 @@@ 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;
                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 };
                        }
                        if (check_export_ok("$projectroot/$path")) {
                                my $pr = {
 -                                      path => $path,
 -                                      owner => to_utf8($owner),
 +                                      path => $path
                                };
 +                              if ($owner) {
 +                                      $pr->{'owner'} = to_utf8($owner);
 +                              }
                                push @list, $pr;
                        }
                }
@@@ -3108,10 -2985,6 +3108,10 @@@ sub search_projects_list 
        return @$projlist
                unless ($tagfilter || $search_re);
  
 +      # searching projects require filling to be run before it;
 +      fill_project_list_info($projlist,
 +                             $tagfilter  ? 'ctags' : (),
 +                             $search_re ? ('path', 'descr') : ());
        my @projects;
   PROJECT:
        foreach my $pr (@$projlist) {
@@@ -3867,12 -3740,7 +3867,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);
@@@ -3916,7 -3784,6 +3916,7 @@@ sub print_feed_meta 
                                '-type' => "application/$type+xml"
                        );
  
 +                      $href_params{'extra_options'} = undef;
                        $href_params{'action'} = $type;
                        $link_attr{'-href'} = href(%href_params);
                        print "<link ".
@@@ -3967,27 -3834,12 +3967,27 @@@ sub print_header_links 
        }
  }
  
 +sub print_nav_breadcrumbs_path {
 +      my $dirprefix = undef;
 +      while (my $part = shift) {
 +              $dirprefix .= "/" if defined $dirprefix;
 +              $dirprefix .= $part;
 +              print $cgi->a({-href => href(project => undef,
 +                                           project_filter => $dirprefix,
 +                                           action => "project_list")},
 +                            esc_html($part)) . " / ";
 +      }
 +}
 +
  sub print_nav_breadcrumbs {
        my %opts = @_;
  
        print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
        if (defined $project) {
 -              print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
 +              my @dirname = split '/', $project;
 +              my $projectbasename = pop @dirname;
 +              print_nav_breadcrumbs_path(@dirname);
 +              print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));
                if (defined $action) {
                        my $action_print = $action ;
                        if (defined $opts{-action_extra}) {
                        print " / $opts{-action_extra}";
                }
                print "\n";
 +      } elsif (defined $project_filter) {
 +              print_nav_breadcrumbs_path(split '/', $project_filter);
        }
  }
  
@@@ -4124,11 -3974,9 +4124,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"
@@@ -4491,33 -4339,30 +4491,33 @@@ sub git_print_log 
        }
  
        # print log
 -      my $signoff = 0;
 -      my $empty = 0;
 +      my $skip_blank_line = 0;
        foreach my $line (@$log) {
 -              if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
 -                      $signoff = 1;
 -                      $empty = 0;
 +              if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
                        if (! $opts{'-remove_signoff'}) {
                                print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
 -                              next;
 -                      } else {
 -                              # remove signoff lines
 -                              next;
 +                              $skip_blank_line = 1;
                        }
 -              } else {
 -                      $signoff = 0;
 +                      next;
 +              }
 +
 +              if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) {
 +                      if (! $opts{'-remove_signoff'}) {
 +                              print "<span class=\"signoff\">" . esc_html($1) . ": " .
 +                                      "<a href=\"" . esc_html($2) . "\">" . esc_html($2) . "</a>" .
 +                                      "</span><br/>\n";
 +                              $skip_blank_line = 1;
 +                      }
 +                      next;
                }
  
                # print only one empty line
                # do not print empty line after signoff
                if ($line eq "") {
 -                      next if ($empty || $signoff);
 -                      $empty = 1;
 +                      next if ($skip_blank_line);
 +                      $skip_blank_line = 1;
                } else {
 -                      $empty = 0;
 +                      $skip_blank_line = 0;
                }
  
                print format_log_line_html($line) . "<br/>\n";
  
        if ($opts{'-final_empty_line'}) {
                # end with single empty line
 -              print "<br/>\n" unless $empty;
 +              print "<br/>\n" unless $skip_blank_line;
        }
  }
  
@@@ -5027,186 -4872,10 +5027,186 @@@ sub git_difftree_body 
        print "</table>\n";
  }
  
 -sub print_sidebyside_diff_chunk {
 -      my @chunk = @_;
 +# Print context lines and then rem/add lines in a side-by-side manner.
 +sub print_sidebyside_diff_lines {
 +      my ($ctx, $rem, $add) = @_;
 +
 +      # print context block before add/rem block
 +      if (@$ctx) {
 +              print join '',
 +                      '<div class="chunk_block ctx">',
 +                              '<div class="old">',
 +                              @$ctx,
 +                              '</div>',
 +                              '<div class="new">',
 +                              @$ctx,
 +                              '</div>',
 +                      '</div>';
 +      }
 +
 +      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 {
 +              print join '',
 +                      '<div class="chunk_block chg">',
 +                              '<div class="old">',
 +                              @$rem,
 +                              '</div>',
 +                              '<div class="new">',
 +                              @$add,
 +                              '</div>',
 +                      '</div>';
 +      }
 +}
 +
 +# Print context lines and then rem/add lines in inline manner.
 +sub print_inline_diff_lines {
 +      my ($ctx, $rem, $add) = @_;
 +
 +      print @$ctx, @$rem, @$add;
 +}
 +
 +# Format removed and added line, mark changed part and HTML-format them.
 +# Implementation is based on contrib/diff-highlight
 +sub format_rem_add_lines_pair {
 +      my ($rem, $add, $num_parents) = @_;
 +
 +      # We need to untabify lines before split()'ing them;
 +      # otherwise offsets would be invalid.
 +      chomp $rem;
 +      chomp $add;
 +      $rem = untabify($rem);
 +      $add = untabify($add);
 +
 +      my @rem = split(//, $rem);
 +      my @add = split(//, $add);
 +      my ($esc_rem, $esc_add);
 +      # Ignore leading +/- characters for each parent.
 +      my ($prefix_len, $suffix_len) = ($num_parents, 0);
 +      my ($prefix_has_nonspace, $suffix_has_nonspace);
 +
 +      my $shorter = (@rem < @add) ? @rem : @add;
 +      while ($prefix_len < $shorter) {
 +              last if ($rem[$prefix_len] ne $add[$prefix_len]);
 +
 +              $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
 +              $prefix_len++;
 +      }
 +
 +      while ($prefix_len + $suffix_len < $shorter) {
 +              last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
 +
 +              $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
 +              $suffix_len++;
 +      }
 +
 +      # Mark lines that are different from each other, but have some common
 +      # part that isn't whitespace.  If lines are completely different, don't
 +      # mark them because that would make output unreadable, especially if
 +      # diff consists of multiple lines.
 +      if ($prefix_has_nonspace || $suffix_has_nonspace) {
 +              $esc_rem = esc_html_hl_regions($rem, 'marked',
 +                      [$prefix_len, @rem - $suffix_len], -nbsp=>1);
 +              $esc_add = esc_html_hl_regions($add, 'marked',
 +                      [$prefix_len, @add - $suffix_len], -nbsp=>1);
 +      } else {
 +              $esc_rem = esc_html($rem, -nbsp=>1);
 +              $esc_add = esc_html($add, -nbsp=>1);
 +      }
 +
 +      return format_diff_line(\$esc_rem, 'rem'),
 +             format_diff_line(\$esc_add, 'add');
 +}
 +
 +# HTML-format diff context, removed and added lines.
 +sub format_ctx_rem_add_lines {
 +      my ($ctx, $rem, $add, $num_parents) = @_;
 +      my (@new_ctx, @new_rem, @new_add);
 +      my $can_highlight = 0;
 +      my $is_combined = ($num_parents > 1);
 +
 +      # Highlight if every removed line has a corresponding added line.
 +      if (@$add > 0 && @$add == @$rem) {
 +              $can_highlight = 1;
 +
 +              # Highlight lines in combined diff only if the chunk contains
 +              # diff between the same version, e.g.
 +              #
 +              #    - a
 +              #   -  b
 +              #    + c
 +              #   +  d
 +              #
 +              # Otherwise the highlightling would be confusing.
 +              if ($is_combined) {
 +                      for (my $i = 0; $i < @$add; $i++) {
 +                              my $prefix_rem = substr($rem->[$i], 0, $num_parents);
 +                              my $prefix_add = substr($add->[$i], 0, $num_parents);
 +
 +                              $prefix_rem =~ s/-/+/g;
 +
 +                              if ($prefix_rem ne $prefix_add) {
 +                                      $can_highlight = 0;
 +                                      last;
 +                              }
 +                      }
 +              }
 +      }
 +
 +      if ($can_highlight) {
 +              for (my $i = 0; $i < @$add; $i++) {
 +                      my ($line_rem, $line_add) = format_rem_add_lines_pair(
 +                              $rem->[$i], $add->[$i], $num_parents);
 +                      push @new_rem, $line_rem;
 +                      push @new_add, $line_add;
 +              }
 +      } else {
 +              @new_rem = map { format_diff_line($_, 'rem') } @$rem;
 +              @new_add = map { format_diff_line($_, 'add') } @$add;
 +      }
 +
 +      @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
 +
 +      return (\@new_ctx, \@new_rem, \@new_add);
 +}
 +
 +# Print context lines and then rem/add lines.
 +sub print_diff_lines {
 +      my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
 +      my $is_combined = $num_parents > 1;
 +
 +      ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
 +              $num_parents);
 +
 +      if ($diff_style eq 'sidebyside' && !$is_combined) {
 +              print_sidebyside_diff_lines($ctx, $rem, $add);
 +      } else {
 +              # default 'inline' style and unknown styles
 +              print_inline_diff_lines($ctx, $rem, $add);
 +      }
 +}
 +
 +sub print_diff_chunk {
 +      my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
        my (@ctx, @rem, @add);
  
 +      # The class of the previous line.
 +      my $prev_class = '';
 +
        return unless @chunk;
  
        # incomplete last line might be among removed or added lines,
  
                # print chunk headers
                if ($class && $class eq 'chunk_header') {
 -                      print $line;
 +                      print format_diff_line($line, $class, $from, $to);
                        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 = ();
 +              ## print from accumulator when have some add/rem lines or end
 +              # of chunk (flush context lines), or when have add and rem
 +              # lines and new block is reached (otherwise add/rem lines could
 +              # be reordered)
 +              if (!$class || ((@rem || @add) && $class eq 'ctx') ||
 +                  (@rem && @add && $class ne $prev_class)) {
 +                      print_diff_lines(\@ctx, \@rem, \@add,
 +                                       $diff_style, $num_parents);
 +                      @ctx = @rem = @add = ();
                }
  
                ## adding lines to accumulator
                if ($class eq 'ctx') {
                        push @ctx, $line;
                }
 +
 +              $prev_class = $class;
        }
  }
  
@@@ -5376,19 -5079,27 +5376,19 @@@ sub git_patchset_body 
  
                        next PATCH if ($patch_line =~ m/^diff /);
  
 -                      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";
 +                      my $class = diff_line_class($patch_line, \%from, \%to);
  
 -                      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;
 +                      if ($class eq 'chunk_header') {
 +                              print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
 +                              @chunk = ();
                        }
 +
 +                      push @chunk, [ $class, $patch_line ];
                }
  
        } continue {
                if (@chunk) {
 -                      print_sidebyside_diff_chunk(@chunk);
 +                      print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @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);
  
  sub sort_projects_list {
        my ($projlist, $order) = @_;
 -      my @projects;
  
 -      my %order_info = (
 -              project => { key => 'path', type => 'str' },
 -              descr => { key => 'descr_long', type => 'str' },
 -              owner => { key => 'owner', type => 'str' },
 -              age => { key => 'age', type => 'num' }
 -      );
 -      my $oi = $order_info{$order};
 -      return @$projlist unless defined $oi;
 -      if ($oi->{'type'} eq 'str') {
 -              @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist;
 -      } else {
 -              @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist;
 +      sub order_str {
 +              my $key = shift;
 +              return sub { $a->{$key} cmp $b->{$key} };
        }
  
 -      return @projects;
 +      sub order_num_then_undef {
 +              my $key = shift;
 +              return sub {
 +                      defined $a->{$key} ?
 +                              (defined $b->{$key} ? $a->{$key} <=> $b->{$key} : -1) :
 +                              (defined $b->{$key} ? 1 : 0)
 +              };
 +      }
 +
 +      my %orderings = (
 +              project => order_str('path'),
 +              descr => order_str('descr_long'),
 +              owner => order_str('owner'),
 +              age => order_num_then_undef('age'),
 +      );
 +
 +      my $ordering = $orderings{$order};
 +      return defined $ordering ? sort $ordering @$projlist : @$projlist;
  }
  
  # returns a hash of categories, containing the list of project
@@@ -5625,25 -5266,14 +5625,25 @@@ 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" .
 -                    "<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\">" .
 +                                      -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";
 +              unless ($omit_owner) {
 +                      print "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
 +              }
 +              unless ($omit_age_column) {
 +                      print "<td class=\"". age_class($pr->{'age'}) . "\">" .
 +                          (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n";
 +              }
 +              print"<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") . " | " .
@@@ -5668,16 -5298,12 +5668,16 @@@ sub git_project_list_body 
        # 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,
                                         'search_regexp' => $search_regexp,
                                         'tagfilter'  => $tagfilter)
                if ($tagfilter || $search_regexp);
 +      # fill the rest
 +      my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
 +      push @all_fields, ('age', 'age_string') unless($omit_age_column);
 +      push @all_fields, 'owner' unless($omit_owner);
 +      @projects = fill_project_list_info(\@projects, @all_fields);
  
        $order ||= $default_projects_order;
        $from = 0 unless defined $from;
                }
                print_sort_th('project', $order, 'Project');
                print_sort_th('descr', $order, 'Description');
 -              print_sort_th('owner', $order, 'Owner');
 -              print_sort_th('age', $order, 'Last Change');
 +              print_sort_th('owner', $order, 'Owner') unless $omit_owner;
 +              print_sort_th('age', $order, 'Last Change') unless $omit_age_column;
                print "<th></th>\n" . # for links
                      "</tr>\n";
        }
@@@ -6365,7 -5991,7 +6365,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, -override => 1) . "\n" .
 -            "</p>" .
 -            $cgi->end_form() . "\n";
 +
 +      git_project_search_form($searchtext, $search_use_regexp);
        git_project_list_body(\@list, $order);
        git_footer_html();
  }
@@@ -6388,9 -6017,7 +6388,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");
        }
@@@ -6449,9 -6076,7 +6449,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);
  
        print "<div class=\"title\">&nbsp;</div>\n";
        print "<table class=\"projects_list\">\n" .
 -            "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
 -            "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
 +            "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n";
 +        unless ($omit_owner) {
 +              print  "<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>".format_timestamp_html(\%cd)."</td></tr>\n";
@@@ -7187,28 -6810,6 +7187,28 @@@ sub snapshot_name 
        return wantarray ? ($name, $name) : $name;
  }
  
 +sub exit_if_unmodified_since {
 +      my ($latest_epoch) = @_;
 +      our $cgi;
 +
 +      my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
 +      if (defined $if_modified) {
 +              my $since;
 +              if (eval { require HTTP::Date; 1; }) {
 +                      $since = HTTP::Date::str2time($if_modified);
 +              } elsif (eval { require Time::ParseDate; 1; }) {
 +                      $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
 +              }
 +              if (defined $since && $latest_epoch <= $since) {
 +                      my %latest_date = parse_date($latest_epoch);
 +                      print $cgi->header(
 +                              -last_modified => $latest_date{'rfc2822'},
 +                              -status => '304 Not Modified');
 +                      goto DONE_GITWEB;
 +              }
 +      }
 +}
 +
  sub git_snapshot {
        my $format = $input_params{'snapshot_format'};
        if (!@snapshot_fmts) {
  
        my ($name, $prefix) = snapshot_name($project, $hash);
        my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
 +
 +      my %co = parse_commit($hash);
 +      exit_if_unmodified_since($co{'committer_epoch'}) if %co;
 +
        my $cmd = quote_command(
                git_cmd(), 'archive',
                "--format=$known_snapshot_formats{$format}{'format'}",
        }
  
        $filename =~ s/(["\\])/\\$1/g;
 +      my %latest_date;
 +      if (%co) {
 +              %latest_date = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
 +      }
 +
        print $cgi->header(
                -type => $known_snapshot_formats{$format}{'type'},
                -content_disposition => 'inline; filename="' . $filename . '"',
 +              %co ? (-last_modified => $latest_date{'rfc2822'}) : (),
                -status => '200 OK');
  
        open my $fd, "-|", $cmd
@@@ -8036,14 -7627,33 +8036,14 @@@ sub git_feed 
        if (defined($commitlist[0])) {
                %latest_commit = %{$commitlist[0]};
                my $latest_epoch = $latest_commit{'committer_epoch'};
 -              %latest_date   = parse_date($latest_epoch, $latest_commit{'comitter_tz'});
 -              my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
 -              if (defined $if_modified) {
 -                      my $since;
 -                      if (eval { require HTTP::Date; 1; }) {
 -                              $since = HTTP::Date::str2time($if_modified);
 -                      } elsif (eval { require Time::ParseDate; 1; }) {
 -                              $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
 -                      }
 -                      if (defined $since && $latest_epoch <= $since) {
 -                              print $cgi->header(
 -                                      -type => $content_type,
 -                                      -charset => 'utf-8',
 -                                      -last_modified => $latest_date{'rfc2822'},
 -                                      -status => '304 Not Modified');
 -                              return;
 -                      }
 -              }
 -              print $cgi->header(
 -                      -type => $content_type,
 -                      -charset => 'utf-8',
 -                      -last_modified => $latest_date{'rfc2822'});
 -      } else {
 -              print $cgi->header(
 -                      -type => $content_type,
 -                      -charset => 'utf-8');
 +              exit_if_unmodified_since($latest_epoch);
 +              %latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'});
        }
 +      print $cgi->header(
 +              -type => $content_type,
 +              -charset => 'utf-8',
 +              %latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (),
 +              -status => '200 OK');
  
        # Optimization: skip generating the body if client asks only
        # for Last-Modified date.
                $feed_type = 'history';
        }
        $title .= " $feed_type";
 +      $title = esc_html($title);
        my $descr = git_get_project_description($project);
        if (defined $descr) {
                $descr = esc_html($descr);
@@@ -8258,7 -7867,7 +8258,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");
        }
                -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>$title OPML Export</title>
 +  <title>$title OPML Export$filter</title>
  </head>
  <body>
  <outline text="git RSS feeds">