]> git.ipfire.org Git - thirdparty/git.git/blobdiff - gitweb/gitweb.perl
gitweb: speed up project listing on large work trees by limiting find depth
[thirdparty/git.git] / gitweb / gitweb.perl
index a6383dc85b99ead405e1961888d06bb8ff8f3931..48e21dad6cec580cd874e32e981a86dc909893f7 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,6 +606,8 @@ sub href(%) {
                order => "o",
                searchtext => "s",
                searchtype => "st",
+               snapshot_format => "sf",
+               extra_options => "opt",
        );
        my %mapping = @mapping;
 
@@ -564,7 +630,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;
@@ -824,11 +896,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 +940,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 +963,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 +1326,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
 
@@ -1386,15 +1513,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 +1599,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 +1616,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 +1660,7 @@ sub git_get_last_activity {
                my $age = time - $timestamp;
                return ($age, age_string($age));
        }
+       return (undef, undef);
 }
 
 sub git_get_references {
@@ -1757,6 +1904,7 @@ sub parse_commits {
                ($arg ? ($arg) : ()),
                ("--max-count=" . $maxcount),
                ("--skip=" . $skip),
+               @extra_options,
                $commit_id,
                "--",
                ($filename ? ($filename) : ())
@@ -2150,9 +2298,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 +2357,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',
@@ -2584,6 +2746,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";
        }
 }
 
@@ -2643,7 +2819,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" .
@@ -3025,44 +3201,10 @@ sub git_patchset_body {
                                }
                        } 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";
@@ -3163,10 +3305,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 +3395,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\">" .
@@ -3280,8 +3422,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);
 
@@ -3308,8 +3448,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";
@@ -3590,7 +3731,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 +3771,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";
        }
@@ -4091,8 +4232,6 @@ sub git_blob {
 }
 
 sub git_tree {
-       my $have_snapshot = gitweb_have_snapshot();
-
        if (!defined $hash_base) {
                $hash_base = "HEAD";
        }
@@ -4126,11 +4265,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);
@@ -4194,31 +4332,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 = to_utf8(basename($project));
        my $cmd;
-       if ($suffix eq 'zip') {
-               $filename .= "-$hash.$suffix";
-               $cmd = "$git archive --format=zip --prefix=\'$name\'/ $hash";
-       } else {
-               $filename .= "-$hash.tar.$suffix";
-               $cmd = "$git archive --format=tar --prefix=\'$name\'/ $hash | $command";
+       $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');
 
@@ -4228,7 +4379,6 @@ sub git_snapshot {
        print <$fd>;
        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
        close $fd;
-
 }
 
 sub git_log {
@@ -4347,8 +4497,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,
@@ -4387,9 +4535,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";
@@ -5191,7 +5339,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;