Merge branch 'jc/blame' (early part)
[gitweb.git] / gitweb / gitweb.perl
index bf58c3bac8ff807bb2640815668a49a3e4c723fc..dbfb0441a6a59e6fe069a515a2d293f4d860e143 100755 (executable)
 use File::Basename qw(basename);
 binmode STDOUT, ':utf8';
 
+BEGIN {
+       CGI->compile() if $ENV{'MOD_PERL'};
+}
+
 our $cgi = new CGI;
 our $version = "++GIT_VERSION++";
 our $my_url = $cgi->url();
 # source of projects list
 our $projects_list = "++GITWEB_LIST++";
 
+# default order of projects list
+# valid values are none, project, descr, owner, and age
+our $default_projects_order = "project";
+
 # show repository only if this file exists
 # (only effective if this variable evaluates to true)
 our $export_ok = "++GITWEB_EXPORT_OK++";
 # (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 = (
        #       '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
        # To disable system wide have in $GITWEB_CONFIG
        # $feature{'snapshot'}{'default'} = [undef];
        # To have project specific config enable override in $GITWEB_CONFIG
-       # $feature{'blame'}{'override'} = 1;
-       # and in project config gitweb.snapshot = none|gzip|bzip2;
+       # $feature{'snapshot'}{'override'} = 1;
+       # and in project config gitweb.snapshot = none|gzip|bzip2|zip;
        'snapshot' => {
                'sub' => \&feature_snapshot,
                'override' => 0,
                #         => [content-encoding, suffix, program]
                'default' => ['x-gzip', 'gz', 'gzip']},
 
+       # Enable text search, which will list the commits which match author,
+       # committer or commit text to a given string.  Enabled by default.
+       # 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.
        # projects matching $projname/*.git will not be shown in the main
        # projects list, instead a '+' mark will be added to $projname
        # there and a 'forks' view will be enabled for the project, listing
-       # all the forks. This feature is supported only if project list
-       # is taken from a directory, not file.
+       # all the forks. If project list is taken from a file, forks have
+       # to be listed after the main project.
 
        # To enable system wide have in $GITWEB_CONFIG
        # $feature{'forks'}{'default'} = [1];
@@ -213,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 ();
        }
@@ -227,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');
 
@@ -346,14 +398,6 @@ sub check_export_ok {
        }
 }
 
-our $searchtext = $cgi->param('s');
-if (defined $searchtext) {
-       if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) {
-               die_error(undef, "Invalid search parameter");
-       }
-       $searchtext = quotemeta $searchtext;
-}
-
 our $searchtype = $cgi->param('st');
 if (defined $searchtype) {
        if ($searchtype =~ m/[^a-z]/) {
@@ -361,6 +405,18 @@ sub check_export_ok {
        }
 }
 
+our $searchtext = $cgi->param('s');
+our $search_regexp;
+if (defined $searchtext) {
+       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");
+       }
+       $search_regexp = quotemeta $searchtext;
+}
+
 # now read PATH_INFO and use it as alternative to parameters
 sub evaluate_path_info {
        return if defined $project;
@@ -425,6 +481,7 @@ sub evaluate_path_info {
        "history" => \&git_history,
        "log" => \&git_log,
        "rss" => \&git_rss,
+       "atom" => \&git_atom,
        "search" => \&git_search,
        "search_help" => \&git_search_help,
        "shortlog" => \&git_shortlog,
@@ -433,16 +490,23 @@ sub evaluate_path_info {
        "tags" => \&git_tags,
        "tree" => \&git_tree,
        "snapshot" => \&git_snapshot,
+       "object" => \&git_object,
        # those below don't need $project
        "opml" => \&git_opml,
        "project_list" => \&git_project_list,
        "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");
@@ -459,7 +523,8 @@ sub evaluate_path_info {
 
 sub href(%) {
        my %params = @_;
-       my $href = $my_uri;
+       # default is to use -absolute url() i.e. $my_uri
+       my $href = $params{-full} ? $my_url : $my_uri;
 
        # XXX: Warning: If you touch this, check the search form for updating,
        # too.
@@ -544,10 +609,18 @@ sub validate_refname {
        return $input;
 }
 
-# very thin wrapper for decode("utf8", $str, Encode::FB_DEFAULT);
+# 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;
-       return decode("utf8", $str, Encode::FB_DEFAULT);
+       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
@@ -575,7 +648,7 @@ ($;%)
        my %opts = @_;
 
        $str = to_utf8($str);
-       $str = escapeHTML($str);
+       $str = $cgi->escapeHTML($str);
        if ($opts{'-nbsp'}) {
                $str =~ s/ /&nbsp;/g;
        }
@@ -583,7 +656,21 @@ ($;%)
        return $str;
 }
 
-# Make control characterss "printable".
+# quote control characters and escape filename to HTML
+sub esc_path {
+       my $str = shift;
+       my %opts = @_;
+
+       $str = to_utf8($str);
+       $str = $cgi->escapeHTML($str);
+       if ($opts{'-nbsp'}) {
+               $str =~ s/ /&nbsp;/g;
+       }
+       $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
+       return $str;
+}
+
+# Make control characters "printable", using character escape codes (CEC)
 sub quot_cec {
        my $cntrl = shift;
        my %es = ( # character escape codes, aka escape sequences
@@ -603,22 +690,14 @@ sub quot_cec {
        return "<span class=\"cntrl\">$chr</span>";
 }
 
-# Alternatively use unicode control pictures codepoints.
+# Alternatively use unicode control pictures codepoints,
+# Unicode "printable representation" (PR)
 sub quot_upr {
        my $cntrl = shift;
        my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
        return "<span class=\"cntrl\">$chr</span>";
 }
 
-# quote control characters and escape filename to HTML
-sub esc_path {
-       my $str = shift;
-
-       $str = esc_html($str);
-       $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
-       return $str;
-}
-
 # git may return quoted and escaped filenames
 sub unquote {
        my $str = shift;
@@ -702,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";
@@ -812,21 +893,19 @@ sub file_type_long {
 
 ## ----------------------------------------------------------------------
 ## functions returning short HTML fragments, or transforming HTML fragments
-## which don't beling to other sections
+## which don't belong to other sections
 
 # format line of commit message.
 sub format_log_line_html {
        my $line = shift;
 
        $line = esc_html($line, -nbsp=>1);
-       if ($line =~ m/([0-9a-fA-F]{40})/) {
+       if ($line =~ m/([0-9a-fA-F]{8,40})/) {
                my $hash_text = $1;
-               if (git_get_type($hash_text) eq "commit") {
-                       my $link =
-                               $cgi->a({-href => href(action=>"commit", hash=>$hash_text),
-                                       -class => "text"}, $hash_text);
-                       $line =~ s/$hash_text/$link/;
-               }
+               my $link =
+                       $cgi->a({-href => href(action=>"object", hash=>$hash_text),
+                               -class => "text"}, $hash_text);
+               $line =~ s/$hash_text/$link/;
        }
        return $line;
 }
@@ -848,7 +927,8 @@ sub format_ref_marker {
                                $name = $ref;
                        }
 
-                       $markers .= " <span class=\"$type\">" . esc_html($name) . "</span>";
+                       $markers .= " <span class=\"$type\" title=\"$ref\">" .
+                                   esc_html($name) . "</span>";
                }
        }
 
@@ -874,23 +954,285 @@ sub format_subject_html {
        }
 }
 
+# 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 $char = substr($line, 0, 1);
+       my ($from, $to) = @_;
        my $diff_class = "";
 
        chomp $line;
 
-       if ($char eq '+') {
-               $diff_class = " add";
-       } elsif ($char eq "-") {
-               $diff_class = " rem";
-       } elsif ($char eq "@") {
-               $diff_class = " chunk_header";
-       } elsif ($char eq "\\") {
-               $diff_class = " incomplete";
+       if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
+               # combined diff
+               my $prefix = substr($line, 0, scalar @{$from->{'href'}});
+               if ($line =~ m/^\@{3}/) {
+                       $diff_class = " chunk_header";
+               } elsif ($line =~ m/^\\/) {
+                       $diff_class = " incomplete";
+               } elsif ($prefix =~ tr/+/+/) {
+                       $diff_class = " add";
+               } elsif ($prefix =~ tr/-/-/) {
+                       $diff_class = " rem";
+               }
+       } else {
+               # assume ordinary diff
+               my $char = substr($line, 0, 1);
+               if ($char eq '+') {
+                       $diff_class = " add";
+               } elsif ($char eq '-') {
+                       $diff_class = " rem";
+               } elsif ($char eq '@') {
+                       $diff_class = " chunk_header";
+               } elsif ($char eq "\\") {
+                       $diff_class = " incomplete";
+               }
        }
        $line = untabify($line);
+       if ($from && $to && $line =~ m/^\@{2} /) {
+               my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
+                       $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
+
+               $from_lines = 0 unless defined $from_lines;
+               $to_lines   = 0 unless defined $to_lines;
+
+               if ($from->{'href'}) {
+                       $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
+                                            -class=>"list"}, $from_text);
+               }
+               if ($to->{'href'}) {
+                       $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
+                                            -class=>"list"}, $to_text);
+               }
+               $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
+                       "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
+               return "<div class=\"diff$diff_class\">$line</div>\n";
+       } elsif ($from && $to && $line =~ m/^\@{3}/) {
+               my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
+               my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
+
+               @from_text = split(' ', $ranges);
+               for (my $i = 0; $i < @from_text; ++$i) {
+                       ($from_start[$i], $from_nlines[$i]) =
+                               (split(',', substr($from_text[$i], 1)), 0);
+               }
+
+               $to_text   = pop @from_text;
+               $to_start  = pop @from_start;
+               $to_nlines = pop @from_nlines;
+
+               $line = "<span class=\"chunk_info\">$prefix ";
+               for (my $i = 0; $i < @from_text; ++$i) {
+                       if ($from->{'href'}[$i]) {
+                               $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
+                                                 -class=>"list"}, $from_text[$i]);
+                       } else {
+                               $line .= $from_text[$i];
+                       }
+                       $line .= " ";
+               }
+               if ($to->{'href'}) {
+                       $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
+                                         -class=>"list"}, $to_text);
+               } else {
+                       $line .= $to_text;
+               }
+               $line .= " $prefix</span>" .
+                        "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
+               return "<div class=\"diff$diff_class\">$line</div>\n";
+       }
        return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
 }
 
