Code

Merge branch 'maint'
[git.git] / gitweb / gitweb.perl
index 90243fdf982c02dcb0ca9e3b6b128c2e7ad3d342..a6383dc85b99ead405e1961888d06bb8ff8f3931 100755 (executable)
@@ -94,6 +94,13 @@ our $default_text_plain_charset  = undef;
 # (relative to the current git repository)
 our $mimetypes_file = undef;
 
+# assume this charset if line contains non-UTF-8 characters;
+# it should be valid encoding (see Encoding::Supported(3pm) for list),
+# for which encoding all byte sequences are valid, for example
+# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
+# could be even 'utf-8' for the old behavior)
+our $fallback_encoding = 'latin1';
+
 # You define site-wide feature defaults here; override them with
 # $GITWEB_CONFIG as necessary.
 our %feature = (
@@ -102,10 +109,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 +139,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 +148,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 +251,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 +267,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 +398,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 +497,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");
@@ -564,6 +609,20 @@ sub validate_refname {
        return $input;
 }
 
+# decode sequences of octets in utf8 into Perl's internal form,
+# which is utf-8 with utf8 flag set if needed.  gitweb writes out
+# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
+sub to_utf8 {
+       my $str = shift;
+       my $res;
+       eval { $res = decode_utf8($str, Encode::FB_CROAK); };
+       if (defined $res) {
+               return $res;
+       } else {
+               return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
+       }
+}
+
 # quote unsafe chars, but keep the slash, even when it's not
 # correct, but quoted slashes look too horrible in bookmarks
 sub esc_param {
@@ -588,7 +647,7 @@ sub esc_html ($;%) {
        my $str = shift;
        my %opts = @_;
 
-       $str = decode_utf8($str);
+       $str = to_utf8($str);
        $str = $cgi->escapeHTML($str);
        if ($opts{'-nbsp'}) {
                $str =~ s/ /&nbsp;/g;
@@ -602,7 +661,7 @@ sub esc_path {
        my $str = shift;
        my %opts = @_;
 
-       $str = decode_utf8($str);
+       $str = to_utf8($str);
        $str = $cgi->escapeHTML($str);
        if ($opts{'-nbsp'}) {
                $str =~ s/ /&nbsp;/g;
@@ -722,7 +781,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";
@@ -885,7 +946,7 @@ sub format_subject_html {
 
        if (length($short) < length($long)) {
                return $cgi->a({-href => $href, -class => "list subject",
-                               -title => decode_utf8($long)},
+                               -title => to_utf8($long)},
                       esc_html($short) . $extra);
        } else {
                return $cgi->a({-href => $href, -class => "list subject"},
@@ -893,7 +954,200 @@ sub format_subject_html {
        }
 }
 
-# format patch (diff) line (rather not to be used for diff headers)
+# format git diff header line, i.e. "diff --(git|combined|cc) ..."
+sub format_git_diff_header_line {
+       my $line = shift;
+       my $diffinfo = shift;
+       my ($from, $to) = @_;
+
+       if ($diffinfo->{'nparents'}) {
+               # combined diff
+               $line =~ s!^(diff (.*?) )"?.*$!$1!;
+               if ($to->{'href'}) {
+                       $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
+                                        esc_path($to->{'file'}));
+               } else { # file was deleted (no href)
+                       $line .= esc_path($to->{'file'});
+               }
+       } else {
+               # "ordinary" diff
+               $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
+               if ($from->{'href'}) {
+                       $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
+                                        'a/' . esc_path($from->{'file'}));
+               } else { # file was added (no href)
+                       $line .= 'a/' . esc_path($from->{'file'});
+               }
+               $line .= ' ';
+               if ($to->{'href'}) {
+                       $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
+                                        'b/' . esc_path($to->{'file'}));
+               } else { # file was deleted
+                       $line .= 'b/' . esc_path($to->{'file'});
+               }
+       }
+
+       return "<div class=\"diff header\">$line</div>\n";
+}
+
+# format extended diff header line, before patch itself
+sub format_extended_diff_header_line {
+       my $line = shift;
+       my $diffinfo = shift;
+       my ($from, $to) = @_;
+
+       # match <path>
+       if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
+               $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
+                                      esc_path($from->{'file'}));
+       }
+       if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
+               $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
+                                esc_path($to->{'file'}));
+       }
+       # match single <mode>
+       if ($line =~ m/\s(\d{6})$/) {
+               $line .= '<span class="info"> (' .
+                        file_type_long($1) .
+                        ')</span>';
+       }
+       # match <hash>
+       if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
+               # can match only for combined diff
+               $line = 'index ';
+               for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+                       if ($from->{'href'}[$i]) {
+                               $line .= $cgi->a({-href=>$from->{'href'}[$i],
+                                                 -class=>"hash"},
+                                                substr($diffinfo->{'from_id'}[$i],0,7));
+                       } else {
+                               $line .= '0' x 7;
+                       }
+                       # separator
+                       $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
+               }
+               $line .= '..';
+               if ($to->{'href'}) {
+                       $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
+                                        substr($diffinfo->{'to_id'},0,7));
+               } else {
+                       $line .= '0' x 7;
+               }
+
+       } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
+               # can match only for ordinary diff
+               my ($from_link, $to_link);
+               if ($from->{'href'}) {
+                       $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
+                                            substr($diffinfo->{'from_id'},0,7));
+               } else {
+                       $from_link = '0' x 7;
+               }
+               if ($to->{'href'}) {
+                       $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
+                                          substr($diffinfo->{'to_id'},0,7));
+               } else {
+                       $to_link = '0' x 7;
+               }
+               my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
+               $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
+       }
+
+       return $line . "<br/>\n";
+}
+
+# format from-file/to-file diff header
+sub format_diff_from_to_header {
+       my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
+       my $line;
+       my $result = '';
+
+       $line = $from_line;
+       #assert($line =~ m/^---/) if DEBUG;
+       # no extra formatting for "^--- /dev/null"
+       if (! $diffinfo->{'nparents'}) {
+               # ordinary (single parent) diff
+               if ($line =~ m!^--- "?a/!) {
+                       if ($from->{'href'}) {
+                               $line = '--- a/' .
+                                       $cgi->a({-href=>$from->{'href'}, -class=>"path"},
+                                               esc_path($from->{'file'}));
+                       } else {
+                               $line = '--- a/' .
+                                       esc_path($from->{'file'});
+                       }
+               }
+               $result .= qq!<div class="diff from_file">$line</div>\n!;
+
+       } else {
+               # combined diff (merge commit)
+               for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+                       if ($from->{'href'}[$i]) {
+                               $line = '--- ' .
+                                       $cgi->a({-href=>href(action=>"blobdiff",
+                                                            hash_parent=>$diffinfo->{'from_id'}[$i],
+                                                            hash_parent_base=>$parents[$i],
+                                                            file_parent=>$from->{'file'}[$i],
+                                                            hash=>$diffinfo->{'to_id'},
+                                                            hash_base=>$hash,
+                                                            file_name=>$to->{'file'}),
+                                                -class=>"path",
+                                                -title=>"diff" . ($i+1)},
+                                               $i+1) .
+                                       '/' .
+                                       $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
+                                               esc_path($from->{'file'}[$i]));
+                       } else {
+                               $line = '--- /dev/null';
+                       }
+                       $result .= qq!<div class="diff from_file">$line</div>\n!;
+               }
+       }
+
+       $line = $to_line;
+       #assert($line =~ m/^\+\+\+/) if DEBUG;
+       # no extra formatting for "^+++ /dev/null"
+       if ($line =~ m!^\+\+\+ "?b/!) {
+               if ($to->{'href'}) {
+                       $line = '+++ b/' .
+                               $cgi->a({-href=>$to->{'href'}, -class=>"path"},
+                                       esc_path($to->{'file'}));
+               } else {
+                       $line = '+++ b/' .
+                               esc_path($to->{'file'});
+               }
+       }
+       $result .= qq!<div class="diff to_file">$line</div>\n!;
+
+       return $result;
+}
+
+# create note for patch simplified by combined diff
+sub format_diff_cc_simplified {
+       my ($diffinfo, @parents) = @_;
+       my $result = '';
+
+       $result .= "<div class=\"diff header\">" .
+                  "diff --cc ";
+       if (!is_deleted($diffinfo)) {
+               $result .= $cgi->a({-href => href(action=>"blob",
+                                                 hash_base=>$hash,
+                                                 hash=>$diffinfo->{'to_id'},
+                                                 file_name=>$diffinfo->{'to_file'}),
+                                   -class => "path"},
+                                  esc_path($diffinfo->{'to_file'}));
+       } else {
+               $result .= esc_path($diffinfo->{'to_file'});
+       }
+       $result .= "</div>\n" . # class="diff header"
+                  "<div class=\"diff nodifferences\">" .
+                  "Simple merge" .
+                  "</div>\n"; # class="diff nodifferences"
+
+       return $result;
+}
+
+# format patch (diff) line (not to be used for diff headers)
 sub format_diff_line {
        my $line = shift;
        my ($from, $to) = @_;
@@ -1054,6 +1308,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 +1355,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;
 }
 
