git-gc --auto: run "repack -A -d -l" as necessary.
[gitweb.git] / gitweb / gitweb.perl
index c3921cb0baf162b8294fe47b2fff749114f3061f..b2bae1b250a5316c85ffb221b3b088c9e8cda8c7 100755 (executable)
@@ -71,6 +71,9 @@ BEGIN
 # source of projects list
 our $projects_list = "++GITWEB_LIST++";
 
+# the width (in characters) of the projects list "Description" column
+our $projects_list_description_width = 25;
+
 # default order of projects list
 # valid values are none, project, descr, owner, and age
 our $default_projects_order = "project";
@@ -94,6 +97,66 @@ BEGIN
 # (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';
+
+# rename detection options for git-diff and git-diff-tree
+# - default is '-M', with the cost proportional to
+#   (number of removed files) * (number of new files).
+# - more costly is '-C' (which implies '-M'), with the cost proportional to
+#   (number of changed files + number of removed files) * (number of new files)
+# - even more costly is '-C', '--find-copies-harder' with cost
+#   (number of files in the original tree) * (number of new files)
+# - one might want to include '-B' option, e.g. '-B', '-M'
+our @diff_opts = ('-M'); # taken from git_commit
+
+# information about snapshot formats that gitweb is capable of serving
+our %known_snapshot_formats = (
+       # name => {
+       #       'display' => display name,
+       #       'type' => mime type,
+       #       'suffix' => filename suffix,
+       #       'format' => --format for git-archive,
+       #       'compressor' => [compressor command and arguments]
+       #                       (array reference, optional)}
+       #
+       'tgz' => {
+               'display' => 'tar.gz',
+               'type' => 'application/x-gzip',
+               'suffix' => '.tar.gz',
+               'format' => 'tar',
+               'compressor' => ['gzip']},
+
+       'tbz2' => {
+               'display' => 'tar.bz2',
+               'type' => 'application/x-bzip2',
+               'suffix' => '.tar.bz2',
+               'format' => 'tar',
+               'compressor' => ['bzip2']},
+
+       'zip' => {
+               'display' => 'zip',
+               'type' => 'application/x-zip',
+               'suffix' => '.zip',
+               'format' => 'zip'},
+);
+
+# Aliases so we understand old gitweb.snapshot values in repository
+# configuration.
+our %known_snapshot_format_aliases = (
+       'gzip'  => 'tgz',
+       'bzip2' => 'tbz2',
+
+       # backward compatibility: legacy gitweb config support
+       'x-gzip' => undef, 'gz' => undef,
+       'x-bzip2' => undef, 'bz2' => undef,
+       'x-zip' => undef, '' => undef,
+);
+
 # You define site-wide feature defaults here; override them with
 # $GITWEB_CONFIG as necessary.
 our %feature = (
@@ -124,20 +187,22 @@ BEGIN
                'override' => 0,
                'default' => [0]},
 
-       # Enable the 'snapshot' link, providing a compressed tarball of any
+       # Enable the 'snapshot' link, providing a compressed archive of any
        # tree. This can potentially generate high traffic if you have large
        # project.
 
+       # Value is a list of formats defined in %known_snapshot_formats that
+       # you wish to offer.
        # To disable system wide have in $GITWEB_CONFIG
-       # $feature{'snapshot'}{'default'} = [undef];
+       # $feature{'snapshot'}{'default'} = [];
        # To have project specific config enable override in $GITWEB_CONFIG
        # $feature{'snapshot'}{'override'} = 1;
-       # and in project config gitweb.snapshot = none|gzip|bzip2|zip;
+       # and in project config, a comma-separated list of formats or "none"
+       # to disable.  Example: gitweb.snapshot = tbz2,zip;
        'snapshot' => {
                'sub' => \&feature_snapshot,
                'override' => 0,
-               #         => [content-encoding, suffix, program]
-               'default' => ['x-gzip', 'gz', 'gzip']},
+               'default' => ['tgz']},
 
        # Enable text search, which will list the commits which match author,
        # committer or commit text to a given string.  Enabled by default.
@@ -236,28 +301,15 @@ sub feature_blame {
 }
 
 sub feature_snapshot {
-       my ($ctype, $suffix, $command) = @_;
+       my (@fmts) = @_;
 
        my ($val) = git_get_project_config('snapshot');
 
-       if ($val eq 'gzip') {
-               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 ();
+       if ($val) {
+               @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
        }
 
-       return ($ctype, $suffix, $command);
-}
-
-sub gitweb_have_snapshot {
-       my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
-       my $have_snapshot = (defined $ctype && defined $suffix);
-
-       return $have_snapshot;
+       return @fmts;
 }
 
 sub feature_grep {
@@ -300,15 +352,17 @@ sub check_export_ok {
                (!$export_ok || -e "$dir/$export_ok"));
 }
 
-# rename detection options for git-diff and git-diff-tree
-# - default is '-M', with the cost proportional to
-#   (number of removed files) * (number of new files).
-# - more costly is '-C' (or '-C', '-M'), with the cost proportional to
-#   (number of changed files + number of removed files) * (number of new files)
-# - even more costly is '-C', '--find-copies-harder' with cost
-#   (number of files in the original tree) * (number of new files)
-# - one might want to include '-B' option, e.g. '-B', '-M'
-our @diff_opts = ('-M'); # taken from git_commit
+# process alternate names for backward compatibility
+# filter out unsupported (unknown) snapshot formats
+sub filter_snapshot_fmts {
+       my @fmts = @_;
+
+       @fmts = map {
+               exists $known_snapshot_format_aliases{$_} ?
+                      $known_snapshot_format_aliases{$_} : $_} @fmts;
+       @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
+
+}
 
 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
 do $GITWEB_CONFIG if -e $GITWEB_CONFIG;
@@ -376,6 +430,22 @@ sub check_export_ok {
        }
 }
 
