Code

filter-branch: also don't fail in map() if a commit cannot be mapped
[git.git] / gitweb / gitweb.perl
index 53ae0b83f15a8324064781e066a243f08557125f..c3921cb0baf162b8294fe47b2fff749114f3061f 100755 (executable)
@@ -102,10 +102,13 @@ our %feature = (
        #       'override' => allow-override (boolean),
        #       'default' => [ default options...] (array reference)}
        #
-       # if feature is overridable (it means that allow-override has true value,
+       # if feature is overridable (it means that allow-override has true value),
        # then feature-sub will be called with default options as parameters;
        # return value of feature-sub indicates if to enable specified feature
        #
+       # if there is no 'sub' key (no feature-sub), then feature cannot be
+       # overriden
+       #
        # use gitweb_check_feature(<feature>) to check if <feature> is enabled
 
        # Enable the 'blame' blob view, showing the last commit that modified
@@ -129,7 +132,7 @@ our %feature = (
        # $feature{'snapshot'}{'default'} = [undef];
        # To have project specific config enable override in $GITWEB_CONFIG
        # $feature{'snapshot'}{'override'} = 1;
-       # and in project config gitweb.snapshot = none|gzip|bzip2;
+       # and in project config gitweb.snapshot = none|gzip|bzip2|zip;
        'snapshot' => {
                'sub' => \&feature_snapshot,
                'override' => 0,
@@ -138,10 +141,24 @@ our %feature = (
 
        # Enable text search, which will list the commits which match author,
        # committer or commit text to a given string.  Enabled by default.
+       # Project specific override is not supported.
        'search' => {
                'override' => 0,
                'default' => [1]},
 
+       # Enable grep search, which will list the files in currently selected
+       # tree containing the given string. Enabled by default. This can be
+       # potentially CPU-intensive, of course.
+
+       # To enable system wide have in $GITWEB_CONFIG
+       # $feature{'grep'}{'default'} = [1];
+       # To have project specific config enable override in $GITWEB_CONFIG
+       # $feature{'grep'}{'override'} = 1;
+       # and in project config gitweb.grep = 0|1;
+       'grep' => {
+               '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.
@@ -227,6 +244,8 @@ sub feature_snapshot {
                return ('x-gzip', 'gz', 'gzip');
        } elsif ($val eq 'bzip2') {
                return ('x-bzip2', 'bz2', 'bzip2');
+       } elsif ($val eq 'zip') {
+               return ('x-zip', 'zip', '');
        } elsif ($val eq 'none') {
                return ();
        }
@@ -241,6 +260,18 @@ sub gitweb_have_snapshot {
        return $have_snapshot;
 }
 
+sub feature_grep {
+       my ($val) = git_get_project_config('grep', '--bool');
+
+       if ($val eq 'true') {
+               return (1);
+       } elsif ($val eq 'false') {
+               return (0);
+       }
+
+       return ($_[0]);
+}
+
 sub feature_pickaxe {
        my ($val) = git_get_project_config('pickaxe', '--bool');
 
@@ -360,22 +391,23 @@ if (defined $page) {
        }
 }
 
+our $searchtype = $cgi->param('st');
+if (defined $searchtype) {
+       if ($searchtype =~ m/[^a-z]/) {
+               die_error(undef, "Invalid searchtype parameter");
+       }
+}
+
 our $searchtext = $cgi->param('s');
+our $search_regexp;
 if (defined $searchtext) {
-       if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) {
+       if ($searchtype ne 'grep' and $searchtype ne 'pickaxe' and $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;
-}
-
-our $searchtype = $cgi->param('st');
-if (defined $searchtype) {
-       if ($searchtype =~ m/[^a-z]/) {
-               die_error(undef, "Invalid searchtype parameter");
-       }
+       $search_regexp = quotemeta $searchtext;
 }
 
 # now read PATH_INFO and use it as alternative to parameters
@@ -458,10 +490,16 @@ my %actions = (
        "project_index" => \&git_project_index,
 );
 
-if (defined $project) {
-       $action ||= 'summary';
-} else {
-       $action ||= 'project_list';
+if (!defined $action) {
+       if (defined $hash) {
+               $action = git_get_type($hash);
+       } elsif (defined $hash_base && defined $file_name) {
+               $action = git_get_type("$hash_base:$file_name");
+       } elsif (defined $project) {
+               $action = 'summary';
+       } else {
+               $action = 'project_list';
+       }
 }
 if (!defined($actions{$action})) {
        die_error(undef, "Unknown action");
@@ -722,7 +760,9 @@ sub chop_str {
 sub age_class {
        my $age = shift;
 
-       if ($age < 60*60*2) {
+       if (!defined $age) {
+               return "noage";
+       } elsif ($age < 60*60*2) {
                return "age0";
        } elsif ($age < 60*60*24*2) {
                return "age1";
@@ -1054,6 +1094,11 @@ sub git_get_hash_by_path {
        my $line = <$fd>;
        close $fd or return undef;
 
+       if (!defined $line) {
+               # there is no tree or hash given by $path at $base
+               return undef;
+       }
+
        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
        $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
        if (defined $type && $type ne $2) {
@@ -1096,7 +1141,9 @@ sub git_get_project_description {
        open my $fd, "$projectroot/$path/description" or return undef;
        my $descr = <$fd>;
        close $fd;
-       chomp $descr;
+       if (defined $descr) {
+               chomp $descr;
+       }
        return $descr;
 }
 
@@ -1247,7 +1294,8 @@ sub git_get_last_activity {
             'refs/heads') or return;
        my $most_recent = <$fd>;
        close $fd or return;
-       if ($most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
+       if (defined $most_recent &&
+           $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
                my $timestamp = $1;
                my $age = time - $timestamp;
                return ($age, age_string($age));
@@ -1370,8 +1418,12 @@ sub parse_commit_text {
 
        pop @commit_lines; # Remove '\0'
 
+       if (! @commit_lines) {
+               return;
+       }
+
        my $header = shift @commit_lines;
-       if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
+       if ($header !~ m/^[0-9a-fA-F]{40}/) {
                return;
        }
        ($co{'id'}, my @parents) = split ' ', $header;
@@ -1878,6 +1930,8 @@ EOF
                }
                print "\n";
        }
+       print "</div>\n";
+
        my ($have_search) = gitweb_check_feature('search');
        if ((defined $project) && ($have_search)) {
                if (!defined $searchtext) {
@@ -1900,14 +1954,13 @@ 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', 'grep', 'author', 'committer', 'pickaxe']) .
                      $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
                      " search:\n",
                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
                      "</div>" .
                      $cgi->end_form() . "\n";
        }
-       print "</div>\n";
 }
 
 sub git_footer_html {
@@ -2330,7 +2383,13 @@ sub git_difftree_body {
        my $alternate = 1;
        my $patchno = 0;
        foreach my $line (@{$difftree}) {
-               my %diff = parse_difftree_raw_line($line);
+               my $diff;
+               if (ref($line) eq "HASH") {
+                       # pre-parsed (or generated by hand)
+                       $diff = $line;
+               } else {
+                       $diff = parse_difftree_raw_line($line);
+               }
 
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
@@ -2339,21 +2398,22 @@ sub git_difftree_body {
                }
                $alternate ^= 1;
 
-               if (exists $diff{'nparents'}) { # combined diff
+               if (exists $diff->{'nparents'}) { # combined diff
 
-                       fill_from_file_info(\%diff, @parents);
+                       fill_from_file_info($diff, @parents)
+                               unless exists $diff->{'from_file'};
 
-                       if ($diff{'to_id'} ne ('0' x 40)) {
+                       if ($diff->{'to_id'} ne ('0' x 40)) {
                                # file exists in the result (child) commit
                                print "<td>" .
-                                     $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
-                                                            file_name=>$diff{'to_file'},
+                                     $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+                                                            file_name=>$diff->{'to_file'},
                                                             hash_base=>$hash),
-                                             -class => "list"}, esc_path($diff{'to_file'})) .
+                                             -class => "list"}, esc_path($diff->{'to_file'})) .
                                      "</td>\n";
                        } else {
                                print "<td>" .
-                                     esc_path($diff{'to_file'}) .
+                                     esc_path($diff->{'to_file'}) .
                                      "</td>\n";
                        }
 
@@ -2368,11 +2428,11 @@ sub git_difftree_body {
 
                        my $has_history = 0;
                        my $not_deleted = 0;
-                       for (my $i = 0; $i < $diff{'nparents'}; $i++) {
+                       for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
                                my $hash_parent = $parents[$i];
-                               my $from_hash = $diff{'from_id'}[$i];
-                               my $from_path = $diff{'from_file'}[$i];
-                               my $status = $diff{'status'}[$i];
+                               my $from_hash = $diff->{'from_id'}[$i];
+                               my $from_path = $diff->{'from_file'}[$i];
+                               my $status = $diff->{'status'}[$i];
 
                                $has_history ||= ($status ne 'A');
                                $not_deleted ||= ($status ne 'D');
@@ -2388,17 +2448,17 @@ sub git_difftree_body {
                                                      "blob" . ($i+1)) .
                                              " | </td>\n";
                                } else {
-                                       if ($diff{'to_id'} eq $from_hash) {
+                                       if ($diff->{'to_id'} eq $from_hash) {
                                                print "<td class=\"link nochange\">";
                                        } else {
                                                print "<td class=\"link\">";
                                        }
                                        print $cgi->a({-href => href(action=>"blobdiff",
-                                                                    hash=>$diff{'to_id'},
+                                                                    hash=>$diff->{'to_id'},
                                                                     hash_parent=>$from_hash,
                                                                     hash_base=>$hash,
                                                                     hash_parent_base=>$hash_parent,
-                                                                    file_name=>$diff{'to_file'},
+                                                                    file_name=>$diff->{'to_file'},
                                                                     file_parent=>$from_path)},
                                                      "diff" . ($i+1)) .
                                              " | </td>\n";
@@ -2408,15 +2468,15 @@ sub git_difftree_body {
                        print "<td class=\"link\">";
                        if ($not_deleted) {
                                print $cgi->a({-href => href(action=>"blob",
-                                                            hash=>$diff{'to_id'},
-                                                            file_name=>$diff{'to_file'},
+                                                            hash=>$diff->{'to_id'},
+                                                            file_name=>$diff->{'to_file'},
                                                             hash_base=>$hash)},
                                              "blob");
                                print " | " if ($has_history);
                        }
                        if ($has_history) {
                                print $cgi->a({-href => href(action=>"history",
-                                                            file_name=>$diff{'to_file'},
+                                                            file_name=>$diff->{'to_file'},
                                                             hash_base=>$hash)},
                                              "history");
                        }
@@ -2429,29 +2489,29 @@ sub git_difftree_body {
 
                my ($to_mode_oct, $to_mode_str, $to_file_type);
                my ($from_mode_oct, $from_mode_str, $from_file_type);
-               if ($diff{'to_mode'} ne ('0' x 6)) {
-                       $to_mode_oct = oct $diff{'to_mode'};
+               if ($diff->{'to_mode'} ne ('0' x 6)) {
+                       $to_mode_oct = oct $diff->{'to_mode'};
                        if (S_ISREG($to_mode_oct)) { # only for regular file
                                $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
                        }
-                       $to_file_type = file_type($diff{'to_mode'});
+                       $to_file_type = file_type($diff->{'to_mode'});
                }
-               if ($diff{'from_mode'} ne ('0' x 6)) {
-                       $from_mode_oct = oct $diff{'from_mode'};
+               if ($diff->{'from_mode'} ne ('0' x 6)) {
+                       $from_mode_oct = oct $diff->{'from_mode'};
                        if (S_ISREG($to_mode_oct)) { # only for regular file
                                $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
                        }
-                       $from_file_type = file_type($diff{'from_mode'});
+                       $from_file_type = file_type($diff->{'from_mode'});
                }
 
-               if ($diff{'status'} eq "A") { # created
+               if ($diff->{'status'} eq "A") { # created
                        my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
                        $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
                        $mode_chng   .= "]</span>";
                        print "<td>";
-                       print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
-                                                    hash_base=>$hash, file_name=>$diff{'file'}),
-                                     -class => "list"}, esc_path($diff{'file'}));
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+                                                    hash_base=>$hash, file_name=>$diff->{'file'}),
+                                     -class => "list"}, esc_path($diff->{'file'}));
                        print "</td>\n";
                        print "<td>$mode_chng</td>\n";
                        print "<td class=\"link\">";
@@ -2461,17 +2521,17 @@ sub git_difftree_body {
                                print $cgi->a({-href => "#patch$patchno"}, "patch");
                                print " | ";
                        }
-                       print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
-                                                    hash_base=>$hash, file_name=>$diff{'file'})},
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+                                                    hash_base=>$hash, file_name=>$diff->{'file'})},
                                      "blob");
                        print "</td>\n";
 
-               } elsif ($diff{'status'} eq "D") { # deleted
+               } elsif ($diff->{'status'} eq "D") { # deleted
                        my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
                        print "<td>";
-                       print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
-                                                    hash_base=>$parent, file_name=>$diff{'file'}),
-                                      -class => "list"}, esc_path($diff{'file'}));
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
+                                                    hash_base=>$parent, file_name=>$diff->{'file'}),
+                                      -class => "list"}, esc_path($diff->{'file'}));
                        print "</td>\n";
                        print "<td>$mode_chng</td>\n";
                        print "<td class=\"link\">";
@@ -2481,22 +2541,22 @@ sub git_difftree_body {
                                print $cgi->a({-href => "#patch$patchno"}, "patch");
                                print " | ";
                        }
-                       print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
-                                                    hash_base=>$parent, file_name=>$diff{'file'})},
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
+                                                    hash_base=>$parent, file_name=>$diff->{'file'})},
                                      "blob") . " | ";
                        if ($have_blame) {
                                print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
-                                                            file_name=>$diff{'file'})},
+                                                            file_name=>$diff->{'file'})},
                                              "blame") . " | ";
                        }
                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
