Merge branch 'pb/web'
authorJunio C Hamano <junkio@cox.net>
Sun, 5 Nov 2006 01:13:38 +0000 (17:13 -0800)
committerJunio C Hamano <junkio@cox.net>
Sun, 5 Nov 2006 01:13:38 +0000 (17:13 -0800)
* pb/web:
gitweb: Support for 'forks'

1  2 
gitweb/gitweb.perl
diff --combined gitweb/gitweb.perl
index 3759be37b8830ececee270c212a99d1a03f8b9fa,9237184a70041aea63eba304e04977920a71596e..3c6fd7ca47551eb808a40ca2cc7ed1690f19cc06
@@@ -51,8 -51,12 +51,8 @@@ our $site_footer = "++GITWEB_SITE_FOOTE
  
  # URI of stylesheets
  our @stylesheets = ("++GITWEB_CSS++");
 -our $stylesheet;
 -# default is not to define style sheet, but it can be overwritten later
 -undef $stylesheet;
 -
 -# URI of default stylesheet
 -our $stylesheet = "++GITWEB_CSS++";
 +# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
 +our $stylesheet = undef;
  # URI of GIT logo (72x27 size)
  our $logo = "++GITWEB_LOGO++";
  # URI of GIT favicon, assumed to be image/png type
@@@ -76,7 -80,7 +76,7 @@@ our $strict_export = "++GITWEB_STRICT_E
  
  # list of git base URLs used for URL to where fetch project from,
  # i.e. full URL is "$git_base_url/$project"
 -our @git_base_url_list = ("++GITWEB_BASE_URL++");
 +our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
  
  # default blob_plain mimetype and default charset for text/plain blob
  our $default_blob_plain_mimetype = 'text/plain';
@@@ -160,6 -164,21 +160,21 @@@ our %feature = 
        'pathinfo' => {
                'override' => 0,
                'default' => [0]},
+       # Make gitweb consider projects in project root subdirectories
+       # to be forks of existing projects. Given project $projname.git,
+       # projects matching $projname/*.git will not be shown in the main
+       # projects list, instead a '+' mark will be added to $projname
+       # there and a 'forks' view will be enabled for the project, listing
+       # all the forks. This feature is supported only if project list
+       # is taken from a directory, not file.
+       # To enable system wide have in $GITWEB_CONFIG
+       # $feature{'forks'}{'default'} = [1];
+       # Project specific override is not supported.
+       'forks' => {
+               'override' => 0,
+               'default' => [0]},
  );
  
  sub gitweb_check_feature {
@@@ -405,6 -424,7 +420,7 @@@ my %actions = 
        "commitdiff" => \&git_commitdiff,
        "commitdiff_plain" => \&git_commitdiff_plain,
        "commit" => \&git_commit,
+       "forks" => \&git_forks,
        "heads" => \&git_heads,
        "history" => \&git_history,
        "log" => \&git_log,
@@@ -554,17 -574,12 +570,17 @@@ sub esc_url 
  }
  
  # replace invalid utf8 character with SUBSTITUTION sequence
 -sub esc_html {
 +sub esc_html ($;%) {
        my $str = shift;
 +      my %opts = @_;
 +
        $str = to_utf8($str);
        $str = escapeHTML($str);
        $str =~ s/\014/^L/g; # escape FORM FEED (FF) character (e.g. in COPYING file)
        $str =~ s/\033/^[/g; # "escape" ESCAPE (\e) character (e.g. commit 20a3847d8a5032ce41f90dcc68abfb36e6fee9b1)
 +      if ($opts{'-nbsp'}) {
 +              $str =~ s/ /&nbsp;/g;
 +      }
        return $str;
  }
  
@@@ -789,7 -804,7 +805,7 @@@ sub format_diff_line 
                $diff_class = " incomplete";
        }
        $line = untabify($line);
 -      return "<div class=\"diff$diff_class\">" . esc_html($line) . "</div>\n";
 +      return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
  }
  
  ## ----------------------------------------------------------------------