+my %allowed_options = (
+       "--no-merges" => [ qw(rss atom log shortlog history) ],
+);
+
+our @extra_options = $cgi->param('opt');
+if (defined @extra_options) {
+       foreach my $opt (@extra_options) {
+               if (not exists $allowed_options{$opt}) {
+                       die_error(undef, "Invalid option parameter");
+               }
+               if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
+                       die_error(undef, "Invalid option parameter for this action");
+               }
+       }
+}
+
 our $hash_parent_base = $cgi->param('hpb');
 if (defined $hash_parent_base) {
        if (!validate_refname($hash_parent_base)) {
@@ -401,9 +471,6 @@ 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");
        }
@@ -535,6 +602,8 @@ (%)
                order => "o",
                searchtext => "s",
                searchtype => "st",
+               snapshot_format => "sf",
+               extra_options => "opt",
        );
        my %mapping = @mapping;
 
@@ -557,7 +626,13 @@ (%)
        for (my $i = 0; $i < @mapping; $i += 2) {
                my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
                if (defined $params{$name}) {
-                       push @result, $symbol . "=" . esc_param($params{$name});
+                       if (ref($params{$name}) eq "ARRAY") {
+                               foreach my $par (@{$params{$name}}) {
+                                       push @result, $symbol . "=" . esc_param($par);
+                               }
+                       } else {
+                               push @result, $symbol . "=" . esc_param($params{$name});
+                       }
                }
        }
        $href .= "?" . join(';', @result) if scalar @result;
@@ -602,6 +677,20 @@ sub validate_refname {
        return $input;
 }
 