-                                                    file_name=>$diff{'file'})},
+                                                    file_name=>$diff->{'file'})},
                                      "history");
                        print "</td>\n";
 
-               } elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed
+               } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
                        my $mode_chnge = "";
-                       if ($diff{'from_mode'} != $diff{'to_mode'}) {
+                       if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
                                $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
                                if ($from_file_type ne $to_file_type) {
                                        $mode_chnge .= " from $from_file_type to $to_file_type";
@@ -2511,9 +2571,9 @@ sub git_difftree_body {
                                $mode_chnge .= "]</span>\n";
                        }
                        print "<td>";
-                       print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
-                                                    hash_base=>$hash, file_name=>$diff{'file'}),
-                                     -class => "list"}, esc_path($diff{'file'}));
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+                                                    hash_base=>$hash, file_name=>$diff->{'file'}),
+                                     -class => "list"}, esc_path($diff->{'file'}));
                        print "</td>\n";
                        print "<td>$mode_chnge</td>\n";
                        print "<td class=\"link\">";
@@ -2522,70 +2582,70 @@ sub git_difftree_body {
                                $patchno++;
                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
                                      " | ";
-                       } elsif ($diff{'to_id'} ne $diff{'from_id'}) {
+                       } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
                                # "commit" view and modified file (not onlu mode changed)
                                print $cgi->a({-href => href(action=>"blobdiff",
-                                                            hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
+                                                            hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
                                                             hash_base=>$hash, hash_parent_base=>$parent,
-                                                            file_name=>$diff{'file'})},
+                                                            file_name=>$diff->{'file'})},
                                              "diff") .
                                      " | ";
                        }