@@@ -865,7 -880,7 +881,7 @@@ sub git_get_hash_by_path 
        close $fd or return undef;
  
        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
 -      $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
 +      $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
        if (defined $type && $type ne $2) {
                # type doesn't match
                return undef;
@@@ -897,15 -912,19 +913,21 @@@ sub git_get_project_url_list 
  }
  
  sub git_get_projects_list {
+       my ($filter) = @_;
        my @list;
  
+       $filter ||= '';
+       $filter =~ s/\.git$//;
        if (-d $projects_list) {
                # search in directory
-               my $dir = $projects_list;
+               my $dir = $projects_list . ($filter ? "/$filter" : '');
 +              # remove the trailing "/"
 +              $dir =~ s!/+$!!;
                my $pfxlen = length("$dir");
  
+               my $check_forks = gitweb_check_feature('forks');
                File::Find::find({
                        follow_fast => 1, # follow symbolic links
                        dangling_symlinks => 0, # ignore dangling symlinks, silently
  
                                my $subdir = substr($File::Find::name, $pfxlen + 1);
                                # we check related file in $projectroot
-                               if (check_export_ok("$projectroot/$subdir")) {
-                                       push @list, { path => $subdir };
+                               if ($check_forks and $subdir =~ m#/.#) {
+                                       $File::Find::prune = 1;
+                               } elsif (check_export_ok("$projectroot/$filter/$subdir")) {
+                                       push @list, { path => ($filter ? "$filter/" : '') . $subdir };
                                        $File::Find::prune = 1;
                                }
                        },
@@@ -983,24 -1004,6 +1007,24 @@@ sub git_get_project_owner 
        return $owner;
  }
  
 +sub git_get_last_activity {
 +      my ($path) = @_;
 +      my $fd;
 +
 +      $git_dir = "$projectroot/$path";
 +      open($fd, "-|", git_cmd(), 'for-each-ref',
 +           '--format=%(refname) %(committer)',
 +           '--sort=-committerdate',
 +           'refs/heads') or return;
 +      my $most_recent = <$fd>;
 +      close $fd or return;
 +      if ($most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
 +              my $timestamp = $1;
 +              my $age = time - $timestamp;
 +              return ($age, age_string($age));
 +      }
 +}
 +
  sub git_get_references {
        my $type = shift || "";
        my %refs;
@@@ -1107,6 -1110,24 +1131,6 @@@ sub parse_tag 
        return %tag
  }
  
 -sub git_get_last_activity {
 -      my ($path) = @_;
 -      my $fd;
 -
 -      $git_dir = "$projectroot/$path";
 -      open($fd, "-|", git_cmd(), 'for-each-ref',
 -           '--format=%(refname) %(committer)',
 -           '--sort=-committerdate',
 -           'refs/heads') or return;
 -      my $most_recent = <$fd>;
 -      close $fd or return;
 -      if ($most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
 -              my $timestamp = $1;
 -              my $age = time - $timestamp;
 -              return ($age, age_string($age));
 -      }
 -}
 -
  sub parse_commit {
        my $commit_id = shift;
        my $commit_text = shift;
                @commit_lines = @$commit_text;
        } else {
                local $/ = "\0";
 -              open my $fd, "-|", git_cmd(), "rev-list", "--header", "--parents", "--max-count=1", $commit_id
 +              open my $fd, "-|", git_cmd(), "rev-list",
 +                      "--header", "--parents", "--max-count=1",
 +                      $commit_id, "--"
                        or return;
                @commit_lines = split '\n', <$fd>;
                close $fd or return;
@@@ -1284,7 -1303,7 +1308,7 @@@ sub parse_ls_tree_line ($;%) 
        my %res;
  
        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
 -      $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
 +      $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
  
        $res{'mode'} = $1;
        $res{'type'} = $2;
  ## ......................................................................
  ## parse to array of hashes functions
  
 -sub git_get_refs_list {
 -      my $type = shift || "";
 -      my %refs;
 -      my @reflist;
 +sub git_get_heads_list {
 +      my $limit = shift;
 +      my @headslist;
  
 -      my @refs;
 -      open my $fd, "-|", $GIT, "peek-remote", "$projectroot/$project/"
 +      open my $fd, '-|', git_cmd(), 'for-each-ref',
 +              ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
 +              '--format=%(objectname) %(refname) %(subject)%00%(committer)',
 +              'refs/heads'
                or return;
        while (my $line = <$fd>) {
 -              chomp $line;
 -              if ($line =~ m/^([0-9a-fA-F]{40})\trefs\/($type\/?([^\^]+))(\^\{\})?$/) {
 -                      if (defined $refs{$1}) {
 -                              push @{$refs{$1}}, $2;
 -                      } else {
 -                              $refs{$1} = [ $2 ];
 -                      }
 +              my %ref_item;
  
 -                      if (! $4) { # unpeeled, direct reference
 -                              push @refs, { hash => $1, name => $3 }; # without type
 -                      } elsif ($3 eq $refs[-1]{'name'}) {
 -                              # most likely a tag is followed by its peeled
 -                              # (deref) one, and when that happens we know the
 -                              # previous one was of type 'tag'.
 -                              $refs[-1]{'type'} = "tag";
 -                      }
 +              chomp $line;
 +              my ($refinfo, $committerinfo) = split(/\0/, $line);
 +              my ($hash, $name, $title) = split(' ', $refinfo, 3);
 +              my ($committer, $epoch, $tz) =
 +                      ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
 +              $name =~ s!^refs/heads/!!;
 +
 +              $ref_item{'name'}  = $name;
 +              $ref_item{'id'}    = $hash;
 +              $ref_item{'title'} = $title || '(no commit message)';
 +              $ref_item{'epoch'} = $epoch;
 +              if ($epoch) {
 +                      $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
 +              } else {
 +                      $ref_item{'age'} = "unknown";
                }
 +
 +              push @headslist, \%ref_item;
        }
        close $fd;
  
 -      foreach my $ref (@refs) {
 -              my $ref_file = $ref->{'name'};
 -              my $ref_id   = $ref->{'hash'};
 +      return wantarray ? @headslist : \@headslist;
 +}
 +
 +sub git_get_tags_list {
 +      my $limit = shift;
 +      my @tagslist;
 +
 +      open my $fd, '-|', git_cmd(), 'for-each-ref',
 +              ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
 +              '--format=%(objectname) %(objecttype) %(refname) '.
 +              '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
 +              'refs/tags'
 +              or return;
 +      while (my $line = <$fd>) {
 +              my %ref_item;
 +
 +              chomp $line;
 +              my ($refinfo, $creatorinfo) = split(/\0/, $line);
 +              my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
 +              my ($creator, $epoch, $tz) =
 +                      ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
 +              $name =~ s!^refs/tags/!!;
 +
 +              $ref_item{'type'} = $type;
 +              $ref_item{'id'} = $id;
 +              $ref_item{'name'} = $name;
 +              if ($type eq "tag") {
 +                      $ref_item{'subject'} = $title;
 +                      $ref_item{'reftype'} = $reftype;
 +                      $ref_item{'refid'}   = $refid;
 +              } else {
 +                      $ref_item{'reftype'} = $type;
 +                      $ref_item{'refid'}   = $id;
 +              }
  
 -              my $type = $ref->{'type'} || git_get_type($ref_id) || next;
 -              my %ref_item = parse_ref($ref_file, $ref_id, $type);
 +              if ($type eq "tag" || $type eq "commit") {
 +                      $ref_item{'epoch'} = $epoch;
 +                      if ($epoch) {
 +                              $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
 +                      } else {
 +                              $ref_item{'age'} = "unknown";
 +                      }
 +              }
  
 -              push @reflist, \%ref_item;
 +              push @tagslist, \%ref_item;
        }
 -      # sort refs by age
 -      @reflist = sort {$b->{'epoch'} <=> $a->{'epoch'}} @reflist;
 -      return (\@reflist, \%refs);
 +      close $fd;
 +
 +      return wantarray ? @tagslist : \@tagslist;
  }
  
  ## ----------------------------------------------------------------------
@@@ -1993,19 -1971,19 +2017,19 @@@ sub git_difftree_body 
                        print "</td>\n";
                        print "<td>$mode_chnge</td>\n";
                        print "<td class=\"link\">";
 -                      if ($diff{'to_id'} ne $diff{'from_id'}) { # modified
 -                              if ($action eq 'commitdiff') {
 -                                      # link to patch
 -                                      $patchno++;
 -                                      print $cgi->a({-href => "#patch$patchno"}, "patch");
 -                              } else {
 -                                      print $cgi->a({-href => href(action=>"blobdiff",
 -                                                                   hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 -                                                                   hash_base=>$hash, hash_parent_base=>$parent,
 -                                                                   file_name=>$diff{'file'})},
 -                                                    "diff");
 -                              }
 -                              print " | ";
 +                      if ($action eq 'commitdiff') {
 +                              # link to patch
 +                              $patchno++;
 +                              print $cgi->a({-href => "#patch$patchno"}, "patch") .
 +                                    " | ";
 +                      } elsif ($diff{'to_id'} ne $diff{'from_id'}) {
 +                              # "commit" view and modified file (not onlu mode changed)
 +                              print $cgi->a({-href => href(action=>"blobdiff",
 +                                                           hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 +                                                           hash_base=>$hash, hash_parent_base=>$parent,
 +                                                           file_name=>$diff{'file'})},
 +                                            "diff") .
 +                                    " | ";
                        }
                        print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
                                                     hash_base=>$hash, file_name=>$diff{'file'})},
                                      -class => "list"}, esc_html($diff{'from_file'})) .
                              " with " . (int $diff{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
                              "<td class=\"link\">";
 -                      if ($diff{'to_id'} ne $diff{'from_id'}) {
 -                              if ($action eq 'commitdiff') {
 -                                      # link to patch
 -                                      $patchno++;
 -                                      print $cgi->a({-href => "#patch$patchno"}, "patch");
 -                              } else {
 -                                      print $cgi->a({-href => href(action=>"blobdiff",
 -                                                                   hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 -                                                                   hash_base=>$hash, hash_parent_base=>$parent,
 -                                                                   file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})},
 -                                                    "diff");
 -                              }
 -                              print " | ";
 +                      if ($action eq 'commitdiff') {
 +                              # link to patch
 +                              $patchno++;
 +                              print $cgi->a({-href => "#patch$patchno"}, "patch") .
 +                                    " | ";
 +                      } elsif ($diff{'to_id'} ne $diff{'from_id'}) {
 +                              # "commit" view and modified file (not only pure rename or copy)
 +                              print $cgi->a({-href => href(action=>"blobdiff",
 +                                                           hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 +                                                           hash_base=>$hash, hash_parent_base=>$parent,
 +                                                           file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})},
 +                                            "diff") .
 +                                    " | ";
                        }
                        print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
                                                     hash_base=>$parent, file_name=>$diff{'from_file'})},
@@@ -2099,6 -2077,13 +2123,6 @@@ sub git_patchset_body 
                        }
                        $patch_idx++;
  
 -                      # for now, no extended header, hence we skip empty patches
 -                      # companion to  next LINE if $in_header;
 -                      if ($diffinfo->{'from_id'} eq $diffinfo->{'to_id'}) { # no change
 -                              $in_header = 1;
 -                              next LINE;
 -                      }
 -
                        if ($diffinfo->{'status'} eq "A") { # added
                                print "<div class=\"diff_info\">" . file_type($diffinfo->{'to_mode'}) . ":" .
                                      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
  
  # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
  
+ sub git_project_list_body {
+       my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
+       my $check_forks = gitweb_check_feature('forks');
+       my @projects;
+       foreach my $pr (@$projlist) {
+               my (@aa) = git_get_last_activity($pr->{'path'});
+               unless (@aa) {
+                       next;
+               }
+               ($pr->{'age'}, $pr->{'age_string'}) = @aa;
+               if (!defined $pr->{'descr'}) {
+                       my $descr = git_get_project_description($pr->{'path'}) || "";
+                       $pr->{'descr'} = chop_str($descr, 25, 5);
+               }
+               if (!defined $pr->{'owner'}) {
+                       $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || "";
+               }
+               if ($check_forks) {
+                       my $pname = $pr->{'path'};
+                       $pname =~ s/\.git$//;
+                       $pr->{'forks'} = -d "$projectroot/$pname";
+               }
+               push @projects, $pr;
+       }
+       $order ||= "project";
+       $from = 0 unless defined $from;
+       $to = $#projects if (!defined $to || $#projects < $to);
+       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" .
+                     "</tr>\n";
+       }
+       my $alternate = 1;
+       for (my $i = $from; $i <= $to; $i++) {
+               my $pr = $projects[$i];
+               if ($alternate) {
+                       print "<tr class=\"dark\">\n";
+               } else {
+                       print "<tr class=\"light\">\n";
+               }
+               $alternate ^= 1;
+               if ($check_forks) {
+                       print "<td>";
+                       if ($pr->{'forks'}) {
+                               print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
+                       }
+                       print "</td>\n";
+               }
+               print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
+                                       -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
+                     "<td>" . esc_html($pr->{'descr'}) . "</td>\n" .
+                     "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
+               print "<td class=\"". age_class($pr->{'age'}) . "\">" .
+                     $pr->{'age_string'} . "</td>\n" .
+                     "<td class=\"link\">" .
+                     $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
+                     $cgi->a({-href => '/git-browser/by-commit.html?r='.$pr->{'path'}}, "graphiclog") . " | " .
+                     $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
+                     $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
+                     ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
+                     "</td>\n" .
+                     "</tr>\n";
+       }
+       if (defined $extra) {
+               print "<tr>\n";
+               if ($check_forks) {
+                       print "<td></td>\n";
+               }
+               print "<td colspan=\"5\">$extra</td>\n" .
+                     "</tr>\n";
+       }
+       print "</table>\n";
+ }
  sub git_shortlog_body {
        # uses global variable $project
        my ($revlist, $from, $to, $refs, $extra) = @_;
@@@ -2305,7 -2408,8 +2447,7 @@@ sub git_tags_body 
        for (my $i = $from; $i <= $to; $i++) {
                my $entry = $taglist->[$i];
                my %tag = %$entry;
 -              my $comment_lines = $tag{'comment'};
 -              my $comment = shift @$comment_lines;
 +              my $comment = $tag{'subject'};
                my $comment_short;
                if (defined $comment) {
                        $comment_short = chop_str($comment, 30, 5);
                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
                if ($tag{'reftype'} eq "commit") {
                        print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") .
 -                            " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'refid'})}, "log");
 +                            " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log");
                } elsif ($tag{'reftype'} eq "blob") {
                        print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
                }
@@@ -2363,23 -2467,23 +2505,23 @@@ sub git_heads_body 
        my $alternate = 1;
        for (my $i = $from; $i <= $to; $i++) {
                my $entry = $headlist->[$i];
 -              my %tag = %$entry;
 -              my $curr = $tag{'id'} eq $head;
 +              my %ref = %$entry;
 +              my $curr = $ref{'id'} eq $head;
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
                } else {
                        print "<tr class=\"light\">\n";
                }
                $alternate ^= 1;
 -              print "<td><i>$tag{'age'}</i></td>\n" .
 -                    ($tag{'id'} eq $head ? "<td class=\"current_head\">" : "<td>") .
 -                    $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'}),
 -                             -class => "list name"},esc_html($tag{'name'})) .
 +              print "<td><i>$ref{'age'}</i></td>\n" .
 +                    ($curr ? "<td class=\"current_head\">" : "<td>") .
 +                    $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'name'}),
 +                             -class => "list name"},esc_html($ref{'name'})) .
                      "</td>\n" .
                      "<td class=\"link\">" .
 -                    $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . " | " .
 -                    $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log") . " | " .
 -                    $cgi->a({-href => href(action=>"tree", hash=>$tag{'name'}, hash_base=>$tag{'name'})}, "tree") .
 +                    $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'name'})}, "shortlog") . " | " .
 +                    $cgi->a({-href => href(action=>"log", hash=>$ref{'name'})}, "log") . " | " .
 +                    $cgi->a({-href => href(action=>"tree", hash=>$ref{'name'}, hash_base=>$ref{'name'})}, "tree") .
                      "</td>\n" .
                      "</tr>";
        }