+# decode sequences of octets in utf8 into Perl's internal form,
+# which is utf-8 with utf8 flag set if needed.  gitweb writes out
+# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
+sub to_utf8 {
+       my $str = shift;
+       my $res;
+       eval { $res = decode_utf8($str, Encode::FB_CROAK); };
+       if (defined $res) {
+               return $res;
+       } else {
+               return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
+       }
+}
+
 # quote unsafe chars, but keep the slash, even when it's not
 # correct, but quoted slashes look too horrible in bookmarks
 sub esc_param {
@@ -626,7 +715,7 @@ ($;%)
        my $str = shift;
        my %opts = @_;
 
-       $str = decode_utf8($str);
+       $str = to_utf8($str);
        $str = $cgi->escapeHTML($str);
        if ($opts{'-nbsp'}) {
                $str =~ s/ /&nbsp;/g;
@@ -640,7 +729,7 @@ sub esc_path {
        my $str = shift;
        my %opts = @_;
 
-       $str = decode_utf8($str);
+       $str = to_utf8($str);
        $str = $cgi->escapeHTML($str);
        if ($opts{'-nbsp'}) {
                $str =~ s/ /&nbsp;/g;
@@ -803,11 +892,25 @@ sub age_string {
        return $age_str;
 }
 
+use constant {
+       S_IFINVALID => 0030000,
+       S_IFGITLINK => 0160000,
+};
+
+# submodule/subproject, a commit object reference
+sub S_ISGITLINK($) {
+       my $mode = shift;
+
+       return (($mode & S_IFMT) == S_IFGITLINK)
+}
+
 # convert file mode in octal to symbolic file mode string
 sub mode_str {
        my $mode = oct shift;
 
-       if (S_ISDIR($mode & S_IFMT)) {
+       if (S_ISGITLINK($mode)) {
+               return 'm---------';
+       } elsif (S_ISDIR($mode & S_IFMT)) {
                return 'drwxr-xr-x';
        } elsif (S_ISLNK($mode)) {
                return 'lrwxrwxrwx';
@@ -833,7 +936,9 @@ sub file_type {
                $mode = oct $mode;
        }
 
-       if (S_ISDIR($mode & S_IFMT)) {
+       if (S_ISGITLINK($mode)) {
+               return "submodule";
+       } elsif (S_ISDIR($mode & S_IFMT)) {
                return "directory";
        } elsif (S_ISLNK($mode)) {
                return "symlink";
@@ -854,7 +959,9 @@ sub file_type_long {
                $mode = oct $mode;
        }
 
-       if (S_ISDIR($mode & S_IFMT)) {
+       if (S_ISGITLINK($mode)) {
+               return "submodule";
+       } elsif (S_ISDIR($mode & S_IFMT)) {
                return "directory";
        } elsif (S_ISLNK($mode)) {
                return "symlink";
@@ -925,7 +1032,7 @@ sub format_subject_html {
 
        if (length($short) < length($long)) {
                return $cgi->a({-href => $href, -class => "list subject",
-                               -title => decode_utf8($long)},
+                               -title => to_utf8($long)},
                       esc_html($short) . $extra);
        } else {
                return $cgi->a({-href => $href, -class => "list subject"},
@@ -933,7 +1040,200 @@ sub format_subject_html {
        }
 }
 
-# format patch (diff) line (rather not to be used for diff headers)
+# format git diff header line, i.e. "diff --(git|combined|cc) ..."
+sub format_git_diff_header_line {
+       my $line = shift;
+       my $diffinfo = shift;
+       my ($from, $to) = @_;
+
+       if ($diffinfo->{'nparents'}) {
+               # combined diff
+               $line =~ s!^(diff (.*?) )"?.*$!$1!;
+               if ($to->{'href'}) {
+                       $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
+                                        esc_path($to->{'file'}));
+               } else { # file was deleted (no href)
+                       $line .= esc_path($to->{'file'});
+               }
+       } else {
+               # "ordinary" diff
+               $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
+               if ($from->{'href'}) {
+                       $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
+                                        'a/' . esc_path($from->{'file'}));
+               } else { # file was added (no href)
+                       $line .= 'a/' . esc_path($from->{'file'});
+               }
+               $line .= ' ';
+               if ($to->{'href'}) {
+                       $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
+                                        'b/' . esc_path($to->{'file'}));
+               } else { # file was deleted
+                       $line .= 'b/' . esc_path($to->{'file'});
+               }
+       }
+
+       return "<div class=\"diff header\">$line</div>\n";
+}
+
+# format extended diff header line, before patch itself
+sub format_extended_diff_header_line {
+       my $line = shift;
+       my $diffinfo = shift;
+       my ($from, $to) = @_;
+
+       # match <path>
+       if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
+               $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
+                                      esc_path($from->{'file'}));
+       }
+       if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
+               $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
+                                esc_path($to->{'file'}));
+       }
+       # match single <mode>
+       if ($line =~ m/\s(\d{6})$/) {
+               $line .= '<span class="info"> (' .
+                        file_type_long($1) .
+                        ')</span>';
+       }
+       # match <hash>
+       if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
+               # can match only for combined diff
+               $line = 'index ';
+               for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+                       if ($from->{'href'}[$i]) {
+                               $line .= $cgi->a({-href=>$from->{'href'}[$i],
+                                                 -class=>"hash"},
+                                                substr($diffinfo->{'from_id'}[$i],0,7));
+                       } else {
+                               $line .= '0' x 7;
+                       }
+                       # separator
+                       $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
+               }
+               $line .= '..';
+               if ($to->{'href'}) {
+                       $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
+                                        substr($diffinfo->{'to_id'},0,7));
+               } else {
+                       $line .= '0' x 7;
+               }
+
+       } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
+               # can match only for ordinary diff
+               my ($from_link, $to_link);
+               if ($from->{'href'}) {
+                       $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
+                                            substr($diffinfo->{'from_id'},0,7));
+               } else {
+                       $from_link = '0' x 7;
+               }
+               if ($to->{'href'}) {
+                       $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
+                                          substr($diffinfo->{'to_id'},0,7));
+               } else {
+                       $to_link = '0' x 7;
+               }
+               my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
+               $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
+       }
+
+       return $line . "<br/>\n";
+}
+
+# format from-file/to-file diff header
+sub format_diff_from_to_header {
+       my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
+       my $line;
+       my $result = '';
+
+       $line = $from_line;
+       #assert($line =~ m/^---/) if DEBUG;
+       # no extra formatting for "^--- /dev/null"
+       if (! $diffinfo->{'nparents'}) {
+               # ordinary (single parent) diff
+               if ($line =~ m!^--- "?a/!) {
+                       if ($from->{'href'}) {
+                               $line = '--- a/' .
+                                       $cgi->a({-href=>$from->{'href'}, -class=>"path"},
+                                               esc_path($from->{'file'}));
+                       } else {
+                               $line = '--- a/' .
+                                       esc_path($from->{'file'});
+                       }
+               }
+               $result .= qq!<div class="diff from_file">$line</div>\n!;
+
+       } else {
+               # combined diff (merge commit)
+               for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+                       if ($from->{'href'}[$i]) {
+                               $line = '--- ' .
+                                       $cgi->a({-href=>href(action=>"blobdiff",
+                                                            hash_parent=>$diffinfo->{'from_id'}[$i],
+                                                            hash_parent_base=>$parents[$i],
+                                                            file_parent=>$from->{'file'}[$i],
+                                                            hash=>$diffinfo->{'to_id'},
+                                                            hash_base=>$hash,
+                                                            file_name=>$to->{'file'}),
+                                                -class=>"path",
+                                                -title=>"diff" . ($i+1)},
+                                               $i+1) .
+                                       '/' .
+                                       $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
+                                               esc_path($from->{'file'}[$i]));
+                       } else {
+                               $line = '--- /dev/null';
+                       }
+                       $result .= qq!<div class="diff from_file">$line</div>\n!;
+               }
+       }
+
+       $line = $to_line;
+       #assert($line =~ m/^\+\+\+/) if DEBUG;
+       # no extra formatting for "^+++ /dev/null"
+       if ($line =~ m!^\+\+\+ "?b/!) {
+               if ($to->{'href'}) {
+                       $line = '+++ b/' .
+                               $cgi->a({-href=>$to->{'href'}, -class=>"path"},
+                                       esc_path($to->{'file'}));
+               } else {
+                       $line = '+++ b/' .
+                               esc_path($to->{'file'});
+               }
+       }
+       $result .= qq!<div class="diff to_file">$line</div>\n!;
+
+       return $result;
+}
+
+# create note for patch simplified by combined diff
+sub format_diff_cc_simplified {
+       my ($diffinfo, @parents) = @_;
+       my $result = '';
+
+       $result .= "<div class=\"diff header\">" .
+                  "diff --cc ";
+       if (!is_deleted($diffinfo)) {
+               $result .= $cgi->a({-href => href(action=>"blob",
+                                                 hash_base=>$hash,
+                                                 hash=>$diffinfo->{'to_id'},
+                                                 file_name=>$diffinfo->{'to_file'}),
+                                   -class => "path"},
+                                  esc_path($diffinfo->{'to_file'}));
+       } else {
+               $result .= esc_path($diffinfo->{'to_file'});
+       }
+       $result .= "</div>\n" . # class="diff header"
+                  "<div class=\"diff nodifferences\">" .
+                  "Simple merge" .
+                  "</div>\n"; # class="diff nodifferences"
+
+       return $result;
+}
+
+# format patch (diff) line (not to be used for diff headers)
 sub format_diff_line {
        my $line = shift;
        my ($from, $to) = @_;
@@ -1022,6 +1322,43 @@ sub format_diff_line {
        return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
 }
 
+# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
+# linked.  Pass the hash of the tree/commit to snapshot.
+sub format_snapshot_links {
+       my ($hash) = @_;
+       my @snapshot_fmts = gitweb_check_feature('snapshot');
+       @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
+       my $num_fmts = @snapshot_fmts;
+       if ($num_fmts > 1) {
+               # A parenthesized list of links bearing format names.
+               # e.g. "snapshot (_tar.gz_ _zip_)"
+               return "snapshot (" . join(' ', map
+                       $cgi->a({
+                               -href => href(
+                                       action=>"snapshot",
+                                       hash=>$hash,
+                                       snapshot_format=>$_
+                               )
+                       }, $known_snapshot_formats{$_}{'display'})
+               , @snapshot_fmts) . ")";
+       } elsif ($num_fmts == 1) {
+               # A single "snapshot" link whose tooltip bears the format name.
+               # i.e. "_snapshot_"
+               my ($fmt) = @snapshot_fmts;
+               return
+                       $cgi->a({
+                               -href => href(
+                                       action=>"snapshot",
+                                       hash=>$hash,
+                                       snapshot_format=>$fmt
+                               ),
+                               -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
+                       }, "snapshot");
+       } else { # $num_fmts == 0
+               return undef;
+       }
+}
+
 ## ----------------------------------------------------------------------
 ## git utility subroutines, invoking git commands
 
@@ -1175,6 +1512,7 @@ sub git_get_projects_list {
 
                File::Find::find({
                        follow_fast => 1, # follow symbolic links
+                       follow_skip => 2, # ignore duplicates
                        dangling_symlinks => 0, # ignore dangling symlinks, silently
                        wanted => sub {
                                # skip project-list toplevel, if we get it.
@@ -1239,7 +1577,7 @@ sub git_get_projects_list {
                        if (check_export_ok("$projectroot/$path")) {
                                my $pr = {
                                        path => $path,
-                                       owner => decode_utf8($owner),
+                                       owner => to_utf8($owner),
                                };
                                push @list, $pr;
                                (my $forks_path = $path) =~ s/\.git$//;
@@ -1251,12 +1589,12 @@ sub git_get_projects_list {
        return @list;
 }
 
-sub git_get_project_owner {
-       my $project = shift;
-       my $owner;
+our $gitweb_project_owner = undef;
+sub git_get_project_list_from_file {
 
-       return undef unless $project;
+       return if (defined $gitweb_project_owner);
 
+       $gitweb_project_owner = {};
        # read from file (url-encoded):
        # 'git%2Fgit.git Linus+Torvalds'
        # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
@@ -1268,13 +1606,25 @@ sub git_get_project_owner {
                        my ($pr, $ow) = split ' ', $line;
                        $pr = unescape($pr);
                        $ow = unescape($ow);
-                       if ($pr eq $project) {
-                               $owner = decode_utf8($ow);
-                               last;
-                       }
+                       $gitweb_project_owner->{$pr} = to_utf8($ow);
                }
                close $fd;
        }
+}
+
+sub git_get_project_owner {
+       my $project = shift;
+       my $owner;
+
+       return undef unless $project;
+
+       if (!defined $gitweb_project_owner) {
+               git_get_project_list_from_file();
+       }
+
+       if (exists $gitweb_project_owner->{$project}) {
+               $owner = $gitweb_project_owner->{$project};
+       }
        if (!defined $owner) {
                $owner = get_file_owner("$projectroot/$project");
        }
@@ -1300,6 +1650,7 @@ sub git_get_last_activity {
                my $age = time - $timestamp;
                return ($age, age_string($age));
        }
+       return (undef, undef);
 }
 
 sub git_get_references {
@@ -1543,6 +1894,7 @@ sub parse_commits {
                ($arg ? ($arg) : ()),
                ("--max-count=" . $maxcount),
                ("--skip=" . $skip),
+               @extra_options,
                $commit_id,
                "--",
                ($filename ? ($filename) : ())
@@ -1659,6 +2011,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
 
@@ -1759,7 +2153,7 @@ sub get_file_owner {
        }
        my $owner = $gcos;
        $owner =~ s/[,;].*$//;
-       return decode_utf8($owner);
+       return to_utf8($owner);
 }
 
 ## ......................................................................
@@ -1842,7 +2236,7 @@ sub git_header_html {
 
        my $title = "$site_name";
        if (defined $project) {
-               $title .= " - " . decode_utf8($project);
+               $title .= " - " . to_utf8($project);
                if (defined $action) {
                        $title .= "/$action";
                        if (defined $file_name) {
@@ -1894,9 +2288,17 @@ sub git_header_html {
                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 RSS feed (no merges)" '.
+                      'href="%s" type="application/rss+xml" />'."\n",
+                      esc_param($project), href(action=>"rss",
+                                                extra_options=>"--no-merges"));
                printf('<link rel="alternate" title="%s log Atom feed" '.
                       'href="%s" type="application/atom+xml" />'."\n",
                       esc_param($project), href(action=>"atom"));
+               printf('<link rel="alternate" title="%s log Atom feed (no merges)" '.
+                      'href="%s" type="application/atom+xml" />'."\n",
+                      esc_param($project), href(action=>"atom",
+                                                extra_options=>"--no-merges"));
        } else {
                printf('<link rel="alternate" title="%s projects list" '.
                       'href="%s" type="text/plain; charset=utf-8"/>'."\n",
@@ -1945,12 +2347,18 @@ sub git_header_html {
                } else {
                        $search_hash = "HEAD";
                }
+               my $action = $my_uri;
+               my ($use_pathinfo) = gitweb_check_feature('pathinfo');
+               if ($use_pathinfo) {
+                       $action .= "/$project";
+               } else {
+                       $cgi->param("p", $project);
+               }
                $cgi->param("a", "search");
                $cgi->param("h", $search_hash);
-               $cgi->param("p", $project);
-               print $cgi->startform(-method => "get", -action => $my_uri) .
+               print $cgi->startform(-method => "get", -action => $action) .
                      "<div class=\"search\">\n" .
-                     $cgi->hidden(-name => "p") . "\n" .
+                     (!$use_pathinfo && $cgi->hidden(-name => "p") . "\n") .
                      $cgi->hidden(-name => "a") . "\n" .
                      $cgi->hidden(-name => "h") . "\n" .
                      $cgi->popup_menu(-name => 'st', -default => 'commit',
@@ -2116,7 +2524,7 @@ sub git_print_page_path {
 
        print "<div class=\"page_path\">";
        print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
-                     -title => 'tree root'}, decode_utf8("[$project]"));
+                     -title => 'tree root'}, to_utf8("[$project]"));
        print " / ";
        if (defined $name) {
                my @dirname = split '/', $name;
@@ -2328,6 +2736,20 @@ sub git_print_tree_entry {
                                      "history");
                }
                print "</td>\n";
+       } else {
+               # unknown object: we can only present history for it
+               # (this includes 'commit' object, i.e. submodule support)
+               print "<td class=\"list\">" .
+                     esc_path($t->{'name'}) .
+                     "</td>\n";
+               print "<td class=\"link\">";
+               if (defined $hash_base) {
+                       print $cgi->a({-href => href(action=>"history",
+                                                    hash_base=>$hash_base,
+                                                    file_name=>"$basedir$t->{'name'}")},
+                                     "history");
+               }
+               print "</td>\n";
        }
 }
 
@@ -2366,6 +2788,11 @@ sub from_ids_eq {
        }
 }
 
+sub is_deleted {
+       my $diffinfo = shift;
+
+       return $diffinfo->{'to_id'} eq ('0' x 40);
+}
 
 sub git_difftree_body {
        my ($difftree, $hash, @parents) = @_;
@@ -2380,6 +2807,26 @@ sub git_difftree_body {
        print "<table class=\"" .
              (@parents > 1 ? "combined " : "") .
              "diff_tree\">\n";
+
+       # header only for combined diff in 'commitdiff' view
+       my $has_header = @parents > 1 && $action eq 'commitdiff';
+       if ($has_header) {
+               # table header
+               print "<thead><tr>\n" .
+                      "<th></th><th></th>\n"; # filename, patchN link
+               for (my $i = 0; $i < @parents; $i++) {
+                       my $par = $parents[$i];
+                       print "<th>" .
+                             $cgi->a({-href => href(action=>"commitdiff",
+                                                    hash=>$hash, hash_parent=>$par),
+                                      -title => 'commitdiff to parent number ' .
+                                                 ($i+1) . ': ' . substr($par,0,7)},
+                                     $i+1) .
+                             "&nbsp;</th>\n";
+               }
+               print "</tr></thead>\n<tbody>\n";
+       }
+
        my $alternate = 1;
        my $patchno = 0;
        foreach my $line (@{$difftree}) {
@@ -2403,7 +2850,7 @@ sub git_difftree_body {
                        fill_from_file_info($diff, @parents)
                                unless exists $diff->{'from_file'};
 
-                       if ($diff->{'to_id'} ne ('0' x 40)) {
+                       if (!is_deleted($diff)) {
                                # file exists in the result (child) commit
                                print "<td>" .
                                      $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
@@ -2652,6 +3099,7 @@ sub git_difftree_body {
                } # we should not encounter Unmerged (U) or Unknown (X) status
                print "</tr>\n";
        }
+       print "</tbody>" if $has_header;
        print "</table>\n";
 }
 
@@ -2716,13 +3164,35 @@ sub git_patchset_body {
                        # advance raw git-diff output if needed
                        $patch_idx++ if defined $diffinfo;
 
-                       # read and prepare patch information
-                       if (ref($difftree->[$patch_idx]) eq "HASH") {
-                               # pre-parsed (or generated by hand)
-                               $diffinfo = $difftree->[$patch_idx];
-                       } else {
-                               $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
+                       # compact combined diff output can have some patches skipped
+                       # find which patch (using pathname of result) we are at now
+                       my $to_name;
+                       if ($diff_header[0] =~ m!^diff --cc "?(.*)"?$!) {
+                               $to_name = $1;
                        }
+
+                       do {
+                               # read and prepare patch information
+                               if (ref($difftree->[$patch_idx]) eq "HASH") {
+                                       # pre-parsed (or generated by hand)
+                                       $diffinfo = $difftree->[$patch_idx];
+                               } else {
+                                       $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
+                               }
+
+                               # check if current raw line has no patch (it got simplified)
+                               if (defined $to_name && $to_name ne $diffinfo->{'to_file'}) {
+                                       print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
+                                             format_diff_cc_simplified($diffinfo, @hash_parents) .
+                                             "</div>\n";  # class="patch"
+
+                                       $patch_idx++;
+                                       $patch_number++;
+                               }
+                       } until (!defined $to_name || $to_name eq $diffinfo->{'to_file'} ||
+                                $patch_idx > $#$difftree);
+                       # modifies %from, %to hashes
+                       parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
                        if ($diffinfo->{'nparents'}) {
                                # combined diff
                                $from{'file'} = [];
@@ -2752,7 +3222,7 @@ sub git_patchset_body {
                        }
 
                        $to{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'};
-                       if ($diffinfo->{'to_id'} ne ('0' x 40)) { # file exists in result
+                       if (!is_deleted($diffinfo)) { # file exists in result
                                $to{'href'} = href(action=>"blob", hash_base=>$hash,
                                                   hash=>$diffinfo->{'to_id'},
                                                   file_name=>$to{'file'});
@@ -2766,105 +3236,15 @@ sub git_patchset_body {
 
                # print "git diff" header
                $patch_line = shift @diff_header;
-               if ($diffinfo->{'nparents'}) {
-
-                       # combined diff
-                       $patch_line =~ s!^(diff (.*?) )"?.*$!$1!;
-                       if ($to{'href'}) {
-                               $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"},
-                                                      esc_path($to{'file'}));
-                       } else { # file was deleted
-                               $patch_line .= esc_path($to{'file'});
-                       }
-
-               } else {
-
-                       $patch_line =~ s!^(diff (.*?) )"?a/.*$!$1!;
-                       if ($from{'href'}) {
-                               $patch_line .= $cgi->a({-href => $from{'href'}, -class => "path"},
-                                                      'a/' . esc_path($from{'file'}));
-                       } else { # file was added
-                               $patch_line .= 'a/' . esc_path($from{'file'});
-                       }
-                       $patch_line .= ' ';
-                       if ($to{'href'}) {
-                               $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"},
-                                                      'b/' . esc_path($to{'file'}));
-                       } else { # file was deleted
-                               $patch_line .= 'b/' . esc_path($to{'file'});
-                       }
-
-               }
-               print "<div class=\"diff header\">$patch_line</div>\n";
+               print format_git_diff_header_line($patch_line, $diffinfo,
+                                                 \%from, \%to);
 
                # print extended diff header
                print "<div class=\"diff extended_header\">\n" if (@diff_header > 0);
        EXTENDED_HEADER:
                foreach $patch_line (@diff_header) {
-                       # match <path>
-                       if ($patch_line =~ s!^((copy|rename) from ).*$!$1! && $from{'href'}) {
-                               $patch_line .= $cgi->a({-href=>$from{'href'}, -class=>"path"},
-                                                      esc_path($from{'file'}));
-                       }
-                       if ($patch_line =~ s!^((copy|rename) to ).*$!$1! && $to{'href'}) {
-                               $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"path"},
-                                                      esc_path($to{'file'}));
-                       }
-                       # match single <mode>
-                       if ($patch_line =~ m/\s(\d{6})$/) {
-                               $patch_line .= '<span class="info"> (' .
-                                              file_type_long($1) .
-                                              ')</span>';
-                       }
-                       # match <hash>
-                       if ($patch_line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
-                               # can match only for combined diff
-                               $patch_line = 'index ';
-                               for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
-                                       if ($from{'href'}[$i]) {
-                                               $patch_line .= $cgi->a({-href=>$from{'href'}[$i],
-                                                                       -class=>"hash"},
-                                                                      substr($diffinfo->{'from_id'}[$i],0,7));
-                                       } else {
-                                               $patch_line .= '0' x 7;
-                                       }
-                                       # separator
-                                       $patch_line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
-                               }
-                               $patch_line .= '..';
-                               if ($to{'href'}) {
-                                       $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"hash"},
-                                                              substr($diffinfo->{'to_id'},0,7));
-                               } else {
-                                       $patch_line .= '0' x 7;
-                               }
-
-                       } elsif ($patch_line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
-                               # can match only for ordinary diff
-                               my ($from_link, $to_link);
-                               if ($from{'href'}) {
-                                       $from_link = $cgi->a({-href=>$from{'href'}, -class=>"hash"},
-                                                            substr($diffinfo->{'from_id'},0,7));
-                               } else {
-                                       $from_link = '0' x 7;
-                               }
-                               if ($to{'href'}) {
-                                       $to_link = $cgi->a({-href=>$to{'href'}, -class=>"hash"},
-                                                          substr($diffinfo->{'to_id'},0,7));
-                               } else {
-                                       $to_link = '0' x 7;
-                               }
-                               #affirm {
-                               #       my ($from_hash, $to_hash) =
-                               #               ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/);
-                               #       my ($from_id, $to_id) =
-                               #               ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
-                               #       ($from_hash eq $from_id) && ($to_hash eq $to_id);
-                               #} if DEBUG;
-                               my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
-                               $patch_line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
-                       }
-                       print $patch_line . "<br/>\n";
+                       print format_extended_diff_header_line($patch_line, $diffinfo,
+                                                              \%from, \%to);
                }
                print "</div>\n"  if (@diff_header > 0); # class="diff extended_header"
 
@@ -2876,24 +3256,15 @@ sub git_patchset_body {
                }
                next PATCH if ($patch_line =~ m/^diff /);
                #assert($patch_line =~ m/^---/) if DEBUG;
-               if (!$diffinfo->{'nparents'} && # not from-file line for combined diff
-                   $from{'href'} && $patch_line =~ m!^--- "?a/!) {
-                       $patch_line = '--- a/' .
-                                     $cgi->a({-href=>$from{'href'}, -class=>"path"},
-                                             esc_path($from{'file'}));
-               }
-               print "<div class=\"diff from_file\">$patch_line</div>\n";
+               #assert($patch_line eq $last_patch_line) if DEBUG;
 
                $patch_line = <$fd>;
                chomp $patch_line;
+               #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
 
-               #assert($patch_line =~ m/^+++/) if DEBUG;
-               if ($to{'href'} && $patch_line =~ m!^\+\+\+ "?b/!) {
-                       $patch_line = '+++ b/' .
-                                     $cgi->a({-href=>$to{'href'}, -class=>"path"},
-                                             esc_path($to{'file'}));
-               }
-               print "<div class=\"diff to_file\">$patch_line</div>\n";
+               print format_diff_from_to_header($last_patch_line, $patch_line,
+                                                $diffinfo, \%from, \%to,
+                                                @hash_parents);
 
                # the patch itself
        LINE:
@@ -2909,6 +3280,27 @@ sub git_patchset_body {
                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";
@@ -2936,11 +3328,11 @@ sub git_project_list_body {
                ($pr->{'age'}, $pr->{'age_string'}) = @aa;
                if (!defined $pr->{'descr'}) {
                        my $descr = git_get_project_description($pr->{'path'}) || "";
-                       $pr->{'descr_long'} = decode_utf8($descr);
-                       $pr->{'descr'} = chop_str($descr, 25, 5);
+                       $pr->{'descr_long'} = to_utf8($descr);
+                       $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
                }
                if (!defined $pr->{'owner'}) {
-                       $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || "";
+                       $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
                }
                if ($check_forks) {
                        my $pname = $pr->{'path'};
@@ -3027,7 +3419,7 @@ sub git_project_list_body {
                      "<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";
+                     "<td><i>" . esc_html(chop_str($pr->{'owner'}, 15)) . "</i></td>\n";
                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
                      (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
                      "<td class=\"link\">" .
@@ -3054,8 +3446,6 @@ sub git_shortlog_body {
        # uses global variable $project
        my ($commitlist, $from, $to, $refs, $extra) = @_;
 
-       my $have_snapshot = gitweb_have_snapshot();
-
        $from = 0 unless defined $from;
        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
 
@@ -3082,8 +3472,9 @@ 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 ($have_snapshot) {
-                       print " | " . $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot");
+               my $snapshot_links = format_snapshot_links($commit);
+               if (defined $snapshot_links) {
+                       print " | " . $snapshot_links;
                }
                print "</td>\n" .
                      "</tr>\n";
@@ -3364,7 +3755,7 @@ sub git_project_index {
 
        foreach my $pr (@projects) {
                if (!exists $pr->{'owner'}) {
-                       $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}");
+                       $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
                }
 
                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
@@ -3404,7 +3795,7 @@ 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>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
        if (defined $cd{'rfc2822'}) {
                print "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
        }
@@ -3865,8 +4256,6 @@ sub git_blob {
 }
 
 sub git_tree {
-       my $have_snapshot = gitweb_have_snapshot();
-
        if (!defined $hash_base) {
                $hash_base = "HEAD";
        }
@@ -3900,11 +4289,10 @@ sub git_tree {
                                                       hash_base=>"HEAD", file_name=>$file_name)},
                                        "HEAD"),
                }
-               if ($have_snapshot) {
+               my $snapshot_links = format_snapshot_links($hash);
+               if (defined $snapshot_links) {
                        # FIXME: Should be available when we have no hash base as well.
-                       push @views_nav,
-                               $cgi->a({-href => href(action=>"snapshot", hash=>$hash)},
-                                       "snapshot");
+                       push @views_nav, $snapshot_links;
                }
                git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
@@ -3968,31 +4356,44 @@ sub git_tree {
 }
 
 sub git_snapshot {
-       my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
-       my $have_snapshot = (defined $ctype && defined $suffix);
-       if (!$have_snapshot) {
+       my @supported_fmts = gitweb_check_feature('snapshot');
+       @supported_fmts = filter_snapshot_fmts(@supported_fmts);
+
+       my $format = $cgi->param('sf');
+       if (!@supported_fmts) {
                die_error('403 Permission denied', "Permission denied");
        }
+       # default to first supported snapshot format
+       $format ||= $supported_fmts[0];
+       if ($format !~ m/^[a-z0-9]+$/) {
+               die_error(undef, "Invalid snapshot format parameter");
+       } elsif (!exists($known_snapshot_formats{$format})) {
+               die_error(undef, "Unknown snapshot format");
+       } elsif (!grep($_ eq $format, @supported_fmts)) {
+               die_error(undef, "Unsupported snapshot format");
+       }
 
        if (!defined $hash) {
                $hash = git_get_head_hash($project);
        }
 
-       my $git = git_cmd_str();
+       my $git_command = 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 $filename = decode_utf8(basename($project));
        my $cmd;
-       if ($suffix eq 'zip') {
-               $filename .= "-$hash.$suffix";
-               $cmd = "$git archive --format=zip --prefix=\'$name\'/ $hash";
-       } else {
-               $filename .= "-$hash.tar.$suffix";
-               $cmd = "$git archive --format=tar --prefix=\'$name\'/ $hash | $command";
+       $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
+       $cmd = "$git_command archive " .
+               "--format=$known_snapshot_formats{$format}{'format'} " .
+               "--prefix=\'$name\'/ $hash";
+       if (exists $known_snapshot_formats{$format}{'compressor'}) {
+               $cmd .= ' | ' . join ' ', @{$known_snapshot_formats{$format}{'compressor'}};
        }
 
        print $cgi->header(
-               -type => "application/$ctype",
+               -type => $known_snapshot_formats{$format}{'type'},
                -content_disposition => 'inline; filename="' . "$filename" . '"',
                -status => '200 OK');
 
@@ -4002,7 +4403,6 @@ sub git_snapshot {
        print <$fd>;
        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
        close $fd;
-
 }
 
 sub git_log {
@@ -4121,8 +4521,6 @@ sub git_commit {
        my $refs = git_get_references();
        my $ref = format_ref_marker($refs, $co{'id'});
 
-       my $have_snapshot = gitweb_have_snapshot();
-
        git_header_html(undef, $expires);
        git_print_page_nav('commit', '',
                           $hash, $co{'tree'}, $hash,
@@ -4161,9 +4559,9 @@ sub git_commit {
              "<td class=\"link\">" .
              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
                      "tree");
-       if ($have_snapshot) {
-               print " | " .
-                     $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, "snapshot");
+       my $snapshot_links = format_snapshot_links($hash);
+       if (defined $snapshot_links) {
+               print " | " . $snapshot_links;
        }
        print "</td>" .
              "</tr>\n";
@@ -4422,7 +4820,11 @@ sub git_commitdiff {
                die_error(undef, "Unknown commit object");
        }
 
-       # we need to prepare $formats_nav before any parameter munging
+       # choose format for commitdiff for merge
+       if (! defined $hash_parent && @{$co{'parents'}} > 1) {
+               $hash_parent = '--cc';
+       }
+       # we need to prepare $formats_nav before almost any parameter munging
        my $formats_nav;
        if ($format eq 'html') {
                $formats_nav =
@@ -4430,14 +4832,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)) .
@@ -4455,6 +4865,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 {
@@ -4467,9 +4888,10 @@ sub git_commitdiff {
        }
 
        my $hash_parent_param = $hash_parent;
-       if (!defined $hash_parent) {
+       if (!defined $hash_parent_param) {
+               # --cc for multiple parents, --root for parentless
                $hash_parent_param =
-                       @{$co{'parents'}} > 1 ? '-c' : $co{'parent'} || '--root';
+                       @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
        }
 
        # read commitdiff
@@ -4546,10 +4968,14 @@ sub git_commitdiff {
 
        # write patch
        if ($format eq 'html') {
-               git_difftree_body(\@difftree, $hash, $hash_parent || @{$co{'parents'}});
+               my $use_parents = !defined $hash_parent ||
+                       $hash_parent eq '-c' || $hash_parent eq '--cc';
+               git_difftree_body(\@difftree, $hash,
+                                 $use_parents ? @{$co{'parents'}} : $hash_parent);
                print "<br/>\n";
 
-               git_patchset_body($fd, \@difftree, $hash, $hash_parent || @{$co{'parents'}});
+               git_patchset_body($fd, \@difftree, $hash,
+                                 $use_parents ? @{$co{'parents'}} : $hash_parent);
                close $fd;
                print "</div>\n"; # class="page_body"
                git_footer_html();
@@ -4937,7 +5363,7 @@ sub git_feed {
 
        # 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 @commitlist = parse_commits($head, 150, 0, undef, $file_name);
 
        my %latest_commit;
        my %latest_date;