-                       print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
-                                                    hash_base=>$hash, file_name=>$diff{'file'})},
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+                                                    hash_base=>$hash, file_name=>$diff->{'file'})},
                                       "blob") . " | ";
                        if ($have_blame) {
                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
-                                                            file_name=>$diff{'file'})},
+                                                            file_name=>$diff->{'file'})},
                                              "blame") . " | ";
                        }
                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
-                                                    file_name=>$diff{'file'})},
+                                                    file_name=>$diff->{'file'})},
                                      "history");
                        print "</td>\n";
 
-               } elsif ($diff{'status'} eq "R" || $diff{'status'} eq "C") { # renamed or copied
+               } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
                        my %status_name = ('R' => 'moved', 'C' => 'copied');
-                       my $nstatus = $status_name{$diff{'status'}};
+                       my $nstatus = $status_name{$diff->{'status'}};
                        my $mode_chng = "";
-                       if ($diff{'from_mode'} != $diff{'to_mode'}) {
+                       if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
                                # mode also for directories, so we cannot use $to_mode_str
                                $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
                        }
                        print "<td>" .
                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
-                                                    hash=>$diff{'to_id'}, file_name=>$diff{'to_file'}),
-                                     -class => "list"}, esc_path($diff{'to_file'})) . "</td>\n" .
+                                                    hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
+                                     -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
                              "<td><span class=\"file_status $nstatus\">[$nstatus from " .
                              $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
