Code

gitweb: optimize git_shortlog_body.
[git.git] / gitweb / gitweb.perl
index 3c6fd7ca47551eb808a40ca2cc7ed1690f19cc06..d2ddac8bf3bae0c2ead4583a3368db2d6bd9fe81 100755 (executable)
@@ -120,7 +120,7 @@ our %feature = (
        # 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;
+       # $feature{'snapshot'}{'override'} = 1;
        # and in project config gitweb.snapshot = none|gzip|bzip2;
        'snapshot' => {
                'sub' => \&feature_snapshot,
@@ -425,6 +425,7 @@ my %actions = (
        "history" => \&git_history,
        "log" => \&git_log,
        "rss" => \&git_rss,
+       "atom" => \&git_atom,
        "search" => \&git_search,
        "search_help" => \&git_search_help,
        "shortlog" => \&git_shortlog,
@@ -433,6 +434,7 @@ my %actions = (
        "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,
@@ -459,7 +461,8 @@ exit;
 
 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.
@@ -576,20 +579,87 @@ sub esc_html ($;%) {
 
        $str = to_utf8($str);
        $str = escapeHTML($str);
-       $str =~ s/\014/^L/g; # escape FORM FEED (FF) character (e.g. in COPYING file)
-       $str =~ s/\033/^[/g; # "escape" ESCAPE (\e) character (e.g. commit 20a3847d8a5032ce41f90dcc68abfb36e6fee9b1)
        if ($opts{'-nbsp'}) {
                $str =~ s/ / /g;
        }
+       $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
        return $str;
 }
 
+# quote control characters and escape filename to HTML
+sub esc_path {
+       my $str = shift;
+       my %opts = @_;
+
+       $str = to_utf8($str);
+       $str = escapeHTML($str);
+       if ($opts{'-nbsp'}) {
+               $str =~ s/ / /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
+                  "\t" => '\t',   # tab            (HT)
+                  "\n" => '\n',   # line feed      (LF)
+                  "\r" => '\r',   # carrige return (CR)
+                  "\f" => '\f',   # form feed      (FF)
+                  "\b" => '\b',   # backspace      (BS)
+                  "\a" => '\a',   # alarm (bell)   (BEL)
+                  "\e" => '\e',   # escape         (ESC)
+                  "\013" => '\v', # vertical tab   (VT)
+                  "\000" => '\0', # nul character  (NUL)
+                  );
+       my $chr = ( (exists $es{$cntrl})
+                   ? $es{$cntrl}
+                   : sprintf('\%03o', ord($cntrl)) );
+       return "<span class=\"cntrl\">$chr</span>";
+}
+
+# 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>";
+}
+
 # git may return quoted and escaped filenames
 sub unquote {
        my $str = shift;
+
+       sub unq {
+               my $seq = shift;
+               my %es = ( # character escape codes, aka escape sequences
+                       't' => "\t",   # tab            (HT, TAB)
+                       'n' => "\n",   # newline        (NL)
+                       'r' => "\r",   # return         (CR)
+                       'f' => "\f",   # form feed      (FF)
+                       'b' => "\b",   # backspace      (BS)
+                       'a' => "\a",   # alarm (bell)   (BEL)
+                       'e' => "\e",   # escape         (ESC)
+                       'v' => "\013", # vertical tab   (VT)
+               );
+
+               if ($seq =~ m/^[0-7]{1,3}$/) {
+                       # octal char sequence
+                       return chr(oct($seq));
+               } elsif (exists $es{$seq}) {
+                       # C escape sequence, aka character escape code
+                       return $es{$seq}
+               }
+               # quoted ordinary character
+               return $seq;
+       }
+
        if ($str =~ m/^"(.*)"$/) {
+               # needs unquoting
                $str = $1;
-               $str =~ s/\\([0-7]{1,3})/chr(oct($1))/eg;
+               $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
        }
        return $str;
 }
@@ -723,24 +793,47 @@ sub file_type {
        }
 }
 
+# convert file mode in octal to file type description string
+sub file_type_long {
+       my $mode = shift;
+
+       if ($mode !~ m/^[0-7]+$/) {
+               return $mode;
+       } else {
+               $mode = oct $mode;
+       }
+
+       if (S_ISDIR($mode & S_IFMT)) {
+               return "directory";
+       } elsif (S_ISLNK($mode)) {
+               return "symlink";
+       } elsif (S_ISREG($mode)) {
+               if ($mode & S_IXUSR) {
+                       return "executable";
+               } else {
+                       return "file";
+               };
+       } else {
+               return "unknown";
+       }
+}
+
+
 ## ----------------------------------------------------------------------
 ## functions returning short HTML fragments, or transforming HTML fragments
 ## which don't beling to other sections
 
-# format line of commit message or tag comment
+# format line of commit message.
 sub format_log_line_html {
        my $line = shift;
 
-       $line = esc_html($line);
-       $line =~ s/ /&nbsp;/g;
-       if ($line =~ m/([0-9a-fA-F]{40})/) {
+       $line = esc_html($line, -nbsp=>1);
+       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;
 }
@@ -762,7 +855,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>";
                }
        }
 
@@ -788,8 +882,10 @@ sub format_subject_html {
        }
 }
 
+# format patch (diff) line (rather not to be used for diff headers)
 sub format_diff_line {
        my $line = shift;
+       my ($from, $to) = @_;
        my $char = substr($line, 0, 1);
        my $diff_class = "";
 
@@ -805,6 +901,25 @@ sub format_diff_line {
                $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";
+       }
        return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
 }
 
@@ -926,7 +1041,7 @@ sub git_get_projects_list {
                $dir =~ s!/+$!!;
                my $pfxlen = length("$dir");
 
-               my $check_forks = gitweb_check_feature('forks');
+               my ($check_forks) = gitweb_check_feature('forks');
 
                File::Find::find({
                        follow_fast => 1, # follow symbolic links
@@ -962,6 +1077,17 @@ sub git_get_projects_list {
                        if (!defined $path) {
                                next;
                        }
+                       if ($filter ne '') {
+                               # looking for forks;
+                               my $pfx = substr($path, 0, length($filter));
+                               if ($pfx ne $filter) {
+                                       next;
+                               }
+                               my $sfx = substr($path, length($filter));
+                               if ($sfx !~ /^\/.*\.git$/) {
+                                       next;
+                               }
+                       }
                        if (check_export_ok("$projectroot/$path")) {
                                my $pr = {
                                        path => $path,
@@ -1013,8 +1139,9 @@ sub git_get_last_activity {
 
        $git_dir = "$projectroot/$path";
        open($fd, "-|", git_cmd(), 'for-each-ref',
-            '--format=%(refname) %(committer)',
+            '--format=%(committer)',
             '--sort=-committerdate',
+            '--count=1',
             'refs/heads') or return;
        my $most_recent = <$fd>;
        close $fd or return;
@@ -1028,14 +1155,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 {
@@ -1079,10 +1207,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);
@@ -1090,9 +1220,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;
 }
 
@@ -1165,8 +1295,9 @@ sub parse_commit {
                        $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'};
                        }
@@ -1175,7 +1306,12 @@ 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'}) {
@@ -1509,7 +1645,7 @@ sub git_header_html {
                if (defined $action) {
                        $title .= "/$action";
                        if (defined $file_name) {
-                               $title .= " - " . esc_html($file_name);
+                               $title .= " - " . esc_path($file_name);
                                if ($action eq "tree" && $file_name !~ m|/$|) {
                                        $title .= "/";
                                }
@@ -1553,14 +1689,17 @@ EOF
                }
        }
        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"));
        }
@@ -1626,7 +1765,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") . " ";
@@ -1780,20 +1921,20 @@ sub git_print_page_path {
                        $fullname .= ($fullname ? '/' : '') . $dir;
                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
                                                     hash_base=>$hb),
-                                     -title => $fullname}, esc_html($dir));
+                                     -title => esc_html($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 => $name}, esc_html($basename));
+                                     -title => esc_html($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 => $name}, esc_html($basename));
+                                     -title => esc_html($name)}, esc_path($basename));
                        print " / ";
                } else {
-                       print esc_html($basename);
+                       print esc_path($basename);
                }
        }
        print "<br/></div>\n";
