Merge branch 'mr/gitweb-snapshot'
authorJunio C Hamano <gitster@pobox.com>
Tue, 24 Nov 2009 06:28:31 +0000 (22:28 -0800)
committerJunio C Hamano <gitster@pobox.com>
Tue, 24 Nov 2009 06:28:31 +0000 (22:28 -0800)
* mr/gitweb-snapshot:
t/gitweb-lib: Split HTTP response with non-GNU sed
gitweb: Smarter snapshot names
gitweb: Document current snapshot rules via new tests
t/gitweb-lib.sh: Split gitweb output into headers and body
gitweb: check given hash before trying to create snapshot

1  2 
gitweb/gitweb.perl
t/t9501-gitweb-standalone-http-status.sh
diff --combined gitweb/gitweb.perl
index 681e635090f81e2cf8df7cd10494451c22f993e4,d8dfd950a445756eb50d8905f3ba63100f20544c..f94536c680c05932595f0766fde1d87b5a8f2162
@@@ -297,19 -297,6 +297,19 @@@ our %feature = 
                'override' => 0,
                'default' => [1]},
  
 +      # Enable showing size of blobs in a 'tree' view, in a separate
 +      # column, similar to what 'ls -l' does.  This cost a bit of IO.
 +
 +      # To disable system wide have in $GITWEB_CONFIG
 +      # $feature{'show-sizes'}{'default'} = [0];
 +      # To have project specific config enable override in $GITWEB_CONFIG
 +      # $feature{'show-sizes'}{'override'} = 1;
 +      # and in project config gitweb.showsizes = 0|1;
 +      'show-sizes' => {
 +              'sub' => sub { feature_bool('showsizes', @_) },
 +              'override' => 0,
 +              'default' => [1]},
 +
        # Make gitweb use an alternative format of the URLs which can be
        # more readable and natural-looking: project name is embedded
        # directly in the path and the query string contains other
@@@ -1096,7 -1083,8 +1096,7 @@@ sub to_utf8 
  # correct, but quoted slashes look too horrible in bookmarks
  sub esc_param {
        my $str = shift;
 -      $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
 -      $str =~ s/\+/%2B/g;
 +      $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
        $str =~ s/ /\+/g;
        return $str;
  }
@@@ -1607,29 -1595,6 +1607,29 @@@ sub git_get_avatar 
        }
  }
  
 +sub format_search_author {
 +      my ($author, $searchtype, $displaytext) = @_;
 +      my $have_search = gitweb_check_feature('search');
 +
 +      if ($have_search) {
 +              my $performed = "";
 +              if ($searchtype eq 'author') {
 +                      $performed = "authored";
 +              } elsif ($searchtype eq 'committer') {
 +                      $performed = "committed";
 +              }
 +
 +              return $cgi->a({-href => href(action=>"search", hash=>$hash,
 +                              searchtext=>$author,
 +                              searchtype=>$searchtype), class=>"list",
 +                              title=>"Search for commits $performed by $author"},
 +                              $displaytext);
 +
 +      } else {
 +              return $displaytext;
 +      }
 +}
 +
  # format the author name of the given commit with the given tag
  # the author name is chopped and escaped according to the other
  # optional parameters (see chop_str).
@@@ -1638,10 -1603,8 +1638,10 @@@ sub format_author_html 
        my $co = shift;
        my $author = chop_and_escape_str($co->{'author_name'}, @_);
        return "<$tag class=\"author\">" .
 -             git_get_avatar($co->{'author_email'}, -pad_after => 1) .
 -             $author . "</$tag>";
 +             format_search_author($co->{'author_name'}, "author",
 +                     git_get_avatar($co->{'author_email'}, -pad_after => 1) .
 +                     $author) .
 +             "</$tag>";
  }
  
  # format git diff header line, i.e. "diff --(git|combined|cc) ..."
