Implement git clone -v
[gitweb.git] / gitweb / gitweb.perl
index 5351da2c116e2bb37b7e1f6983c374f6f344ed95..11168006cffe9bd2f6c8c27bd1d034c41667e39a 100755 (executable)
@@ -27,6 +27,13 @@ BEGIN
 our $my_url = $cgi->url();
 our $my_uri = $cgi->url(-absolute => 1);
 
+# if we're called with PATH_INFO, we have to strip that
+# from the URL to find our real URL
+if (my $path_info = $ENV{"PATH_INFO"}) {
+       $my_url =~ s,\Q$path_info\E$,,;
+       $my_uri =~ s,\Q$path_info\E$,,;
+}
+
 # core git executable to use
 # this can just be "git" if your webserver has a sensible PATH
 our $GIT = "++GIT_BINDIR++/git";
@@ -275,6 +282,44 @@ BEGIN
        'forks' => {
                'override' => 0,
                'default' => [0]},
+
+       # Insert custom links to the action bar of all project pages.
+       # This enables you mainly to link to third-party scripts integrating
+       # into gitweb; e.g. git-browser for graphical history representation
+       # or custom web-based repository administration interface.
+
+       # The 'default' value consists of a list of triplets in the form
+       # (label, link, position) where position is the label after which
+       # to inster the link and link is a format string where %n expands
+       # to the project name, %f to the project path within the filesystem,
+       # %h to the current hash (h gitweb parameter) and %b to the current
+       # hash base (hb gitweb parameter).
+
+       # To enable system wide have in $GITWEB_CONFIG e.g.
+       # $feature{'actions'}{'default'} = [('graphiclog',
+       #       '/git-browser/by-commit.html?r=%n', 'summary')];
+       # Project specific override is not supported.
+       'actions' => {
+               'override' => 0,
+               'default' => []},
+
+       # Allow gitweb scan project content tags described in ctags/
+       # of project repository, and display the popular Web 2.0-ish
+       # "tag cloud" near the project list. Note that this is something
+       # COMPLETELY different from the normal Git tags.
+
+       # gitweb by itself can show existing tags, but it does not handle
+       # tagging itself; you need an external application for that.
+       # For an example script, check Girocco's cgi/tagproj.cgi.
+       # You may want to install the HTML::TagCloud Perl module to get
+       # a pretty tag cloud instead of just a list of tags.
+
+       # To enable system wide have in $GITWEB_CONFIG
+       # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
+       # Project specific override is not supported.
+       'ctags' => {
+               'override' => 0,
+               'default' => [0]},
 );
 
 sub gitweb_check_feature {
@@ -539,7 +584,7 @@ sub evaluate_path_info {
 
 # dispatch
 my %actions = (
-       "blame" => \&git_blame2,
+       "blame" => \&git_blame,
        "blobdiff" => \&git_blobdiff,
        "blobdiff_plain" => \&git_blobdiff_plain,
        "blob" => \&git_blob,
@@ -775,7 +820,7 @@ sub quot_cec {
        );
        my $chr = ( (exists $es{$cntrl})
                    ? $es{$cntrl}
-                   : sprintf('\%03o', ord($cntrl)) );
+                   : sprintf('\%2x', ord($cntrl)) );
        if ($opts{-nohtml}) {
                return $chr;
        } else {
@@ -1090,13 +1135,23 @@ sub format_log_line_html {
 }
 
 # format marker of refs pointing to given object
+
+# the destination action is chosen based on object type and current context:
+# - for annotated tags, we choose the tag view unless it's the current view
+#   already, in which case we go to shortlog view
+# - for other refs, we keep the current view if we're in history, shortlog or
+#   log view, and select shortlog otherwise
 sub format_ref_marker {
        my ($refs, $id) = @_;
        my $markers = '';
 
        if (defined $refs->{$id}) {
                foreach my $ref (@{$refs->{$id}}) {
+                       # this code exploits the fact that non-lightweight tags are the
+                       # only indirect objects, and that they are the only objects for which
+                       # we want to use tag instead of shortlog as action
                        my ($type, $name) = qw();
+                       my $indirect = ($ref =~ s/\^\{\}$//);
                        # e.g. tags/v2.6.11 or heads/next
                        if ($ref =~ m!^(.*?)s?/(.*)$!) {
                                $type = $1;
@@ -1106,8 +1161,29 @@ sub format_ref_marker {
                                $name = $ref;
                        }
 
-                       $markers .= " <span class=\"$type\" title=\"$ref\">" .
-                                   esc_html($name) . "</span>";
+                       my $class = $type;
+                       $class .= " indirect" if $indirect;
+
+                       my $dest_action = "shortlog";
+
+                       if ($indirect) {
+                               $dest_action = "tag" unless $action eq "tag";
+                       } elsif ($action =~ /^(history|(short)?log)$/) {
+                               $dest_action = $action;
+                       }
+
+                       my $dest = "";
+                       $dest .= "refs/" unless $ref =~ m!^refs/!;
+                       $dest .= $ref;
+
+                       my $link = $cgi->a({
+                               -href => href(
+                                       action=>$dest_action,
+                                       hash=>$dest
+                               )}, $name);
+
+                       $markers .= " <span class=\"$class\" title=\"$ref\">" .
+                               $link . "</span>";
                }
        }
 
@@ -1724,6 +1800,67 @@ sub git_get_project_description {
        return $descr;
 }
 
+sub git_get_project_ctags {
+       my $path = shift;
+       my $ctags = {};
+
+       $git_dir = "$projectroot/$path";
+       foreach (<$git_dir/ctags/*>) {
+               open CT, $_ or next;
+               my $val = <CT>;
+               chomp $val;
+               close CT;
+               my $ctag = $_; $ctag =~ s#.*/##;
+               $ctags->{$ctag} = $val;
+       }
+       $ctags;
+}
+
+sub git_populate_project_tagcloud {
+       my $ctags = shift;
+
+       # First, merge different-cased tags; tags vote on casing
+       my %ctags_lc;
+       foreach (keys %$ctags) {
+               $ctags_lc{lc $_}->{count} += $ctags->{$_};
+               if (not $ctags_lc{lc $_}->{topcount}
+                   or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
+                       $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
+                       $ctags_lc{lc $_}->{topname} = $_;
+               }
+       }
+
+       my $cloud;
+       if (eval { require HTML::TagCloud; 1; }) {
+               $cloud = HTML::TagCloud->new;
+               foreach (sort keys %ctags_lc) {
+                       # Pad the title with spaces so that the cloud looks
+                       # less crammed.
+                       my $title = $ctags_lc{$_}->{topname};
+                       $title =~ s/ /&nbsp;/g;
+                       $title =~ s/^/&nbsp;/g;
+                       $title =~ s/$/&nbsp;/g;
+                       $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
+               }
+       } else {
+               $cloud = \%ctags_lc;
+       }
+       $cloud;
+}
+
+sub git_show_project_tagcloud {
+       my ($cloud, $count) = @_;
+       print STDERR ref($cloud)."..\n";
+       if (ref $cloud eq 'HTML::TagCloud') {
+               return $cloud->html_and_css($count);
+       } else {
+               my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
+               return '<p align="center">' . join (', ', map {
+                       "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
+               } splice(@tags, 0, $count)) . '</p>';
+       }
+}
+
 sub git_get_project_url_list {
        my $path = shift;
 
@@ -1772,9 +1909,7 @@ sub git_get_projects_list {
 
                                my $subdir = substr($File::Find::name, $pfxlen + 1);
                                # we check related file in $projectroot
-                               if ($check_forks and $subdir =~ m#/.#) {
-                                       $File::Find::prune = 1;
-                               } elsif (check_export_ok("$projectroot/$filter/$subdir")) {
+                               if (check_export_ok("$projectroot/$filter/$subdir")) {
                                        push @list, { path => ($filter ? "$filter/" : '') . $subdir };
                                        $File::Find::prune = 1;
                                }
@@ -1918,7 +2053,7 @@ sub git_get_references {
 
        while (my $line = <$fd>) {
                chomp $line;
-               if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type/?[^^]+)!) {
+               if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
                        if (defined $refs{$1}) {
                                push @{$refs{$1}}, $2;
                        } else {
@@ -2092,7 +2227,7 @@ sub parse_commit_text {
                        last;
                }
        }
-       if ($co{'title'} eq "") {
+       if (! defined $co{'title'} || $co{'title'} eq "") {
                $co{'title'} = $co{'title_short'} = '(no commit message)';
        }
        # remove added spaces
@@ -2726,13 +2861,26 @@ sub git_print_page_nav {
                        }
                }
        }
+
        $arg{'tree'}{'hash'} = $treehead if defined $treehead;
        $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
 
+       my @actions = gitweb_check_feature('actions');
+       while (@actions) {
+               my ($label, $link, $pos) = (shift(@actions), shift(@actions), shift(@actions));
+               @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
+               # munch munch
+               $link =~ s#%n#$project#g;
+               $link =~ s#%f#$git_dir#g;
+               $treehead ? $link =~ s#%h#$treehead#g : $link =~ s#%h##g;
+               $treebase ? $link =~ s#%b#$treebase#g : $link =~ s#%b##g;
+               $arg{$label}{'_href'} = $link;
+       }
+
        print "<div class=\"page_nav\">\n" .
                (join " | ",
                 map { $_ eq $current ?
-                      $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
+                      $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
                 } @navs);
        print "<br/>\n$extra<br/>\n" .
              "</div>\n";
@@ -3535,21 +3683,25 @@ sub git_patchset_body {
 
 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
 
-sub git_project_list_body {
-       my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
-
-       my ($check_forks) = gitweb_check_feature('forks');
-
+# fills project list info (age, description, owner, forks) for each
+# project in the list, removing invalid projects from returned list
+# NOTE: modifies $projlist, but does not remove entries from it
+sub fill_project_list_info {
+       my ($projlist, $check_forks) = @_;
        my @projects;
+
+       my $show_ctags = gitweb_check_feature('ctags');
+ PROJECT:
        foreach my $pr (@$projlist) {
-               my (@aa) = git_get_last_activity($pr->{'path'});
-               unless (@aa) {
-                       next;
+               my (@activity) = git_get_last_activity($pr->{'path'});
+               unless (@activity) {
+                       next PROJECT;
                }
-               ($pr->{'age'}, $pr->{'age_string'}) = @aa;
+               ($pr->{'age'}, $pr->{'age_string'}) = @activity;
                if (!defined $pr->{'descr'}) {
                        my $descr = git_get_project_description($pr->{'path'}) || "";
-                       $pr->{'descr_long'} = to_utf8($descr);
+                       $descr = to_utf8($descr);
+                       $pr->{'descr_long'} = $descr;
                        $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
                }
                if (!defined $pr->{'owner'}) {
@@ -3561,66 +3713,98 @@ sub git_project_list_body {
                            ($pname !~ /\/$/) &&
                            (-d "$projectroot/$pname")) {
                                $pr->{'forks'} = "-d $projectroot/$pname";
-                       }
-                       else {
+                       }       else {
                                $pr->{'forks'} = 0;
                        }
                }
+               $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
                push @projects, $pr;
        }
 
+       return @projects;
+}
+
+# print 'sort by' <th> element, generating 'sort by $name' replay link
+# if that order is not selected
+sub print_sort_th {
+       my ($name, $order, $header) = @_;
+       $header ||= ucfirst($name);
+
+       if ($order eq $name) {
+               print "<th>$header</th>\n";
+       } else {
+               print "<th>" .
+                     $cgi->a({-href => href(-replay=>1, order=>$name),
+                              -class => "header"}, $header) .
+                     "</th>\n";
+       }
+}
+
+sub git_project_list_body {
+       # actually uses global variable $project
+       my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
+
+       my ($check_forks) = gitweb_check_feature('forks');
+       my @projects = fill_project_list_info($projlist, $check_forks);
+
        $order ||= $default_projects_order;
        $from = 0 unless defined $from;
        $to = $#projects if (!defined $to || $#projects < $to);
 
+       my %order_info = (
+               project => { key => 'path', type => 'str' },
+               descr => { key => 'descr_long', type => 'str' },
+               owner => { key => 'owner', type => 'str' },
+               age => { key => 'age', type => 'num' }
+       );
+       my $oi = $order_info{$order};
+       if ($oi->{'type'} eq 'str') {
+               @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
+       } else {
+               @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
+       }
+
+       my $show_ctags = gitweb_check_feature('ctags');
+       if ($show_ctags) {
+               my %ctags;
+               foreach my $p (@projects) {
+                       foreach my $ct (keys %{$p->{'ctags'}}) {
+                               $ctags{$ct} += $p->{'ctags'}->{$ct};
+                       }
+               }
+               my $cloud = git_populate_project_tagcloud(\%ctags);
+               print git_show_project_tagcloud($cloud, 64);
+       }
+
        print "<table class=\"project_list\">\n";
        unless ($no_header) {
                print "<tr>\n";
                if ($check_forks) {
                        print "<th></th>\n";
                }
-               if ($order eq "project") {
-                       @projects = sort {$a->{'path'} cmp $b->{'path'}} @projects;
-                       print "<th>Project</th>\n";
-               } else {
-                       print "<th>" .
-                             $cgi->a({-href => href(project=>undef, order=>'project'),
-                                      -class => "header"}, "Project") .
-                             "</th>\n";
-               }
-               if ($order eq "descr") {
-                       @projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects;
-                       print "<th>Description</th>\n";
-               } else {
-                       print "<th>" .
-                             $cgi->a({-href => href(project=>undef, order=>'descr'),
-                                      -class => "header"}, "Description") .
-                             "</th>\n";
-               }
-               if ($order eq "owner") {
-                       @projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects;
-                       print "<th>Owner</th>\n";
-               } else {
-                       print "<th>" .
-                             $cgi->a({-href => href(project=>undef, order=>'owner'),
-                                      -class => "header"}, "Owner") .
-                             "</th>\n";
-               }
-               if ($order eq "age") {
-                       @projects = sort {$a->{'age'} <=> $b->{'age'}} @projects;
-                       print "<th>Last Change</th>\n";
-               } else {
-                       print "<th>" .
-                             $cgi->a({-href => href(project=>undef, order=>'age'),
-                                      -class => "header"}, "Last Change") .
-                             "</th>\n";
-               }
-               print "<th></th>\n" .
+               print_sort_th('project', $order, 'Project');
+               print_sort_th('descr', $order, 'Description');
+               print_sort_th('owner', $order, 'Owner');
+               print_sort_th('age', $order, 'Last Change');
+               print "<th></th>\n" . # for links
                      "</tr>\n";
        }
        my $alternate = 1;
+       my $tagfilter = $cgi->param('by_tag');
        for (my $i = $from; $i <= $to; $i++) {
                my $pr = $projects[$i];
+
+               next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
+               next if $searchtext and not $pr->{'path'} =~ /$searchtext/
+                       and not $pr->{'descr_long'} =~ /$searchtext/;
+               # Weed out forks or non-matching entries of search
+               if ($check_forks) {
+                       my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
+                       $forkbase="^$forkbase" if $forkbase;
+                       next if not $searchtext and not $tagfilter and $show_ctags
+                               and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
+               }
+
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
                } else {
@@ -3955,6 +4139,11 @@ sub git_project_list {
                close $fd;
                print "</div>\n";
        }
+       print $cgi->startform(-method => "get") .
+             "<p class=\"projsearch\">Search:\n" .
+             $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
+             "</p>" .
+             $cgi->end_form() . "\n";
        git_project_list_body(\@list, $order);
        git_footer_html();
 }
@@ -4026,10 +4215,10 @@ sub git_summary {
 
        print "<div class=\"title\">&nbsp;</div>\n";
        print "<table class=\"projects_list\">\n" .
-             "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
-             "<tr><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
+             "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
+             "<tr id=\"metadata_owner\"><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";
+               print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
        }
 
        # use per project git URL list in $projectroot/$project/cloneurl
@@ -4039,9 +4228,23 @@ sub git_summary {
        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
        foreach my $git_url (@url_list) {
                next unless $git_url;
-               print "<tr><td>$url_tag</td><td>$git_url</td></tr>\n";
+               print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
                $url_tag = "";
        }
+
+       # Tag cloud
+       my $show_ctags = (gitweb_check_feature('ctags'))[0];
+       if ($show_ctags) {
+               my $ctags = git_get_project_ctags($project);
+               my $cloud = git_populate_project_tagcloud($ctags);
+               print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
+               print "</td>\n<td>" unless %$ctags;
+               print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
+               print "</td>\n<td>" if %$ctags;
+               print git_show_project_tagcloud($cloud, 48);
+               print "</td></tr>";
+       }
+
        print "</table>\n";
 
        if (-s "$projectroot/$project/README.html") {
@@ -4080,10 +4283,10 @@ sub git_summary {
 
        if (@forklist) {
                git_print_header_div('forks');
-               git_project_list_body(\@forklist, undef, 0, 15,
+               git_project_list_body(\@forklist, 'age', 0, 15,
                                      $#forklist <= 15 ? undef :
                                      $cgi->a({-href => href(action=>"forks")}, "..."),
-                                     'noheader');
+                                     'no_header');
        }
 
        git_footer_html();
@@ -4128,7 +4331,7 @@ sub git_tag {
        git_footer_html();
 }
 
-sub git_blame2 {
+sub git_blame {
        my $fd;
        my $ftype;
 
@@ -4235,103 +4438,6 @@ sub git_blame2 {
        git_footer_html();
 }
 
-sub git_blame {
-       my $fd;
-
-       my ($have_blame) = gitweb_check_feature('blame');
-       if (!$have_blame) {
-               die_error('403 Permission denied', "Permission denied");
-       }
-       die_error('404 Not Found', "File name not defined") if (!$file_name);
-       $hash_base ||= git_get_head_hash($project);
-       die_error(undef, "Couldn't find base commit") unless ($hash_base);
-       my %co = parse_commit($hash_base)
-               or die_error(undef, "Reading commit failed");
-       if (!defined $hash) {
-               $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
-                       or die_error(undef, "Error lookup file");
-       }
-       open ($fd, "-|", git_cmd(), "annotate", '-l', '-t', '-r', $file_name, $hash_base)
-               or die_error(undef, "Open git-annotate failed");
-       git_header_html();
-       my $formats_nav =
-               $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
-                       "blob") .
-               " | " .
-               $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
-                       "history") .
-               " | " .
-               $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
-                       "HEAD");
-       git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
-       git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
-       git_print_page_path($file_name, 'blob', $hash_base);
-       print "<div class=\"page_body\">\n";
-       print <<HTML;
-<table class="blame">
-  <tr>
-    <th>Commit</th>
-    <th>Age</th>
-    <th>Author</th>
-    <th>Line</th>
-    <th>Data</th>
-  </tr>
-HTML
-       my @line_class = (qw(light dark));
-       my $line_class_len = scalar (@line_class);
-       my $line_class_num = $#line_class;
-       while (my $line = <$fd>) {
-               my $long_rev;
-               my $short_rev;
-               my $author;
-               my $time;
-               my $lineno;
-               my $data;
-               my $age;
-               my $age_str;
-               my $age_class;
-
-               chomp $line;
-               $line_class_num = ($line_class_num + 1) % $line_class_len;
-
-               if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) [+-]\d\d\d\d\t(\d+)\)(.*)$/) {
-                       $long_rev = $1;
-                       $author   = $2;
-                       $time     = $3;
-                       $lineno   = $4;
-                       $data     = $5;
-               } else {
-                       print qq(  <tr><td colspan="5" class="error">Unable to parse: $line</td></tr>\n);
-                       next;
-               }
-               $short_rev  = substr ($long_rev, 0, 8);
-               $age        = time () - $time;
-               $age_str    = age_string ($age);
-               $age_str    =~ s/ /&nbsp;/g;
-               $age_class  = age_class($age);
-               $author     = esc_html ($author);
-               $author     =~ s/ /&nbsp;/g;
-
-               $data = untabify($data);
-               $data = esc_html ($data);
-
-               print <<HTML;
-  <tr class="$line_class[$line_class_num]">
-    <td class="sha1"><a href="${\href (action=>"commit", hash=>$long_rev)}" class="text">$short_rev..</a></td>
-    <td class="$age_class">$age_str</td>
-    <td>$author</td>
-    <td class="linenr"><a id="$lineno" href="#$lineno" class="linenr">$lineno</a></td>
-    <td class="pre">$data</td>
-  </tr>
-HTML
-       } # while (my $line = <$fd>)
-       print "</table>\n\n";
-       close $fd
-               or print "Reading blob failed.\n";
-       print "</div>";
-       git_footer_html();
-}
-
 sub git_tags {
        my $head = git_get_head_hash($project);
        git_header_html();
@@ -4498,6 +4604,7 @@ sub git_tree {
                        $hash = $hash_base;
                }
        }
+       die_error(404, "No such tree") unless defined($hash);
        $/ = "\0";
        open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
                or die_error(500, "Open git-ls-tree failed");
@@ -4538,8 +4645,8 @@ sub git_tree {
                if ($basedir ne '' && substr($basedir, -1) ne '/') {
                        $basedir .= '/';
                }
+               git_print_page_path($file_name, 'tree', $hash_base);
        }
-       git_print_page_path($file_name, 'tree', $hash_base);
        print "<div class=\"page_body\">\n";
        print "<table class=\"tree\">\n";
        my $alternate = 1;
@@ -5551,7 +5658,11 @@ sub git_shortlog {
        }
        my $refs = git_get_references();
 
-       my @commitlist = parse_commits($hash, 101, (100 * $page));
+       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 = '';