@@ -1849,12 +1990,73 @@ sub git_print_log ($;%) {
        }
 }
 
+# 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,
@@ -1865,16 +2067,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_html($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 " | " .
@@ -1892,12 +2109,12 @@ sub git_print_tree_entry {
                print "<td class=\"list\">";
                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
                                             file_name=>"$basedir$t->{'name'}", %base_key)},
-                             esc_html($t->{'name'}));
+                             esc_path($t->{'name'}));
                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,
@@ -1913,7 +2130,7 @@ sub git_print_tree_entry {
 
 sub git_difftree_body {
        my ($difftree, $hash, $parent) = @_;
-
+       my ($have_blame) = gitweb_check_feature('blame');
        print "<div class=\"list_head\">\n";
        if ($#{$difftree} > 10) {
                print(($#{$difftree} + 1) . " files changed:\n");
@@ -1957,7 +2174,7 @@ sub git_difftree_body {
                        print "<td>";
                        print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
                                                     hash_base=>$hash, file_name=>$diff{'file'}),
-                                     -class => "list"}, esc_html($diff{'file'}));
+                                     -class => "list"}, esc_path($diff{'file'}));
                        print "</td>\n";
                        print "<td>$mode_chng</td>\n";
                        print "<td class=\"link\">";
@@ -1965,7 +2182,11 @@ 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
@@ -1973,7 +2194,7 @@ sub git_difftree_body {
                        print "<td>";
                        print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
                                                     hash_base=>$parent, file_name=>$diff{'file'}),
-                                      -class => "list"}, esc_html($diff{'file'}));
+                                      -class => "list"}, esc_path($diff{'file'}));
                        print "</td>\n";
                        print "<td>$mode_chng</td>\n";
                        print "<td class=\"link\">";
