Code

Merge branch 'js/rebase-detached'
[git.git] / gitweb / gitweb.perl
index dbfb0441a6a59e6fe069a515a2d293f4d860e143..e788ef90c981e93f59400332da8bb7de1c2b3952 100755 (executable)
@@ -35,6 +35,10 @@ our $GIT = "++GIT_BINDIR++/git";
 #our $projectroot = "/pub/scm";
 our $projectroot = "++GITWEB_PROJECTROOT++";
 
+# fs traversing limit for getting project list
+# the number is relative to the projectroot
+our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
+
 # target of the home link on top of all pages
 our $home_link = $my_uri || "/";
 
@@ -71,6 +75,9 @@ our $logo_label = "git homepage";
 # 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";
@@ -101,6 +108,59 @@ our $mimetypes_file = undef;
 # 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 = (
@@ -131,20 +191,22 @@ our %feature = (
                '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.
@@ -243,28 +305,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 {
@@ -307,15 +356,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;
@@ -383,6 +434,22 @@ if (defined $hash_base) {
        }
 }
 
+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)) {
@@ -408,9 +475,6 @@ if (defined $searchtype) {
 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");
        }
@@ -542,9 +606,20 @@ sub href(%) {
                order => "o",
                searchtext => "s",
                searchtype => "st",
+               snapshot_format => "sf",
+               extra_options => "opt",
        );
        my %mapping = @mapping;
 
+       if ($params{-replay}) {
+               while (my ($name, $symbol) = each %mapping) {
+                       if (!exists $params{$name}) {
+                               # to allow for multivalued params we use arrayref form
+                               $params{$name} = [ $cgi->param($symbol) ];
+                       }
+               }
+       }
+
        $params{'project'} = $project unless exists $params{'project'};
 
        my ($use_pathinfo) = gitweb_check_feature('pathinfo');
@@ -564,7 +639,13 @@ sub href(%) {
        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;
@@ -774,6 +855,23 @@ sub chop_str {
        return "$body$tail";
 }
 
+# takes the same arguments as chop_str, but also wraps a <span> around the
+# result with a title attribute if it does get chopped. Additionally, the
+# string is HTML-escaped.
+sub chop_and_escape_str {
+       my $str = shift;
+       my $len = shift;
+       my $add_len = shift || 10;
+
+       my $chopped = chop_str($str, $len, $add_len);
+       if ($chopped eq $str) {
+               return esc_html($chopped);
+       } else {
+               return qq{<span title="} . esc_html($str) . qq{">} .
+                       esc_html($chopped) . qq{</span>};
+       }
+}
+
 ## ----------------------------------------------------------------------
 ## functions returning short strings
 
@@ -824,11 +922,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';
@@ -854,7 +966,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";
@@ -875,7 +989,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";
@@ -1236,6 +1352,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
 
@@ -1279,20 +1432,121 @@ sub git_get_type {
        return $type;
 }
 
+# repository configuration
+our $config_file = '';
+our %config;
+
+# store multiple values for single key as anonymous array reference
+# single values stored directly in the hash, not as [ <value> ]
+sub hash_set_multi {
+       my ($hash, $key, $value) = @_;
+
+       if (!exists $hash->{$key}) {
+               $hash->{$key} = $value;
+       } elsif (!ref $hash->{$key}) {
+               $hash->{$key} = [ $hash->{$key}, $value ];
+       } else {
+               push @{$hash->{$key}}, $value;
+       }
+}
+
+# return hash of git project configuration
+# optionally limited to some section, e.g. 'gitweb'
+sub git_parse_project_config {
+       my $section_regexp = shift;
+       my %config;
+
+       local $/ = "\0";
+
+       open my $fh, "-|", git_cmd(), "config", '-z', '-l',
+               or return;
+
+       while (my $keyval = <$fh>) {
+               chomp $keyval;
+               my ($key, $value) = split(/\n/, $keyval, 2);
+
+               hash_set_multi(\%config, $key, $value)
+                       if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
+       }
+       close $fh;
+
+       return %config;
+}
+
+# convert config value to boolean, 'true' or 'false'
+# no value, number > 0, 'true' and 'yes' values are true
+# rest of values are treated as false (never as error)
+sub config_to_bool {
+       my $val = shift;
+
+       # strip leading and trailing whitespace
+       $val =~ s/^\s+//;
+       $val =~ s/\s+$//;
+
+       return (!defined $val ||               # section.key
+               ($val =~ /^\d+$/ && $val) ||   # section.key = 1
+               ($val =~ /^(?:true|yes)$/i));  # section.key = true
+}
+
+# convert config value to simple decimal number
+# an optional value suffix of 'k', 'm', or 'g' will cause the value
+# to be multiplied by 1024, 1048576, or 1073741824
+sub config_to_int {
+       my $val = shift;
+
+       # strip leading and trailing whitespace
+       $val =~ s/^\s+//;
+       $val =~ s/\s+$//;
+
+       if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
+               $unit = lc($unit);
+               # unknown unit is treated as 1
+               return $num * ($unit eq 'g' ? 1073741824 :
+                              $unit eq 'm' ?    1048576 :
+                              $unit eq 'k' ?       1024 : 1);
+       }
+       return $val;
+}
+
+# convert config value to array reference, if needed
+sub config_to_multi {
+       my $val = shift;
+
+       return ref($val) ? $val : [ $val ];
+}
+
 sub git_get_project_config {
        my ($key, $type) = @_;
 
+       # key sanity check
        return unless ($key);
        $key =~ s/^gitweb\.//;
        return if ($key =~ m/\W/);
 
-       my @x = (git_cmd(), 'config');
-       if (defined $type) { push @x, $type; }
-       push @x, "--get";
-       push @x, "gitweb.$key";
-       my $val = qx(@x);
-       chomp $val;
-       return ($val);
+       # type sanity check
+       if (defined $type) {
+               $type =~ s/^--//;
+               $type = undef
+                       unless ($type eq 'bool' || $type eq 'int');
+       }
+
+       # get config
+       if (!defined $config_file ||
+           $config_file ne "$git_dir/config") {
+               %config = git_parse_project_config('gitweb');
+               $config_file = "$git_dir/config";
+       }
+
+       # ensure given type
+       if (!defined $type) {
+               return $config{"gitweb.$key"};
+       } elsif ($type eq 'bool') {
+               # backward compatibility: 'git config --bool' returns true/false
+               return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
+       } elsif ($type eq 'int') {
+               return config_to_int($config{"gitweb.$key"});
+       }
+       return $config{"gitweb.$key"};
 }
 
 # get hash of given path at given ref
@@ -1352,7 +1606,9 @@ sub git_get_path_by_hash {
 sub git_get_project_description {
        my $path = shift;
 
-       open my $fd, "$projectroot/$path/description" or return undef;
+       $git_dir = "$projectroot/$path";
+       open my $fd, "$projectroot/$path/description"
+               or return git_get_project_config('description');
        my $descr = <$fd>;
        close $fd;
        if (defined $descr) {
@@ -1364,7 +1620,11 @@ sub git_get_project_description {
 sub git_get_project_url_list {
        my $path = shift;
 
-       open my $fd, "$projectroot/$path/cloneurl" or return;
+       $git_dir = "$projectroot/$path";
+       open my $fd, "$projectroot/$path/cloneurl"
+               or return wantarray ?
+               @{ config_to_multi(git_get_project_config('url')) } :
+                  config_to_multi(git_get_project_config('url'));
        my @git_project_url_list = map { chomp; $_ } <$fd>;
        close $fd;
 
@@ -1386,15 +1646,22 @@ sub git_get_projects_list {
                # remove the trailing "/"
                $dir =~ s!/+$!!;
                my $pfxlen = length("$dir");
+               my $pfxdepth = ($dir =~ tr!/!!);
 
                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.
                                return if (m!^[/.]$!);
                                # only directories can be git repositories
                                return unless (-d $_);
+                               # don't traverse too deep (Find is super slow on os x)
+                               if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
+                                       $File::Find::prune = 1;
+                                       return;
+                               }
 
                                my $subdir = substr($File::Find::name, $pfxlen + 1);
                                # we check related file in $projectroot
@@ -1465,12 +1732,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'
@@ -1482,13 +1749,25 @@ sub git_get_project_owner {
                        my ($pr, $ow) = split ' ', $line;
                        $pr = unescape($pr);
                        $ow = unescape($ow);
-                       if ($pr eq $project) {
-                               $owner = to_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");
        }
@@ -1514,6 +1793,7 @@ sub git_get_last_activity {
                my $age = time - $timestamp;
                return ($age, age_string($age));
        }
+       return (undef, undef);
 }
 
 sub git_get_references {
@@ -1576,7 +1856,7 @@ sub parse_date {
        $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;
+                            1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
 
        $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
        my $local = $epoch + ((int $1 + ($2/60)) * 3600);
@@ -1757,6 +2037,7 @@ sub parse_commits {
                ($arg ? ($arg) : ()),
                ("--max-count=" . $maxcount),
                ("--skip=" . $skip),
+               @extra_options,
                $commit_id,
                "--",
                ($filename ? ($filename) : ())
@@ -1825,12 +2106,12 @@ sub parse_difftree_raw_line {
                $res{'to_mode'} = $2;
                $res{'from_id'} = $3;
                $res{'to_id'} = $4;
-               $res{'status'} = $5;
+               $res{'status'} = $res{'status_str'} = $5;
                $res{'similarity'} = $6;
                if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
                        ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
                } else {
-                       $res{'file'} = unquote($7);
+                       $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
                }
        }
        # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
@@ -1841,6 +2122,7 @@ sub parse_difftree_raw_line {
                $res{'to_mode'} = pop @{$res{'from_mode'}};
                $res{'from_id'} = [ split(' ', $3) ];
                $res{'to_id'} = pop @{$res{'from_id'}};
+               $res{'status_str'} = $4;
                $res{'status'} = [ split('', $4) ];
                $res{'to_file'} = unquote($5);
        }
@@ -1852,6 +2134,19 @@ sub parse_difftree_raw_line {
        return wantarray ? %res : \%res;
 }
 
+# wrapper: return parsed line of git-diff-tree "raw" output
+# (the argument might be raw line, or parsed info)
+sub parsed_difftree_line {
+       my $line_or_ref = shift;
+
+       if (ref($line_or_ref) eq "HASH") {
+               # pre-parsed (or generated by hand)
+               return $line_or_ref;
+       } else {
+               return parse_difftree_raw_line($line_or_ref);
+       }
+}
+
 # parse line of git-ls-tree output
 sub parse_ls_tree_line ($;%) {
        my $line = shift;
@@ -1884,7 +2179,10 @@ sub parse_from_to_diffinfo {
                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'};
+                       $from->{'file'}[$i] =
+                               defined $diffinfo->{'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],
@@ -1895,7 +2193,8 @@ sub parse_from_to_diffinfo {
                        }
                }
        } else {
-               $from->{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'};
+               # ordinary (not combined) diff
+               $from->{'file'} = $diffinfo->{'from_file'};
                if ($diffinfo->{'status'} ne "A") { # not new (added) file
                        $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
                                               hash=>$diffinfo->{'from_id'},
@@ -1905,7 +2204,7 @@ sub parse_from_to_diffinfo {
                }
        }
 
-       $to->{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'};
+       $to->{'file'} = $diffinfo->{'to_file'};
        if (!is_deleted($diffinfo)) { # file exists in result
                $to->{'href'} = href(action=>"blob", hash_base=>$hash,
                                     hash=>$diffinfo->{'to_id'},
@@ -2150,9 +2449,17 @@ EOF
                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",
@@ -2201,12 +2508,18 @@ EOF
                } 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',
@@ -2312,7 +2625,7 @@ sub format_paging_nav {
 
        if ($page > 0) {
                $paging_nav .= " &sdot; " .
-                       $cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page-1),
+                       $cgi->a({-href => href(-replay=>1, page=>$page-1),
                                 -accesskey => "p", -title => "Alt-p"}, "prev");
        } else {
                $paging_nav .= " &sdot; prev";
@@ -2320,7 +2633,7 @@ sub format_paging_nav {
 
        if ($nrevs >= (100 * ($page+1)-1)) {
                $paging_nav .= " &sdot; " .
-                       $cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page+1),
+                       $cgi->a({-href => href(-replay=>1, page=>$page+1),
                                 -accesskey => "n", -title => "Alt-n"}, "next");
        } else {
                $paging_nav .= " &sdot; next";
@@ -2584,12 +2897,27 @@ 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";
        }
 }
 
 ## ......................................................................
 ## functions printing large fragments of HTML
 
+# get pre-image filenames for merge (combined) diff
 sub fill_from_file_info {
        my ($diff, @parents) = @_;
 
@@ -2606,28 +2934,25 @@ sub fill_from_file_info {
        return $diff;
 }
 
-# parameters can be strings, or references to arrays of strings
-sub from_ids_eq {
-       my ($a, $b) = @_;
+# is current raw difftree line of file deletion
+sub is_deleted {
+       my $diffinfo = shift;
 
-       if (ref($a) eq "ARRAY" && ref($b) eq "ARRAY" && @$a == @$b) {
-               for (my $i = 0; $i < @$a; ++$i) {
-                       return 0 unless ($a->[$i] eq $b->[$i]);
-               }
-               return 1;
-       } elsif (!ref($a) && !ref($b)) {
-               return $a eq $b;
-       } else {
-               return 0;
-       }
+       return $diffinfo->{'status_str'} =~ /D/;
 }
 
-sub is_deleted {
-       my $diffinfo = shift;
+# does patch correspond to [previous] difftree raw line
+# $diffinfo  - hashref of parsed raw diff format
+# $patchinfo - hashref of parsed patch diff format
+#              (the same keys as in $diffinfo)
+sub is_patch_split {
+       my ($diffinfo, $patchinfo) = @_;
 
-       return $diffinfo->{'to_id'} eq ('0' x 40);
+       return defined $diffinfo && defined $patchinfo
+               && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
 }
 
+
 sub git_difftree_body {
        my ($difftree, $hash, @parents) = @_;
        my ($parent) = $parents[0];
@@ -2643,7 +2968,7 @@ sub git_difftree_body {
              "diff_tree\">\n";
 
        # header only for combined diff in 'commitdiff' view
-       my $has_header = @parents > 1 && $action eq 'commitdiff';
+       my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
        if ($has_header) {
                # table header
                print "<thead><tr>\n" .
@@ -2664,13 +2989,7 @@ sub git_difftree_body {
        my $alternate = 1;
        my $patchno = 0;
        foreach my $line (@{$difftree}) {
-               my $diff;
-               if (ref($line) eq "HASH") {
-                       # pre-parsed (or generated by hand)
-                       $diff = $line;
-               } else {
-                       $diff = parse_difftree_raw_line($line);
-               }
+               my $diff = parsed_difftree_line($line);
 
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
@@ -2941,10 +3260,12 @@ sub git_patchset_body {
        my ($fd, $difftree, $hash, @hash_parents) = @_;
        my ($hash_parent) = $hash_parents[0];
 
+       my $is_combined = (@hash_parents > 1);
        my $patch_idx = 0;
        my $patch_number = 0;
        my $patch_line;
        my $diffinfo;
+       my $to_name;
        my (%from, %to);
 
        print "<div class=\"patchset\">\n";
@@ -2958,140 +3279,85 @@ sub git_patchset_body {
 
  PATCH:
        while ($patch_line) {
-               my @diff_header;
-               my ($from_id, $to_id);
-
-               # git diff header
-               #assert($patch_line =~ m/^diff /) if DEBUG;
-               #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
-               $patch_number++;
-               push @diff_header, $patch_line;
-
-               # extended diff header
-       EXTENDED_HEADER:
-               while ($patch_line = <$fd>) {
-                       chomp $patch_line;
-
-                       last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
 
-                       if ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) {
-                               $from_id = $1;
-                               $to_id   = $2;
-                       } elsif ($patch_line =~ m/^index ((?:[0-9a-fA-F]{40},)+[0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) {
-                               $from_id = [ split(',', $1) ];
-                               $to_id   = $2;
-                       }
-
-                       push @diff_header, $patch_line;
+               # parse "git diff" header line
+               if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
+                       # $1 is from_name, which we do not use
+                       $to_name = unquote($2);
+                       $to_name =~ s!^b/!!;
+               } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
+                       # $1 is 'cc' or 'combined', which we do not use
+                       $to_name = unquote($2);
+               } else {
+                       $to_name = undef;
                }
-               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 &&
-                   defined $from_id && defined $to_id &&
-                   from_ids_eq($diffinfo->{'from_id'}, $from_id) &&
-                   $diffinfo->{'to_id'} eq $to_id) {
+               if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
                        # this is continuation of a split patch
                        print "<div class=\"patch cont\">\n";
                } else {
                        # advance raw git-diff output if needed
                        $patch_idx++ if defined $diffinfo;
 
-                       # 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;
-                       }
+                       # read and prepare patch information
+                       $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
 
-                       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'}) {
+                       # compact combined diff output can have some patches skipped
+                       # find which patch (using pathname of result) we are at now;
+                       if ($is_combined) {
+                               while ($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++;
+
+                                       last if $patch_idx > $#$difftree;
+                                       $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
                                }
-                       } until (!defined $to_name || $to_name eq $diffinfo->{'to_file'} ||
-                                $patch_idx > $#$difftree);
+                       }
+
                        # modifies %from, %to hashes
                        parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
-                       if ($diffinfo->{'nparents'}) {
-                               # combined diff
-                               $from{'file'} = [];
-                               $from{'href'} = [];
-                               fill_from_file_info($diffinfo, @hash_parents)
-                                       unless exists $diffinfo->{'from_file'};
-                               for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
-                                       $from{'file'}[$i] = $diffinfo->{'from_file'}[$i] || $diffinfo->{'to_file'};
-                                       if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
-                                               $from{'href'}[$i] = href(action=>"blob",
-                                                                        hash_base=>$hash_parents[$i],
-                                                                        hash=>$diffinfo->{'from_id'}[$i],
-                                                                        file_name=>$from{'file'}[$i]);
-                                       } else {
-                                               $from{'href'}[$i] = undef;
-                                       }
-                               }
-                       } else {
-                               $from{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'};
-                               if ($diffinfo->{'status'} ne "A") { # not new (added) file
-                                       $from{'href'} = href(action=>"blob", hash_base=>$hash_parent,
-                                                            hash=>$diffinfo->{'from_id'},
-                                                            file_name=>$from{'file'});
-                               } else {
-                                       delete $from{'href'};
-                               }
-                       }
 
-                       $to{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'};
-                       if (!is_deleted($diffinfo)) { # file exists in result
-                               $to{'href'} = href(action=>"blob", hash_base=>$hash,
-                                                  hash=>$diffinfo->{'to_id'},
-                                                  file_name=>$to{'file'});
-                       } else {
-                               delete $to{'href'};
-                       }
                        # this is first patch for raw difftree line with $patch_idx index
                        # we index @$difftree array from 0, but number patches from 1
                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
                }
 
+               # git diff header
+               #assert($patch_line =~ m/^diff /) if DEBUG;
+               #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
+               $patch_number++;
                # print "git diff" header
-               $patch_line = shift @diff_header;
                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);
+               print "<div class=\"diff extended_header\">\n";
        EXTENDED_HEADER:
-               foreach $patch_line (@diff_header) {
+               while ($patch_line = <$fd>) {
+                       chomp $patch_line;
+
+                       last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
+
                        print format_extended_diff_header_line($patch_line, $diffinfo,
                                                               \%from, \%to);
                }
-               print "</div>\n"  if (@diff_header > 0); # class="diff extended_header"
+               print "</div>\n"; # class="diff extended_header"
 
                # from-file/to-file diff header
-               $patch_line = $last_patch_line;
                if (! $patch_line) {
                        print "</div>\n"; # class="patch"
                        last PATCH;
                }
                next PATCH if ($patch_line =~ m/^diff /);
                #assert($patch_line =~ m/^---/) if DEBUG;
-               #assert($patch_line eq $last_patch_line) if DEBUG;
 
+               my $last_patch_line = $patch_line;
                $patch_line = <$fd>;
                chomp $patch_line;
                #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
@@ -3116,16 +3382,11 @@ sub git_patchset_body {
 
        # 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;
+       for (++$patch_idx if $patch_number > 0;
             $patch_idx < @$difftree;
-            $patch_idx++) {
+            ++$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]);
-               }
+               $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
 
                # generate anchor for "patch" links in difftree / whatchanged part
                print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
@@ -3163,10 +3424,10 @@ sub git_project_list_body {
                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);
+                       $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'};
@@ -3253,7 +3514,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>" . chop_and_escape_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\">" .
@@ -3280,8 +3541,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);
 
@@ -3297,9 +3556,10 @@ sub git_shortlog_body {
                        print "<tr class=\"light\">\n";
                }
                $alternate ^= 1;
+               my $author = chop_and_escape_str($co{'author_name'}, 10);
                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
-                     "<td><i>" . esc_html(chop_str($co{'author_name'}, 10)) . "</i></td>\n" .
+                     "<td><i>" . $author . "</i></td>\n" .
                      "<td>";
                print format_subject_html($co{'title'}, $co{'title_short'},
                                          href(action=>"commit", hash=>$commit), $ref);
@@ -3308,8 +3568,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";
@@ -3346,9 +3607,10 @@ sub git_history_body {
                        print "<tr class=\"light\">\n";
                }
                $alternate ^= 1;
+       # shortlog uses      chop_str($co{'author_name'}, 10)
+               my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
-                     # shortlog uses      chop_str($co{'author_name'}, 10)
-                     "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 3)) . "</i></td>\n" .
+                     "<td><i>" . $author . "</i></td>\n" .
                      "<td>";
                # originally git_history used chop_str($co{'title'}, 50)
                print format_subject_html($co{'title'}, $co{'title_short'},
@@ -3502,11 +3764,12 @@ sub git_search_grep_body {
                        print "<tr class=\"light\">\n";
                }
                $alternate ^= 1;
+               my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
-                     "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
+                     "<td><i>" . $author . "</i></td>\n" .
                      "<td>" .
                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
-                              esc_html(chop_str($co{'title'}, 50)) . "<br/>");
+                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
                my $comment = $co{'comment'};
                foreach my $line (@$comment) {
                        if ($line =~ m/^(.*)($search_regexp)(.*)$/i) {
@@ -3590,7 +3853,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'});
@@ -3630,7 +3893,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";
        }
@@ -3755,11 +4018,11 @@ sub git_blame2 {
                or die_error(undef, "Open git-blame failed");
        git_header_html();
        my $formats_nav =
-               $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
+               $cgi->a({-href => href(action=>"blob", -replay=>1)},
                        "blob") .
                " | " .
-               $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
-                       "history") .
+               $cgi->a({-href => href(action=>"history", -replay=>1)},
+                       "history") .
                " | " .
                $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
                        "HEAD");
@@ -4035,18 +4298,15 @@ sub git_blob {
                if (defined $file_name) {
                        if ($have_blame) {
                                $formats_nav .=
-                                       $cgi->a({-href => href(action=>"blame", hash_base=>$hash_base,
-                                                              hash=>$hash, file_name=>$file_name)},
+                                       $cgi->a({-href => href(action=>"blame", -replay=>1)},
                                                "blame") .
                                        " | ";
                        }
                        $formats_nav .=
-                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
-                                                      hash=>$hash, file_name=>$file_name)},
+                               $cgi->a({-href => href(action=>"history", -replay=>1)},
                                        "history") .
                                " | " .
-                               $cgi->a({-href => href(action=>"blob_plain",
-                                                      hash=>$hash, file_name=>$file_name)},
+                               $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
                                        "raw") .
                                " | " .
                                $cgi->a({-href => href(action=>"blob",
@@ -4054,7 +4314,8 @@ sub git_blob {
                                        "HEAD");
                } else {
                        $formats_nav .=
-                               $cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "raw");
+                               $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
+                                       "raw");
                }
                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
@@ -4091,8 +4352,6 @@ sub git_blob {
 }
 
 sub git_tree {
-       my $have_snapshot = gitweb_have_snapshot();
-
        if (!defined $hash_base) {
                $hash_base = "HEAD";
        }
@@ -4119,18 +4378,16 @@ sub git_tree {
                my @views_nav = ();
                if (defined $file_name) {
                        push @views_nav,
-                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
-                                                      hash=>$hash, file_name=>$file_name)},
+                               $cgi->a({-href => href(action=>"history", -replay=>1)},
                                        "history"),
                                $cgi->a({-href => href(action=>"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);
@@ -4194,33 +4451,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 $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');
 
@@ -4230,7 +4498,6 @@ sub git_snapshot {
        print <$fd>;
        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
        close $fd;
-
 }
 
 sub git_log {
@@ -4285,7 +4552,7 @@ sub git_log {
        }
        if ($#commitlist >= 100) {
                print "<div class=\"page_nav\">\n";
-               print $cgi->a({-href => href(action=>"log", hash=>$hash, page=>$page+1),
+               print $cgi->a({-href => href(-replay=>1, page=>$page+1),
                               -accesskey => "n", -title => "Alt-n"}, "next");
                print "</div>\n";
        }
@@ -4349,8 +4616,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,
@@ -4389,9 +4654,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";
@@ -4519,8 +4784,8 @@ sub git_blobdiff {
                }
 
                %diffinfo = parse_difftree_raw_line($difftree[0]);
-               $file_parent ||= $diffinfo{'from_file'} || $file_name || $diffinfo{'file'};
-               $file_name   ||= $diffinfo{'to_file'}   || $diffinfo{'file'};
+               $file_parent ||= $diffinfo{'from_file'} || $file_name;
+               $file_name   ||= $diffinfo{'to_file'};
 
                $hash_parent ||= $diffinfo{'from_id'};
                $hash        ||= $diffinfo{'to_id'};
@@ -4581,10 +4846,7 @@ sub git_blobdiff {
        # header
        if ($format eq 'html') {
                my $formats_nav =
-                       $cgi->a({-href => href(action=>"blobdiff_plain",
-                                              hash=>$hash, hash_parent=>$hash_parent,
-                                              hash_base=>$hash_base, hash_parent_base=>$hash_parent_base,
-                                              file_name=>$file_name, file_parent=>$file_parent)},
+                       $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
                                "raw");
                git_header_html(undef, $expires);
                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
@@ -4658,8 +4920,7 @@ sub git_commitdiff {
        my $formats_nav;
        if ($format eq 'html') {
                $formats_nav =
-                       $cgi->a({-href => href(action=>"commitdiff_plain",
-                                              hash=>$hash, hash_parent=>$hash_parent)},
+                       $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
                                "raw");
 
                if (defined $hash_parent &&
@@ -4854,27 +5115,20 @@ sub git_history {
                                               file_name=>$file_name)},
                                "first");
                $paging_nav .= " &sdot; " .
-                       $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
-                                              file_name=>$file_name, page=>$page-1),
+                       $cgi->a({-href => href(-replay=>1, page=>$page-1),
                                 -accesskey => "p", -title => "Alt-p"}, "prev");
        } else {
                $paging_nav .= "first";
                $paging_nav .= " &sdot; prev";
        }
-       if ($#commitlist >= 100) {
-               $paging_nav .= " &sdot; " .
-                       $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
-                                              file_name=>$file_name, page=>$page+1),
-                                -accesskey => "n", -title => "Alt-n"}, "next");
-       } else {
-               $paging_nav .= " &sdot; next";
-       }
        my $next_link = '';
        if ($#commitlist >= 100) {
                $next_link =
-                       $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
-                                              file_name=>$file_name, page=>$page+1),
+                       $cgi->a({-href => href(-replay=>1, page=>$page+1),
                                 -accesskey => "n", -title => "Alt-n"}, "next");
+               $paging_nav .= " &sdot; $next_link";
+       } else {
+               $paging_nav .= " &sdot; next";
        }
 
        git_header_html();
@@ -4944,30 +5198,23 @@ sub git_search {
                                                       searchtext=>$searchtext, searchtype=>$searchtype)},
                                        "first");
                        $paging_nav .= " &sdot; " .
-                               $cgi->a({-href => href(action=>"search", hash=>$hash,
-                                                      searchtext=>$searchtext, searchtype=>$searchtype,
-                                                      page=>$page-1),
+                               $cgi->a({-href => href(-replay=>1, page=>$page-1),
                                         -accesskey => "p", -title => "Alt-p"}, "prev");
                } else {
                        $paging_nav .= "first";
                        $paging_nav .= " &sdot; prev";
                }
+               my $next_link = '';
                if ($#commitlist >= 100) {
-                       $paging_nav .= " &sdot; " .
-                               $cgi->a({-href => href(action=>"search", hash=>$hash,
-                                                      searchtext=>$searchtext, searchtype=>$searchtype,
-                                                      page=>$page+1),
+                       $next_link =
+                               $cgi->a({-href => href(-replay=>1, page=>$page+1),
                                         -accesskey => "n", -title => "Alt-n"}, "next");
+                       $paging_nav .= " &sdot; $next_link";
                } else {
                        $paging_nav .= " &sdot; next";
                }
-               my $next_link = '';
+
                if ($#commitlist >= 100) {
-                       $next_link =
-                               $cgi->a({-href => href(action=>"search", hash=>$hash,
-                                                      searchtext=>$searchtext, searchtype=>$searchtype,
-                                                      page=>$page+1),
-                                        -accesskey => "n", -title => "Alt-n"}, "next");
                }
 
                git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
@@ -5011,12 +5258,13 @@ sub git_search {
                                                print "<tr class=\"light\">\n";
                                        }
                                        $alternate ^= 1;
+                                       my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
                                        print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
-                                             "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
+                                             "<td><i>" . $author . "</i></td>\n" .
                                              "<td>" .
                                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
                                                      -class => "list subject"},
-                                                     esc_html(chop_str($co{'title'}, 50)) . "<br/>");
+                                                     chop_and_escape_str($co{'title'}, 50) . "<br/>");
                                        while (my $setref = shift @files) {
                                                my %set = %$setref;
                                                print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
@@ -5165,7 +5413,7 @@ sub git_shortlog {
        my $next_link = '';
        if ($#commitlist >= 100) {
                $next_link =
-                       $cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1),
+                       $cgi->a({-href => href(-replay=>1, page=>$page+1),
                                 -accesskey => "n", -title => "Alt-n"}, "next");
        }
 
@@ -5193,7 +5441,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;