@@@ -2402,25 -2506,9 +2544,9 @@@ sub git_project_list 
        }
  
        my @list = git_get_projects_list();
-       my @projects;
        if (!@list) {
                die_error(undef, "No projects found");
        }
-       foreach my $pr (@list) {
-               my (@aa) = git_get_last_activity($pr->{'path'});
-               unless (@aa) {
-                       next;
-               }
-               ($pr->{'age'}, $pr->{'age_string'}) = @aa;
-               if (!defined $pr->{'descr'}) {
-                       my $descr = git_get_project_description($pr->{'path'}) || "";
-                       $pr->{'descr'} = chop_str($descr, 25, 5);
-               }
-               if (!defined $pr->{'owner'}) {
-                       $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || "";
-               }
-               push @projects, $pr;
-       }
  
        git_header_html();
        if (-f $home_text) {
                close $fd;
                print "</div>\n";
        }
-       print "<table class=\"project_list\">\n" .
-             "<tr>\n";
-       $order ||= "project";
-       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";
+       git_project_list_body(\@list, $order);
+       git_footer_html();
+ }
+ sub git_forks {
+       my $order = $cgi->param('o');
+       if (defined $order && $order !~ m/project|descr|owner|age/) {
+               die_error(undef, "Unknown order parameter");
        }
-       print "<th></th>\n" .
-             "</tr>\n";
-       my $alternate = 1;
-       foreach my $pr (@projects) {
-               if ($alternate) {
-                       print "<tr class=\"dark\">\n";
-               } else {
-                       print "<tr class=\"light\">\n";
-               }
-               $alternate ^= 1;
-               print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
-                                       -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
-                     "<td>" . esc_html($pr->{'descr'}) . "</td>\n" .
-                     "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
-               print "<td class=\"". age_class($pr->{'age'}) . "\">" .
-                     $pr->{'age_string'} . "</td>\n" .
-                     "<td class=\"link\">" .
-                     $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
-                     $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
-                     $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
-                     $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
-                     "</td>\n" .
-                     "</tr>\n";
+       my @list = git_get_projects_list($project);
+       if (!@list) {
+               die_error(undef, "No forks found");
        }
-       print "</table>\n";
+       git_header_html();
+       git_print_page_nav('','');
+       git_print_header_div('summary', "$project forks");
+       git_project_list_body(\@list, $order);
        git_footer_html();
  }
  
  sub git_project_index {
-       my @projects = git_get_projects_list();
+       my @projects = git_get_projects_list($project);
  
        print $cgi->header(
                -type => 'text/plain',
@@@ -2529,9 -2572,22 +2610,13 @@@ sub git_summary 
  
        my $owner = git_get_project_owner($project);
  
 -      my ($reflist, $refs) = git_get_refs_list();
 -
 -      my @taglist;
 -      my @headlist;
 -      foreach my $ref (@$reflist) {
 -              if ($ref->{'name'} =~ s!^heads/!!) {
 -                      push @headlist, $ref;
 -              } else {
 -                      $ref->{'name'} =~ s!^tags/!!;
 -                      push @taglist, $ref;
 -              }
 -      }
 +      my $refs = git_get_references();
 +      my @taglist  = git_get_tags_list(15);
 +      my @headlist = git_get_heads_list(15);
+       my @forklist;
+       if (gitweb_check_feature('forks')) {
+               @forklist = git_get_projects_list($project);
+       }
  
        git_header_html();
        git_print_page_nav('summary','', $head);
        }
  
        open my $fd, "-|", git_cmd(), "rev-list", "--max-count=17",
 -              git_get_head_hash($project)
 +              git_get_head_hash($project), "--"
                or die_error(undef, "Open git-rev-list failed");
        my @revlist = map { chomp; $_ } <$fd>;
        close $fd;
                               $cgi->a({-href => href(action=>"heads")}, "..."));
        }
  