@@ -1985,10 +2206,12 @@ sub git_difftree_body {
                        }
                        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=>"blame", hash_base=>$parent,
-                                                    file_name=>$diff{'file'})},
-                                     "blame") . " | ";
+                                     "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=>"history", hash_base=>$parent,
                                                     file_name=>$diff{'file'})},
                                      "history");
@@ -2013,7 +2236,7 @@ sub git_difftree_body {
                        print "<td>";
                        print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
                                                     hash_base=>$hash, file_name=>$diff{'file'}),
-                                     -class => "list"}, esc_html($diff{'file'}));
+                                     -class => "list"}, esc_path($diff{'file'}));
                        print "</td>\n";
                        print "<td>$mode_chnge</td>\n";
                        print "<td class=\"link\">";
@@ -2032,11 +2255,13 @@ sub git_difftree_body {
                                      " | ";
                        }
                        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=>"blame", hash_base=>$hash,
-                                                    file_name=>$diff{'file'})},
-                                     "blame") . " | ";
+                                                    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=>"history", hash_base=>$hash,
                                                     file_name=>$diff{'file'})},
                                      "history");
@@ -2053,11 +2278,11 @@ sub git_difftree_body {
                        print "<td>" .
                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
                                                     hash=>$diff{'to_id'}, file_name=>$diff{'to_file'}),
-                                     -class => "list"}, esc_html($diff{'to_file'})) . "</td>\n" .
+                                     -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_html($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') {
@@ -2074,14 +2299,16 @@ sub git_difftree_body {
                                              "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=>"blame", hash_base=>$parent,
-                                                    file_name=>$diff{'from_file'})},
-                                     "blame") . " | ";
-                       print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
-                                                   file_name=>$diff{'from_file'})},
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
+                                                    hash_base=>$parent, file_name=>$diff{'to_file'})},
+                                     "blob") . " | ";
+                       if ($have_blame) {
+                               print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
+                                                            file_name=>$diff{'to_file'})},
+                                             "blame") . " | ";
+                       }
+                       print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
+                                                   file_name=>$diff{'to_file'})},
                                      "history");
                        print "</td>\n";
 
