Merge branch 'jc/checkout-merge-base'
[gitweb.git] / gitweb / gitweb.perl
index 3368f2af7cda4965bbee68860ab9cd8daa942ec8..7e477af9567cff322d18fc9a1fbfe507732418dc 100755 (executable)
@@ -168,7 +168,8 @@ BEGIN
        #       'suffix' => filename suffix,
        #       'format' => --format for git-archive,
        #       'compressor' => [compressor command and arguments]
-       #                       (array reference, optional)}
+       #                       (array reference, optional)
+       #       'disabled' => boolean (optional)}
        #
        'tgz' => {
                'display' => 'tar.gz',
@@ -184,6 +185,14 @@ BEGIN
                'format' => 'tar',
                'compressor' => ['bzip2']},
 
+       'txz' => {
+               'display' => 'tar.xz',
+               'type' => 'application/x-xz',
+               'suffix' => '.tar.xz',
+               'format' => 'tar',
+               'compressor' => ['xz'],
+               'disabled' => 1},
+
        'zip' => {
                'display' => 'zip',
                'type' => 'application/x-zip',
@@ -196,6 +205,7 @@ BEGIN
 our %known_snapshot_format_aliases = (
        'gzip'  => 'tgz',
        'bzip2' => 'tbz2',
+       'xz'    => 'txz',
 
        # backward compatibility: legacy gitweb config support
        'x-gzip' => undef, 'gz' => undef,
@@ -295,6 +305,19 @@ BEGIN
                '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
@@ -427,7 +450,7 @@ sub gitweb_get_feature {
                @{$feature{$name}{'default'}});
        if (!$override) { return @defaults; }
        if (!defined $sub) {
-               warn "feature $name is not overrideable";
+               warn "feature $name is not overridable";
                return @defaults;
        }
        return $sub->(@defaults);
@@ -516,7 +539,8 @@ sub filter_snapshot_fmts {
                exists $known_snapshot_format_aliases{$_} ?
                       $known_snapshot_format_aliases{$_} : $_} @fmts;
        @fmts = grep {
-               exists $known_snapshot_formats{$_} } @fmts;
+               exists $known_snapshot_formats{$_} &&
+               !$known_snapshot_formats{$_}{'disabled'}} @fmts;
 }
 
 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
@@ -967,10 +991,13 @@ sub href {
                        if (defined $params{'hash_parent_base'}) {
                                $href .= esc_url($params{'hash_parent_base'});
                                # skip the file_parent if it's the same as the file_name
-                               delete $params{'file_parent'} if $params{'file_parent'} eq $params{'file_name'};
-                               if (defined $params{'file_parent'} && $params{'file_parent'} !~ /\.\./) {
-                                       $href .= ":/".esc_url($params{'file_parent'});
-                                       delete $params{'file_parent'};
+                               if (defined $params{'file_parent'}) {
+                                       if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
+                                               delete $params{'file_parent'};
+                                       } elsif ($params{'file_parent'} !~ /\.\./) {
+                                               $href .= ":/".esc_url($params{'file_parent'});
+                                               delete $params{'file_parent'};
+                                       }
                                }
                                $href .= "..";
                                delete $params{'hash_parent'};
@@ -1096,8 +1123,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;
 }
@@ -1537,10 +1563,10 @@ sub format_subject_html {
                $long =~ s/[[:cntrl:]]/?/g;
                return $cgi->a({-href => $href, -class => "list subject",
                                -title => to_utf8($long)},
-                      esc_html($short) . $extra);
+                      esc_html($short)) . $extra;
        } else {
                return $cgi->a({-href => $href, -class => "list subject"},
-                      esc_html($long)  . $extra);
+                      esc_html($long)) . $extra;
        }
 }
 
@@ -1608,6 +1634,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).
@@ -1616,8 +1665,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) ..."
@@ -1997,16 +2048,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;
@@ -2598,7 +2660,7 @@ sub parse_commit_text {
                } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
                        push @parents, $1;
                } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
-                       $co{'author'} = $1;
+                       $co{'author'} = to_utf8($1);
                        $co{'author_epoch'} = $2;
                        $co{'author_tz'} = $3;
                        if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
@@ -2608,10 +2670,9 @@ sub parse_commit_text {
                                $co{'author_name'} = $co{'author'};
                        }
                } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
-                       $co{'committer'} = $1;
+                       $co{'committer'} = to_utf8($1);
                        $co{'committer_epoch'} = $2;
                        $co{'committer_tz'} = $3;
-                       $co{'committer_name'} = $co{'committer'};
                        if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
                                $co{'committer_name'}  = $1;
                                $co{'committer_email'} = $2;
@@ -2779,16 +2840,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;
@@ -3352,22 +3428,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) {
@@ -3414,10 +3486,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)
@@ -3436,8 +3509,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>" .
@@ -3605,6 +3682,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'},
@@ -3650,12 +3730,14 @@ sub git_print_tree_entry {
        } 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 " | " .
@@ -4340,6 +4422,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) = @_;
@@ -4386,7 +4508,8 @@ sub git_shortlog_body {
 
 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});
@@ -4420,7 +4543,7 @@ sub git_history_body {
                      $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) {
@@ -4883,6 +5006,17 @@ sub git_blame_common {
        my $formats_nav =
                $cgi->a({-href => href(action=>"blob", -replay=>1)},
                        "blob") .
+               " | ";
+       if ($format eq 'incremental') {
+               $formats_nav .=
+                       $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
+                               "blame") . " (non-incremental)";
+       } else {
+               $formats_nav .=
+                       $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
+                               "blame") . " (incremental)";
+       }
+       $formats_nav .=
                " | " .
                $cgi->a({-href => href(action=>"history", -replay=>1)},
                        "history") .
@@ -5198,7 +5332,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);
                }
        }
@@ -5221,10 +5356,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
@@ -5235,7 +5374,6 @@ sub git_tree {
        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) {
@@ -5251,7 +5389,8 @@ sub git_tree {
                        # 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;
@@ -5284,8 +5423,10 @@ sub git_tree {
                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";
@@ -5294,7 +5435,7 @@ sub git_tree {
                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";
@@ -5312,6 +5453,43 @@ sub git_tree {
        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) {
@@ -5323,32 +5501,33 @@ sub git_snapshot {
                die_error(400, "Invalid snapshot format parameter");
        } elsif (!exists($known_snapshot_formats{$format})) {
                die_error(400, "Unknown snapshot format");
+       } elsif ($known_snapshot_formats{$format}{'disabled'}) {
+               die_error(403, "Snapshot format not allowed");
        } elsif (!grep($_ eq $format, @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
@@ -5359,22 +5538,57 @@ sub git_snapshot {
        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 $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 $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
 
-       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)},
@@ -5383,50 +5597,26 @@ sub git_log {
        }
 
        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)
@@ -5459,7 +5649,7 @@ sub git_commit {
                        } @$parents ) .
                        ')';
        }
-       if (gitweb_check_feature('patches')) {
+       if (gitweb_check_feature('patches') && @$parents <= 1) {
                $formats_nav .= " | " .
                        $cgi->a({-href => href(action=>"patch", -replay=>1)},
                                "patch");
@@ -5747,7 +5937,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");
@@ -5955,7 +6145,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 {
@@ -5963,70 +6153,9 @@ 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 {
@@ -6290,44 +6419,8 @@ sub git_search_help {
 }
 
 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);
 }
 
 ## ......................................................................