Code

Merge branch 'jc/checkout' (early part)
[git.git] / gitweb / gitweb.perl
index 79e518a9122d7f0874f2e593aea141b4be28ef18..e49eb91d69c2344163ab9477883777476c789bc7 100755 (executable)
@@ -18,6 +18,10 @@ use File::Find qw();
 use File::Basename qw(basename);
 binmode STDOUT, ':utf8';
 
+BEGIN {
+       CGI->compile() if $ENV{'MOD_PERL'};
+}
+
 our $cgi = new CGI;
 our $version = "++GIT_VERSION++";
 our $my_url = $cgi->url();
@@ -128,6 +132,12 @@ our %feature = (
                #         => [content-encoding, suffix, program]
                'default' => ['x-gzip', 'gz', 'gzip']},
 
+       # Enable text search, which will list the commits which match author,
+       # committer or commit text to a given string.  Enabled by default.
+       'search' => {
+               'override' => 0,
+               'default' => [1]},
+
        # Enable the pickaxe search, which will list the commits that modified
        # a given string in a file. This can be practical and quite faster
        # alternative to 'blame', but still potentially CPU-intensive.
@@ -351,6 +361,9 @@ if (defined $searchtext) {
        if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) {
                die_error(undef, "Invalid search parameter");
        }
+       if (length($searchtext) < 2) {
+               die_error(undef, "At least two characters are required for search parameter");
+       }
        $searchtext = quotemeta $searchtext;
 }
 
