Merge branch 'pb/gitweb-tagcloud' into pb/gitweb
authorShawn O. Pearce <spearce@spearce.org>
Fri, 3 Oct 2008 14:41:25 +0000 (07:41 -0700)
committerShawn O. Pearce <spearce@spearce.org>
Fri, 3 Oct 2008 14:41:25 +0000 (07:41 -0700)
* pb/gitweb-tagcloud:
gitweb: Support for simple project search form
gitweb: Make the by_tag filter delve in forks as well
gitweb: Support for tag clouds
... (+ many updates from master) ...

Conflicts:
gitweb/gitweb.perl

1  2 
gitweb/gitweb.perl
diff --combined gitweb/gitweb.perl
index 453cbac7d8da0411e40852b84be69d727f493817,b46af77da0bd61aed5eb7977fecb03d4034d4911..11168006cffe9bd2f6c8c27bd1d034c41667e39a
@@@ -27,6 -27,13 +27,13 @@@ our $version = "++GIT_VERSION++"
  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";
@@@ -276,25 -283,23 +283,43 @@@ our %feature = 
                '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 {
@@@ -795,7 -800,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 {
@@@ -1775,6 -1780,67 +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;
  
@@@ -1823,9 -1889,7 +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;
                                }
@@@ -2777,26 -2841,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";
@@@ -3606,6 -3657,7 +3690,7 @@@ sub fill_project_list_info 
        my ($projlist, $check_forks) = @_;
        my @projects;
  
+       my $show_ctags = gitweb_check_feature('ctags');
   PROJECT:
        foreach my $pr (@$projlist) {
                my (@activity) = git_get_last_activity($pr->{'path'});
                                $pr->{'forks'} = 0;
                        }
                }
+               $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
                push @projects, $pr;
        }
  
        return @projects;
  }
  
 -# print 'sort by' <th> element, either sorting by $key if $name eq $order
 -# (changing $list), or generating 'sort by $name' replay link otherwise
 +# print 'sort by' <th> element, generating 'sort by $name' replay link
 +# if that order is not selected
  sub print_sort_th {
 -      my ($str_sort, $name, $order, $key, $header, $list) = @_;
 -      $key    ||= $name;
 +      my ($name, $order, $header) = @_;
        $header ||= ucfirst($name);
  
        if ($order eq $name) {
 -              if ($str_sort) {
 -                      @$list = sort {$a->{$key} cmp $b->{$key}} @$list;
 -              } else {
 -                      @$list = sort {$a->{$key} <=> $b->{$key}} @$list;
 -              }
                print "<th>$header</th>\n";
        } else {
                print "<th>" .
        }
  }
  
 -sub print_sort_th_str {
 -      print_sort_th(1, @_);
 -}
 -
 -sub print_sort_th_num {
 -      print_sort_th(0, @_);
 -}
 -
  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');
        $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";
                }
 -              print_sort_th_str('project', $order, 'path',
 -                                'Project', \@projects);
 -              print_sort_th_str('descr', $order, 'descr_long',
 -                                'Description', \@projects);
 -              print_sort_th_str('owner', $order, 'owner',
 -                                'Owner', \@projects);
 -              print_sort_th_num('age', $order, 'age',
 -                                'Last Change', \@projects);
 +              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 {
@@@ -4027,6 -4111,11 +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();
  }
@@@ -4098,10 -4187,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
        @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") {
  
        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();
@@@ -4473,6 -4576,7 +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");
                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;