@@ -1192,7 +1453,7 @@ sub git_get_projects_list {
                        if (check_export_ok("$projectroot/$path")) {
                                my $pr = {
                                        path => $path,
-                                       owner => decode_utf8($owner),
+                                       owner => to_utf8($owner),
                                };
                                push @list, $pr;
                                (my $forks_path = $path) =~ s/\.git$//;
@@ -1222,7 +1483,7 @@ sub git_get_project_owner {
                        $pr = unescape($pr);
                        $ow = unescape($ow);
                        if ($pr eq $project) {
-                               $owner = decode_utf8($ow);
+                               $owner = to_utf8($ow);
                                last;
                        }
                }
@@ -1247,7 +1508,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 +1632,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;
@@ -1607,6 +1873,48 @@ sub parse_ls_tree_line ($;%) {
        return wantarray ? %res : \%res;
 }
 
+# generates _two_ hashes, references to which are passed as 2 and 3 argument
+sub parse_from_to_diffinfo {
+       my ($diffinfo, $from, $to, @parents) = @_;
+
+       if ($diffinfo->{'nparents'}) {
+               # combined diff
+               $from->{'file'} = [];
+               $from->{'href'} = [];
+               fill_from_file_info($diffinfo, @parents)
+                       unless exists $diffinfo->{'from_file'};
+               for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+                       $from->{'file'}[$i] = $diffinfo->{'from_file'}[$i] || $diffinfo->{'to_file'};
+                       if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
+                               $from->{'href'}[$i] = href(action=>"blob",
+                                                          hash_base=>$parents[$i],
+                                                          hash=>$diffinfo->{'from_id'}[$i],
+                                                          file_name=>$from->{'file'}[$i]);
+                       } else {
+                               $from->{'href'}[$i] = undef;
+                       }
+               }
+       } else {
+               $from->{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'};
+               if ($diffinfo->{'status'} ne "A") { # not new (added) file
+                       $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
+                                              hash=>$diffinfo->{'from_id'},
+                                              file_name=>$from->{'file'});
+               } else {
+                       delete $from->{'href'};
+               }
+       }
+
+       $to->{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'};
+       if (!is_deleted($diffinfo)) { # file exists in result
+               $to->{'href'} = href(action=>"blob", hash_base=>$hash,
+                                    hash=>$diffinfo->{'to_id'},
+                                    file_name=>$to->{'file'});
+       } else {
+               delete $to->{'href'};
+       }
+}
+
 ## ......................................................................
 ## parse to array of hashes functions
 
@@ -1707,7 +2015,7 @@ sub get_file_owner {
        }
        my $owner = $gcos;
        $owner =~ s/[,;].*$//;
-       return decode_utf8($owner);
+       return to_utf8($owner);
 }
 
 ## ......................................................................