@@ -944,7 +1286,7 @@ sub git_get_project_config {
        $key =~ s/^gitweb\.//;
        return if ($key =~ m/\W/);
 
-       my @x = (git_cmd(), 'repo-config');
+       my @x = (git_cmd(), 'config');
        if (defined $type) { push @x, $type; }
        push @x, "--get";
        push @x, "gitweb.$key";
@@ -966,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) {
@@ -975,6 +1322,30 @@ sub git_get_hash_by_path {
        return $3;
 }
 
+# get path of entry with given hash at given tree-ish (ref)
+# used to get 'from' filename for combined diff (merge commit) for renames
+sub git_get_path_by_hash {
+       my $base = shift || return;
+       my $hash = shift || return;
+
+       local $/ = "\0";
+
+       open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
+               or return undef;
+       while (my $line = <$fd>) {
+               chomp $line;
+
+               #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
+               #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
+               if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
+                       close $fd;
+                       return $1;
+               }
+       }
+       close $fd;
+       return undef;
+}
+
 ## ......................................................................
 ## git utility functions, directly accessing git repository
 
@@ -984,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;
 }
 
@@ -1005,6 +1378,8 @@ sub git_get_projects_list {
        $filter ||= '';
        $filter =~ s/\.git$//;
 
+       my ($check_forks) = gitweb_check_feature('forks');
+
        if (-d $projects_list) {
                # search in directory
                my $dir = $projects_list . ($filter ? "/$filter" : '');
@@ -1012,8 +1387,6 @@ sub git_get_projects_list {
                $dir =~ s!/+$!!;
                my $pfxlen = length("$dir");
 
-               my ($check_forks) = gitweb_check_feature('forks');
-
                File::Find::find({
                        follow_fast => 1, # follow symbolic links
                        dangling_symlinks => 0, # ignore dangling symlinks, silently
@@ -1039,7 +1412,9 @@ sub git_get_projects_list {
                # 'git%2Fgit.git Linus+Torvalds'
                # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
                # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
+               my %paths;
                open my ($fd), $projects_list or return;
+       PROJECT:
                while (my $line = <$fd>) {
                        chomp $line;
                        my ($path, $owner) = split ' ', $line;
@@ -1052,11 +1427,27 @@ sub git_get_projects_list {
                                # looking for forks;
                                my $pfx = substr($path, 0, length($filter));
                                if ($pfx ne $filter) {
-                                       next;
+                                       next PROJECT;
                                }
                                my $sfx = substr($path, length($filter));
                                if ($sfx !~ /^\/.*\.git$/) {
-                                       next;
+                                       next PROJECT;
+                               }
+                       } elsif ($check_forks) {
+                       PATH:
+                               foreach my $filter (keys %paths) {
+                                       # looking for forks;
+                                       my $pfx = substr($path, 0, length($filter));
+                                       if ($pfx ne $filter) {
+                                               next PATH;
+                                       }
+                                       my $sfx = substr($path, length($filter));
+                                       if ($sfx !~ /^\/.*\.git$/) {
+                                               next PATH;
+                                       }
+                                       # is a fork, don't include it in
+                                       # the list
+                                       next PROJECT;
                                }
                        }
                        if (check_export_ok("$projectroot/$path")) {
@@ -1064,12 +1455,13 @@ sub git_get_projects_list {
                                        path => $path,
                                        owner => to_utf8($owner),
                                };
-                               push @list, $pr
+                               push @list, $pr;
+                               (my $forks_path = $path) =~ s/\.git$//;
+                               $paths{$forks_path}++;
                        }
                }
                close $fd;
        }
-       @list = sort {$a->{'path'} cmp $b->{'path'}} @list;
        return @list;
 }
 
@@ -1110,12 +1502,14 @@ sub git_get_last_activity {
 
        $git_dir = "$projectroot/$path";
        open($fd, "-|", git_cmd(), 'for-each-ref',
-            '--format=%(refname) %(committer)',
+            '--format=%(committer)',
             '--sort=-committerdate',
+            '--count=1',
             'refs/heads') or return;
        my $most_recent = <$fd>;
        close $fd or return;
-       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));
@@ -1125,14 +1519,15 @@ sub git_get_last_activity {
 sub git_get_references {
        my $type = shift || "";
        my %refs;
-       # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c      refs/tags/v2.6.11
-       # c39ae07f393806ccf406ef966e9a15afc43cc36a      refs/tags/v2.6.11^{}
-       open my $fd, "-|", $GIT, "peek-remote", "$projectroot/$project/"
+       # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
+       # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
+       open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
+               ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
                or return;
 
        while (my $line = <$fd>) {
                chomp $line;
-               if ($line =~ m/^([0-9a-fA-F]{40})\trefs\/($type\/?[^\^]+)/) {
+               if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type/?[^^]+)!) {
                        if (defined $refs{$1}) {
                                push @{$refs{$1}}, $2;
                        } else {
@@ -1176,10 +1571,12 @@ sub parse_date {
        $date{'mday'} = $mday;
        $date{'day'} = $days[$wday];
        $date{'month'} = $months[$mon];
-       $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
-                          $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
+       $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
+                            $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
        $date{'mday-time'} = sprintf "%d %s %02d:%02d",
                             $mday, $months[$mon], $hour ,$min;
+       $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
+                            1900+$year, $mon, $mday, $hour ,$min, $sec;
 
        $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
        my $local = $epoch + ((int $1 + ($2/60)) * 3600);
@@ -1187,9 +1584,9 @@ sub parse_date {
        $date{'hour_local'} = $hour;
        $date{'minute_local'} = $min;
        $date{'tz_local'} = $tz;
-       $date{'iso-tz'} = sprintf ("%04d-%02d-%02d %02d:%02d:%02d %s",
-                                  1900+$year, $mon+1, $mday,
-                                  $hour, $min, $sec, $tz);
+       $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
+                                 1900+$year, $mon+1, $mday,
+                                 $hour, $min, $sec, $tz);
        return %date;
 }
 
@@ -1228,42 +1625,35 @@ sub parse_tag {
        return %tag
 }
 
-sub parse_commit {
-       my $commit_id = shift;
-       my $commit_text = shift;
-
-       my @commit_lines;
+sub parse_commit_text {
+       my ($commit_text, $withparents) = @_;
+       my @commit_lines = split '\n', $commit_text;
        my %co;
 
-       if (defined $commit_text) {
-               @commit_lines = @$commit_text;
-       } else {
-               local $/ = "\0";
-               open my $fd, "-|", git_cmd(), "rev-list",
-                       "--header", "--parents", "--max-count=1",
-                       $commit_id, "--"
-                       or return;
-               @commit_lines = split '\n', <$fd>;
-               close $fd or return;
-               pop @commit_lines;
+       pop @commit_lines; # Remove '\0'
+
+       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;
-       $co{'parents'} = \@parents;
-       $co{'parent'} = $parents[0];
        while (my $line = shift @commit_lines) {
                last if $line eq "\n";
                if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
                        $co{'tree'} = $1;
+               } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
+                       push @parents, $1;
                } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
                        $co{'author'} = $1;
                        $co{'author_epoch'} = $2;
                        $co{'author_tz'} = $3;
-                       if ($co{'author'} =~ m/^([^<]+) </) {
-                               $co{'author_name'} = $1;
+                       if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
+                               $co{'author_name'}  = $1;
+                               $co{'author_email'} = $2;
                        } else {
                                $co{'author_name'} = $co{'author'};
                        }
@@ -1272,12 +1662,19 @@ sub parse_commit {
                        $co{'committer_epoch'} = $2;
                        $co{'committer_tz'} = $3;
                        $co{'committer_name'} = $co{'committer'};
-                       $co{'committer_name'} =~ s/ <.*//;
+                       if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
+                               $co{'committer_name'}  = $1;
+                               $co{'committer_email'} = $2;
+                       } else {
+                               $co{'committer_name'} = $co{'committer'};
+                       }
                }
        }
        if (!defined $co{'tree'}) {
                return;
        };
+       $co{'parents'} = \@parents;
+       $co{'parent'} = $parents[0];
 
        foreach my $title (@commit_lines) {
                $title =~ s/^    //;
@@ -1327,6 +1724,52 @@ sub parse_commit {
        return %co;
 }
 
+sub parse_commit {
+       my ($commit_id) = @_;
+       my %co;
+
+       local $/ = "\0";
+
+       open my $fd, "-|", git_cmd(), "rev-list",
+               "--parents",
+               "--header",
+               "--max-count=1",
+               $commit_id,
+               "--",
+               or die_error(undef, "Open git-rev-list failed");
+       %co = parse_commit_text(<$fd>, 1);
+       close $fd;
+
+       return %co;
+}
+
+sub parse_commits {
+       my ($commit_id, $maxcount, $skip, $arg, $filename) = @_;
+       my @cos;
+
+       $maxcount ||= 1;
+       $skip ||= 0;
+
+       local $/ = "\0";
+
+       open my $fd, "-|", git_cmd(), "rev-list",
+               "--header",
+               ($arg ? ($arg) : ()),
+               ("--max-count=" . $maxcount),
+               ("--skip=" . $skip),
+               $commit_id,
+               "--",
+               ($filename ? ($filename) : ())
+               or die_error(undef, "Open git-rev-list failed");
+       while (my $line = <$fd>) {
+               my %co = parse_commit_text($line);
+               push @cos, \%co;
+       }
+       close $fd;
+
+       return wantarray ? @cos : \@cos;
+}
+
 # parse ref from ref_file, given by ref_id, with given type
 sub parse_ref {
        my $ref_file = shift;
@@ -1390,6 +1833,17 @@ sub parse_difftree_raw_line {
                        $res{'file'} = unquote($7);
                }
        }
+       # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
+       # combined diff (for merge commit)
+       elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
+               $res{'nparents'}  = length($1);
+               $res{'from_mode'} = [ split(' ', $2) ];
+               $res{'to_mode'} = pop @{$res{'from_mode'}};
+               $res{'from_id'} = [ split(' ', $3) ];
+               $res{'to_id'} = pop @{$res{'from_id'}};
+               $res{'status'} = [ split('', $4) ];
+               $res{'to_file'} = unquote($5);
+       }
        # 'c512b523472485aef4fff9e57b229d9d243c967f'
        elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
                $res{'commit'} = $1;
@@ -1419,6 +1873,48 @@ ($;%)
        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
 
@@ -1602,7 +2098,7 @@ sub git_header_html {
 
        my $title = "$site_name";
        if (defined $project) {
-               $title .= " - $project";
+               $title .= " - " . to_utf8($project);
                if (defined $action) {
                        $title .= "/$action";
                        if (defined $file_name) {
@@ -1627,6 +2123,7 @@ sub git_header_html {
        }
        print $cgi->header(-type=>$content_type, -charset => 'utf-8',
                           -status=> $status, -expires => $expires);
+       my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
        print <<EOF;
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -1635,7 +2132,7 @@ sub git_header_html {
 <!-- git core binaries version $git_version -->
 <head>
 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
-<meta name="generator" content="gitweb/$version git/$git_version"/>
+<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
 <meta name="robots" content="index, nofollow"/>
 <title>$title</title>
 EOF
@@ -1650,14 +2147,17 @@ sub git_header_html {
                }
        }
        if (defined $project) {
-               printf('<link rel="alternate" title="%s log" '.
-                      'href="%s" type="application/rss+xml"/>'."\n",
+               printf('<link rel="alternate" title="%s log RSS feed" '.
+                      'href="%s" type="application/rss+xml" />'."\n",
                       esc_param($project), href(action=>"rss"));
+               printf('<link rel="alternate" title="%s log Atom feed" '.
+                      'href="%s" type="application/atom+xml" />'."\n",
+                      esc_param($project), href(action=>"atom"));
        } else {
                printf('<link rel="alternate" title="%s projects list" '.
                       'href="%s" type="text/plain; charset=utf-8"/>'."\n",
                       $site_name, href(project=>undef, action=>"project_index"));
-               printf('<link rel="alternate" title="%s projects logs" '.
+               printf('<link rel="alternate" title="%s projects feeds" '.
                       'href="%s" type="text/x-opml"/>'."\n",
                       $site_name, href(project=>undef, action=>"opml"));
        }
@@ -1685,6 +2185,11 @@ sub git_header_html {
                        print " / $action";
                }
                print "\n";
+       }
+       print "</div>\n";
+
+       my ($have_search) = gitweb_check_feature('search');
+       if ((defined $project) && ($have_search)) {
                if (!defined $searchtext) {
                        $searchtext = "";
                }
@@ -1705,14 +2210,13 @@ sub git_header_html {
                      $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 {
@@ -1723,7 +2227,9 @@ sub git_footer_html {
                        print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
                }
                print $cgi->a({-href => href(action=>"rss"),
-                             -class => "rss_logo"}, "RSS") . "\n";
+                             -class => "rss_logo"}, "RSS") . " ";
+               print $cgi->a({-href => href(action=>"atom"),
+                             -class => "rss_logo"}, "Atom") . "\n";
        } else {
                print $cgi->a({-href => href(project=>undef, action=>"opml"),
                              -class => "rss_logo"}, "OPML") . " ";
@@ -1773,16 +2279,16 @@ sub git_print_page_nav {
        my %arg = map { $_ => {action=>$_} } @navs;
        if (defined $head) {
                for (qw(commit commitdiff)) {
-                       $arg{$_}{hash} = $head;
+                       $arg{$_}{'hash'} = $head;
                }
                if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
                        for (qw(shortlog log)) {
-                               $arg{$_}{hash} = $head;
+                               $arg{$_}{'hash'} = $head;
                        }
                }
        }
-       $arg{tree}{hash} = $treehead if defined $treehead;
-       $arg{tree}{hash_base} = $treebase if defined $treebase;
+       $arg{'tree'}{'hash'} = $treehead if defined $treehead;
+       $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
 
        print "<div class=\"page_nav\">\n" .
                (join " | ",
@@ -1830,9 +2336,9 @@ sub git_print_header_div {
        my ($action, $title, $hash, $hash_base) = @_;
        my %args = ();
 
-       $args{action} = $action;
-       $args{hash} = $hash if $hash;
-       $args{hash_base} = $hash_base if $hash_base;
+       $args{'action'} = $action;
+       $args{'hash'} = $hash if $hash;
+       $args{'hash_base'} = $hash_base if $hash_base;
 
        print "<div class=\"header\">\n" .
              $cgi->a({-href => href(%args), -class => "title"},
@@ -1866,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'}, "[$project]");
+                     -title => 'tree root'}, to_utf8("[$project]"));
        print " / ";
        if (defined $name) {
                my @dirname = split '/', $name;
@@ -1877,17 +2383,17 @@ sub git_print_page_path {
                        $fullname .= ($fullname ? '/' : '') . $dir;
                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
                                                     hash_base=>$hb),
-                                     -title => esc_html($fullname)}, esc_path($dir));
+                                     -title => $fullname}, esc_path($dir));
                        print " / ";
                }
                if (defined $type && $type eq 'blob') {
                        print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
                                                     hash_base=>$hb),
-                                     -title => esc_html($name)}, esc_path($basename));
+                                     -title => $name}, esc_path($basename));
                } elsif (defined $type && $type eq 'tree') {
                        print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
                                                     hash_base=>$hb),
-                                     -title => esc_html($name)}, esc_path($basename));
+                                     -title => $name}, esc_path($basename));
                        print " / ";
                } else {
                        print esc_path($basename);
@@ -1946,12 +2452,73 @@ ($;%)
        }
 }
 
