gitweb: Support for tag clouds
authorPetr Baudis <pasky@suse.cz>
Thu, 2 Oct 2008 15:13:02 +0000 (17:13 +0200)
committerShawn O. Pearce <spearce@spearce.org>
Fri, 3 Oct 2008 14:29:56 +0000 (07:29 -0700)
The "Content tags" (nothing to do with usual Git tags!) are free-form
strings that are attached to random projects and displayed in the
well-known Web2.0-ish tag cloud above project list.

The feature will make use of HTML::TagCloud if available, but will
still display (less pretty) list of tags in case the module is not
installed.

The tagging itself is not done by gitweb - user-provided external
helper CGI needs to be provided; one example is the tagproj.cgi
of Girocco. This functionality might get integrated to gitweb
in the future.

The tags are stored one-per-file in ctags/ subdirectory. The reason
they are not stored in the project config file is that you usually
want to give anyone (even CGI scripts) permission to create new tags
and they are non-essential information, and thus you would make
the ctags/ subdirectory world-writable.

Signed-off-by: Petr Baudis <petr.baudis@novartis.com>
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
gitweb/gitweb.perl

index 83f810ad46cd2dfcea37308791ba64870ebfbd3c..0cb29705b2e8b343b162554ceef094741400fe56 100755 (executable)
@@ -282,6 +282,24 @@ our %feature = (
        'forks' => {
                'override' => 0,
                'default' => [0]},
+
+       # 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 {
@@ -1762,6 +1780,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;
 
@@ -3580,6 +3659,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'});
@@ -3606,6 +3686,7 @@ sub fill_project_list_info {
                                $pr->{'forks'} = 0;
                        }
                }
+               $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
                push @projects, $pr;
        }
 
@@ -3652,6 +3733,18 @@ sub git_project_list_body {
        $from = 0 unless defined $from;
        $to = $#projects if (!defined $to || $#projects < $to);
 
+       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";
@@ -3670,8 +3763,10 @@ sub git_project_list_body {
                      "</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'}};
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
                } else {
@@ -4093,6 +4188,20 @@ sub git_summary {
                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") {