-                                                    hash=>$diff{'from_id'}, file_name=>$diff{'from_file'}),
-                                     -class => "list"}, esc_path($diff{'from_file'})) .
-                             " with " . (int $diff{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
+                                                    hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
+                                     -class => "list"}, esc_path($diff->{'from_file'})) .
+                             " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
                              "<td class=\"link\">";
                        if ($action eq 'commitdiff') {
                                # link to patch
                                $patchno++;
                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
                                      " | ";
-                       } elsif ($diff{'to_id'} ne $diff{'from_id'}) {
+                       } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
                                # "commit" view and modified file (not only pure rename or copy)
                                print $cgi->a({-href => href(action=>"blobdiff",
-                                                            hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
+                                                            hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
                                                             hash_base=>$hash, hash_parent_base=>$parent,
-                                                            file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})},
+                                                            file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
                                              "diff") .
                                      " | ";
                        }
-                       print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
-                                                    hash_base=>$parent, file_name=>$diff{'to_file'})},
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+                                                    hash_base=>$parent, file_name=>$diff->{'to_file'})},
                                      "blob") . " | ";
                        if ($have_blame) {
                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
-                                                            file_name=>$diff{'to_file'})},
+                                                            file_name=>$diff->{'to_file'})},
                                              "blame") . " | ";
                        }
                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