+# return link target (what link points to)
+sub git_get_link_target {
+       my $hash = shift;
+       my $link_target;
+
+       # read link
+       open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
+               or return;
+       {
+               local $/;
+               $link_target = <$fd>;
+       }
+       close $fd
+               or return;
+
+       return $link_target;
+}
+
+# given link target, and the directory (basedir) the link is in,
+# return target of link relative to top directory (top tree);
+# return undef if it is not possible (including absolute links).
+sub normalize_link_target {
+       my ($link_target, $basedir, $hash_base) = @_;
+
+       # we can normalize symlink target only if $hash_base is provided
+       return unless $hash_base;
+
+       # absolute symlinks (beginning with '/') cannot be normalized
+       return if (substr($link_target, 0, 1) eq '/');
+
+       # normalize link target to path from top (root) tree (dir)
+       my $path;
+       if ($basedir) {
+               $path = $basedir . '/' . $link_target;
+       } else {
+               # we are in top (root) tree (dir)
+               $path = $link_target;
+       }
+
+       # remove //, /./, and /../
+       my @path_parts;
+       foreach my $part (split('/', $path)) {
+               # discard '.' and ''
+               next if (!$part || $part eq '.');
+               # handle '..'
+               if ($part eq '..') {
+                       if (@path_parts) {
+                               pop @path_parts;
+                       } else {
+                               # link leads outside repository (outside top dir)
+                               return;
+                       }
+               } else {
+                       push @path_parts, $part;
+               }
+       }
+       $path = join('/', @path_parts);
+
+       return $path;
+}
+
 # print tree entry (row of git_tree), but without encompassing <tr> element
 sub git_print_tree_entry {
        my ($t, $basedir, $hash_base, $have_blame) = @_;
 
        my %base_key = ();
-       $base_key{hash_base} = $hash_base if defined $hash_base;
+       $base_key{'hash_base'} = $hash_base if defined $hash_base;
 
        # The format of a table row is: mode list link.  Where mode is
        # the mode of the entry, list is the name of the entry, an href,
@@ -1962,16 +2529,31 @@ sub git_print_tree_entry {
                print "<td class=\"list\">" .
                        $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
                                               file_name=>"$basedir$t->{'name'}", %base_key),
-                               -class => "list"}, esc_path($t->{'name'})) . "</td>\n";
+                               -class => "list"}, esc_path($t->{'name'}));
+               if (S_ISLNK(oct $t->{'mode'})) {
+                       my $link_target = git_get_link_target($t->{'hash'});
+                       if ($link_target) {
+                               my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
+                               if (defined $norm_target) {
+                                       print " -> " .
+                                             $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
+                                                                    file_name=>$norm_target),
+                                                      -title => $norm_target}, esc_path($link_target));
+                               } else {
+                                       print " -> " . esc_path($link_target);
+                               }
+                       }
+               }
+               print "</td>\n";
                print "<td class=\"link\">";
                print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