@@ -2095,114 +2322,182 @@ sub git_patchset_body {
        my ($fd, $difftree, $hash, $hash_parent) = @_;
 
        my $patch_idx = 0;
-       my $in_header = 0;
-       my $patch_found = 0;
+       my $patch_line;
        my $diffinfo;
+       my (%from, %to);
+       my ($from_id, $to_id);
 
        print "<div class=\"patchset\">\n";
 
-       LINE:
-       while (my $patch_line = <$fd>) {
+       # skip to first patch
+       while ($patch_line = <$fd>) {
                chomp $patch_line;
 
-               if ($patch_line =~ m/^diff /) { # "git diff" header
-                       # beginning of patch (in patchset)
-                       if ($patch_found) {
-                               # close previous patch
-                               print "</div>\n"; # class="patch"
-                       } else {
-                               # first patch in patchset
-                               $patch_found = 1;
+               last if ($patch_line =~ m/^diff /);
+       }
+
+ PATCH:
+       while ($patch_line) {
+               my @diff_header;
+
+               # git diff header
+               #assert($patch_line =~ m/^diff /) if DEBUG;
+               #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
+               push @diff_header, $patch_line;
+
+               # extended diff header
+       EXTENDED_HEADER:
+               while ($patch_line = <$fd>) {
+                       chomp $patch_line;
+
+                       last EXTENDED_HEADER if ($patch_line =~ m/^--- /);
+
+                       if ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) {
+                               $from_id = $1;
+                               $to_id   = $2;
                        }
-                       print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
 
+                       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
+                       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]);
                        }
-                       $patch_idx++;
-
-                       if ($diffinfo->{'status'} eq "A") { # added
-                               print "<div class=\"diff_info\">" . file_type($diffinfo->{'to_mode'}) . ":" .
-                                     $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
-                                                            hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
-                                             $diffinfo->{'to_id'}) . " (new)" .
-                                     "</div>\n"; # class="diff_info"
-
-                       } elsif ($diffinfo->{'status'} eq "D") { # deleted
-                               print "<div class=\"diff_info\">" . file_type($diffinfo->{'from_mode'}) . ":" .
-                                     $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
-                                                            hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
-                                             $diffinfo->{'from_id'}) . " (deleted)" .
-                                     "</div>\n"; # class="diff_info"
-
-                       } elsif ($diffinfo->{'status'} eq "R" || # renamed
-                                $diffinfo->{'status'} eq "C" || # copied
-                                $diffinfo->{'status'} eq "2") { # with two filenames (from git_blobdiff)
-                               print "<div class=\"diff_info\">" .
-                                     file_type($diffinfo->{'from_mode'}) . ":" .
-                                     $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
-                                                            hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'from_file'})},
-                                             $diffinfo->{'from_id'}) .
-                                     " -> " .
-                                     file_type($diffinfo->{'to_mode'}) . ":" .
-                                     $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
-                                                            hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'to_file'})},
-                                             $diffinfo->{'to_id'});
-                               print "</div>\n"; # class="diff_info"
-
-                       } else { # modified, mode changed, ...
-                               print "<div class=\"diff_info\">" .
-                                     file_type($diffinfo->{'from_mode'}) . ":" .
-                                     $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
-                                                            hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
-                                             $diffinfo->{'from_id'}) .
-                                     " -> " .
-                                     file_type($diffinfo->{'to_mode'}) . ":" .
-                                     $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
-                                                            hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
-                                             $diffinfo->{'to_id'});
-                               print "</div>\n"; # class="diff_info"
+                       $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'});
                        }
+                       if ($diffinfo->{'status'} ne "D") { # not deleted file
+                               $to{'href'} = href(action=>"blob", hash_base=>$hash,
+                                                  hash=>$diffinfo->{'to_id'},
+                                                  file_name=>$to{'file'});
+                       }
+                       # this is first patch for raw difftree line with $patch_idx index
+                       # we index @$difftree array from 0, but number patches from 1
+                       print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
+               }
 
-                       #print "<div class=\"diff extended_header\">\n";
-                       $in_header = 1;
-                       next LINE;
-               } # start of patch in patchset
-
+               # 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 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 "</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'}));
+               }
+               print "<div class=\"diff from_file\">$patch_line</div>\n";
 
-               if ($in_header && $patch_line =~ m/^---/) {
-                       #print "</div>\n"; # class="diff extended_header"
-                       $in_header = 0;
+               $patch_line = <$fd>;
+               #last PATCH unless $patch_line;
+               chomp $patch_line;
 
-                       my $file = $diffinfo->{'from_file'};
-                       $file  ||= $diffinfo->{'file'};
-                       $file = $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
-                                                      hash=>$diffinfo->{'from_id'}, file_name=>$file),
-                                       -class => "list"}, esc_html($file));
-                       $patch_line =~ s|a/.*$|a/$file|g;
-                       print "<div class=\"diff from_file\">$patch_line</div>\n";
+               #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";
 
-                       $patch_line = <$fd>;
+               # the patch itself
+       LINE:
+               while ($patch_line = <$fd>) {
                        chomp $patch_line;
 
-                       #$patch_line =~ m/^+++/;
-                       $file    = $diffinfo->{'to_file'};
-                       $file  ||= $diffinfo->{'file'};
-                       $file = $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
-                                                      hash=>$diffinfo->{'to_id'}, file_name=>$file),
-                                       -class => "list"}, esc_html($file));
-                       $patch_line =~ s|b/.*|b/$file|g;
-                       print "<div class=\"diff to_file\">$patch_line</div>\n";
+                       next PATCH if ($patch_line =~ m/^diff /);
 
