Code

Merge gitk changes from Paul Mackerras at git://ozlabs.org/~paulus/gitk
[git.git] / gitweb / gitweb.perl
index a0c6a9b23994c8d4a9d73621fb9ce248f7a20cd3..a8b5fad26631207fcc17a6339ba735877c0902bd 100755 (executable)
@@ -1081,7 +1081,16 @@ sub evaluate_and_validate_params {
                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;
+               }
        }
 }
 
@@ -1742,20 +1751,61 @@ sub esc_html_hl_regions {
        return $out;
 }
 
-# highlight match (if any), and escape HTML
-sub esc_html_match_hl {
+# return positions of beginning and end of each match
+sub matchpos_list {
        my ($str, $regexp) = @_;
-       return esc_html($str) unless defined $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
 
@@ -3023,15 +3073,15 @@ sub filter_forks_from_projects_list {
 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);
 
        # searching projects require filling to be run before it;
        fill_project_list_info($projlist,
                               $tagfilter  ? 'ctags' : (),
-                              $searchtext ? ('path', 'descr') : ());
+                              $search_re ? ('path', 'descr') : ());
        my @projects;
  PROJECT:
        foreach my $pr (@$projlist) {
@@ -3042,10 +3092,10 @@ sub search_projects_list {
                                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;
@@ -5206,7 +5256,7 @@ sub git_patchset_body {
 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
 
 sub git_project_search_form {
-       my ($searchtext, $search_use_regexp);
+       my ($searchtext, $search_use_regexp) = @_;
 
        my $limit = '';
        if ($project_filter) {
@@ -5404,8 +5454,13 @@ sub git_project_list_rows {
                                       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" .
@@ -5429,16 +5484,16 @@ sub git_project_list_body {
        my $show_ctags  = gitweb_check_feature('ctags');
        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);
        # 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);
 
@@ -5716,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);
 
@@ -5725,7 +5780,7 @@ sub git_heads_body {
        for (my $i = $from; $i <= $to; $i++) {
                my $entry = $headlist->[$i];
                my %ref = %$entry;
-               my $curr = $ref{'id'} eq $head;
+               my $curr = defined $head_at && $ref{'id'} eq $head_at;
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
                } else {
@@ -5998,9 +6053,10 @@ sub git_search_files {
        my $alternate = 1;
        my $matches = 0;
        my $lastfile = '';
+       my $file_href;
        while (my $line = <$fd>) {
                chomp $line;
-               my ($file, $file_href, $lno, $ltext, $binary);
+               my ($file, $lno, $ltext, $binary);
                last if ($matches++ > 1000);
                if ($line =~ /^Binary file (.+) matches$/) {
                        $file = $1;