-                                            file_name=>"$basedir$t->{'name'}", %base_key)},
-                             "blob");
+                                            file_name=>"$basedir$t->{'name'}", %base_key)},
+                             "blob");
                if ($have_blame) {
                        print " | " .
                              $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
-                                                          file_name=>"$basedir$t->{'name'}", %base_key)},
-                                           "blame");
+                                                    file_name=>"$basedir$t->{'name'}", %base_key)},
+                                     "blame");
                }
                if (defined $hash_base) {
                        print " | " .
@@ -1993,8 +2575,8 @@ sub git_print_tree_entry {
                print "</td>\n";
                print "<td class=\"link\">";
                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
-                                            file_name=>"$basedir$t->{'name'}", %base_key)},
-                             "tree");
+                                            file_name=>"$basedir$t->{'name'}", %base_key)},
+                             "tree");
                if (defined $hash_base) {
                        print " | " .
                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
@@ -2008,8 +2590,47 @@ sub git_print_tree_entry {
 ## ......................................................................
 ## functions printing large fragments of HTML
 
+sub fill_from_file_info {
+       my ($diff, @parents) = @_;
+
+       $diff->{'from_file'} = [ ];
+       $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
+       for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
+               if ($diff->{'status'}[$i] eq 'R' ||
+                   $diff->{'status'}[$i] eq 'C') {
+                       $diff->{'from_file'}[$i] =
+                               git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
+               }
+       }
+
+       return $diff;
+}
+
+# parameters can be strings, or references to arrays of strings
+sub from_ids_eq {
+       my ($a, $b) = @_;
+
+       if (ref($a) eq "ARRAY" && ref($b) eq "ARRAY" && @$a == @$b) {
+               for (my $i = 0; $i < @$a; ++$i) {
+                       return 0 unless ($a->[$i] eq $b->[$i]);
+               }
+               return 1;
+       } elsif (!ref($a) && !ref($b)) {
+               return $a eq $b;
+       } else {
+               return 0;
+       }
+}
+
+sub is_deleted {
+       my $diffinfo = shift;
+
+       return $diffinfo->{'to_id'} eq ('0' x 40);
+}
+
 sub git_difftree_body {
-       my ($difftree, $hash, $parent) = @_;
+       my ($difftree, $hash, @parents) = @_;
+       my ($parent) = $parents[0];
        my ($have_blame) = gitweb_check_feature('blame');
        print "<div class=\"list_head\">\n";
        if ($#{$difftree} > 10) {
@@ -2017,11 +2638,39 @@ sub git_difftree_body {
        }
        print "</div>\n";
 
-       print "<table class=\"diff_tree\">\n";
+       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}) {
-               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";
@@ -2030,31 +2679,120 @@ sub git_difftree_body {
                }
                $alternate ^= 1;
 
-               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 (exists $diff->{'nparents'}) { # combined diff
+
+                       fill_from_file_info($diff, @parents)
+                               unless exists $diff->{'from_file'};
+
+                       if (!is_deleted($diff)) {
+                               # file exists in the result (child) commit
+                               print "<td>" .
+                                     $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'})) .
+                                     "</td>\n";
+                       } else {
+                               print "<td>" .
+                                     esc_path($diff->{'to_file'}) .
+                                     "</td>\n";
+                       }
+
+                       if ($action eq 'commitdiff') {
+                               # link to patch
+                               $patchno++;
+                               print "<td class=\"link\">" .
+                                     $cgi->a({-href => "#patch$patchno"}, "patch") .
+                                     " | " .
+                                     "</td>\n";
+                       }
+
+                       my $has_history = 0;
+                       my $not_deleted = 0;
+                       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];
+
+                               $has_history ||= ($status ne 'A');
+                               $not_deleted ||= ($status ne 'D');
+
+                               if ($status eq 'A') {
+                                       print "<td  class=\"link\" align=\"right\"> | </td>\n";
+                               } elsif ($status eq 'D') {
+                                       print "<td class=\"link\">" .
+                                             $cgi->a({-href => href(action=>"blob",
+                                                                    hash_base=>$hash,
+                                                                    hash=>$from_hash,
+                                                                    file_name=>$from_path)},
+                                                     "blob" . ($i+1)) .
+                                             " | </td>\n";
+                               } else {
+                                       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_parent=>$from_hash,
+                                                                    hash_base=>$hash,
+                                                                    hash_parent_base=>$hash_parent,
+                                                                    file_name=>$diff->{'to_file'},
+                                                                    file_parent=>$from_path)},
+                                                     "diff" . ($i+1)) .
+                                             " | </td>\n";
+                               }
+                       }
+
+                       print "<td class=\"link\">";
+                       if ($not_deleted) {
+                               print $cgi->a({-href => href(action=>"blob",
+                                                            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'},
+                                                            hash_base=>$hash)},
+                                             "history");
+                       }
+                       print "</td>\n";
+
+                       print "</tr>\n";
+                       next; # instead of 'else' clause, to avoid extra indent
+               }
+               # else ordinary diff
+
+               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 (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\">";
@@ -2062,15 +2800,19 @@ sub git_difftree_body {
                                # link to patch
                                $patchno++;
                                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'})},
+                                     "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\">";
@@ -2080,26 +2822,24 @@ 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'})},
-                                     "blob") . " | ";
+                       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'})},
-                                             "blame") . " | ";
+                               print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
+                                                            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 != $to_file_type) {
+                               if ($from_file_type ne $to_file_type) {
                                        $mode_chnge .= " from $from_file_type to $to_file_type";
                                }
                                if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
@@ -2112,9 +2852,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\">";
@@ -2123,89 +2863,89 @@ 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'})},
-                                     "blob") . " | ";
+                       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'})},
-                                             "blame") . " | ";
+                               print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
+                                                            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{'from_id'},
-                                                    hash_base=>$parent, file_name=>$diff{'from_file'})},
-                                     "blob") . " | ";
+                       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'})},
-                                             "blame") . " | ";
+                               print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
+                                                            file_name=>$diff->{'to_file'})},
+                                             "blame") . " | ";
                        }
-                       print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
-                                                   file_name=>$diff{'from_file'})},
+                       print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
+                                                   file_name=>$diff->{'to_file'})},
                                      "history");
                        print "</td>\n";
 
                } # we should not encounter Unmerged (U) or Unknown (X) status
                print "</tr>\n";
        }
+       print "</tbody>" if $has_header;
        print "</table>\n";
 }
 
 sub git_patchset_body {
-       my ($fd, $difftree, $hash, $hash_parent) = @_;
+       my ($fd, $difftree, $hash, @hash_parents) = @_;
+       my ($hash_parent) = $hash_parents[0];
 
        my $patch_idx = 0;
+       my $patch_number = 0;
        my $patch_line;
        my $diffinfo;
        my (%from, %to);
-       my ($from_id, $to_id);
 
        print "<div class=\"patchset\">\n";
 
@@ -2219,10 +2959,12 @@ sub git_patchset_body {
  PATCH:
        while ($patch_line) {
                my @diff_header;
+               my ($from_id, $to_id);
 
                # git diff header
                #assert($patch_line =~ m/^diff /) if DEBUG;
                #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
+               $patch_number++;
                push @diff_header, $patch_line;
 
                # extended diff header
@@ -2230,47 +2972,96 @@ sub git_patchset_body {
                while ($patch_line = <$fd>) {
                        chomp $patch_line;
 
-                       last EXTENDED_HEADER if ($patch_line =~ m/^--- /);
+                       last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
 
                        if ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) {
                                $from_id = $1;
                                $to_id   = $2;
+                       } elsif ($patch_line =~ m/^index ((?:[0-9a-fA-F]{40},)+[0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) {
+                               $from_id = [ split(',', $1) ];
+                               $to_id   = $2;
                        }
 
                        push @diff_header, $patch_line;
                }
-               #last PATCH unless $patch_line;
                my $last_patch_line = $patch_line;
 
                # check if current patch belong to current raw line
                # and parse raw git-diff line if needed
                if (defined $diffinfo &&
-                   $diffinfo->{'from_id'} eq $from_id &&
-                   $diffinfo->{'to_id'}   eq $to_id) {
-                       # this is split patch
+                   defined $from_id && defined $to_id &&
+                   from_ids_eq($diffinfo->{'from_id'}, $from_id) &&
+                   $diffinfo->{'to_id'} eq $to_id) {
+                       # 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;
                        }
-                       $from{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'};
-                       $to{'file'}   = $diffinfo->{'to_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'});
+
+                       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'} = [];
+                               $from{'href'} = [];
+                               fill_from_file_info($diffinfo, @hash_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=>$hash_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'};
+                               }
                        }
-                       if ($diffinfo->{'status'} ne "D") { # not deleted file
+
+                       $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'};
                        }
                        # this is first patch for raw difftree line with $patch_idx index
                        # we index @$difftree array from 0, but number patches from 1
@@ -2279,91 +3070,35 @@ sub git_patchset_body {
 
                # print "git diff" header
                $patch_line = shift @diff_header;
-               $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 <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/) {
-                               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"
 
                # from-file/to-file diff header
                $patch_line = $last_patch_line;
-               #assert($patch_line =~ m/^---/) if DEBUG;
-               if ($from{'href'}) {
-                       $patch_line = '--- a/' .
-                                     $cgi->a({-href=>$from{'href'}, -class=>"path"},
-                                             esc_path($from{'file'}));
+               if (! $patch_line) {
+                       print "</div>\n"; # class="patch"
+                       last PATCH;
                }
-               print "<div class=\"diff from_file\">$patch_line</div>\n";
+               next PATCH if ($patch_line =~ m/^diff /);
+               #assert($patch_line =~ m/^---/) if DEBUG;
+               #assert($patch_line eq $last_patch_line) if DEBUG;
 
                $patch_line = <$fd>;
-               #last PATCH unless $patch_line;
                chomp $patch_line;
+               #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
 
-               #assert($patch_line =~ m/^+++/) if DEBUG;
-               if ($to{'href'}) {
-                       $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:
@@ -2372,13 +3107,42 @@ sub git_patchset_body {
 
                        next PATCH if ($patch_line =~ m/^diff /);
 
-                       print format_diff_line($patch_line);
+                       print format_diff_line($patch_line, \%from, \%to);
                }
 
        } continue {
                print "</div>\n"; # class="patch"
        }
 
+       # 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"
 }
 
@@ -2398,6 +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'} = to_utf8($descr);
                        $pr->{'descr'} = chop_str($descr, 25, 5);
                }
                if (!defined $pr->{'owner'}) {
@@ -2417,7 +3182,7 @@ sub git_project_list_body {
                push @projects, $pr;
        }
 
-       $order ||= "project";
+       $order ||= $default_projects_order;
        $from = 0 unless defined $from;
        $to = $#projects if (!defined $to || $#projects < $to);
 
@@ -2433,7 +3198,7 @@ sub git_project_list_body {
                } else {
                        print "<th>" .
                              $cgi->a({-href => href(project=>undef, order=>'project'),
-                                      -class => "header"}, "Project") .
+                                      -class => "header"}, "Project") .
                              "</th>\n";
                }
                if ($order eq "descr") {
@@ -2442,7 +3207,7 @@ sub git_project_list_body {
                } else {
                        print "<th>" .
                              $cgi->a({-href => href(project=>undef, order=>'descr'),
-                                      -class => "header"}, "Description") .
+                                      -class => "header"}, "Description") .
                              "</th>\n";
                }
                if ($order eq "owner") {
@@ -2451,7 +3216,7 @@ sub git_project_list_body {
                } else {
                        print "<th>" .
                              $cgi->a({-href => href(project=>undef, order=>'owner'),
-                                      -class => "header"}, "Owner") .
+                                      -class => "header"}, "Owner") .
                              "</th>\n";
                }
                if ($order eq "age") {
@@ -2460,7 +3225,7 @@ sub git_project_list_body {
                } else {
                        print "<th>" .
                              $cgi->a({-href => href(project=>undef, order=>'age'),
-                                      -class => "header"}, "Last Change") .
+                                      -class => "header"}, "Last Change") .
                              "</th>\n";
                }
                print "<th></th>\n" .