-                       next LINE;
+                       print format_diff_line($patch_line, \%from, \%to);
                }
-               next LINE if $in_header;
 
-               print format_diff_line($patch_line);
+       } continue {
+               print "</div>\n"; # class="patch"
        }
-       print "</div>\n" if $patch_found; # class="patch"
 
        print "</div>\n"; # class="patchset"
 }
@@ -2212,7 +2507,7 @@ sub git_patchset_body {
 sub git_project_list_body {
        my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
 
-       my $check_forks = gitweb_check_feature('forks');
+       my ($check_forks) = gitweb_check_feature('forks');
 
        my @projects;
        foreach my $pr (@$projlist) {
@@ -2223,6 +2518,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'}) {
@@ -2230,8 +2526,14 @@ sub git_project_list_body {
                }
                if ($check_forks) {
                        my $pname = $pr->{'path'};
-                       $pname =~ s/\.git$//;
-                       $pr->{'forks'} = -d "$projectroot/$pname";
+                       if (($pname =~ s/\.git$//) &&
+                           ($pname !~ /\/$/) &&
+                           (-d "$projectroot/$pname")) {
+                               $pr->{'forks'} = "-d $projectroot/$pname";
+                       }
+                       else {
+                               $pr->{'forks'} = 0;
+                       }
                }
                push @projects, $pr;
        }
@@ -2252,7 +2554,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") {
@@ -2261,7 +2563,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") {
@@ -2270,7 +2572,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") {
@@ -2279,7 +2581,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" .
@@ -2297,19 +2599,22 @@ sub git_project_list_body {
                if ($check_forks) {
                        print "<td>";
                        if ($pr->{'forks'}) {
+                               print "<!-- $pr->{'forks'} -->\n";
                                print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
                        }
                        print "</td>\n";
                }
                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" .
                      "<td class=\"link\">" .
                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
-                     $cgi->a({-href => '/git-browser/by-commit.html?r='.$pr->{'path'}}, "graphiclog") . " | " .
+                     $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
                      ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
@@ -2331,6 +2636,8 @@ sub git_shortlog_body {
        # uses global variable $project
        my ($revlist, $from, $to, $refs, $extra) = @_;
 
+       my $have_snapshot = gitweb_have_snapshot();
+
        $from = 0 unless defined $from;
        $to = $#{$revlist} if (!defined $to || $#{$revlist} < $to);
 
@@ -2358,7 +2665,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" .
@@ -2611,10 +2918,14 @@ sub git_summary {
        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;
-       if (gitweb_check_feature('forks')) {
+       my ($check_forks) = gitweb_check_feature('forks');
+
+       if ($check_forks) {
                @forklist = git_get_projects_list($project);
        }
 
@@ -2646,6 +2957,8 @@ sub git_summary {
                }
        }
 
+       # we need to request one more than 16 (0..15) to check if
+       # those 16 are all
        open my $fd, "-|", git_cmd(), "rev-list", "--max-count=17",
                git_get_head_hash($project), "--"
                or die_error(undef, "Open git-rev-list failed");
@@ -2653,23 +2966,27 @@ sub git_summary {
        close $fd;
        git_print_header_div('shortlog');
        git_shortlog_body(\@revlist, 0, 15, $refs,
+                         $#revlist <=  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');
        }
@@ -2704,7 +3021,8 @@ sub git_tag {
        print "<div class=\"page_body\">";
        my $comment = $tag{'comment'};
        foreach my $line (@$comment) {
-               print esc_html($line) . "<br/>\n";
+               chomp $line;
+               print esc_html($line, -nbsp=>1) . "<br/>\n";
        }
        print "</div>\n";
        git_footer_html();
@@ -2773,6 +3091,7 @@ HTML
                        }
                }
                my $data = $_;
+               chomp $data;
                my $rev = substr($full_rev, 0, 8);
                my $author = $meta->{'author'};
                my %date = parse_date($meta->{'author-time'},
@@ -2997,10 +3316,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))) {
@@ -3037,13 +3359,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";
@@ -3168,8 +3501,7 @@ sub git_snapshot {
        my $filename = basename($project) . "-$hash.tar.$suffix";
 
        print $cgi->header(
-               -type => 'application/x-tar',
-               -content_encoding => $ctype,
+               -type => "application/$ctype",
                -content_disposition => 'inline; filename="' . "$filename" . '"',
                -status => '200 OK');
 
@@ -3243,6 +3575,7 @@ sub git_log {
 }
 
 sub git_commit {
+       $hash ||= $hash_base || "HEAD";
        my %co = parse_commit($hash);
        if (!%co) {
                die_error(undef, "Unknown commit object");
@@ -3250,15 +3583,46 @@ 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=>"commitdiff",
+                                                      hash=>$_)},
+                                       esc_html(substr($_, 0, 7)));
+                       } @$parents ) .
+                       ')';
+       }
+
        if (!defined $parent) {
                $parent = "--root";
        }
-       open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
-               @diff_opts, $parent, $hash, "--"
-               or die_error(undef, "Open git-diff-tree failed");
-       my @difftree = map { chomp; $_ } <$fd>;
-       close $fd or die_error(undef, "Reading git-diff-tree failed");
+       my @difftree;
+       if (@$parents <= 1) {
+               # difftree output is not printed for merges
+               open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
+                       @diff_opts, $parent, $hash, "--"
+                               or die_error(undef, "Open git-diff-tree failed");
+               @difftree = map { chomp; $_ } <$fd>;
+               close $fd or die_error(undef, "Reading git-diff-tree failed");
+       }
 
        # non-textual hash id's can be cached
        my $expires;
@@ -3270,16 +3634,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);
@@ -3320,7 +3678,7 @@ sub git_commit {
        }
        print "</td>" .
              "</tr>\n";
-       my $parents = $co{'parents'};
+
        foreach my $par (@$parents) {
                print "<tr>" .
                      "<td>parent</td>" .
@@ -3342,11 +3700,61 @@ sub git_commit {
        git_print_log($co{'comment'});
        print "</div>\n";
 
-       git_difftree_body(\@difftree, $hash, $parent);
+       if (@$parents <= 1) {
+               # do not output difftree/whatchanged for merges
+               git_difftree_body(\@difftree, $hash, $parent);
+       }
 
        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';
 
@@ -3501,8 +3909,8 @@ sub git_blobdiff {
 
        } else {
                while (my $line = <$fd>) {
-                       $line =~ s!a/($hash|$hash_parent)!'a/'.esc_html($diffinfo{'from_file'})!eg;
-                       $line =~ s!b/($hash|$hash_parent)!'b/'.esc_html($diffinfo{'to_file'})!eg;
+                       $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
+                       $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
 
                        print $line;
 
@@ -3520,6 +3928,7 @@ 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");
@@ -3582,7 +3991,8 @@ sub git_commitdiff {
                        $hash_parent, $hash, "--"
                        or die_error(undef, "Open git-diff-tree failed");
 
-               while (chomp(my $line = <$fd>)) {
+               while (my $line = <$fd>) {
+                       chomp $line;
                        # empty line ends raw part of diff-tree output
                        last unless $line;
                        push @difftree, $line;
@@ -3859,7 +4269,7 @@ sub git_search {
                                                print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
                                                                             hash=>$set{'id'}, file_name=>$set{'file'}),
                                                              -class => "list"},
-                                                             "<span class=\"match\">" . esc_html($set{'file'}) . "</span>") .
+                                                             "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
                                                      "<br/>\n";
                                        }
                                        print "</td>\n" .
@@ -3939,70 +4349,237 @@ sub git_shortlog {
 }
 
 ## ......................................................................
-## feeds (RSS, OPML)
+## feeds (RSS, Atom; OPML)
 
-sub git_rss {
-       # http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
+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';
        open my $fd, "-|", git_cmd(), "rev-list", "--max-count=150",
-               git_get_head_hash($project), "--"
+               $head, "--", (defined $file_name ? $file_name : ())
                or die_error(undef, "Open git-rev-list failed");
        my @revlist = map { chomp; $_ } <$fd>;
        close $fd or die_error(undef, "Reading git-rev-list failed");
-       print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
-       print <<XML;
-<?xml version="1.0" encoding="utf-8"?>
+
+       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($revlist[0])) {
+               %latest_commit = parse_commit($revlist[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";
+               }
+       }
 
+       # contents
        for (my $i = 0; $i <= $#revlist; $i++) {
                my $commit = $revlist[$i];
                my %co = parse_commit($commit);
                # 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'});
+               my %cd = parse_date($co{'author_epoch'});
+
+               # get list of changed files
                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
-                       $co{'parent'}, $co{'id'}, "--"
+                       $co{'parent'}, $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');
+                       }
+                       # 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');
                        }
-                       my $file = esc_html(unquote($7));
-                       $file = to_utf8($file);
-                       print "$file<br/>\n";
+                       $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 {