@@ -1790,7 +2098,7 @@ sub git_header_html {
 
        my $title = "$site_name";
        if (defined $project) {
-               $title .= " - " . decode_utf8($project);
+               $title .= " - " . to_utf8($project);
                if (defined $action) {
                        $title .= "/$action";
                        if (defined $file_name) {
@@ -1878,6 +2186,8 @@ EOF
                }
                print "\n";
        }
+       print "</div>\n";
+
        my ($have_search) = gitweb_check_feature('search');
        if ((defined $project) && ($have_search)) {
                if (!defined $searchtext) {
@@ -1900,14 +2210,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 {
@@ -2063,7 +2372,7 @@ sub git_print_page_path {
 
        print "<div class=\"page_path\">";
        print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
-                     -title => 'tree root'}, decode_utf8("[$project]"));
+                     -title => 'tree root'}, to_utf8("[$project]"));
        print " / ";
        if (defined $name) {
                my @dirname = split '/', $name;
@@ -2313,6 +2622,11 @@ sub from_ids_eq {
        }
 }
 
+sub is_deleted {
+       my $diffinfo = shift;
+
+       return $diffinfo->{'to_id'} eq ('0' x 40);
+}
 
 sub git_difftree_body {
        my ($difftree, $hash, @parents) = @_;
@@ -2327,6 +2641,26 @@ sub git_difftree_body {
        print "<table class=\"" .
              (@parents > 1 ? "combined " : "") .
              "diff_tree\">\n";
+
+       # header only for combined diff in 'commitdiff' view
+       my $has_header = @parents > 1 && $action eq 'commitdiff';
+       if ($has_header) {
+               # table header
+               print "<thead><tr>\n" .
+                      "<th></th><th></th>\n"; # filename, patchN link
+               for (my $i = 0; $i < @parents; $i++) {
+                       my $par = $parents[$i];
+                       print "<th>" .
+                             $cgi->a({-href => href(action=>"commitdiff",
+                                                    hash=>$hash, hash_parent=>$par),
+                                      -title => 'commitdiff to parent number ' .
+                                                 ($i+1) . ': ' . substr($par,0,7)},
+                                     $i+1) .
+                             "&nbsp;</th>\n";
+               }
+               print "</tr></thead>\n<tbody>\n";
+       }
+
        my $alternate = 1;
        my $patchno = 0;
        foreach my $line (@{$difftree}) {
@@ -2350,7 +2684,7 @@ sub git_difftree_body {
                        fill_from_file_info($diff, @parents)
                                unless exists $diff->{'from_file'};
 
-                       if ($diff->{'to_id'} ne ('0' x 40)) {
+                       if (!is_deleted($diff)) {
                                # file exists in the result (child) commit
                                print "<td>" .
                                      $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
@@ -2599,6 +2933,7 @@ sub git_difftree_body {
                } # we should not encounter Unmerged (U) or Unknown (X) status
                print "</tr>\n";
        }
+       print "</tbody>" if $has_header;
        print "</table>\n";
 }
 
@@ -2654,21 +2989,44 @@ 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
                        $patch_idx++ if defined $diffinfo;
 
-                       # read and prepare patch information
-                       if (ref($difftree->[$patch_idx]) eq "HASH") {
-                               # pre-parsed (or generated by hand)
-                               $diffinfo = $difftree->[$patch_idx];
-                       } else {
-                               $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
+                       # compact combined diff output can have some patches skipped
+                       # find which patch (using pathname of result) we are at now
+                       my $to_name;
+                       if ($diff_header[0] =~ m!^diff --cc "?(.*)"?$!) {
+                               $to_name = $1;
                        }
+
+                       do {
+                               # read and prepare patch information
+                               if (ref($difftree->[$patch_idx]) eq "HASH") {
+                                       # pre-parsed (or generated by hand)
+                                       $diffinfo = $difftree->[$patch_idx];
+                               } else {
+                                       $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
+                               }
+
+                               # check if current raw line has no patch (it got simplified)
+                               if (defined $to_name && $to_name ne $diffinfo->{'to_file'}) {
+                                       print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
+                                             format_diff_cc_simplified($diffinfo, @hash_parents) .
+                                             "</div>\n";  # class="patch"
+
+                                       $patch_idx++;
+                                       $patch_number++;
+                               }
+                       } until (!defined $to_name || $to_name eq $diffinfo->{'to_file'} ||
+                                $patch_idx > $#$difftree);
+                       # modifies %from, %to hashes
+                       parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
                        if ($diffinfo->{'nparents'}) {
                                # combined diff
                                $from{'file'} = [];
@@ -2696,8 +3054,9 @@ sub git_patchset_body {
                                        delete $from{'href'};
                                }
                        }
+
                        $to{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'};
-                       if ($diffinfo->{'status'} ne "D") { # not deleted file
+                       if (!is_deleted($diffinfo)) { # file exists in result
                                $to{'href'} = href(action=>"blob", hash_base=>$hash,
                                                   hash=>$diffinfo->{'to_id'},
                                                   file_name=>$to{'file'});
@@ -2711,105 +3070,15 @@ sub git_patchset_body {
 
                # print "git diff" header
                $patch_line = shift @diff_header;
-               if ($diffinfo->{'nparents'}) {
-
-                       # combined diff
-                       $patch_line =~ s!^(diff (.*?) )"?.*$!$1!;
-                       if ($to{'href'}) {
-                               $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"},
-                                                      esc_path($to{'file'}));
-                       } else { # file was deleted
-                               $patch_line .= esc_path($to{'file'});
-                       }
-
-               } else {
-
-                       $patch_line =~ s!^(diff (.*?) )"?a/.*$!$1!;
-                       if ($from{'href'}) {
-                               $patch_line .= $cgi->a({-href => $from{'href'}, -class => "path"},
-                                                      'a/' . esc_path($from{'file'}));
-                       } else { # file was added
-                               $patch_line .= 'a/' . esc_path($from{'file'});
-                       }
-                       $patch_line .= ' ';
-                       if ($to{'href'}) {
-                               $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"},
-                                                      'b/' . esc_path($to{'file'}));
-                       } else { # file was deleted
-                               $patch_line .= 'b/' . esc_path($to{'file'});
-                       }
-
-               }
-               print "<div class=\"diff header\">$patch_line</div>\n";
+               print format_git_diff_header_line($patch_line, $diffinfo,
+                                                 \%from, \%to);
 
                # print extended diff header
                print "<div class=\"diff extended_header\">\n" if (@diff_header > 0);
        EXTENDED_HEADER:
                foreach $patch_line (@diff_header) {
-                       # 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'}));
-                       }
-                       if ($patch_line =~ s!^((copy|rename) to ).*$!$1! && $to{'href'}) {
-                               $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"path"},
-                                                      esc_path($to{'file'}));
-                       }
-                       # match single <mode>
-                       if ($patch_line =~ m/\s(\d{6})$/) {
-                               $patch_line .= '<span class="info"> (' .
-                                              file_type_long($1) .
-                                              ')</span>';
-                       }
-                       # match <hash>
-                       if ($patch_line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
-                               # can match only for combined diff
-                               $patch_line = 'index ';
-                               for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
-                                       if ($from{'href'}[$i]) {
-                                               $patch_line .= $cgi->a({-href=>$from{'href'}[$i],
-                                                                       -class=>"hash"},
-                                                                      substr($diffinfo->{'from_id'}[$i],0,7));
-                                       } else {
-                                               $patch_line .= '0' x 7;
-                                       }
-                                       # separator
-                                       $patch_line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
-                               }
-                               $patch_line .= '..';
-                               if ($to{'href'}) {
-                                       $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"hash"},
-                                                              substr($diffinfo->{'to_id'},0,7));
-                               } else {
-                                       $patch_line .= '0' x 7;
-                               }
-
-                       } elsif ($patch_line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
-                               # can match only for ordinary diff
-                               my ($from_link, $to_link);
-                               if ($from{'href'}) {
-                                       $from_link = $cgi->a({-href=>$from{'href'}, -class=>"hash"},
-                                                            substr($diffinfo->{'from_id'},0,7));
-                               } else {
-                                       $from_link = '0' x 7;
-                               }
-                               if ($to{'href'}) {
-                                       $to_link = $cgi->a({-href=>$to{'href'}, -class=>"hash"},
-                                                          substr($diffinfo->{'to_id'},0,7));
-                               } else {
-                                       $to_link = '0' x 7;
-                               }
-                               #affirm {
-                               #       my ($from_hash, $to_hash) =
-                               #               ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/);
-                               #       my ($from_id, $to_id) =
-                               #               ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
-                               #       ($from_hash eq $from_id) && ($to_hash eq $to_id);
-                               #} if DEBUG;
-                               my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
-                               $patch_line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
-                       }
-                       print $patch_line . "<br/>\n";
+                       print format_extended_diff_header_line($patch_line, $diffinfo,
+                                                              \%from, \%to);
                }
                print "</div>\n"  if (@diff_header > 0); # class="diff extended_header"
 
@@ -2821,24 +3090,15 @@ sub git_patchset_body {
                }
                next PATCH if ($patch_line =~ m/^diff /);
                #assert($patch_line =~ m/^---/) if DEBUG;
-               if (!$diffinfo->{'nparents'} && # not from-file line for combined diff
-                   $from{'href'} && $patch_line =~ m!^--- "?a/!) {
-                       $patch_line = '--- a/' .
-                                     $cgi->a({-href=>$from{'href'}, -class=>"path"},
-                                             esc_path($from{'file'}));
-               }
-               print "<div class=\"diff from_file\">$patch_line</div>\n";
+               #assert($patch_line eq $last_patch_line) if DEBUG;
 
                $patch_line = <$fd>;
                chomp $patch_line;
+               #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
 
-               #assert($patch_line =~ m/^+++/) if DEBUG;
-               if ($to{'href'} && $patch_line =~ m!^\+\+\+ "?b/!) {
-                       $patch_line = '+++ b/' .
-                                     $cgi->a({-href=>$to{'href'}, -class=>"path"},
-                                             esc_path($to{'file'}));
-               }
-               print "<div class=\"diff to_file\">$patch_line</div>\n";
+               print format_diff_from_to_header($last_patch_line, $patch_line,
+                                                $diffinfo, \%from, \%to,
+                                                @hash_parents);
 
                # the patch itself
        LINE:
@@ -2853,7 +3113,35 @@ sub git_patchset_body {
        } continue {
                print "</div>\n"; # class="patch"
        }
-       print "<div class=\"diff nodifferences\">No differences found</div>\n" if (!$patch_number);
+
+       # for compact combined (--cc) format, with chunk and patch simpliciaction
+       # patchset might be empty, but there might be unprocessed raw lines
+       for ($patch_idx++ if $patch_number > 0;
+            $patch_idx < @$difftree;
+            $patch_idx++) {
+               # read and prepare patch information
+               if (ref($difftree->[$patch_idx]) eq "HASH") {
+                       # pre-parsed (or generated by hand)
+                       $diffinfo = $difftree->[$patch_idx];
+               } else {
+                       $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
+               }
+
+               # generate anchor for "patch" links in difftree / whatchanged part
+               print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
+                     format_diff_cc_simplified($diffinfo, @hash_parents) .
+                     "</div>\n";  # class="patch"
+
+               $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"
 }
@@ -2874,7 +3162,7 @@ sub git_project_list_body {
                ($pr->{'age'}, $pr->{'age_string'}) = @aa;
                if (!defined $pr->{'descr'}) {
                        my $descr = git_get_project_description($pr->{'path'}) || "";
-                       $pr->{'descr_long'} = decode_utf8($descr);
+                       $pr->{'descr_long'} = to_utf8($descr);
                        $pr->{'descr'} = chop_str($descr, 25, 5);
                }
                if (!defined $pr->{'owner'}) {
@@ -2967,7 +3255,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") . " | " .
@@ -3221,7 +3509,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) || "";
@@ -3319,7 +3607,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);
@@ -3342,8 +3630,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";
@@ -3366,11 +3657,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');
@@ -3402,6 +3695,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" .
@@ -3906,19 +4204,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 = to_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
@@ -4343,7 +4648,11 @@ sub git_commitdiff {
                die_error(undef, "Unknown commit object");
        }
 
-       # we need to prepare $formats_nav before any parameter munging
+       # choose format for commitdiff for merge
+       if (! defined $hash_parent && @{$co{'parents'}} > 1) {
+               $hash_parent = '--cc';
+       }
+       # we need to prepare $formats_nav before almost any parameter munging
        my $formats_nav;
        if ($format eq 'html') {
                $formats_nav =
@@ -4351,14 +4660,22 @@ sub git_commitdiff {
                                               hash=>$hash, hash_parent=>$hash_parent)},
                                "raw");
 
-               if (defined $hash_parent) {
+               if (defined $hash_parent &&
+                   $hash_parent ne '-c' && $hash_parent ne '--cc') {
                        # commitdiff with two commits given
                        my $hash_parent_short = $hash_parent;
                        if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
                                $hash_parent_short = substr($hash_parent, 0, 7);
                        }
                        $formats_nav .=
-                               ' (from: ' .
+                               ' (from';
+                       for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
+                               if ($co{'parents'}[$i] eq $hash_parent) {
+                                       $formats_nav .= ' parent ' . ($i+1);
+                                       last;
+                               }
+                       }
+                       $formats_nav .= ': ' .
                                $cgi->a({-href => href(action=>"commitdiff",
                                                       hash=>$hash_parent)},
                                        esc_html($hash_parent_short)) .
@@ -4376,6 +4693,17 @@ sub git_commitdiff {
                                ')';
                } else {
                        # merge commit
+                       if ($hash_parent eq '--cc') {
+                               $formats_nav .= ' | ' .
+                                       $cgi->a({-href => href(action=>"commitdiff",
+                                                              hash=>$hash, hash_parent=>'-c')},
+                                               'combined');
+                       } else { # $hash_parent eq '-c'
+                               $formats_nav .= ' | ' .
+                                       $cgi->a({-href => href(action=>"commitdiff",
+                                                              hash=>$hash, hash_parent=>'--cc')},
+                                               'compact');
+                       }
                        $formats_nav .=
                                ' (merge: ' .
                                join(' ', map {
@@ -4388,9 +4716,10 @@ sub git_commitdiff {
        }
 
        my $hash_parent_param = $hash_parent;
-       if (!defined $hash_parent) {
+       if (!defined $hash_parent_param) {
+               # --cc for multiple parents, --root for parentless
                $hash_parent_param =
-                       @{$co{'parents'}} > 1 ? '-c' : $co{'parent'} || '--root';
+                       @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
        }
 
        # read commitdiff
@@ -4467,10 +4796,14 @@ TEXT
 
        # write patch
        if ($format eq 'html') {
-               git_difftree_body(\@difftree, $hash, $hash_parent || @{$co{'parents'}});
+               my $use_parents = !defined $hash_parent ||
+                       $hash_parent eq '-c' || $hash_parent eq '--cc';
+               git_difftree_body(\@difftree, $hash,
+                                 $use_parents ? @{$co{'parents'}} : $hash_parent);
                print "<br/>\n";
 
-               git_patchset_body($fd, \@difftree, $hash, $hash_parent || @{$co{'parents'}});
+               git_patchset_body($fd, \@difftree, $hash,
+                                 $use_parents ? @{$co{'parents'}} : $hash_parent);
                close $fd;
                print "</div>\n"; # class="page_body"
                git_footer_html();
@@ -4581,6 +4914,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();
 
@@ -4593,7 +4932,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 = '';
@@ -4642,8 +4981,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>) {
@@ -4697,6 +5038,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();
 }
 
@@ -4707,6 +5115,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>
@@ -4881,7 +5303,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