@@ -2485,10 +3250,12 @@ sub git_project_list_body {
                }
                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
-                     "<td>" . esc_html($pr->{'descr'}) . "</td>\n" .
+                     "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
+                                       -class => "list", -title => $pr->{'descr_long'}},
+                                       esc_html($pr->{'descr'})) . "</td>\n" .
                      "<td><i>" . chop_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") . " | " .
@@ -2511,18 +3278,19 @@ sub git_project_list_body {
 
 sub git_shortlog_body {
        # uses global variable $project
-       my ($revlist, $from, $to, $refs, $extra) = @_;
+       my ($commitlist, $from, $to, $refs, $extra) = @_;
+
+       my $have_snapshot = gitweb_have_snapshot();
 
        $from = 0 unless defined $from;
-       $to = $#{$revlist} if (!defined $to || $#{$revlist} < $to);
+       $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
 
        print "<table class=\"shortlog\" cellspacing=\"0\">\n";
        my $alternate = 1;
        for (my $i = $from; $i <= $to; $i++) {
-               my $commit = $revlist->[$i];
-               #my $ref = defined $refs ? format_ref_marker($refs, $commit) : '';
+               my %co = %{$commitlist->[$i]};
+               my $commit = $co{'id'};
                my $ref = format_ref_marker($refs, $commit);
-               my %co = parse_commit($commit);
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
                } else {
@@ -2540,7 +3308,7 @@ sub git_shortlog_body {
                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
-               if (gitweb_have_snapshot()) {
+               if ($have_snapshot) {
                        print " | " . $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot");
                }
                print "</td>\n" .
@@ -2556,23 +3324,19 @@ sub git_shortlog_body {
 
 sub git_history_body {
        # Warning: assumes constant type (blob or tree) during history
-       my ($revlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
+       my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
 
        $from = 0 unless defined $from;
-       $to = $#{$revlist} unless (defined $to && $to <= $#{$revlist});
+       $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
 
        print "<table class=\"history\" cellspacing=\"0\">\n";
        my $alternate = 1;
        for (my $i = $from; $i <= $to; $i++) {
-               if ($revlist->[$i] !~ m/^([0-9a-fA-F]{40})/) {
-                       next;
-               }
-
-               my $commit = $1;
-               my %co = parse_commit($commit);
+               my %co = %{$commitlist->[$i]};
                if (!%co) {
                        next;
                }
+               my $commit = $co{'id'};
 
                my $ref = format_ref_marker($refs, $commit);
 
@@ -2640,8 +3404,12 @@ sub git_tags_body {
                        print "<tr class=\"light\">\n";
                }
                $alternate ^= 1;
-               print "<td><i>$tag{'age'}</i></td>\n" .
-                     "<td>" .
+               if (defined $tag{'age'}) {
+                       print "<td><i>$tag{'age'}</i></td>\n";
+               } else {
+                       print "<td></td>\n";
+               }
+               print "<td>" .
                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
                               -class => "list name"}, esc_html($tag{'name'})) .
                      "</td>\n" .
@@ -2715,13 +3483,65 @@ sub git_heads_body {
        print "</table>\n";
 }
 
+sub git_search_grep_body {
+       my ($commitlist, $from, $to, $extra) = @_;
+       $from = 0 unless defined $from;
+       $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
+
+       print "<table class=\"grep\" cellspacing=\"0\">\n";
+       my $alternate = 1;
+       for (my $i = $from; $i <= $to; $i++) {
+               my %co = %{$commitlist->[$i]};
+               if (!%co) {
+                       next;
+               }
+               my $commit = $co{'id'};
+               if ($alternate) {
+                       print "<tr class=\"dark\">\n";
+               } else {
+                       print "<tr class=\"light\">\n";
+               }
+               $alternate ^= 1;
+               print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
+                     "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
+                     "<td>" .
+                     $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
+                              esc_html(chop_str($co{'title'}, 50)) . "<br/>");
+               my $comment = $co{'comment'};
+               foreach my $line (@$comment) {
+                       if ($line =~ m/^(.*)($search_regexp)(.*)$/i) {
+                               my $lead = esc_html($1) || "";
+                               $lead = chop_str($lead, 30, 10);
+                               my $match = esc_html($2) || "";
+                               my $trail = esc_html($3) || "";
+                               $trail = chop_str($trail, 30, 10);
+                               my $text = "$lead<span class=\"match\">$match</span>$trail";
+                               print chop_str($text, 80, 5) . "<br/>\n";
+                       }
+               }
+               print "</td>\n" .
+                     "<td class=\"link\">" .
+                     $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
+                     " | " .
+                     $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
+               print "</td>\n" .
+                     "</tr>\n";
+       }
+       if (defined $extra) {
+               print "<tr>\n" .
+                     "<td colspan=\"3\">$extra</td>\n" .
+                     "</tr>\n";
+       }
+       print "</table>\n";
+}
+
 ## ======================================================================
 ## ======================================================================
 ## actions
 
 sub git_project_list {
        my $order = $cgi->param('o');
-       if (defined $order && $order !~ m/project|descr|owner|age/) {
+       if (defined $order && $order !~ m/none|project|descr|owner|age/) {
                die_error(undef, "Unknown order parameter");
        }
 
@@ -2744,7 +3564,7 @@ sub git_project_list {
 
 sub git_forks {
        my $order = $cgi->param('o');
-       if (defined $order && $order !~ m/project|descr|owner|age/) {
+       if (defined $order && $order !~ m/none|project|descr|owner|age/) {
                die_error(undef, "Unknown order parameter");
        }
 
@@ -2770,7 +3590,7 @@ sub git_project_index {
 
        foreach my $pr (@projects) {
                if (!exists $pr->{'owner'}) {
-                       $pr->{'owner'} = get_file_owner("$projectroot/$project");
+                       $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}");
                }
 
                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
@@ -2786,15 +3606,17 @@ sub git_project_index {
 
 sub git_summary {
        my $descr = git_get_project_description($project) || "none";
-       my $head = git_get_head_hash($project);
-       my %co = parse_commit($head);
-       my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
+       my %co = parse_commit("HEAD");
+       my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
+       my $head = $co{'id'};
 
        my $owner = git_get_project_owner($project);
 
        my $refs = git_get_references();
-       my @taglist  = git_get_tags_list(15);
-       my @headlist = git_get_heads_list(15);
+       # These get_*_list functions return one more to allow us to see if
+       # there are more ...
+       my @taglist  = git_get_tags_list(16);
+       my @headlist = git_get_heads_list(16);
        my @forklist;
        my ($check_forks) = gitweb_check_feature('forks');
 
@@ -2808,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";
@@ -2830,32 +3655,36 @@ sub git_summary {
                }
        }
 
-       open my $fd, "-|", git_cmd(), "rev-list", "--max-count=17",
-               git_get_head_hash($project), "--"
-               or die_error(undef, "Open git-rev-list failed");
-       my @revlist = map { chomp; $_ } <$fd>;
-       close $fd;
-       git_print_header_div('shortlog');
-       git_shortlog_body(\@revlist, 0, 15, $refs,
-                         $cgi->a({-href => href(action=>"shortlog")}, "..."));
+       # we need to request one more than 16 (0..15) to check if
+       # those 16 are all
+       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');
                git_tags_body(\@taglist, 0, 15,
+                             $#taglist <=  15 ? undef :
                              $cgi->a({-href => href(action=>"tags")}, "..."));
        }
 
        if (@headlist) {
                git_print_header_div('heads');
                git_heads_body(\@headlist, $head, 0, 15,
+                              $#headlist <= 15 ? undef :
                               $cgi->a({-href => href(action=>"heads")}, "..."));
        }
 
        if (@forklist) {
                git_print_header_div('forks');
                git_project_list_body(\@forklist, undef, 0, 15,
+                                     $#forklist <= 15 ? undef :
                                      $cgi->a({-href => href(action=>"forks")}, "..."),
-                                     'noheader');
+                                     'noheader');
        }
 
        git_footer_html();
@@ -2866,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" .
@@ -2888,8 +3722,8 @@ sub git_tag {
        print "<div class=\"page_body\">";
        my $comment = $tag{'comment'};
        foreach my $line (@$comment) {
-               chomp($line);
-               print esc_html($line) . "<br/>\n";
+               chomp $line;
+               print esc_html($line, -nbsp=>1) . "<br/>\n";
        }
        print "</div>\n";
        git_footer_html();
@@ -2914,7 +3748,7 @@ sub git_blame2 {
        }
        $ftype = git_get_type($hash);
        if ($ftype !~ "blob") {
-               die_error("400 Bad Request", "Object is not a blob");
+               die_error('400 Bad Request', "Object is not a blob");
        }
        open ($fd, "-|", git_cmd(), "blame", '-p', '--',
              $file_name, $hash_base)
@@ -2958,11 +3792,11 @@ sub git_blame2 {
                        }
                }
                my $data = $_;
-               chomp($data);
+               chomp $data;
                my $rev = substr($full_rev, 0, 8);
                my $author = $meta->{'author'};
                my %date = parse_date($meta->{'author-time'},
-                                     $meta->{'author-tz'});
+                                     $meta->{'author-tz'});
                my $date = $date{'iso-tz'};
                if ($group_size) {
                        $current_color = ++$current_color % $num_colors;
@@ -2974,19 +3808,24 @@ sub git_blame2 {
                        print " rowspan=\"$group_size\"" if ($group_size > 1);
                        print ">";
                        print $cgi->a({-href => href(action=>"commit",
-                                                    hash=>$full_rev,
-                                                    file_name=>$file_name)},
-                                     esc_html($rev));
+                                                    hash=>$full_rev,
+                                                    file_name=>$file_name)},
+                                     esc_html($rev));
                        print "</td>\n";
                }
+               open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
+                       or die_error(undef, "Open git-rev-parse failed");
+               my $parent_commit = <$dd>;
+               close $dd;
+               chomp($parent_commit);
                my $blamed = href(action => 'blame',
-                                 file_name => $meta->{'filename'},
-                                 hash_base => $full_rev);
+                                 file_name => $meta->{'filename'},
+                                 hash_base => $parent_commit);
                print "<td class=\"linenr\">";
                print $cgi->a({ -href => "$blamed#l$orig_lineno",
-                               -id => "l$lineno",
-                               -class => "linenr" },
-                             esc_html($lineno));
+                               -id => "l$lineno",
+                               -class => "linenr" },
+                             esc_html($lineno));
                print "</td>";
                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
                print "</tr>\n";
@@ -3183,10 +4022,13 @@ sub git_blob {
        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
                or die_error(undef, "Couldn't cat $file_name, $hash");
        my $mimetype = blob_mimetype($fd, $file_name);
-       if ($mimetype !~ m/^text\//) {
+       if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)!) {
                close $fd;
                return git_blob_plain($mimetype);
        }
+       # we can have blame only for text/* mimetype
+       $have_blame &&= ($mimetype =~ m!^text/!);
+
        git_header_html(undef, $expires);
        my $formats_nav = '';
        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
@@ -3223,13 +4065,24 @@ sub git_blob {
        }
        git_print_page_path($file_name, "blob", $hash_base);
        print "<div class=\"page_body\">\n";
-       my $nr;
-       while (my $line = <$fd>) {
-               chomp $line;
-               $nr++;
-               $line = untabify($line);
-               printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
-                      $nr, $nr, $nr, esc_html($line, -nbsp=>1);
+       if ($mimetype =~ m!^text/!) {
+               my $nr;
+               while (my $line = <$fd>) {
+                       chomp $line;
+                       $nr++;
+                       $line = untabify($line);
+                       printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
+                              $nr, $nr, $nr, esc_html($line, -nbsp=>1);
+               }
+       } elsif ($mimetype =~ m!^image/!) {
+               print qq!<img type="$mimetype"!;
+               if ($file_name) {
+                       print qq! alt="$file_name" title="$file_name"!;
+               }
+               print qq! src="! .
+                     href(action=>"blob_plain", hash=>$hash,
+                          hash_base=>$hash_base, file_name=>$file_name) .
+                     qq!" />\n!;
        }
        close $fd
                or print "Reading blob failed.\n";
@@ -3351,20 +4204,28 @@ sub git_snapshot {
                $hash = git_get_head_hash($project);
        }
 
-       my $filename = basename($project) . "-$hash.tar.$suffix";
+       my $git = git_cmd_str();
+       my $name = $project;
+       $name =~ s,([^/])/*\.git$,$1,;
+       $name = basename($name);
+       my $filename = to_utf8($name);
+       $name =~ s/\047/\047\\\047\047/g;
+       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/x-tar',
-               -content_encoding => $ctype,
+               -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
@@ -3382,28 +4243,25 @@ sub git_log {
        }
        my $refs = git_get_references();
 
-       my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
-       open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash, "--"
-               or die_error(undef, "Open git-rev-list failed");
-       my @revlist = map { chomp; $_ } <$fd>;
-       close $fd;
+       my @commitlist = parse_commits($hash, 101, (100 * $page));
 
-       my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#revlist);
+       my $paging_nav = format_paging_nav('log', $hash, $head, $page, (100 * ($page+1)));
 
        git_header_html();
        git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
 
-       if (!@revlist) {
+       if (!@commitlist) {
                my %co = parse_commit($hash);
 
                git_print_header_div('summary', $project);
                print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
        }
-       for (my $i = ($page * 100); $i <= $#revlist; $i++) {
-               my $commit = $revlist[$i];
-               my $ref = format_ref_marker($refs, $commit);
-               my %co = parse_commit($commit);
+       my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
+       for (my $i = 0; $i <= $to; $i++) {
+               my %co = %{$commitlist[$i]};
                next if !%co;
+               my $commit = $co{'id'};
+               my $ref = format_ref_marker($refs, $commit);
                my %ad = parse_date($co{'author_epoch'});
                git_print_header_div('commit',
                               "<span class=\"age\">$co{'age_string'}</span>" .
@@ -3425,10 +4283,17 @@ sub git_log {
                git_print_log($co{'comment'}, -final_empty_line=> 1);
                print "</div>\n";
        }
+       if ($#commitlist >= 100) {
+               print "<div class=\"page_nav\">\n";
+               print $cgi->a({-href => href(action=>"log", hash=>$hash, page=>$page+1),
+                              -accesskey => "n", -title => "Alt-n"}, "next");
+               print "</div>\n";
+       }
        git_footer_html();
 }
 
 sub git_commit {
+       $hash ||= $hash_base || "HEAD";
        my %co = parse_commit($hash);
        if (!%co) {
                die_error(undef, "Unknown commit object");
@@ -3436,14 +4301,44 @@ sub git_commit {
        my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
        my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
 
-       my $parent = $co{'parent'};
+       my $parent  = $co{'parent'};
+       my $parents = $co{'parents'}; # listref
+
+       # we need to prepare $formats_nav before any parameter munging
+       my $formats_nav;
+       if (!defined $parent) {
+               # --root commitdiff
+               $formats_nav .= '(initial)';
+       } elsif (@$parents == 1) {
+               # single parent commit
+               $formats_nav .=
+                       '(parent: ' .
+                       $cgi->a({-href => href(action=>"commit",
+                                              hash=>$parent)},
+                               esc_html(substr($parent, 0, 7))) .
+                       ')';
+       } else {
+               # merge commit
+               $formats_nav .=
+                       '(merge: ' .
+                       join(' ', map {
+                               $cgi->a({-href => href(action=>"commit",
+                                                      hash=>$_)},
+                                       esc_html(substr($_, 0, 7)));
+                       } @$parents ) .
+                       ')';
+       }
+
        if (!defined $parent) {
                $parent = "--root";
        }
+       my @difftree;
        open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
-               @diff_opts, $parent, $hash, "--"
+               @diff_opts,
+               (@$parents <= 1 ? $parent : '-c'),
+               $hash, "--"
                or die_error(undef, "Open git-diff-tree failed");
-       my @difftree = map { chomp; $_ } <$fd>;
+       @difftree = map { chomp; $_ } <$fd>;
        close $fd or die_error(undef, "Reading git-diff-tree failed");
 
        # non-textual hash id's can be cached
@@ -3456,16 +4351,10 @@ sub git_commit {
 
        my $have_snapshot = gitweb_have_snapshot();
 
-       my @views_nav = ();
-       if (defined $file_name && defined $co{'parent'}) {
-               push @views_nav,
-                       $cgi->a({-href => href(action=>"blame", hash_parent=>$parent, file_name=>$file_name)},
-                               "blame");
-       }
        git_header_html(undef, $expires);
        git_print_page_nav('commit', '',
                           $hash, $co{'tree'}, $hash,
-                          join (' | ', @views_nav));
+                          $formats_nav);
 
        if (defined $co{'parent'}) {
                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
@@ -3506,7 +4395,7 @@ sub git_commit {
        }
        print "</td>" .
              "</tr>\n";
-       my $parents = $co{'parents'};
+
        foreach my $par (@$parents) {
                print "<tr>" .
                      "<td>parent</td>" .
@@ -3528,11 +4417,58 @@ sub git_commit {
        git_print_log($co{'comment'});
        print "</div>\n";
 
-       git_difftree_body(\@difftree, $hash, $parent);
+       git_difftree_body(\@difftree, $hash, @$parents);
 
        git_footer_html();
 }
 
+sub git_object {
+       # object is defined by:
+       # - hash or hash_base alone
+       # - hash_base and file_name
+       my $type;
+
+       # - hash or hash_base alone
+       if ($hash || ($hash_base && !defined $file_name)) {
+               my $object_id = $hash || $hash_base;
+
+               my $git_command = git_cmd_str();
+               open my $fd, "-|", "$git_command cat-file -t $object_id 2>/dev/null"
+                       or die_error('404 Not Found', "Object does not exist");
+               $type = <$fd>;
+               chomp $type;
+               close $fd
+                       or die_error('404 Not Found', "Object does not exist");
+
+       # - hash_base and file_name
+       } elsif ($hash_base && defined $file_name) {
+               $file_name =~ s,/+$,,;
+
+               system(git_cmd(), "cat-file", '-e', $hash_base) == 0
+                       or die_error('404 Not Found', "Base object does not exist");
+
+               # here errors should not hapen
+               open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
+                       or die_error(undef, "Open git-ls-tree failed");
+               my $line = <$fd>;
+               close $fd;
+
+               #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
+               unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
+                       die_error('404 Not Found', "File or directory for given base does not exist");
+               }
+               $type = $2;
+               $hash = $3;
+       } else {
+               die_error('404 Not Found', "Not enough information to find object");
+       }
+
+       print $cgi->redirect(-uri => href(action=>$type, -full=>1,
+                                         hash=>$hash, hash_base=>$hash_base,
+                                         file_name=>$file_name),
+                            -status => '302 Found');
+}
+
 sub git_blobdiff {
        my $format = shift || 'html';
 
@@ -3548,7 +4484,7 @@ sub git_blobdiff {
                        # read raw output
                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
                                $hash_parent_base, $hash_base,
-                               "--", $file_name
+                               "--", (defined $file_parent ? $file_parent : ()), $file_name
                                or die_error(undef, "Open git-diff-tree failed");
                        @difftree = map { chomp; $_ } <$fd>;
                        close $fd
@@ -3597,8 +4533,9 @@ sub git_blobdiff {
 
                # open patch output
                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
-                       '-p', $hash_parent_base, $hash_base,
-                       "--", $file_name
+                       '-p', ($format eq 'html' ? "--full-index" : ()),
+                       $hash_parent_base, $hash_base,
+                       "--", (defined $file_parent ? $file_parent : ()), $file_name
                        or die_error(undef, "Open git-diff-tree failed");
        }
 
@@ -3632,7 +4569,8 @@ sub git_blobdiff {
                }
 
                # open patch output
-               open $fd, "-|", git_cmd(), "diff", '-p', @diff_opts,
+               open $fd, "-|", git_cmd(), "diff", @diff_opts,
+                       '-p', ($format eq 'html' ? "--full-index" : ()),
                        $hash_parent, $hash, "--"
                        or die_error(undef, "Open git-diff failed");
        } else  {
@@ -3706,12 +4644,17 @@ sub git_blobdiff_plain {
 
 sub git_commitdiff {
        my $format = shift || 'html';
+       $hash ||= $hash_base || "HEAD";
        my %co = parse_commit($hash);
        if (!%co) {
                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 =
@@ -3719,14 +4662,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)) .
@@ -3744,6 +4695,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 {
@@ -3755,8 +4717,11 @@ sub git_commitdiff {
                }
        }
 
-       if (!defined $hash_parent) {
-               $hash_parent = $co{'parent'} || '--root';
+       my $hash_parent_param = $hash_parent;
+       if (!defined $hash_parent_param) {
+               # --cc for multiple parents, --root for parentless
+               $hash_parent_param =
+                       @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
        }
 
        # read commitdiff
@@ -3765,19 +4730,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 {
@@ -3833,10 +4798,14 @@ sub git_commitdiff {
 
        # write patch
        if ($format eq 'html') {
-               git_difftree_body(\@difftree, $hash, $hash_parent);
+               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);
+               git_patchset_body($fd, \@difftree, $hash,
+                                 $use_parents ? @{$co{'parents'}} : $hash_parent);
                close $fd;
                print "</div>\n"; # class="page_body"
                git_footer_html();
@@ -3876,12 +4845,7 @@ sub git_history {
                $ftype = git_get_type($hash);
        }
 
-       open my $fd, "-|",
-               git_cmd(), "rev-list", $limit, "--full-history", $hash_base, "--", $file_name
-                       or die_error(undef, "Open git-rev-list-failed");
-       my @revlist = map { chomp; $_ } <$fd>;
-       close $fd
-               or die_error(undef, "Reading git-rev-list failed");
+       my @commitlist = parse_commits($hash_base, 101, (100 * $page), "--full-history", $file_name);
 
        my $paging_nav = '';
        if ($page > 0) {
@@ -3897,7 +4861,7 @@ sub git_history {
                $paging_nav .= "first";
                $paging_nav .= " &sdot; prev";
        }
-       if ($#revlist >= (100 * ($page+1)-1)) {
+       if ($#commitlist >= 100) {
                $paging_nav .= " &sdot; " .
                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
                                               file_name=>$file_name, page=>$page+1),
@@ -3906,11 +4870,11 @@ sub git_history {
                $paging_nav .= " &sdot; next";
        }
        my $next_link = '';
-       if ($#revlist >= (100 * ($page+1)-1)) {
+       if ($#commitlist >= 100) {
                $next_link =
                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
                                               file_name=>$file_name, page=>$page+1),
-                                -title => "Alt-n"}, "next");
+                                -accesskey => "n", -title => "Alt-n"}, "next");
        }
 
        git_header_html();
@@ -3918,13 +4882,17 @@ sub git_history {
        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
        git_print_page_path($file_name, $ftype, $hash_base);
 
-       git_history_body(\@revlist, ($page * 100), $#revlist,
+       git_history_body(\@commitlist, 0, 99,
                         $refs, $hash_base, $ftype, $next_link);
 
        git_footer_html();
 }
 
 sub git_search {
+       my ($have_search) = gitweb_check_feature('search');
+       if (!$have_search) {
+               die_error('403 Permission denied', "Permission denied");
+       }
        if (!defined $searchtext) {
                die_error(undef, "Text field empty");
        }
@@ -3935,6 +4903,9 @@ sub git_search {
        if (!%co) {
                die_error(undef, "Unknown commit object");
        }
+       if (!defined $page) {
+               $page = 0;
+       }
 
        $searchtype ||= 'commit';
        if ($searchtype eq 'pickaxe') {
@@ -3945,72 +4916,77 @@ 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();
-       git_print_page_nav('','', $hash,$co{'tree'},$hash);
-       git_print_header_div('commit', esc_html($co{'title'}), $hash);
 
-       print "<table cellspacing=\"0\">\n";
-       my $alternate = 1;
        if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
-               $/ = "\0";
-               open my $fd, "-|", git_cmd(), "rev-list",
-                       "--header", "--parents", $hash, "--"
-                       or next;
-               while (my $commit_text = <$fd>) {
-                       if (!grep m/$searchtext/i, $commit_text) {
-                               next;
-                       }
-                       if ($searchtype eq 'author' && !grep m/\nauthor .*$searchtext/i, $commit_text) {
-                               next;
-                       }
-                       if ($searchtype eq 'committer' && !grep m/\ncommitter .*$searchtext/i, $commit_text) {
-                               next;
-                       }
-                       my @commit_lines = split "\n", $commit_text;
-                       my %co = parse_commit(undef, \@commit_lines);
-                       if (!%co) {
-                               next;
-                       }
-                       if ($alternate) {
-                               print "<tr class=\"dark\">\n";
-                       } else {
-                               print "<tr class=\"light\">\n";
-                       }
-                       $alternate ^= 1;
-                       print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
-                             "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
-                             "<td>" .
-                             $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
-                                      esc_html(chop_str($co{'title'}, 50)) . "<br/>");
-                       my $comment = $co{'comment'};
-                       foreach my $line (@$comment) {
-                               if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
-                                       my $lead = esc_html($1) || "";
-                                       $lead = chop_str($lead, 30, 10);
-                                       my $match = esc_html($2) || "";
-                                       my $trail = esc_html($3) || "";
-                                       $trail = chop_str($trail, 30, 10);
-                                       my $text = "$lead<span class=\"match\">$match</span>$trail";
-                                       print chop_str($text, 80, 5) . "<br/>\n";
-                               }
-                       }
-                       print "</td>\n" .
-                             "<td class=\"link\">" .
-                             $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
-                             " | " .
-                             $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
-                       print "</td>\n" .
-                             "</tr>\n";
+               my $greptype;
+               if ($searchtype eq 'commit') {
+                       $greptype = "--grep=";
+               } elsif ($searchtype eq 'author') {
+                       $greptype = "--author=";
+               } elsif ($searchtype eq 'committer') {
+                       $greptype = "--committer=";
                }
-               close $fd;
+               $greptype .= $search_regexp;
+               my @commitlist = parse_commits($hash, 101, (100 * $page), $greptype);
+
+               my $paging_nav = '';
+               if ($page > 0) {
+                       $paging_nav .=
+                               $cgi->a({-href => href(action=>"search", hash=>$hash,
+                                                      searchtext=>$searchtext, searchtype=>$searchtype)},
+                                       "first");
+                       $paging_nav .= " &sdot; " .
+                               $cgi->a({-href => href(action=>"search", hash=>$hash,
+                                                      searchtext=>$searchtext, searchtype=>$searchtype,
+                                                      page=>$page-1),
+                                        -accesskey => "p", -title => "Alt-p"}, "prev");
+               } else {
+                       $paging_nav .= "first";
+                       $paging_nav .= " &sdot; prev";
+               }
+               if ($#commitlist >= 100) {
+                       $paging_nav .= " &sdot; " .
+                               $cgi->a({-href => href(action=>"search", hash=>$hash,
+                                                      searchtext=>$searchtext, searchtype=>$searchtype,
+                                                      page=>$page+1),
+                                        -accesskey => "n", -title => "Alt-n"}, "next");
+               } else {
+                       $paging_nav .= " &sdot; next";
+               }
+               my $next_link = '';
+               if ($#commitlist >= 100) {
+                       $next_link =
+                               $cgi->a({-href => href(action=>"search", hash=>$hash,
+                                                      searchtext=>$searchtext, searchtype=>$searchtype,
+                                                      page=>$page+1),
+                                        -accesskey => "n", -title => "Alt-n"}, "next");
+               }
+
+               git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
+               git_print_header_div('commit', esc_html($co{'title'}), $hash);
+               git_search_grep_body(\@commitlist, 0, 99, $next_link);
        }
 
        if ($searchtype eq 'pickaxe') {
+               git_print_page_nav('','', $hash,$co{'tree'},$hash);
+               git_print_header_div('commit', esc_html($co{'title'}), $hash);
+
+               print "<table cellspacing=\"0\">\n";
+               my $alternate = 1;
                $/ = "\n";
                my $git_command = git_cmd_str();
+               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>) {
@@ -4061,8 +5037,76 @@ sub git_search {
                        }
                }
                close $fd;
+
+               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";
        }
-       print "</table>\n";
        git_footer_html();
 }
 
@@ -4073,6 +5117,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>
@@ -4101,95 +5159,254 @@ sub git_shortlog {
        }
        my $refs = git_get_references();
 
-       my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
-       open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash, "--"
-               or die_error(undef, "Open git-rev-list failed");
-       my @revlist = map { chomp; $_ } <$fd>;
-       close $fd;
+       my @commitlist = parse_commits($hash, 101, (100 * $page));
 
-       my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#revlist);
+       my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, (100 * ($page+1)));
        my $next_link = '';
-       if ($#revlist >= (100 * ($page+1)-1)) {
+       if ($#commitlist >= 100) {
                $next_link =
                        $cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1),
-                                -title => "Alt-n"}, "next");
+                                -accesskey => "n", -title => "Alt-n"}, "next");
        }
 
-
        git_header_html();
        git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
        git_print_header_div('summary', $project);
 
-       git_shortlog_body(\@revlist, ($page * 100), $#revlist, $refs, $next_link);
+       git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
 
        git_footer_html();
 }
 
 ## ......................................................................
-## feeds (RSS, OPML)
+## feeds (RSS, Atom; OPML)
 
-sub git_rss {
-       # http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
-       open my $fd, "-|", git_cmd(), "rev-list", "--max-count=150",
-               git_get_head_hash($project), "--"
-               or die_error(undef, "Open git-rev-list failed");
-       my @revlist = map { chomp; $_ } <$fd>;
-       close $fd or die_error(undef, "Reading git-rev-list failed");
-       print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
-       print <<XML;
-<?xml version="1.0" encoding="utf-8"?>
+sub git_feed {
+       my $format = shift || 'atom';
+       my ($have_blame) = gitweb_check_feature('blame');
+
+       # Atom: http://www.atomenabled.org/developers/syndication/
+       # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
+       if ($format ne 'rss' && $format ne 'atom') {
+               die_error(undef, "Unknown web feed format");
+       }
+
+       # log/feed of current (HEAD) branch, log of given branch, history of file/directory
+       my $head = $hash || 'HEAD';
+       my @commitlist = parse_commits($head, 150);
+
+       my %latest_commit;
+       my %latest_date;
+       my $content_type = "application/$format+xml";
+       if (defined $cgi->http('HTTP_ACCEPT') &&
+                $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
+               # browser (feed reader) prefers text/xml
+               $content_type = 'text/xml';
+       }
+       if (defined($commitlist[0])) {
+               %latest_commit = %{$commitlist[0]};
+               %latest_date   = parse_date($latest_commit{'author_epoch'});
+               print $cgi->header(
+                       -type => $content_type,
+                       -charset => 'utf-8',
+                       -last_modified => $latest_date{'rfc2822'});
+       } else {
+               print $cgi->header(
+                       -type => $content_type,
+                       -charset => 'utf-8');
+       }
+
+       # Optimization: skip generating the body if client asks only
+       # for Last-Modified date.
+       return if ($cgi->request_method() eq 'HEAD');
+
+       # header variables
+       my $title = "$site_name - $project/$action";
+       my $feed_type = 'log';
+       if (defined $hash) {
+               $title .= " - '$hash'";
+               $feed_type = 'branch log';
+               if (defined $file_name) {
+                       $title .= " :: $file_name";
+                       $feed_type = 'history';
+               }
+       } elsif (defined $file_name) {
+               $title .= " - $file_name";
+               $feed_type = 'history';
+       }
+       $title .= " $feed_type";
+       my $descr = git_get_project_description($project);
+       if (defined $descr) {
+               $descr = esc_html($descr);
+       } else {
+               $descr = "$project " .
+                        ($format eq 'rss' ? 'RSS' : 'Atom') .
+                        " feed";
+       }
+       my $owner = git_get_project_owner($project);
+       $owner = esc_html($owner);
+
+       #header
+       my $alt_url;
+       if (defined $file_name) {
+               $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
+       } elsif (defined $hash) {
+               $alt_url = href(-full=>1, action=>"log", hash=>$hash);
+       } else {
+               $alt_url = href(-full=>1, action=>"summary");
+       }
+       print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
+       if ($format eq 'rss') {
+               print <<XML;
 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
 <channel>
-<title>$project $my_uri $my_url</title>
-<link>${\esc_html("$my_url?p=$project;a=summary")}</link>
-<description>$project log</description>
-<language>en</language>
 XML
+               print "<title>$title</title>\n" .
+                     "<link>$alt_url</link>\n" .
+                     "<description>$descr</description>\n" .
+                     "<language>en</language>\n";
+       } elsif ($format eq 'atom') {
+               print <<XML;
+<feed xmlns="http://www.w3.org/2005/Atom">
+XML
+               print "<title>$title</title>\n" .
+                     "<subtitle>$descr</subtitle>\n" .
+                     '<link rel="alternate" type="text/html" href="' .
+                     $alt_url . '" />' . "\n" .
+                     '<link rel="self" type="' . $content_type . '" href="' .
+                     $cgi->self_url() . '" />' . "\n" .
+                     "<id>" . href(-full=>1) . "</id>\n" .
+                     # use project owner for feed author
+                     "<author><name>$owner</name></author>\n";
+               if (defined $favicon) {
+                       print "<icon>" . esc_url($favicon) . "</icon>\n";
+               }
+               if (defined $logo_url) {
+                       # not twice as wide as tall: 72 x 27 pixels
+                       print "<logo>" . esc_url($logo) . "</logo>\n";
+               }
+               if (! %latest_date) {
+                       # dummy date to keep the feed valid until commits trickle in:
+                       print "<updated>1970-01-01T00:00:00Z</updated>\n";
+               } else {
+                       print "<updated>$latest_date{'iso-8601'}</updated>\n";
+               }
+       }
 
-       for (my $i = 0; $i <= $#revlist; $i++) {
-               my $commit = $revlist[$i];
-               my %co = parse_commit($commit);
+       # contents
+       for (my $i = 0; $i <= $#commitlist; $i++) {
+               my %co = %{$commitlist[$i]};
+               my $commit = $co{'id'};
                # we read 150, we always show 30 and the ones more recent than 48 hours
-               if (($i >= 20) && ((time - $co{'committer_epoch'}) > 48*60*60)) {
+               if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
                        last;
                }
-               my %cd = parse_date($co{'committer_epoch'});
-               open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
-                       $co{'parent'}, $co{'id'}, "--"
+               my %cd = parse_date($co{'author_epoch'});
+
+               # get list of changed files
+               open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+                       $co{'parent'} || "--root",
+                       $co{'id'}, "--", (defined $file_name ? $file_name : ())
                        or next;
                my @difftree = map { chomp; $_ } <$fd>;
                close $fd
                        or next;
-               print "<item>\n" .
-                     "<title>" .
-                     sprintf("%d %s %02d:%02d", $cd{'mday'}, $cd{'month'}, $cd{'hour'}, $cd{'minute'}) . " - " . esc_html($co{'title'}) .
-                     "</title>\n" .
-                     "<author>" . esc_html($co{'author'}) . "</author>\n" .
-                     "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
-                     "<guid isPermaLink=\"true\">" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</guid>\n" .
-                     "<link>" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</link>\n" .
-                     "<description>" . esc_html($co{'title'}) . "</description>\n" .
-                     "<content:encoded>" .
-                     "<![CDATA[\n";
+
+               # print element (entry, item)
+               my $co_url = href(-full=>1, action=>"commit", hash=>$commit);
+               if ($format eq 'rss') {
+                       print "<item>\n" .
+                             "<title>" . esc_html($co{'title'}) . "</title>\n" .
+                             "<author>" . esc_html($co{'author'}) . "</author>\n" .
+                             "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
+                             "<guid isPermaLink=\"true\">$co_url</guid>\n" .
+                             "<link>$co_url</link>\n" .
+                             "<description>" . esc_html($co{'title'}) . "</description>\n" .
+                             "<content:encoded>" .
+                             "<![CDATA[\n";
+               } elsif ($format eq 'atom') {
+                       print "<entry>\n" .
+                             "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
+                             "<updated>$cd{'iso-8601'}</updated>\n" .
+                             "<author>\n" .
+                             "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
+                       if ($co{'author_email'}) {
+                               print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
+                       }
+                       print "</author>\n" .
+                             # use committer for contributor
+                             "<contributor>\n" .
+                             "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
+                       if ($co{'committer_email'}) {
+                               print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
+                       }
+                       print "</contributor>\n" .
+                             "<published>$cd{'iso-8601'}</published>\n" .
+                             "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
+                             "<id>$co_url</id>\n" .
+                             "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
+                             "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
+               }
                my $comment = $co{'comment'};
+               print "<pre>\n";
                foreach my $line (@$comment) {
-                       $line = to_utf8($line);
-                       print "$line<br/>\n";
+                       $line = esc_html($line);
+                       print "$line\n";
                }
-               print "<br/>\n";
-               foreach my $line (@difftree) {
-                       if (!($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/)) {
-                               next;
+               print "</pre><ul>\n";
+               foreach my $difftree_line (@difftree) {
+                       my %difftree = parse_difftree_raw_line($difftree_line);
+                       next if !$difftree{'from_id'};
+
+                       my $file = $difftree{'file'} || $difftree{'to_file'};
+
+                       print "<li>" .
+                             "[" .
+                             $cgi->a({-href => href(-full=>1, action=>"blobdiff",
+                                                    hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
+                                                    hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
+                                                    file_name=>$file, file_parent=>$difftree{'from_file'}),
+                                     -title => "diff"}, 'D');
+                       if ($have_blame) {
+                               print $cgi->a({-href => href(-full=>1, action=>"blame",
+                                                            file_name=>$file, hash_base=>$commit),
+                                             -title => "blame"}, 'B');
                        }
-                       my $file = esc_path(unquote($7));
-                       $file = to_utf8($file);
-                       print "$file<br/>\n";
+                       # if this is not a feed of a file history
+                       if (!defined $file_name || $file_name ne $file) {
+                               print $cgi->a({-href => href(-full=>1, action=>"history",
+                                                            file_name=>$file, hash=>$commit),
+                                             -title => "history"}, 'H');
+                       }
+                       $file = esc_path($file);
+                       print "] ".
+                             "$file</li>\n";
+               }
+               if ($format eq 'rss') {
+                       print "</ul>]]>\n" .
+                             "</content:encoded>\n" .
+                             "</item>\n";
+               } elsif ($format eq 'atom') {
+                       print "</ul>\n</div>\n" .
+                             "</content>\n" .
+                             "</entry>\n";
                }
-               print "]]>\n" .
-                     "</content:encoded>\n" .
-                     "</item>\n";
        }
-       print "</channel></rss>";
+
+       # end of feed
+       if ($format eq 'rss') {
+               print "</channel>\n</rss>\n";
+       }       elsif ($format eq 'atom') {
+               print "</feed>\n";
+       }
+}
+
+sub git_rss {
+       git_feed('rss');
+}
+
+sub git_atom {
+       git_feed('atom');
 }
 
 sub git_opml {