@@ -578,7 +591,7 @@ sub esc_html ($;%) {
        my %opts = @_;
 
        $str = to_utf8($str);
-       $str = escapeHTML($str);
+       $str = $cgi->escapeHTML($str);
        if ($opts{'-nbsp'}) {
                $str =~ s/ /&nbsp;/g;
        }
@@ -592,7 +605,7 @@ sub esc_path {
        my %opts = @_;
 
        $str = to_utf8($str);
-       $str = escapeHTML($str);
+       $str = $cgi->escapeHTML($str);
        if ($opts{'-nbsp'}) {
                $str =~ s/ /&nbsp;/g;
        }
@@ -821,7 +834,7 @@ sub file_type_long {
 
 ## ----------------------------------------------------------------------
 ## functions returning short HTML fragments, or transforming HTML fragments
-## which don't beling to other sections
+## which don't belong to other sections
 
 # format line of commit message.
 sub format_log_line_html {
@@ -973,7 +986,7 @@ sub git_get_project_config {
        $key =~ s/^gitweb\.//;
        return if ($key =~ m/\W/);
 
-       my @x = (git_cmd(), 'repo-config');
+       my @x = (git_cmd(), 'config');
        if (defined $type) { push @x, $type; }
        push @x, "--get";
        push @x, "gitweb.$key";
@@ -1139,8 +1152,9 @@ sub git_get_last_activity {
 
        $git_dir = "$projectroot/$path";
        open($fd, "-|", git_cmd(), 'for-each-ref',
-            '--format=%(refname) %(committer)',
+            '--format=%(committer)',
             '--sort=-committerdate',
+            '--count=1',
             'refs/heads') or return;
        my $most_recent = <$fd>;
        close $fd or return;
@@ -1260,36 +1274,24 @@ sub parse_tag {
        return %tag
 }
 
-sub parse_commit {
-       my $commit_id = shift;
-       my $commit_text = shift;
-
-       my @commit_lines;
+sub parse_commit_text {
+       my ($commit_text, $withparents) = @_;
+       my @commit_lines = split '\n', $commit_text;
        my %co;
 
-       if (defined $commit_text) {
-               @commit_lines = @$commit_text;
-       } else {
-               local $/ = "\0";
-               open my $fd, "-|", git_cmd(), "rev-list",
-                       "--header", "--parents", "--max-count=1",
-                       $commit_id, "--"
-                       or return;
-               @commit_lines = split '\n', <$fd>;
-               close $fd or return;
-               pop @commit_lines;
-       }
+       pop @commit_lines; # Remove '\0'
+
        my $header = shift @commit_lines;
        if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
                return;
        }
        ($co{'id'}, my @parents) = split ' ', $header;
-       $co{'parents'} = \@parents;
-       $co{'parent'} = $parents[0];
        while (my $line = shift @commit_lines) {
                last if $line eq "\n";
                if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
                        $co{'tree'} = $1;
+               } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
+                       push @parents, $1;
                } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
                        $co{'author'} = $1;
                        $co{'author_epoch'} = $2;
@@ -1316,6 +1318,8 @@ sub parse_commit {
        if (!defined $co{'tree'}) {
                return;
        };
+       $co{'parents'} = \@parents;
+       $co{'parent'} = $parents[0];
 
        foreach my $title (@commit_lines) {
                $title =~ s/^    //;
@@ -1365,6 +1369,52 @@ sub parse_commit {
        return %co;
 }
 
+sub parse_commit {
+       my ($commit_id) = @_;
+       my %co;
+
+       local $/ = "\0";
+
+       open my $fd, "-|", git_cmd(), "rev-list",
+               "--parents",
+               "--header",
+               "--max-count=1",
+               $commit_id,
+               "--",
+               or die_error(undef, "Open git-rev-list failed");
+       %co = parse_commit_text(<$fd>, 1);
+       close $fd;
+
+       return %co;
+}
+
+sub parse_commits {
+       my ($commit_id, $maxcount, $skip, $arg, $filename) = @_;
+       my @cos;
+
+       $maxcount ||= 1;
+       $skip ||= 0;
+
+       local $/ = "\0";
+
+       open my $fd, "-|", git_cmd(), "rev-list",
+               "--header",
+               ($arg ? ($arg) : ()),
+               ("--max-count=" . $maxcount),
+               ("--skip=" . $skip),
+               $commit_id,
+               "--",
+               ($filename ? ($filename) : ())
+               or die_error(undef, "Open git-rev-list failed");
+       while (my $line = <$fd>) {
+               my %co = parse_commit_text($line);
+               push @cos, \%co;
+       }
+       close $fd;
+
+       return wantarray ? @cos : \@cos;
+}
+
 # parse ref from ref_file, given by ref_id, with given type
 sub parse_ref {
        my $ref_file = shift;
@@ -1640,7 +1690,7 @@ sub git_header_html {
 
        my $title = "$site_name";
        if (defined $project) {
-               $title .= " - $project";
+               $title .= " - " . to_utf8($project);
                if (defined $action) {
                        $title .= "/$action";
                        if (defined $file_name) {
@@ -1665,6 +1715,7 @@ sub git_header_html {
        }
        print $cgi->header(-type=>$content_type, -charset => 'utf-8',
                           -status=> $status, -expires => $expires);
+       my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
        print <<EOF;
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -1673,7 +1724,7 @@ sub git_header_html {
 <!-- git core binaries version $git_version -->
 <head>
 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
-<meta name="generator" content="gitweb/$version git/$git_version"/>
+<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
 <meta name="robots" content="index, nofollow"/>
 <title>$title</title>
 EOF
@@ -1726,6 +1777,9 @@ EOF
                        print " / $action";
                }
                print "\n";
+       }
+       my ($have_search) = gitweb_check_feature('search');
+       if ((defined $project) && ($have_search)) {
                if (!defined $searchtext) {
                        $searchtext = "";
                }
@@ -1746,7 +1800,7 @@ EOF
                      $cgi->hidden(-name => "a") . "\n" .
                      $cgi->hidden(-name => "h") . "\n" .
                      $cgi->popup_menu(-name => 'st', -default => 'commit',
-                                      -values => ['commit', 'author', 'committer', 'pickaxe']) .
+                                      -values => ['commit', 'author', 'committer', 'pickaxe']) .
                      $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
                      " search:\n",
                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
@@ -1816,16 +1870,16 @@ sub git_print_page_nav {
        my %arg = map { $_ => {action=>$_} } @navs;
        if (defined $head) {
                for (qw(commit commitdiff)) {
-                       $arg{$_}{hash} = $head;
+                       $arg{$_}{'hash'} = $head;
                }
                if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
                        for (qw(shortlog log)) {
-                               $arg{$_}{hash} = $head;
+                               $arg{$_}{'hash'} = $head;
                        }
                }
        }
-       $arg{tree}{hash} = $treehead if defined $treehead;
-       $arg{tree}{hash_base} = $treebase if defined $treebase;
+       $arg{'tree'}{'hash'} = $treehead if defined $treehead;
+       $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
 
        print "<div class=\"page_nav\">\n" .
                (join " | ",
@@ -1873,9 +1927,9 @@ sub git_print_header_div {
        my ($action, $title, $hash, $hash_base) = @_;
        my %args = ();
 
-       $args{action} = $action;
-       $args{hash} = $hash if $hash;
-       $args{hash_base} = $hash_base if $hash_base;
+       $args{'action'} = $action;
+       $args{'hash'} = $hash if $hash;
+       $args{'hash_base'} = $hash_base if $hash_base;
 
        print "<div class=\"header\">\n" .
              $cgi->a({-href => href(%args), -class => "title"},
@@ -1909,7 +1963,7 @@ sub git_print_page_path {
 
        print "<div class=\"page_path\">";
        print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
-                     -title => 'tree root'}, "[$project]");
+                     -title => 'tree root'}, to_utf8("[$project]"));
        print " / ";
        if (defined $name) {
                my @dirname = split '/', $name;
@@ -1920,17 +1974,17 @@ sub git_print_page_path {
                        $fullname .= ($fullname ? '/' : '') . $dir;
                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
                                                     hash_base=>$hb),
-                                     -title => esc_html($fullname)}, esc_path($dir));
+                                     -title => $fullname}, esc_path($dir));
                        print " / ";
                }
                if (defined $type && $type eq 'blob') {
                        print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
                                                     hash_base=>$hb),
-                                     -title => esc_html($name)}, esc_path($basename));
+                                     -title => $name}, esc_path($basename));
                } elsif (defined $type && $type eq 'tree') {
                        print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
                                                     hash_base=>$hb),
-                                     -title => esc_html($name)}, esc_path($basename));
+                                     -title => $name}, esc_path($basename));
                        print " / ";
                } else {
                        print esc_path($basename);
@@ -2185,7 +2239,7 @@ sub git_difftree_body {
                        }
                        print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
                                                     hash_base=>$hash, file_name=>$diff{'file'})},