@@@ -2020,16 -1983,27 +2020,27 @@@ sub quote_command 
  
  # get HEAD ref of given project as hash
  sub git_get_head_hash {
-       my $project = shift;
+       return git_get_full_hash(shift, 'HEAD');
+ }
+ sub git_get_full_hash {
+       return git_get_hash(@_);
+ }
+ sub git_get_short_hash {
+       return git_get_hash(@_, '--short=7');
+ }
+ sub git_get_hash {
+       my ($project, $hash, @options) = @_;
        my $o_git_dir = $git_dir;
        my $retval = undef;
        $git_dir = "$projectroot/$project";
-       if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
-               my $head = <$fd>;
+       if (open my $fd, '-|', git_cmd(), 'rev-parse',
+           '--verify', '-q', @options, $hash) {
+               $retval = <$fd>;
+               chomp $retval if defined $retval;
                close $fd;
-               if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
-                       $retval = $1;
-               }
        }
        if (defined $o_git_dir) {
                $git_dir = $o_git_dir;
@@@ -2801,31 -2775,16 +2812,31 @@@ sub parse_ls_tree_line 
        my %opts = @_;
        my %res;
  
 -      #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
 -      $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
 +      if ($opts{'-l'}) {
 +              #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa   16717  panic.c'
 +              $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
  
 -      $res{'mode'} = $1;
 -      $res{'type'} = $2;
 -      $res{'hash'} = $3;
 -      if ($opts{'-z'}) {
 -              $res{'name'} = $4;
 +              $res{'mode'} = $1;
 +              $res{'type'} = $2;
 +              $res{'hash'} = $3;
 +              $res{'size'} = $4;
 +              if ($opts{'-z'}) {
 +                      $res{'name'} = $5;
 +              } else {
 +                      $res{'name'} = unquote($5);
 +              }
        } else {
 -              $res{'name'} = unquote($4);
 +              #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
 +              $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
 +
 +              $res{'mode'} = $1;
 +              $res{'type'} = $2;
 +              $res{'hash'} = $3;
 +              if ($opts{'-z'}) {
 +                      $res{'name'} = $4;
 +              } else {
 +                      $res{'name'} = unquote($4);
 +              }
        }
  
        return wantarray ? %res : \%res;
@@@ -3363,18 -3322,22 +3374,18 @@@ sub git_print_page_nav 
  }
  
  sub format_paging_nav {
 -      my ($action, $hash, $head, $page, $has_next_link) = @_;
 +      my ($action, $page, $has_next_link) = @_;
        my $paging_nav;
  
  
 -      if ($hash ne $head || $page) {
 -              $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
 -      } else {
 -              $paging_nav .= "HEAD";
 -      }
 -
        if ($page > 0) {
 -              $paging_nav .= " &sdot; " .
 +              $paging_nav .=
 +                      $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
 +                      " &sdot; " .
                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
                                 -accesskey => "p", -title => "Alt-p"}, "prev");
        } else {
 -              $paging_nav .= " &sdot; prev";
 +              $paging_nav .= "first &sdot; prev";
        }
  
        if ($has_next_link) {
@@@ -3421,11 -3384,10 +3432,11 @@@ sub git_print_authorship 
        my $co = shift;
        my %opts = @_;
        my $tag = $opts{-tag} || 'div';
 +      my $author = $co->{'author_name'};
  
        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
        print "<$tag class=\"author_date\">" .
 -            esc_html($co->{'author_name'}) .
 +            format_search_author($author, "author", esc_html($author)) .
              " [$ad{'rfc2822'}";
        print_local_time(%ad) if ($opts{-localtime});
        print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
@@@ -3444,12 -3406,8 +3455,12 @@@ sub git_print_authorship_rows 
        @people = ('author', 'committer') unless @people;
        foreach my $who (@people) {
                my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
 -              print "<tr><td>$who</td><td>" . esc_html($co->{$who}) . "</td>" .
 -                    "<td rowspan=\"2\">" .
 +              print "<tr><td>$who</td><td>" .
 +                    format_search_author($co->{"${who}_name"}, $who,
 +                             esc_html($co->{"${who}_name"})) . " " .
 +                    format_search_author($co->{"${who}_email"}, $who,
 +                             esc_html("<" . $co->{"${who}_email"} . ">")) .
 +                    "</td><td rowspan=\"2\">" .
                      git_get_avatar($co->{"${who}_email"}, -size => 'double') .
                      "</td></tr>\n" .
                      "<tr>" .
@@@ -3617,9 -3575,6 +3628,9 @@@ sub git_print_tree_entry 
        # and link is the action links of the entry.
  
        print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
 +      if (exists $t->{'size'}) {
 +              print "<td class=\"size\">$t->{'size'}</td>\n";
 +      }
        if ($t->{'type'} eq "blob") {
                print "<td class=\"list\">" .
                        $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
        } elsif ($t->{'type'} eq "tree") {
                print "<td class=\"list\">";
                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
 -                                           file_name=>"$basedir$t->{'name'}", %base_key)},
 +                                           file_name=>"$basedir$t->{'name'}",
 +                                           %base_key)},
                              esc_path($t->{'name'}));
                print "</td>\n";
                print "<td class=\"link\">";
                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
 -                                           file_name=>"$basedir$t->{'name'}", %base_key)},
 +                                           file_name=>"$basedir$t->{'name'}",
 +                                           %base_key)},
                              "tree");
                if (defined $hash_base) {
                        print " | " .
@@@ -4357,46 -4310,6 +4368,46 @@@ sub git_project_list_body 
        print "</table>\n";
  }
  
 +sub git_log_body {
 +      # uses global variable $project
 +      my ($commitlist, $from, $to, $refs, $extra) = @_;
 +
 +      $from = 0 unless defined $from;
 +      $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
 +
 +      for (my $i = 0; $i <= $to; $i++) {
 +              my %co = %{$commitlist->[$i]};
 +              next if !%co;
 +              my $commit = $co{'id'};
 +              my $ref = format_ref_marker($refs, $commit);
 +              my %ad = parse_date($co{'author_epoch'});
 +              git_print_header_div('commit',
 +                             "<span class=\"age\">$co{'age_string'}</span>" .
 +                             esc_html($co{'title'}) . $ref,
 +                             $commit);
 +              print "<div class=\"title_text\">\n" .
 +                    "<div class=\"log_link\">\n" .
 +                    $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") .
 +                    "<br/>\n" .
 +                    "</div>\n";
 +                    git_print_authorship(\%co, -tag => 'span');
 +                    print "<br/>\n</div>\n";
 +
 +              print "<div class=\"log_body\">\n";
 +              git_print_log($co{'comment'}, -final_empty_line=> 1);
 +              print "</div>\n";
 +      }
 +      if ($extra) {
 +              print "<div class=\"page_nav\">\n";
 +              print "$extra\n";
 +              print "</div>\n";
 +      }
 +}
 +
  sub git_shortlog_body {
        # uses global variable $project
        my ($commitlist, $from, $to, $refs, $extra) = @_;
  
  sub git_history_body {
        # Warning: assumes constant type (blob or tree) during history
 -      my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
 +      my ($commitlist, $from, $to, $refs, $extra,
 +          $file_name, $file_hash, $ftype) = @_;
  
        $from = 0 unless defined $from;
        $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
  
                if ($ftype eq 'blob') {
 -                      my $blob_current = git_get_hash_by_path($hash_base, $file_name);
 +                      my $blob_current = $file_hash;
                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
                        if (defined $blob_current && defined $blob_parent &&
                                        $blob_current ne $blob_parent) {
@@@ -5164,8 -5076,7 +5175,8 @@@ sub git_blob 
                        chomp $line;
                        $nr++;
                        $line = untabify($line);
 -                      printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
 +                      printf "<div class=\"pre\"><a id=\"l%i\" href=\"" . href(-replay => 1)
 +                              . "#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
                               $nr, $nr, $nr, esc_html($line, -nbsp=>1);
                }
        }
@@@ -5188,14 -5099,10 +5199,14 @@@ sub git_tree 
        }
        die_error(404, "No such tree") unless defined($hash);
  
 +      my $show_sizes = gitweb_check_feature('show-sizes');
 +      my $have_blame = gitweb_check_feature('blame');
 +
        my @entries = ();
        {
                local $/ = "\0";
 -              open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
 +              open my $fd, "-|", git_cmd(), "ls-tree", '-z',
 +                      ($show_sizes ? '-l' : ()), @extra_options, $hash
                        or die_error(500, "Open git-ls-tree failed");
                @entries = map { chomp; $_ } <$fd>;
                close $fd
        my $ref = format_ref_marker($refs, $hash_base);
        git_header_html();
        my $basedir = '';
 -      my $have_blame = gitweb_check_feature('blame');
        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
                my @views_nav = ();
                if (defined $file_name) {
                        # FIXME: Should be available when we have no hash base as well.
                        push @views_nav, $snapshot_links;
                }
 -              git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
 +              git_print_page_nav('tree','', $hash_base, undef, undef,
 +                                 join(' | ', @views_nav));
                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
        } else {
                undef $hash_base;
                undef $up unless $up;
                # based on git_print_tree_entry
                print '<td class="mode">' . mode_str('040000') . "</td>\n";
 +              print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
                print '<td class="list">';
 -              print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
 +              print $cgi->a({-href => href(action=>"tree",
 +                                           hash_base=>$hash_base,
                                             file_name=>$up)},
                              "..");
                print "</td>\n";
                print "</tr>\n";
        }
        foreach my $line (@entries) {
 -              my %t = parse_ls_tree_line($line, -z => 1);
 +              my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
  
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
        git_footer_html();
  }
  
+ sub snapshot_name {
+       my ($project, $hash) = @_;
+       # path/to/project.git  -> project
+       # path/to/project/.git -> project
+       my $name = to_utf8($project);
+       $name =~ s,([^/])/*\.git$,$1,;
+       $name = basename($name);
+       # sanitize name
+       $name =~ s/[[:cntrl:]]/?/g;
+       my $ver = $hash;
+       if ($hash =~ /^[0-9a-fA-F]+$/) {
+               # shorten SHA-1 hash
+               my $full_hash = git_get_full_hash($project, $hash);
+               if ($full_hash =~ /^$hash/ && length($hash) > 7) {
+                       $ver = git_get_short_hash($project, $hash);
+               }
+       } elsif ($hash =~ m!^refs/tags/(.*)$!) {
+               # tags don't need shortened SHA-1 hash
+               $ver = $1;
+       } else {
+               # branches and other need shortened SHA-1 hash
+               if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
+                       $ver = $1;
+               }
+               $ver .= '-' . git_get_short_hash($project, $hash);
+       }
+       # in case of hierarchical branch names
+       $ver =~ s!/!.!g;
+       # name = project-version_string
+       $name = "$name-$ver";
+       return wantarray ? ($name, $name) : $name;
+ }
  sub git_snapshot {
        my $format = $input_params{'snapshot_format'};
        if (!@snapshot_fmts) {
                die_error(403, "Unsupported snapshot format");
        }
  
-       if (!defined $hash) {
-               $hash = git_get_head_hash($project);
+       my $type = git_get_type("$hash^{}");
+       if (!$type) {
+               die_error(404, 'Object does not exist');
+       }  elsif ($type eq 'blob') {
+               die_error(400, 'Object is not a tree-ish');
        }
  
-       my $name = $project;
-       $name =~ s,([^/])/*\.git$,$1,;
-       $name = basename($name);
-       my $filename = to_utf8($name);
-       $name =~ s/\047/\047\\\047\047/g;
-       my $cmd;
-       $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
-       $cmd = quote_command(
+       my ($name, $prefix) = snapshot_name($project, $hash);
+       my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
+       my $cmd = quote_command(
                git_cmd(), 'archive',
                "--format=$known_snapshot_formats{$format}{'format'}",
-               "--prefix=$name/", $hash);
+               "--prefix=$prefix/", $hash);
        if (exists $known_snapshot_formats{$format}{'compressor'}) {
                $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
        }
  
+       $filename =~ s/(["\\])/\\$1/g;
        print $cgi->header(
                -type => $known_snapshot_formats{$format}{'type'},
-               -content_disposition => 'inline; filename="' . "$filename" . '"',
+               -content_disposition => 'inline; filename="' . $filename . '"',
                -status => '200 OK');
  
        open my $fd, "-|", $cmd
        close $fd;
  }
  
 -sub git_log {
 +sub git_log_generic {
 +      my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
 +
        my $head = git_get_head_hash($project);
 -      if (!defined $hash) {
 -              $hash = $head;
 +      if (!defined $base) {
 +              $base = $head;
        }
        if (!defined $page) {
                $page = 0;
        }
        my $refs = git_get_references();
  
 -      my @commitlist = parse_commits($hash, 101, (100 * $page));
 +      my $commit_hash = $base;
 +      if (defined $parent) {
 +              $commit_hash = "$parent..$base";
 +      }
 +      my @commitlist =
 +              parse_commits($commit_hash, 101, (100 * $page),
 +                            defined $file_name ? ($file_name, "--full-history") : ());
  
 -      my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
 +      my $ftype;
 +      if (!defined $file_hash && defined $file_name) {
 +              # some commits could have deleted file in question,
 +              # and not have it in tree, but one of them has to have it
 +              for (my $i = 0; $i < @commitlist; $i++) {
 +                      $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
 +                      last if defined $file_hash;
 +              }
 +      }
 +      if (defined $file_hash) {
 +              $ftype = git_get_type($file_hash);
 +      }
 +      if (defined $file_name && !defined $ftype) {
 +              die_error(500, "Unknown type of object");
 +      }
 +      my %co;
 +      if (defined $file_name) {
 +              %co = parse_commit($base)
 +                      or die_error(404, "Unknown commit object");
 +      }
  
 -      my ($patch_max) = gitweb_get_feature('patches');
 -      if ($patch_max) {
 +
 +      my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
 +      my $next_link = '';
 +      if ($#commitlist >= 100) {
 +              $next_link =
 +                      $cgi->a({-href => href(-replay=>1, page=>$page+1),
 +                               -accesskey => "n", -title => "Alt-n"}, "next");
 +      }
 +      my $patch_max = gitweb_get_feature('patches');
 +      if ($patch_max && !defined $file_name) {
                if ($patch_max < 0 || @commitlist <= $patch_max) {
                        $paging_nav .= " &sdot; " .
                                $cgi->a({-href => href(action=>"patches", -replay=>1)},
        }
  
        git_header_html();
 -      git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
 -
 -      if (!@commitlist) {
 -              my %co = parse_commit($hash);
 -
 -              git_print_header_div('summary', $project);
 -              print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
 +      git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
 +      if (defined $file_name) {
 +              git_print_header_div('commit', esc_html($co{'title'}), $base);
 +      } else {
 +              git_print_header_div('summary', $project)
        }
 -      my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
 -      for (my $i = 0; $i <= $to; $i++) {
 -              my %co = %{$commitlist[$i]};
 -              next if !%co;
 -              my $commit = $co{'id'};
 -              my $ref = format_ref_marker($refs, $commit);
 -              my %ad = parse_date($co{'author_epoch'});
 -              git_print_header_div('commit',
 -                             "<span class=\"age\">$co{'age_string'}</span>" .
 -                             esc_html($co{'title'}) . $ref,
 -                             $commit);
 -              print "<div class=\"title_text\">\n" .
 -                    "<div class=\"log_link\">\n" .
 -                    $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") .
 -                    "<br/>\n" .
 -                    "</div>\n";
 -                    git_print_authorship(\%co, -tag => 'span');
 -                    print "<br/>\n</div>\n";
 +      git_print_page_path($file_name, $ftype, $hash_base)
 +              if (defined $file_name);
 +
 +      $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
 +                   $file_name, $file_hash, $ftype);
  
 -              print "<div class=\"log_body\">\n";
 -              git_print_log($co{'comment'}, -final_empty_line=> 1);
 -              print "</div>\n";
 -      }
 -      if ($#commitlist >= 100) {
 -              print "<div class=\"page_nav\">\n";
 -              print $cgi->a({-href => href(-replay=>1, page=>$page+1),
 -                             -accesskey => "n", -title => "Alt-n"}, "next");
 -              print "</div>\n";
 -      }
        git_footer_html();
  }
  
 +sub git_log {
 +      git_log_generic('log', \&git_log_body,
 +                      $hash, $hash_parent);
 +}
 +
  sub git_commit {
        $hash ||= $hash_base || "HEAD";
        my %co = parse_commit($hash)
                        } @$parents ) .
                        ')';
        }
 -      if (gitweb_check_feature('patches')) {
 +      if (gitweb_check_feature('patches') && @$parents <= 1) {
                $formats_nav .= " | " .
                        $cgi->a({-href => href(action=>"patch", -replay=>1)},
                                "patch");
@@@ -5733,7 -5663,7 +5780,7 @@@ sub git_commitdiff 
                $formats_nav =
                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
                                "raw");
 -              if ($patch_max) {
 +              if ($patch_max && @{$co{'parents'}} <= 1) {
                        $formats_nav .= " | " .
                                $cgi->a({-href => href(action=>"patch", -replay=>1)},
                                        "patch");
@@@ -5941,7 -5871,7 +5988,7 @@@ sub git_commitdiff_plain 
  
  # format-patch-style patches
  sub git_patch {
 -      git_commitdiff(-format => 'patch', -single=> 1);
 +      git_commitdiff(-format => 'patch', -single => 1);
  }
  
  sub git_patches {
  }
  
  sub git_history {
 -      if (!defined $hash_base) {
 -              $hash_base = git_get_head_hash($project);
 -      }
 -      if (!defined $page) {
 -              $page = 0;
 -      }
 -      my $ftype;
 -      my %co = parse_commit($hash_base)
 -          or die_error(404, "Unknown commit object");
 -
 -      my $refs = git_get_references();
 -      my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
 -
 -      my @commitlist = parse_commits($hash_base, 101, (100 * $page),
 -                                     $file_name, "--full-history")
 -          or die_error(404, "No such file or directory on given branch");
 -
 -      if (!defined $hash && defined $file_name) {
 -              # some commits could have deleted file in question,
 -              # and not have it in tree, but one of them has to have it
 -              for (my $i = 0; $i <= @commitlist; $i++) {
 -                      $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
 -                      last if defined $hash;
 -              }
 -      }
 -      if (defined $hash) {
 -              $ftype = git_get_type($hash);
 -      }
 -      if (!defined $ftype) {
 -              die_error(500, "Unknown type of object");
 -      }
 -
 -      my $paging_nav = '';
 -      if ($page > 0) {
 -              $paging_nav .=
 -                      $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
 -                                             file_name=>$file_name)},
 -                              "first");
 -              $paging_nav .= " &sdot; " .
 -                      $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) {
 -              $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";
 -      }
 -
 -      git_header_html();
 -      git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
 -      git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 -      git_print_page_path($file_name, $ftype, $hash_base);
 -
 -      git_history_body(\@commitlist, 0, 99,
 -                       $refs, $hash_base, $ftype, $next_link);
 -
 -      git_footer_html();
 +      git_log_generic('history', \&git_history_body,
 +                      $hash_base, $hash_parent_base,
 +                      $file_name, $hash);
  }
  
  sub git_search {
  }
  
  sub git_shortlog {
 -      my $head = git_get_head_hash($project);
 -      if (!defined $hash) {
 -              $hash = $head;
 -      }
 -      if (!defined $page) {
 -              $page = 0;
 -      }
 -      my $refs = git_get_references();
 -
 -      my $commit_hash = $hash;
 -      if (defined $hash_parent) {
 -              $commit_hash = "$hash_parent..$hash";
 -      }
 -      my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
 -
 -      my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
 -      my $next_link = '';
 -      if ($#commitlist >= 100) {
 -              $next_link =
 -                      $cgi->a({-href => href(-replay=>1, page=>$page+1),
 -                               -accesskey => "n", -title => "Alt-n"}, "next");
 -      }
 -      my $patch_max = gitweb_check_feature('patches');
 -      if ($patch_max) {
 -              if ($patch_max < 0 || @commitlist <= $patch_max) {
 -                      $paging_nav .= " &sdot; " .
 -                              $cgi->a({-href => href(action=>"patches", -replay=>1)},
 -                                      "patches");
 -              }
 -      }
 -
 -      git_header_html();
 -      git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
 -      git_print_header_div('summary', $project);
 -
 -      git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
 -
 -      git_footer_html();
 +      git_log_generic('shortlog', \&git_shortlog_body,
 +                      $hash, $hash_parent);
  }
  
  ## ......................................................................
index d0ff21d426e294cd2120813e7094b017c6ecd766,0688a57e1d8cd252232095203af48013c2cbfc60..0688a57e1d8cd252232095203af48013c2cbfc60
mode 100755,100644..100755
@@@ -75,4 -75,43 +75,43 @@@ test_expect_success 
  test_debug 'cat gitweb.output'
  
  
+ # ----------------------------------------------------------------------
+ # snapshot hash ids
+ test_expect_success 'snapshots: good tree-ish id' '
+       gitweb_run "p=.git;a=snapshot;h=master;sf=tgz" &&
+       grep "Status: 200 OK" gitweb.output
+ '
+ test_debug 'cat gitweb.output'
+ test_expect_success 'snapshots: bad tree-ish id' '
+       gitweb_run "p=.git;a=snapshot;h=frizzumFrazzum;sf=tgz" &&
+       grep "404 - Object does not exist" gitweb.output
+ '
+ test_debug 'cat gitweb.output'
+ test_expect_success 'snapshots: bad tree-ish id (tagged object)' '
+       echo object > tag-object &&
+       git add tag-object &&
+       git commit -m "Object to be tagged" &&
+       git tag tagged-object `git hash-object tag-object` &&
+       gitweb_run "p=.git;a=snapshot;h=tagged-object;sf=tgz" &&
+       grep "400 - Object is not a tree-ish" gitweb.output
+ '
+ test_debug 'cat gitweb.output'
+ test_expect_success 'snapshots: good object id' '
+       ID=`git rev-parse --verify HEAD` &&
+       gitweb_run "p=.git;a=snapshot;h=$ID;sf=tgz" &&
+       grep "Status: 200 OK" gitweb.output
+ '
+ test_debug 'cat gitweb.output'
+ test_expect_success 'snapshots: bad object id' '
+       gitweb_run "p=.git;a=snapshot;h=abcdef01234;sf=tgz" &&
+       grep "404 - Object does not exist" gitweb.output
+ '
+ test_debug 'cat gitweb.output'
  test_done