-                                                   file_name=>$diff{'to_file'})},
+                                                   file_name=>$diff->{'to_file'})},
                                      "history");
                        print "</td>\n";
 
@@ -2647,9 +2707,10 @@ sub git_patchset_body {
                # check if current patch belong to current raw line
                # and parse raw git-diff line if needed
                if (defined $diffinfo &&
+                   defined $from_id && defined $to_id &&
                    from_ids_eq($diffinfo->{'from_id'}, $from_id) &&
                    $diffinfo->{'to_id'} eq $to_id) {
-                       # this is split patch
+                       # this is continuation of a split patch
                        print "<div class=\"patch cont\">\n";
                } else {
                        # advance raw git-diff output if needed
@@ -2689,8 +2750,9 @@ sub git_patchset_body {
                                        delete $from{'href'};
                                }
                        }
+
                        $to{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'};
-                       if ($diffinfo->{'status'} ne "D") { # not deleted file
+                       if ($diffinfo->{'to_id'} ne ('0' x 40)) { # file exists in result
                                $to{'href'} = href(action=>"blob", hash_base=>$hash,
                                                   hash=>$diffinfo->{'to_id'},
                                                   file_name=>$to{'file'});
@@ -2846,7 +2908,14 @@ sub git_patchset_body {
        } continue {
                print "</div>\n"; # class="patch"
        }
-       print "<div class=\"diff nodifferences\">No differences found</div>\n" if (!$patch_number);
+
+       if ($patch_number == 0) {
+               if (@hash_parents > 1) {
+                       print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
+               } else {
+                       print "<div class=\"diff nodifferences\">No differences found</div>\n";
+               }
+       }
 
        print "</div>\n"; # class="patchset"
 }
@@ -2960,7 +3029,7 @@ sub git_project_list_body {
                                        esc_html($pr->{'descr'})) . "</td>\n" .
                      "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
-                     $pr->{'age_string'} . "</td>\n" .
+                     (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
                      "<td class=\"link\">" .
                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
@@ -3214,7 +3283,7 @@ sub git_search_grep_body {
                               esc_html(chop_str($co{'title'}, 50)) . "<br/>");
                my $comment = $co{'comment'};
                foreach my $line (@$comment) {
-                       if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
+                       if ($line =~ m/^(.*)($search_regexp)(.*)$/i) {
                                my $lead = esc_html($1) || "";
                                $lead = chop_str($lead, 30, 10);
                                my $match = esc_html($2) || "";
@@ -3312,7 +3381,7 @@ sub git_project_index {
 sub git_summary {
        my $descr = git_get_project_description($project) || "none";
        my %co = parse_commit("HEAD");
-       my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
+       my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
        my $head = $co{'id'};
 
        my $owner = git_get_project_owner($project);
@@ -3335,8 +3404,11 @@ sub git_summary {
        print "<div class=\"title\">&nbsp;</div>\n";
        print "<table cellspacing=\"0\">\n" .
              "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
-             "<tr><td>owner</td><td>$owner</td></tr>\n" .
-             "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
+             "<tr><td>owner</td><td>$owner</td></tr>\n";
+       if (defined $cd{'rfc2822'}) {
+               print "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
+       }
+
        # use per project git URL list in $projectroot/$project/cloneurl
        # or make project git URL from git base URL and project name
        my $url_tag = "URL";
@@ -3359,11 +3431,13 @@ sub git_summary {
 
        # 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(\@commitlist, 0, 15, $refs,
-                         $#commitlist <=  15 ? undef :
-                         $cgi->a({-href => href(action=>"shortlog")}, "..."));
+       my @commitlist = $head ? parse_commits($head, 17) : ();
+       if (@commitlist) {
+               git_print_header_div('shortlog');
+               git_shortlog_body(\@commitlist, 0, 15, $refs,
+                                 $#commitlist <=  15 ? undef :
+                                 $cgi->a({-href => href(action=>"shortlog")}, "..."));
+       }
 
        if (@taglist) {
                git_print_header_div('tags');
@@ -3395,6 +3469,11 @@ sub git_tag {
        git_header_html();
        git_print_page_nav('','', $head,undef,$head);
        my %tag = parse_tag($hash);
+
+       if (! %tag) {
+               die_error(undef, "Unknown tag object");
+       }
+
        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
        print "<div class=\"title_text\">\n" .
              "<table cellspacing=\"0\">\n" .
@@ -3899,19 +3978,26 @@ sub git_snapshot {
                $hash = git_get_head_hash($project);
        }
 
-       my $filename = decode_utf8(basename($project)) . "-$hash.tar.$suffix";
+       my $git = git_cmd_str();
+       my $name = $project;
+       $name =~ s/\047/\047\\\047\047/g;
+       my $filename = decode_utf8(basename($project));
+       my $cmd;
+       if ($suffix eq 'zip') {
+               $filename .= "-$hash.$suffix";
+               $cmd = "$git archive --format=zip --prefix=\'$name\'/ $hash";
+       } else {
+               $filename .= "-$hash.tar.$suffix";
+               $cmd = "$git archive --format=tar --prefix=\'$name\'/ $hash | $command";
+       }
 
        print $cgi->header(
                -type => "application/$ctype",
                -content_disposition => 'inline; filename="' . "$filename" . '"',
                -status => '200 OK');
 
-       my $git = git_cmd_str();
-       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");
+       open my $fd, "-|", $cmd
+               or die_error(undef, "Execute git-archive failed");
        binmode STDOUT, ':raw';
        print <$fd>;
        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
@@ -4019,14 +4105,13 @@ sub git_commit {
                $parent = "--root";
        }
        my @difftree;
-       if (@$parents <= 1) {
-               # 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");
-               @difftree = map { chomp; $_ } <$fd>;
-               close $fd or die_error(undef, "Reading git-diff-tree failed");
-       }
+       open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
+               @diff_opts,
+               (@$parents <= 1 ? $parent : '-c'),
+               $hash, "--"
+               or die_error(undef, "Open git-diff-tree failed");
+       @difftree = map { chomp; $_ } <$fd>;
+       close $fd or die_error(undef, "Reading git-diff-tree failed");
 
        # non-textual hash id's can be cached
        my $expires;
@@ -4104,10 +4189,7 @@ sub git_commit {
        git_print_log($co{'comment'});
        print "</div>\n";
 
-       if (@$parents <= 1) {
-               # do not output difftree/whatchanged for merges
-               git_difftree_body(\@difftree, $hash, $parent);
-       }
+       git_difftree_body(\@difftree, $hash, @$parents);
 
        git_footer_html();
 }
@@ -4384,8 +4466,10 @@ sub git_commitdiff {
                }
        }
 
+       my $hash_parent_param = $hash_parent;
        if (!defined $hash_parent) {
-               $hash_parent = $co{'parent'} || '--root';
+               $hash_parent_param =
+                       @{$co{'parents'}} > 1 ? '-c' : $co{'parent'} || '--root';
        }
 
        # read commitdiff
@@ -4394,19 +4478,19 @@ sub git_commitdiff {
        if ($format eq 'html') {
                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
                        "--no-commit-id", "--patch-with-raw", "--full-index",
-                       $hash_parent, $hash, "--"
+                       $hash_parent_param, $hash, "--"
                        or die_error(undef, "Open git-diff-tree failed");
 
                while (my $line = <$fd>) {
                        chomp $line;
                        # empty line ends raw part of diff-tree output
                        last unless $line;
-                       push @difftree, $line;
+                       push @difftree, scalar parse_difftree_raw_line($line);
                }
 
        } elsif ($format eq 'plain') {
                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
-                       '-p', $hash_parent, $hash, "--"
+                       '-p', $hash_parent_param, $hash, "--"
                        or die_error(undef, "Open git-diff-tree failed");
 
        } else {
@@ -4462,10 +4546,10 @@ TEXT
 
        # write patch
        if ($format eq 'html') {
-               git_difftree_body(\@difftree, $hash, $hash_parent);
+               git_difftree_body(\@difftree, $hash, $hash_parent || @{$co{'parents'}});
                print "<br/>\n";
 
-               git_patchset_body($fd, \@difftree, $hash, $hash_parent);
+               git_patchset_body($fd, \@difftree, $hash, $hash_parent || @{$co{'parents'}});
                close $fd;
                print "</div>\n"; # class="page_body"
                git_footer_html();
@@ -4576,6 +4660,12 @@ sub git_search {
                        die_error('403 Permission denied', "Permission denied");
                }
        }
+       if ($searchtype eq 'grep') {
+               my ($have_grep) = gitweb_check_feature('grep');
+               if (!$have_grep) {
+                       die_error('403 Permission denied', "Permission denied");
+               }
+       }
 
        git_header_html();
 
@@ -4588,7 +4678,7 @@ sub git_search {
                } elsif ($searchtype eq 'committer') {
                        $greptype = "--committer=";
                }
-               $greptype .= $searchtext;
+               $greptype .= $search_regexp;
                my @commitlist = parse_commits($hash, 101, (100 * $page), $greptype);
 
                my $paging_nav = '';
@@ -4637,8 +4727,10 @@ sub git_search {
                my $alternate = 1;
                $/ = "\n";
                my $git_command = git_cmd_str();
+               my $searchqtext = $searchtext;
+               $searchqtext =~ s/'/'\\''/;
                open my $fd, "-|", "$git_command rev-list $hash | " .
-                       "$git_command diff-tree -r --stdin -S\'$searchtext\'";
+                       "$git_command diff-tree -r --stdin -S\'$searchqtext\'";
                undef %co;
                my @files;
                while (my $line = <$fd>) {
@@ -4692,6 +4784,73 @@ sub git_search {
 
                print "</table>\n";
        }
+
+       if ($searchtype eq 'grep') {
+               git_print_page_nav('','', $hash,$co{'tree'},$hash);
+               git_print_header_div('commit', esc_html($co{'title'}), $hash);
+
+               print "<table cellspacing=\"0\">\n";
+               my $alternate = 1;
+               my $matches = 0;
+               $/ = "\n";
+               open my $fd, "-|", git_cmd(), 'grep', '-n', '-i', '-E', $searchtext, $co{'tree'};
+               my $lastfile = '';
+               while (my $line = <$fd>) {
+                       chomp $line;
+                       my ($file, $lno, $ltext, $binary);
+                       last if ($matches++ > 1000);
+                       if ($line =~ /^Binary file (.+) matches$/) {
+                               $file = $1;
+                               $binary = 1;
+                       } else {
+                               (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
+                       }
+                       if ($file ne $lastfile) {
+                               $lastfile and print "</td></tr>\n";
+                               if ($alternate++) {
+                                       print "<tr class=\"dark\">\n";
+                               } else {
+                                       print "<tr class=\"light\">\n";
+                               }
+                               print "<td class=\"list\">".
+                                       $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
+                                                              file_name=>"$file"),
+                                               -class => "list"}, esc_path($file));
+                               print "</td><td>\n";
+                               $lastfile = $file;
+                       }
+                       if ($binary) {
+                               print "<div class=\"binary\">Binary file</div>\n";
+                       } else {
+                               $ltext = untabify($ltext);
+                               if ($ltext =~ m/^(.*)($searchtext)(.*)$/i) {
+                                       $ltext = esc_html($1, -nbsp=>1);
+                                       $ltext .= '<span class="match">';
+                                       $ltext .= esc_html($2, -nbsp=>1);
+                                       $ltext .= '</span>';
+                                       $ltext .= esc_html($3, -nbsp=>1);
+                               } else {
+                                       $ltext = esc_html($ltext, -nbsp=>1);
+                               }
+                               print "<div class=\"pre\">" .
+                                       $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
+                                                              file_name=>"$file").'#l'.$lno,
+                                               -class => "linenr"}, sprintf('%4i', $lno))
+                                       . ' ' .  $ltext . "</div>\n";
+                       }
+               }
+               if ($lastfile) {
+                       print "</td></tr>\n";
+                       if ($matches > 1000) {
+                               print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
+                       }
+               } else {
+                       print "<div class=\"diff nodifferences\">No matches found</div>\n";
+               }
+               close $fd;
+
+               print "</table>\n";
+       }
        git_footer_html();
 }
 
@@ -4702,6 +4861,20 @@ sub git_search_help {
 <dl>
 <dt><b>commit</b></dt>
 <dd>The commit messages and authorship information will be scanned for the given string.</dd>
+EOT
+       my ($have_grep) = gitweb_check_feature('grep');
+       if ($have_grep) {
+               print <<EOT;
+<dt><b>grep</b></dt>
+<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
+    a different one) are searched for the given
+<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a>
+(POSIX extended) and the matches are listed. On large
+trees, this search can take a while and put some strain on the server, so please use it with
+some consideration.</dd>
+EOT
+       }
+       print <<EOT;
 <dt><b>author</b></dt>
 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given string.</dd>
 <dt><b>committer</b></dt>
@@ -4876,7 +5049,8 @@ XML
 
                # get list of changed files
                open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
-                       $co{'parent'}, $co{'id'}, "--", (defined $file_name ? $file_name : ())
+                       $co{'parent'} || "--root",
+                       $co{'id'}, "--", (defined $file_name ? $file_name : ())
                        or next;
                my @difftree = map { chomp; $_ } <$fd>;
                close $fd