-                                     "blob") . " | ";
+                                     "blob");
                        print "</td>\n";
 
                } elsif ($diff{'status'} eq "D") { # deleted
@@ -2220,7 +2274,7 @@ sub git_difftree_body {
                        my $mode_chnge = "";
                        if ($diff{'from_mode'} != $diff{'to_mode'}) {
                                $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
-                               if ($from_file_type != $to_file_type) {
+                               if ($from_file_type ne $to_file_type) {
                                        $mode_chnge .= " from $from_file_type to $to_file_type";
                                }
                                if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
@@ -2324,7 +2378,6 @@ sub git_patchset_body {
        my $patch_line;
        my $diffinfo;
        my (%from, %to);
-       my ($from_id, $to_id);
 
        print "<div class=\"patchset\">\n";
 
@@ -2338,6 +2391,7 @@ sub git_patchset_body {
  PATCH:
        while ($patch_line) {
                my @diff_header;
+               my ($from_id, $to_id);
 
                # git diff header
                #assert($patch_line =~ m/^diff /) if DEBUG;
@@ -2349,7 +2403,7 @@ sub git_patchset_body {
                while ($patch_line = <$fd>) {
                        chomp $patch_line;
 
-                       last EXTENDED_HEADER if ($patch_line =~ m/^--- /);
+                       last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
 
                        if ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) {
                                $from_id = $1;
@@ -2358,7 +2412,6 @@ sub git_patchset_body {
 
                        push @diff_header, $patch_line;
                }
-               #last PATCH unless $patch_line;
                my $last_patch_line = $patch_line;
 
                # check if current patch belong to current raw line
@@ -2385,11 +2438,15 @@ sub git_patchset_body {
                                $from{'href'} = href(action=>"blob", hash_base=>$hash_parent,
                                                     hash=>$diffinfo->{'from_id'},
                                                     file_name=>$from{'file'});
+                       } else {
+                               delete $from{'href'};
                        }
                        if ($diffinfo->{'status'} ne "D") { # not deleted file
                                $to{'href'} = href(action=>"blob", hash_base=>$hash,
                                                   hash=>$diffinfo->{'to_id'},
                                                   file_name=>$to{'file'});
+                       } else {
+                               delete $to{'href'};
                        }
                        # this is first patch for raw difftree line with $patch_idx index
                        # we index @$difftree array from 0, but number patches from 1
@@ -2421,11 +2478,11 @@ sub git_patchset_body {
                        # match <path>
                        if ($patch_line =~ s!^((copy|rename) from ).*$!$1! && $from{'href'}) {
                                $patch_line .= $cgi->a({-href=>$from{'href'}, -class=>"path"},
-                                                       esc_path($from{'file'}));
+                                                      esc_path($from{'file'}));
                        }
                        if ($patch_line =~ s!^((copy|rename) to ).*$!$1! && $to{'href'}) {
-                               $patch_line = $cgi->a({-href=>$to{'href'}, -class=>"path"},
-                                                     esc_path($to{'file'}));
+                               $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"path"},
+                                                      esc_path($to{'file'}));
                        }
                        # match <mode>
                        if ($patch_line =~ m/\s(\d{6})$/) {
@@ -2464,8 +2521,13 @@ sub git_patchset_body {
 
                # from-file/to-file diff header
                $patch_line = $last_patch_line;
+               if (! $patch_line) {
+                       print "</div>\n"; # class="patch"
+                       last PATCH;
+               }
+               next PATCH if ($patch_line =~ m/^diff /);
                #assert($patch_line =~ m/^---/) if DEBUG;
-               if ($from{'href'}) {
+               if ($from{'href'} && $patch_line =~ m!^--- "?a/!) {
                        $patch_line = '--- a/' .
                                      $cgi->a({-href=>$from{'href'}, -class=>"path"},
                                              esc_path($from{'file'}));
@@ -2473,11 +2535,10 @@ sub git_patchset_body {
                print "<div class=\"diff from_file\">$patch_line</div>\n";
 
                $patch_line = <$fd>;
-               #last PATCH unless $patch_line;
                chomp $patch_line;
 
                #assert($patch_line =~ m/^+++/) if DEBUG;
-               if ($to{'href'}) {
+               if ($to{'href'} && $patch_line =~ m!^\+\+\+ "?b/!) {
                        $patch_line = '+++ b/' .
                                      $cgi->a({-href=>$to{'href'}, -class=>"path"},
                                              esc_path($to{'file'}));
@@ -2633,18 +2694,19 @@ sub git_project_list_body {
 
 sub git_shortlog_body {
        # uses global variable $project
-       my ($revlist, $from, $to, $refs, $extra) = @_;
+       my ($commitlist, $from, $to, $refs, $extra) = @_;
+
+       my $have_snapshot = gitweb_have_snapshot();
 
        $from = 0 unless defined $from;
-       $to = $#{$revlist} if (!defined $to || $#{$revlist} < $to);
+       $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
 
        print "<table class=\"shortlog\" cellspacing=\"0\">\n";
        my $alternate = 1;
        for (my $i = $from; $i <= $to; $i++) {
-               my $commit = $revlist->[$i];
-               #my $ref = defined $refs ? format_ref_marker($refs, $commit) : '';
+               my %co = %{$commitlist->[$i]};
+               my $commit = $co{'id'};
                my $ref = format_ref_marker($refs, $commit);
-               my %co = parse_commit($commit);
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
                } else {
@@ -2662,7 +2724,7 @@ sub git_shortlog_body {
                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
-               if (gitweb_have_snapshot()) {
+               if ($have_snapshot) {
                        print " | " . $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot");
                }
                print "</td>\n" .
@@ -2678,23 +2740,19 @@ sub git_shortlog_body {
 
 sub git_history_body {
        # Warning: assumes constant type (blob or tree) during history
-       my ($revlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
+       my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
 
        $from = 0 unless defined $from;
-       $to = $#{$revlist} unless (defined $to && $to <= $#{$revlist});
+       $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
 
        print "<table class=\"history\" cellspacing=\"0\">\n";
        my $alternate = 1;
        for (my $i = $from; $i <= $to; $i++) {
-               if ($revlist->[$i] !~ m/^([0-9a-fA-F]{40})/) {
-                       next;
-               }
-
-               my $commit = $1;
-               my %co = parse_commit($commit);
+               my %co = %{$commitlist->[$i]};
                if (!%co) {
                        next;
                }
+               my $commit = $co{'id'};
 
                my $ref = format_ref_marker($refs, $commit);
 
@@ -2762,8 +2820,12 @@ sub git_tags_body {
                        print "<tr class=\"light\">\n";
                }
                $alternate ^= 1;
-               print "<td><i>$tag{'age'}</i></td>\n" .
-                     "<td>" .
+               if (defined $tag{'age'}) {
+                       print "<td><i>$tag{'age'}</i></td>\n";
+               } else {
+                       print "<td></td>\n";
+               }
+               print "<td>" .
                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
                               -class => "list name"}, esc_html($tag{'name'})) .
                      "</td>\n" .
@@ -2837,6 +2899,58 @@ sub git_heads_body {
        print "</table>\n";
 }
 
+sub git_search_grep_body {
+       my ($commitlist, $from, $to, $extra) = @_;
+       $from = 0 unless defined $from;
+       $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
+
+       print "<table class=\"grep\" cellspacing=\"0\">\n";
+       my $alternate = 1;
+       for (my $i = $from; $i <= $to; $i++) {
+               my %co = %{$commitlist->[$i]};
+               if (!%co) {
+                       next;
+               }
+               my $commit = $co{'id'};
+               if ($alternate) {
+                       print "<tr class=\"dark\">\n";
+               } else {
+                       print "<tr class=\"light\">\n";
+               }
+               $alternate ^= 1;
+               print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
+                     "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
+                     "<td>" .
+                     $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
+                              esc_html(chop_str($co{'title'}, 50)) . "<br/>");
+               my $comment = $co{'comment'};
+               foreach my $line (@$comment) {
+                       if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
+                               my $lead = esc_html($1) || "";
+                               $lead = chop_str($lead, 30, 10);
+                               my $match = esc_html($2) || "";
+                               my $trail = esc_html($3) || "";
+                               $trail = chop_str($trail, 30, 10);
+                               my $text = "$lead<span class=\"match\">$match</span>$trail";
+                               print chop_str($text, 80, 5) . "<br/>\n";
+                       }
+               }
+               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 (defined $extra) {
+               print "<tr>\n" .
+                     "<td colspan=\"3\">$extra</td>\n" .
+                     "</tr>\n";
+       }
+       print "</table>\n";
+}
+
 ## ======================================================================
 ## ======================================================================
 ## actions
@@ -2892,7 +3006,7 @@ sub git_project_index {
 
        foreach my $pr (@projects) {
                if (!exists $pr->{'owner'}) {
-                       $pr->{'owner'} = get_file_owner("$projectroot/$project");
+                       $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}");
                }
 
                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
@@ -2908,15 +3022,17 @@ sub git_project_index {
 
 sub git_summary {
        my $descr = git_get_project_description($project) || "none";
-       my $head = git_get_head_hash($project);
-       my %co = parse_commit($head);
+       my %co = parse_commit("HEAD");
        my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
+       my $head = $co{'id'};
 
        my $owner = git_get_project_owner($project);
 
        my $refs = git_get_references();
-       my @taglist  = git_get_tags_list(15);
-       my @headlist = git_get_heads_list(15);
+       # These get_*_list functions return one more to allow us to see if
+       # there are more ...
+       my @taglist  = git_get_tags_list(16);
+       my @headlist = git_get_heads_list(16);
        my @forklist;
        my ($check_forks) = gitweb_check_feature('forks');
 
@@ -2952,32 +3068,34 @@ sub git_summary {
                }
        }
 
-       open my $fd, "-|", git_cmd(), "rev-list", "--max-count=17",
-               git_get_head_hash($project), "--"
-               or die_error(undef, "Open git-rev-list failed");
-       my @revlist = map { chomp; $_ } <$fd>;
-       close $fd;
+       # we need to request one more than 16 (0..15) to check if
+       # those 16 are all
+       my @commitlist = parse_commits($head, 17);
        git_print_header_div('shortlog');
-       git_shortlog_body(\@revlist, 0, 15, $refs,
+       git_shortlog_body(\@commitlist, 0, 15, $refs,
+                         $#commitlist <=  15 ? undef :
                          $cgi->a({-href => href(action=>"shortlog")}, "..."));
 
        if (@taglist) {
                git_print_header_div('tags');
                git_tags_body(\@taglist, 0, 15,
+                             $#taglist <=  15 ? undef :
                              $cgi->a({-href => href(action=>"tags")}, "..."));
        }
 
        if (@headlist) {
                git_print_header_div('heads');
                git_heads_body(\@headlist, $head, 0, 15,
+                              $#headlist <= 15 ? undef :
                               $cgi->a({-href => href(action=>"heads")}, "..."));
        }
 
        if (@forklist) {
                git_print_header_div('forks');
                git_project_list_body(\@forklist, undef, 0, 15,
+                                     $#forklist <= 15 ? undef :
                                      $cgi->a({-href => href(action=>"forks")}, "..."),
-                                     'noheader');
+                                     'noheader');
        }
 
        git_footer_html();
@@ -3036,7 +3154,7 @@ sub git_blame2 {
        }
        $ftype = git_get_type($hash);
        if ($ftype !~ "blob") {
-               die_error("400 Bad Request", "Object is not a blob");
+               die_error('400 Bad Request', "Object is not a blob");
        }
        open ($fd, "-|", git_cmd(), "blame", '-p', '--',
              $file_name, $hash_base)
@@ -3084,7 +3202,7 @@ HTML
                my $rev = substr($full_rev, 0, 8);
                my $author = $meta->{'author'};
                my %date = parse_date($meta->{'author-time'},
-                                     $meta->{'author-tz'});
+                                     $meta->{'author-tz'});
                my $date = $date{'iso-tz'};
                if ($group_size) {
                        $current_color = ++$current_color % $num_colors;
@@ -3096,19 +3214,24 @@ HTML
                        print " rowspan=\"$group_size\"" if ($group_size > 1);
                        print ">";
                        print $cgi->a({-href => href(action=>"commit",
-                                                    hash=>$full_rev,
-                                                    file_name=>$file_name)},
-                                     esc_html($rev));
+                                                    hash=>$full_rev,
+                                                    file_name=>$file_name)},
+                                     esc_html($rev));
                        print "</td>\n";
                }
+               open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
+                       or die_error(undef, "Open git-rev-parse failed");
+               my $parent_commit = <$dd>;
+               close $dd;
+               chomp($parent_commit);
                my $blamed = href(action => 'blame',
-                                 file_name => $meta->{'filename'},
-                                 hash_base => $full_rev);
+                                 file_name => $meta->{'filename'},
+                                 hash_base => $parent_commit);
                print "<td class=\"linenr\">";
                print $cgi->a({ -href => "$blamed#l$orig_lineno",
-                               -id => "l$lineno",
-                               -class => "linenr" },
-                             esc_html($lineno));
+                               -id => "l$lineno",
+                               -class => "linenr" },
+                             esc_html($lineno));
                print "</td>";
                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
                print "</tr>\n";
@@ -3487,7 +3610,7 @@ sub git_snapshot {
                $hash = git_get_head_hash($project);
        }
 
-       my $filename = basename($project) . "-$hash.tar.$suffix";
+       my $filename = to_utf8(basename($project)) . "-$hash.tar.$suffix";
 
        print $cgi->header(
                -type => "application/$ctype",
@@ -3498,8 +3621,8 @@ sub git_snapshot {
        my $name = $project;
        $name =~ s/\047/\047\\\047\047/g;
        open my $fd, "-|",
-       "$git archive --format=tar --prefix=\'$name\'/ $hash | $command"
-               or die_error(undef, "Execute git-tar-tree failed.");
+               "$git archive --format=tar --prefix=\'$name\'/ $hash | $command"
+               or die_error(undef, "Execute git-tar-tree failed");
        binmode STDOUT, ':raw';
        print <$fd>;
        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
@@ -3517,28 +3640,25 @@ sub git_log {
        }
        my $refs = git_get_references();
 
-       my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
-       open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash, "--"
-               or die_error(undef, "Open git-rev-list failed");
-       my @revlist = map { chomp; $_ } <$fd>;
-       close $fd;
+       my @commitlist = parse_commits($hash, 101, (100 * $page));
 
-       my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#revlist);
+       my $paging_nav = format_paging_nav('log', $hash, $head, $page, (100 * ($page+1)));
 
        git_header_html();
        git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
 
-       if (!@revlist) {
+       if (!@commitlist) {
                my %co = parse_commit($hash);
 
                git_print_header_div('summary', $project);
                print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
        }
-       for (my $i = ($page * 100); $i <= $#revlist; $i++) {
-               my $commit = $revlist[$i];
-               my $ref = format_ref_marker($refs, $commit);
-               my %co = parse_commit($commit);
+       my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
+       for (my $i = 0; $i <= $to; $i++) {
+               my %co = %{$commitlist[$i]};
                next if !%co;
+               my $commit = $co{'id'};
+               my $ref = format_ref_marker($refs, $commit);
                my %ad = parse_date($co{'author_epoch'});
                git_print_header_div('commit',
                               "<span class=\"age\">$co{'age_string'}</span>" .
@@ -3560,6 +3680,12 @@ sub git_log {
                git_print_log($co{'comment'}, -final_empty_line=> 1);
                print "</div>\n";
        }
+       if ($#commitlist >= 100) {
+               print "<div class=\"page_nav\">\n";
+               print $cgi->a({-href => href(action=>"log", hash=>$hash, page=>$page+1),
+                              -accesskey => "n", -title => "Alt-n"}, "next");
+               print "</div>\n";
+       }
        git_footer_html();
 }
 
@@ -3572,8 +3698,34 @@ sub git_commit {
        my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
        my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
 
-       my $parent = $co{'parent'};
-       my $parents = $co{'parents'};
+       my $parent  = $co{'parent'};
+       my $parents = $co{'parents'}; # listref
+
+       # we need to prepare $formats_nav before any parameter munging
+       my $formats_nav;
+       if (!defined $parent) {
+               # --root commitdiff
+               $formats_nav .= '(initial)';
+       } elsif (@$parents == 1) {
+               # single parent commit
+               $formats_nav .=
+                       '(parent: ' .
+                       $cgi->a({-href => href(action=>"commit",
+                                              hash=>$parent)},
+                               esc_html(substr($parent, 0, 7))) .
+                       ')';
+       } else {
+               # merge commit
+               $formats_nav .=
+                       '(merge: ' .
+                       join(' ', map {
+                               $cgi->a({-href => href(action=>"commit",
+                                                      hash=>$_)},
+                                       esc_html(substr($_, 0, 7)));
+                       } @$parents ) .
+                       ')';
+       }
+
        if (!defined $parent) {
                $parent = "--root";
        }
@@ -3582,7 +3734,7 @@ sub git_commit {
                # difftree output is not printed for merges
                open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
                        @diff_opts, $parent, $hash, "--"
-                               or die_error(undef, "Open git-diff-tree failed");
+                       or die_error(undef, "Open git-diff-tree failed");
                @difftree = map { chomp; $_ } <$fd>;
                close $fd or die_error(undef, "Reading git-diff-tree failed");
        }
@@ -3597,16 +3749,10 @@ sub git_commit {
 
        my $have_snapshot = gitweb_have_snapshot();
 
-       my @views_nav = ();
-       if (defined $file_name && defined $co{'parent'}) {
-               push @views_nav,
-                       $cgi->a({-href => href(action=>"blame", hash_parent=>$parent, file_name=>$file_name)},
-                               "blame");
-       }
        git_header_html(undef, $expires);
        git_print_page_nav('commit', '',
                           $hash, $co{'tree'}, $hash,
-                          join (' | ', @views_nav));
+                          $formats_nav);
 
        if (defined $co{'parent'}) {
                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
@@ -3739,7 +3885,7 @@ sub git_blobdiff {
                        # read raw output
                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
                                $hash_parent_base, $hash_base,
-                               "--", $file_name
+                               "--", (defined $file_parent ? $file_parent : ()), $file_name
                                or die_error(undef, "Open git-diff-tree failed");
                        @difftree = map { chomp; $_ } <$fd>;
                        close $fd
@@ -3788,8 +3934,9 @@ sub git_blobdiff {
 
                # open patch output
                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
-                       '-p', $hash_parent_base, $hash_base,
-                       "--", $file_name
+                       '-p', ($format eq 'html' ? "--full-index" : ()),
+                       $hash_parent_base, $hash_base,
+                       "--", (defined $file_parent ? $file_parent : ()), $file_name
                        or die_error(undef, "Open git-diff-tree failed");
        }
 
@@ -3823,7 +3970,8 @@ sub git_blobdiff {
                }
 
                # open patch output
-               open $fd, "-|", git_cmd(), "diff", '-p', @diff_opts,
+               open $fd, "-|", git_cmd(), "diff", @diff_opts,
+                       '-p', ($format eq 'html' ? "--full-index" : ()),
                        $hash_parent, $hash, "--"
                        or die_error(undef, "Open git-diff failed");
        } else  {
@@ -4068,12 +4216,7 @@ sub git_history {
                $ftype = git_get_type($hash);
        }
 
-       open my $fd, "-|",
-               git_cmd(), "rev-list", $limit, "--full-history", $hash_base, "--", $file_name
-                       or die_error(undef, "Open git-rev-list-failed");
-       my @revlist = map { chomp; $_ } <$fd>;
-       close $fd
-               or die_error(undef, "Reading git-rev-list failed");
+       my @commitlist = parse_commits($hash_base, 101, (100 * $page), "--full-history", $file_name);
 
        my $paging_nav = '';
        if ($page > 0) {
@@ -4089,7 +4232,7 @@ sub git_history {
                $paging_nav .= "first";
                $paging_nav .= " &sdot; prev";
        }
-       if ($#revlist >= (100 * ($page+1)-1)) {
+       if ($#commitlist >= 100) {
                $paging_nav .= " &sdot; " .
                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
                                               file_name=>$file_name, page=>$page+1),
@@ -4098,11 +4241,11 @@ sub git_history {
                $paging_nav .= " &sdot; next";
        }
        my $next_link = '';
-       if ($#revlist >= (100 * ($page+1)-1)) {
+       if ($#commitlist >= 100) {
                $next_link =
                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
                                               file_name=>$file_name, page=>$page+1),
-                                -title => "Alt-n"}, "next");
+                                -accesskey => "n", -title => "Alt-n"}, "next");
        }
 
        git_header_html();
@@ -4110,13 +4253,17 @@ sub git_history {
        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
        git_print_page_path($file_name, $ftype, $hash_base);
 
-       git_history_body(\@revlist, ($page * 100), $#revlist,
+       git_history_body(\@commitlist, 0, 99,
                         $refs, $hash_base, $ftype, $next_link);
 
        git_footer_html();
 }
 
 sub git_search {
+       my ($have_search) = gitweb_check_feature('search');
+       if (!$have_search) {
+               die_error('403 Permission denied', "Permission denied");
+       }
        if (!defined $searchtext) {
                die_error(undef, "Text field empty");
        }
@@ -4127,6 +4274,9 @@ sub git_search {
        if (!%co) {
                die_error(undef, "Unknown commit object");
        }
+       if (!defined $page) {
+               $page = 0;
+       }
 
        $searchtype ||= 'commit';
        if ($searchtype eq 'pickaxe') {
@@ -4139,66 +4289,63 @@ sub git_search {
        }
 
        git_header_html();
-       git_print_page_nav('','', $hash,$co{'tree'},$hash);
-       git_print_header_div('commit', esc_html($co{'title'}), $hash);
 
-       print "<table cellspacing=\"0\">\n";
-       my $alternate = 1;
        if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
-               $/ = "\0";
-               open my $fd, "-|", git_cmd(), "rev-list",
-                       "--header", "--parents", $hash, "--"
-                       or next;
-               while (my $commit_text = <$fd>) {
-                       if (!grep m/$searchtext/i, $commit_text) {
-                               next;
-                       }
-                       if ($searchtype eq 'author' && !grep m/\nauthor .*$searchtext/i, $commit_text) {
-                               next;
-                       }
-                       if ($searchtype eq 'committer' && !grep m/\ncommitter .*$searchtext/i, $commit_text) {
-                               next;
-                       }
-                       my @commit_lines = split "\n", $commit_text;
-                       my %co = parse_commit(undef, \@commit_lines);
-                       if (!%co) {
-                               next;
-                       }
-                       if ($alternate) {
-                               print "<tr class=\"dark\">\n";
-                       } else {
-                               print "<tr class=\"light\">\n";
-                       }
-                       $alternate ^= 1;
-                       print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
-                             "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
-                             "<td>" .
-                             $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
-                                      esc_html(chop_str($co{'title'}, 50)) . "<br/>");
-                       my $comment = $co{'comment'};
-                       foreach my $line (@$comment) {
-                               if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
-                                       my $lead = esc_html($1) || "";
-                                       $lead = chop_str($lead, 30, 10);
-                                       my $match = esc_html($2) || "";
-                                       my $trail = esc_html($3) || "";
-                                       $trail = chop_str($trail, 30, 10);
-                                       my $text = "$lead<span class=\"match\">$match</span>$trail";
-                                       print chop_str($text, 80, 5) . "<br/>\n";
-                               }
-                       }
-                       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";
+               my $greptype;
+               if ($searchtype eq 'commit') {
+                       $greptype = "--grep=";
+               } elsif ($searchtype eq 'author') {
+                       $greptype = "--author=";
+               } elsif ($searchtype eq 'committer') {
+                       $greptype = "--committer=";
                }
-               close $fd;
+               $greptype .= $searchtext;
+               my @commitlist = parse_commits($hash, 101, (100 * $page), $greptype);
+
+               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(action=>"search", hash=>$hash,
+                                                      searchtext=>$searchtext, searchtype=>$searchtype,
+                                                      page=>$page-1),
+                                        -accesskey => "p", -title => "Alt-p"}, "prev");
+               } else {
+                       $paging_nav .= "first";
+                       $paging_nav .= " &sdot; prev";
+               }
+               if ($#commitlist >= 100) {
+                       $paging_nav .= " &sdot; " .
+                               $cgi->a({-href => href(action=>"search", hash=>$hash,
+                                                      searchtext=>$searchtext, searchtype=>$searchtype,
+                                                      page=>$page+1),
+                                        -accesskey => "n", -title => "Alt-n"}, "next");
+               } else {
+                       $paging_nav .= " &sdot; next";
+               }
+               my $next_link = '';
+               if ($#commitlist >= 100) {
+                       $next_link =
+                               $cgi->a({-href => href(action=>"search", hash=>$hash,
+                                                      searchtext=>$searchtext, searchtype=>$searchtype,
+                                                      page=>$page+1),
+                                        -accesskey => "n", -title => "Alt-n"}, "next");
+               }
+
+               git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
+               git_print_header_div('commit', esc_html($co{'title'}), $hash);
+               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 cellspacing=\"0\">\n";
+               my $alternate = 1;
                $/ = "\n";
                my $git_command = git_cmd_str();
                open my $fd, "-|", "$git_command rev-list $hash | " .
@@ -4253,8 +4400,9 @@ sub git_search {
                        }
                }
                close $fd;
+
+               print "</table>\n";
        }
-       print "</table>\n";
        git_footer_html();
 }
 
@@ -4293,26 +4441,21 @@ sub git_shortlog {
        }
        my $refs = git_get_references();
 
-       my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
-       open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash, "--"
-               or die_error(undef, "Open git-rev-list failed");
-       my @revlist = map { chomp; $_ } <$fd>;
-       close $fd;
+       my @commitlist = parse_commits($hash, 101, (100 * $page));
 
-       my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#revlist);
+       my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, (100 * ($page+1)));
        my $next_link = '';
-       if ($#revlist >= (100 * ($page+1)-1)) {
+       if ($#commitlist >= 100) {
                $next_link =
                        $cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1),
-                                -title => "Alt-n"}, "next");
+                                -accesskey => "n", -title => "Alt-n"}, "next");
        }
 
-
        git_header_html();
        git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
        git_print_header_div('summary', $project);
 
-       git_shortlog_body(\@revlist, ($page * 100), $#revlist, $refs, $next_link);
+       git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
 
        git_footer_html();
 }
@@ -4332,11 +4475,7 @@ sub git_feed {
 
        # log/feed of current (HEAD) branch, log of given branch, history of file/directory
        my $head = $hash || 'HEAD';
-       open my $fd, "-|", git_cmd(), "rev-list", "--max-count=150",
-               $head, "--", (defined $file_name ? $file_name : ())
-               or die_error(undef, "Open git-rev-list failed");
-       my @revlist = map { chomp; $_ } <$fd>;
-       close $fd or die_error(undef, "Reading git-rev-list failed");
+       my @commitlist = parse_commits($head, 150);
 
        my %latest_commit;
        my %latest_date;
@@ -4346,8 +4485,8 @@ sub git_feed {
                # browser (feed reader) prefers text/xml
                $content_type = 'text/xml';
        }
-       if (defined($revlist[0])) {
-               %latest_commit = parse_commit($revlist[0]);
+       if (defined($commitlist[0])) {
+               %latest_commit = %{$commitlist[0]};
                %latest_date   = parse_date($latest_commit{'author_epoch'});
                print $cgi->header(
                        -type => $content_type,
@@ -4437,9 +4576,9 @@ XML
        }
 
        # contents
-       for (my $i = 0; $i <= $#revlist; $i++) {
-               my $commit = $revlist[$i];
-               my %co = parse_commit($commit);
+       for (my $i = 0; $i <= $#commitlist; $i++) {
+               my %co = %{$commitlist[$i]};
+               my $commit = $co{'id'};
                # we read 150, we always show 30 and the ones more recent than 48 hours
                if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
                        last;
@@ -4447,7 +4586,7 @@ XML
                my %cd = parse_date($co{'author_epoch'});
 
                # get list of changed files
-               open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+               open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
                        $co{'parent'}, $co{'id'}, "--", (defined $file_name ? $file_name : ())
                        or next;
                my @difftree = map { chomp; $_ } <$fd>;