+       if (@forklist) {
+               git_print_header_div('forks');
+               git_project_list_body(\@forklist, undef, 0, 15,
+                                     $cgi->a({-href => href(action=>"forks")}, "..."),
+                                     'noheader');
+       }
        git_footer_html();
  }
  
@@@ -2692,7 -2755,7 +2784,7 @@@ HTM
                print "<tr class=\"$rev_color[$current_color]\">\n";
                if ($group_size) {
                        print "<td class=\"sha1\"";
 -                      print " title=\"$author, $date\"";
 +                      print " title=\"". esc_html($author) . ", $date\"";
                        print " rowspan=\"$group_size\"" if ($group_size > 1);
                        print ">";
                        print $cgi->a({-href => href(action=>"commit",
@@@ -2823,9 -2886,9 +2915,9 @@@ sub git_tags 
        git_print_page_nav('','', $head,undef,$head);
        git_print_header_div('summary', $project);
  
 -      my ($taglist) = git_get_refs_list("tags");
 -      if (@$taglist) {
 -              git_tags_body($taglist);
 +      my @tagslist = git_get_tags_list();
 +      if (@tagslist) {
 +              git_tags_body(\@tagslist);
        }
        git_footer_html();
  }
@@@ -2836,9 -2899,9 +2928,9 @@@ sub git_heads 
        git_print_page_nav('','', $head,undef,$head);
        git_print_header_div('summary', $project);
  
 -      my ($headlist) = git_get_refs_list("heads");
 -      if (@$headlist) {
 -              git_heads_body($headlist, $head);
 +      my @headslist = git_get_heads_list();
 +      if (@headslist) {
 +              git_heads_body(\@headslist, $head);
        }
        git_footer_html();
  }
@@@ -2951,7 -3014,7 +3043,7 @@@ sub git_blob 
                $nr++;
                $line = untabify($line);
                printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
 -                     $nr, $nr, $nr, esc_html($line);
 +                     $nr, $nr, $nr, esc_html($line, -nbsp=>1);
        }
        close $fd
                or print "Reading blob failed.\n";
@@@ -3105,7 -3168,7 +3197,7 @@@ sub git_log 
        my $refs = git_get_references();
  
        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
 -      open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash
 +      open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash, "--"
                or die_error(undef, "Open git-rev-list failed");
        my @revlist = map { chomp; $_ } <$fd>;
        close $fd;
@@@ -3162,12 -3225,14 +3254,12 @@@ sub git_commit 
        if (!defined $parent) {
                $parent = "--root";
        }
 -      open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $parent, $hash
 +      open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
 +              @diff_opts, $parent, $hash, "--"
                or die_error(undef, "Open git-diff-tree failed");
        my @difftree = map { chomp; $_ } <$fd>;
        close $fd or die_error(undef, "Reading git-diff-tree failed");
  
 -      # filter out commit ID output
 -      @difftree = grep(!/^[0-9a-fA-F]{40}$/, @difftree);
 -
        # non-textual hash id's can be cached
        my $expires;
        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
@@@ -3268,8 -3333,7 +3360,8 @@@ sub git_blobdiff 
        if (defined $hash_base && defined $hash_parent_base) {
                if (defined $file_name) {
                        # read raw output
 -                      open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base,
 +                      open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
 +                              $hash_parent_base, $hash_base,
                                "--", $file_name
                                or die_error(undef, "Open git-diff-tree failed");
                        @difftree = map { chomp; $_ } <$fd>;
                        # try to find filename from $hash
  
                        # read filtered raw output
 -                      open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base
 +                      open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
 +                              $hash_parent_base, $hash_base, "--"
                                or die_error(undef, "Open git-diff-tree failed");
                        @difftree =
                                # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
                }
  
                # open patch output
 -              open $fd, "-|", git_cmd(), "diff", '-p', @diff_opts, $hash_parent, $hash
 +              open $fd, "-|", git_cmd(), "diff", '-p', @diff_opts,
 +                      $hash_parent, $hash, "--"
                        or die_error(undef, "Open git-diff failed");
        } else  {
                die_error('404 Not Found', "Missing one of the blob diff parameters")
@@@ -3432,51 -3494,6 +3524,51 @@@ sub git_commitdiff 
        if (!%co) {
                die_error(undef, "Unknown commit object");
        }
 +
 +      # we need to prepare $formats_nav before any parameter munging
 +      my $formats_nav;
 +      if ($format eq 'html') {
 +              $formats_nav =
 +                      $cgi->a({-href => href(action=>"commitdiff_plain",
 +                                             hash=>$hash, hash_parent=>$hash_parent)},
 +                              "raw");
 +
 +              if (defined $hash_parent) {
 +                      # commitdiff with two commits given
 +                      my $hash_parent_short = $hash_parent;
 +                      if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
 +                              $hash_parent_short = substr($hash_parent, 0, 7);
 +                      }
 +                      $formats_nav .=
 +                              ' (from: ' .
 +                              $cgi->a({-href => href(action=>"commitdiff",
 +                                                     hash=>$hash_parent)},
 +                                      esc_html($hash_parent_short)) .
 +                              ')';
 +              } elsif (!$co{'parent'}) {
 +                      # --root commitdiff
 +                      $formats_nav .= ' (initial)';
 +              } elsif (scalar @{$co{'parents'}} == 1) {
 +                      # single parent commit
 +                      $formats_nav .=
 +                              ' (parent: ' .
 +                              $cgi->a({-href => href(action=>"commitdiff",
 +                                                     hash=>$co{'parent'})},
 +                                      esc_html(substr($co{'parent'}, 0, 7))) .
 +                              ')';
 +              } else {
 +                      # merge commit
 +                      $formats_nav .=
 +                              ' (merge: ' .
 +                              join(' ', map {
 +                                      $cgi->a({-href => href(action=>"commitdiff",
 +                                                             hash=>$_)},
 +                                              esc_html(substr($_, 0, 7)));
 +                              } @{$co{'parents'}} ) .
 +                              ')';
 +              }
 +      }
 +
        if (!defined $hash_parent) {
                $hash_parent = $co{'parent'} || '--root';
        }
        my @difftree;
        if ($format eq 'html') {
                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
 -                      "--patch-with-raw", "--full-index", $hash_parent, $hash
 +                      "--no-commit-id", "--patch-with-raw", "--full-index",
 +                      $hash_parent, $hash, "--"
                        or die_error(undef, "Open git-diff-tree failed");
  
                while (chomp(my $line = <$fd>)) {
                        # empty line ends raw part of diff-tree output
                        last unless $line;
 -                      # filter out commit ID output
 -                      push @difftree, $line
 -                              unless $line =~ m/^[0-9a-fA-F]{40}$/;
 +                      push @difftree, $line;
                }
  
        } elsif ($format eq 'plain') {
                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
 -                      '-p', $hash_parent, $hash
 +                      '-p', $hash_parent, $hash, "--"
                        or die_error(undef, "Open git-diff-tree failed");
  
        } else {
        if ($format eq 'html') {
                my $refs = git_get_references();
                my $ref = format_ref_marker($refs, $co{'id'});
 -              my $formats_nav =
 -                      $cgi->a({-href => href(action=>"commitdiff_plain",
 -                                             hash=>$hash, hash_parent=>$hash_parent)},
 -                              "raw");
  
                git_header_html(undef, $expires);
                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
@@@ -3675,9 -3697,7 +3767,9 @@@ sub git_search 
        my $alternate = 1;
        if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
                $/ = "\0";
 -              open my $fd, "-|", git_cmd(), "rev-list", "--header", "--parents", $hash or next;
 +              open my $fd, "-|", git_cmd(), "rev-list",
 +                      "--header", "--parents", $hash, "--"
 +                      or next;
                while (my $commit_text = <$fd>) {
                        if (!grep m/$searchtext/i, $commit_text) {
                                next;
@@@ -3823,7 -3843,7 +3915,7 @@@ sub git_shortlog 
        my $refs = git_get_references();
  
        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
 -      open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash
 +      open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash, "--"
                or die_error(undef, "Open git-rev-list failed");
        my @revlist = map { chomp; $_ } <$fd>;
        close $fd;
  
  sub git_rss {
        # http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
 -      open my $fd, "-|", git_cmd(), "rev-list", "--max-count=150", git_get_head_hash($project)
 +      open my $fd, "-|", git_cmd(), "rev-list", "--max-count=150",
 +              git_get_head_hash($project), "--"
                or die_error(undef, "Open git-rev-list failed");
        my @revlist = map { chomp; $_ } <$fd>;
        close $fd or die_error(undef, "Reading git-rev-list failed");
@@@ -3876,7 -3895,7 +3968,7 @@@ XM
                }
                my %cd = parse_date($co{'committer_epoch'});
                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
 -                      $co{'parent'}, $co{'id'}
 +                      $co{'parent'}, $co{'id'}, "--"
                        or next;
                my @difftree = map { chomp; $_ } <$fd>;
                close $fd