diff options
Diffstat (limited to 'gitweb')
-rw-r--r-- | gitweb/README | 16 | ||||
-rwxr-xr-x | gitweb/gitweb.perl | 576 | ||||
-rw-r--r-- | gitweb/static/gitweb.css | 7 |
3 files changed, 432 insertions, 167 deletions
diff --git a/gitweb/README b/gitweb/README index a92bde7f14..a3a697bf55 100644 --- a/gitweb/README +++ b/gitweb/README @@ -207,6 +207,15 @@ not include variables usually directly set during build): full description is available as 'title' attribute (usually shown on mouseover). By default set to 25, which might be too small if you use long project descriptions. + * $projects_list_group_categories + Enables the grouping of projects by category on the project list page. + The category of a project is determined by the $GIT_DIR/category + file or the 'gitweb.category' variable in its repository configuration. + Disabled by default. + * $project_list_default_category + Default category for projects for which none is specified. If set + to the empty string, such projects will remain uncategorized and + listed at the top, above categorized projects. * @git_base_url_list List of git base URLs used for URL to where fetch project from, shown in project summary page. Full URL is "$git_base_url/$project". @@ -314,6 +323,13 @@ You can use the following files in repository: from the template during repository creation. You can use the gitweb.description repo configuration variable, but the file takes precedence. + * category (or gitweb.category) + Singe line category of a project, used to group projects if + $projects_list_group_categories is enabled. By default (file and + configuration variable absent), uncategorized projects are put in + the $project_list_default_category category. You can use the + gitweb.category repo configuration variable, but the file takes + precedence. * cloneurl (or multiple-valued gitweb.url) File with repository URL (used for clone and fetch), one per line. Displayed in the project summary page. You can use multiple-valued diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index ac335b6dea..240dd4701c 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -115,6 +115,14 @@ our $projects_list = "++GITWEB_LIST++"; # the width (in characters) of the projects list "Description" column our $projects_list_description_width = 25; +# group projects by category on the projects list +# (enabled if this variable evaluates to true) +our $projects_list_group_categories = 0; + +# default category if none specified +# (leave the empty string for no category) +our $project_list_default_category = ""; + # default order of projects list # valid values are none, project, descr, owner, and age our $default_projects_order = "project"; @@ -186,7 +194,7 @@ our %known_snapshot_formats = ( 'type' => 'application/x-gzip', 'suffix' => '.tar.gz', 'format' => 'tar', - 'compressor' => ['gzip']}, + 'compressor' => ['gzip', '-n']}, 'tbz2' => { 'display' => 'tar.bz2', @@ -412,20 +420,23 @@ our %feature = ( '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. + # Allow gitweb scan project content tags of project repository, + # and display the popular Web 2.0-ish "tag cloud" near the projects + # 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. + # tagging itself; you need to do it externally, outside gitweb. + # The format is described in git_get_project_ctags() subroutine. # 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']; + # $feature{'ctags'}{'default'} = [1]; # Project specific override is not supported. + + # In the future whether ctags editing is enabled might depend + # on the value, but using 1 should always mean no editing of ctags. 'ctags' => { 'override' => 0, 'default' => [0]}, @@ -632,18 +643,30 @@ sub filter_snapshot_fmts { # if it is true then gitweb config would be run for each request. our $per_request_config = 1; +# read and parse gitweb config file given by its parameter. +# returns true on success, false on recoverable error, allowing +# to chain this subroutine, using first file that exists. +# dies on errors during parsing config file, as it is unrecoverable. +sub read_config_file { + my $filename = shift; + return unless defined $filename; + # die if there are errors parsing config file + if (-e $filename) { + do $filename; + die $@ if $@; + return 1; + } + return; +} + our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM); sub evaluate_gitweb_config { our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++"; our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++"; - # die if there are errors parsing config file - if (-e $GITWEB_CONFIG) { - do $GITWEB_CONFIG; - die $@ if $@; - } elsif (-e $GITWEB_CONFIG_SYSTEM) { - do $GITWEB_CONFIG_SYSTEM; - die $@ if $@; - } + + # use first config file that exists + read_config_file($GITWEB_CONFIG) or + read_config_file($GITWEB_CONFIG_SYSTEM); } # Get loadavg of system, to compare against $maxload. @@ -715,6 +738,7 @@ our @cgi_param_mapping = ( snapshot_format => "sf", extra_options => "opt", search_use_regexp => "sr", + ctag => "by_tag", # this must be last entry (for manipulation from JavaScript) javascript => "js" ); @@ -2570,37 +2594,94 @@ sub git_get_path_by_hash { ## ...................................................................... ## git utility functions, directly accessing git repository -sub git_get_project_description { - my $path = shift; +# get the value of config variable either from file named as the variable +# itself in the repository ($GIT_DIR/$name file), or from gitweb.$name +# configuration variable in the repository config file. +sub git_get_file_or_project_config { + my ($path, $name) = @_; $git_dir = "$projectroot/$path"; - open my $fd, '<', "$git_dir/description" - or return git_get_project_config('description'); - my $descr = <$fd>; + open my $fd, '<', "$git_dir/$name" + or return git_get_project_config($name); + my $conf = <$fd>; close $fd; - if (defined $descr) { - chomp $descr; + if (defined $conf) { + chomp $conf; } - return $descr; + return $conf; } -sub git_get_project_ctags { +sub git_get_project_description { my $path = shift; + return git_get_file_or_project_config($path, 'description'); +} + +sub git_get_project_category { + my $path = shift; + return git_get_file_or_project_config($path, 'category'); +} + + +# supported formats: +# * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory) +# - if its contents is a number, use it as tag weight, +# - otherwise add a tag with weight 1 +# * $GIT_DIR/ctags file, each line is a tag (with weight 1) +# the same value multiple times increases tag weight +# * `gitweb.ctag' multi-valued repo config variable +sub git_get_project_ctags { + my $project = shift; my $ctags = {}; - $git_dir = "$projectroot/$path"; - opendir my $dh, "$git_dir/ctags" - or return $ctags; - foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) { - open my $ct, '<', $_ or next; - my $val = <$ct>; - chomp $val; - close $ct; - my $ctag = $_; $ctag =~ s#.*/##; - $ctags->{$ctag} = $val; + $git_dir = "$projectroot/$project"; + if (opendir my $dh, "$git_dir/ctags") { + my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh); + foreach my $tagfile (@files) { + open my $ct, '<', $tagfile + or next; + my $val = <$ct>; + chomp $val if $val; + close $ct; + + (my $ctag = $tagfile) =~ s#.*/##; + if ($val =~ /\d+/) { + $ctags->{$ctag} = $val; + } else { + $ctags->{$ctag} = 1; + } + } + closedir $dh; + + } elsif (open my $fh, '<', "$git_dir/ctags") { + while (my $line = <$fh>) { + chomp $line; + $ctags->{$line}++ if $line; + } + close $fh; + + } else { + my $taglist = config_to_multi(git_get_project_config('ctag')); + foreach my $tag (@$taglist) { + $ctags->{$tag}++; + } } - closedir $dh; - $ctags; + + return $ctags; +} + +# return hash, where keys are content tags ('ctags'), +# and values are sum of weights of given tag in every project +sub git_gather_all_ctags { + my $projects = shift; + my $ctags = {}; + + foreach my $p (@$projects) { + foreach my $ct (keys %{$p->{'ctags'}}) { + $ctags->{$ct} += $p->{'ctags'}->{$ct}; + } + } + + return $ctags; } sub git_populate_project_tagcloud { @@ -2618,33 +2699,49 @@ sub git_populate_project_tagcloud { } my $cloud; + my $matched = $cgi->param('by_tag'); if (eval { require HTML::TagCloud; 1; }) { $cloud = HTML::TagCloud->new; - foreach (sort keys %ctags_lc) { + foreach my $ctag (sort keys %ctags_lc) { # Pad the title with spaces so that the cloud looks # less crammed. - my $title = $ctags_lc{$_}->{topname}; + my $title = esc_html($ctags_lc{$ctag}->{topname}); $title =~ s/ / /g; $title =~ s/^/ /g; $title =~ s/$/ /g; - $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count}); + if (defined $matched && $matched eq $ctag) { + $title = qq(<span class="match">$title</span>); + } + $cloud->add($title, href(project=>undef, ctag=>$ctag), + $ctags_lc{$ctag}->{count}); } } else { - $cloud = \%ctags_lc; + $cloud = {}; + foreach my $ctag (keys %ctags_lc) { + my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1); + if (defined $matched && $matched eq $ctag) { + $title = qq(<span class="match">$title</span>); + } + $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count}; + $cloud->{$ctag}{ctag} = + $cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title); + } } - $cloud; + return $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 { - $cgi->a({-href=>"$home_link?by_tag=$_"}, $cloud->{$_}->{topname}) - } splice(@tags, 0, $count)) . '</p>'; + my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud; + return + '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' . + join (', ', map { + $cloud->{$_}->{'ctag'} + } splice(@tags, 0, $count)) . + '</div>'; } } @@ -2663,21 +2760,23 @@ sub git_get_project_url_list { } sub git_get_projects_list { - my ($filter) = @_; + my $filter = shift || ''; my @list; - $filter ||= ''; $filter =~ s/\.git$//; - my $check_forks = gitweb_check_feature('forks'); - if (-d $projects_list) { # search in directory - my $dir = $projects_list . ($filter ? "/$filter" : ''); + my $dir = $projects_list; # remove the trailing "/" $dir =~ s!/+$!!; - my $pfxlen = length("$dir"); - my $pfxdepth = ($dir =~ tr!/!!); + my $pfxlen = length("$projects_list"); + my $pfxdepth = ($projects_list =~ tr!/!!); + # when filtering, search only given subdirectory + if ($filter) { + $dir .= "/$filter"; + $dir =~ s!/+$!!; + } File::Find::find({ follow_fast => 1, # follow symbolic links @@ -2692,14 +2791,14 @@ sub git_get_projects_list { # only directories can be git repositories return unless (-d $_); # don't traverse too deep (Find is super slow on os x) + # $project_maxdepth excludes depth of $projectroot if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) { $File::Find::prune = 1; return; } - my $subdir = substr($File::Find::name, $pfxlen + 1); + my $path = substr($File::Find::name, $pfxlen + 1); # we check related file in $projectroot - my $path = ($filter ? "$filter/" : '') . $subdir; if (check_export_ok("$projectroot/$path")) { push @list, { path => $path }; $File::Find::prune = 1; @@ -2712,7 +2811,6 @@ sub git_get_projects_list { # 'git%2Fgit.git Linus+Torvalds' # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin' # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman' - my %paths; open my $fd, '<', $projects_list or return; PROJECT: while (my $line = <$fd>) { @@ -2723,32 +2821,9 @@ sub git_get_projects_list { if (!defined $path) { next; } - if ($filter ne '') { - # looking for forks; - my $pfx = substr($path, 0, length($filter)); - if ($pfx ne $filter) { - next PROJECT; - } - my $sfx = substr($path, length($filter)); - if ($sfx !~ /^\/.*\.git$/) { - next PROJECT; - } - } elsif ($check_forks) { - PATH: - foreach my $filter (keys %paths) { - # looking for forks; - my $pfx = substr($path, 0, length($filter)); - if ($pfx ne $filter) { - next PATH; - } - my $sfx = substr($path, length($filter)); - if ($sfx !~ /^\/.*\.git$/) { - next PATH; - } - # is a fork, don't include it in - # the list - next PROJECT; - } + # if $filter is rpovided, check if $path begins with $filter + if ($filter && $path !~ m!^\Q$filter\E/!) { + next; } if (check_export_ok("$projectroot/$path")) { my $pr = { @@ -2756,8 +2831,6 @@ sub git_get_projects_list { owner => to_utf8($owner), }; push @list, $pr; - (my $forks_path = $path) =~ s/\.git$//; - $paths{$forks_path}++; } } close $fd; @@ -2765,6 +2838,98 @@ sub git_get_projects_list { return @list; } +# written with help of Tree::Trie module (Perl Artistic License, GPL compatibile) +# as side effects it sets 'forks' field to list of forks for forked projects +sub filter_forks_from_projects_list { + my $projects = shift; + + my %trie; # prefix tree of directories (path components) + # generate trie out of those directories that might contain forks + foreach my $pr (@$projects) { + my $path = $pr->{'path'}; + $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory + next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git' + next unless ($path); # skip '.git' repository: tests, git-instaweb + next unless (-d $path); # containing directory exists + $pr->{'forks'} = []; # there can be 0 or more forks of project + + # add to trie + my @dirs = split('/', $path); + # walk the trie, until either runs out of components or out of trie + my $ref = \%trie; + while (scalar @dirs && + exists($ref->{$dirs[0]})) { + $ref = $ref->{shift @dirs}; + } + # create rest of trie structure from rest of components + foreach my $dir (@dirs) { + $ref = $ref->{$dir} = {}; + } + # create end marker, store $pr as a data + $ref->{''} = $pr if (!exists $ref->{''}); + } + + # filter out forks, by finding shortest prefix match for paths + my @filtered; + PROJECT: + foreach my $pr (@$projects) { + # trie lookup + my $ref = \%trie; + DIR: + foreach my $dir (split('/', $pr->{'path'})) { + if (exists $ref->{''}) { + # found [shortest] prefix, is a fork - skip it + push @{$ref->{''}{'forks'}}, $pr; + next PROJECT; + } + if (!exists $ref->{$dir}) { + # not in trie, cannot have prefix, not a fork + push @filtered, $pr; + next PROJECT; + } + # If the dir is there, we just walk one step down the trie. + $ref = $ref->{$dir}; + } + # we ran out of trie + # (shouldn't happen: it's either no match, or end marker) + push @filtered, $pr; + } + + return @filtered; +} + +# note: fill_project_list_info must be run first, +# for 'descr_long' and 'ctags' to be filled +sub search_projects_list { + my ($projlist, %opts) = @_; + my $tagfilter = $opts{'tagfilter'}; + my $searchtext = $opts{'searchtext'}; + + return @$projlist + unless ($tagfilter || $searchtext); + + my @projects; + PROJECT: + foreach my $pr (@$projlist) { + + if ($tagfilter) { + next unless ref($pr->{'ctags'}) eq 'HASH'; + next unless + grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}}; + } + + if ($searchtext) { + next unless + $pr->{'path'} =~ /$searchtext/ || + $pr->{'descr_long'} =~ /$searchtext/; + } + + push @projects, $pr; + } + + return @projects; +} + our $gitweb_project_owner = undef; sub git_get_project_list_from_file { @@ -4763,11 +4928,12 @@ sub git_patchset_body { # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . -# fills project list info (age, description, owner, forks) for each -# project in the list, removing invalid projects from returned list +# fills project list info (age, description, owner, category, 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 $projlist = shift; my @projects; my $show_ctags = gitweb_check_feature('ctags'); @@ -4787,23 +4953,59 @@ sub fill_project_list_info { if (!defined $pr->{'owner'}) { $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || ""; } - if ($check_forks) { - my $pname = $pr->{'path'}; - if (($pname =~ s/\.git$//) && - ($pname !~ /\/$/) && - (-d "$projectroot/$pname")) { - $pr->{'forks'} = "-d $projectroot/$pname"; - } else { - $pr->{'forks'} = 0; - } + if ($show_ctags) { + $pr->{'ctags'} = git_get_project_ctags($pr->{'path'}); + } + if ($projects_list_group_categories && !defined $pr->{'category'}) { + my $cat = git_get_project_category($pr->{'path'}) || + $project_list_default_category; + $pr->{'category'} = to_utf8($cat); } - $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'}); + push @projects, $pr; } return @projects; } +sub sort_projects_list { + my ($projlist, $order) = @_; + my @projects; + + 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}; + return @$projlist unless defined $oi; + if ($oi->{'type'} eq 'str') { + @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist; + } else { + @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist; + } + + return @projects; +} + +# returns a hash of categories, containing the list of project +# belonging to each category +sub build_projlist_by_category { + my ($projlist, $from, $to) = @_; + my %categories; + + $from = 0 unless defined $from; + $to = $#$projlist if (!defined $to || $#$projlist < $to); + + for (my $i = $from; $i <= $to; $i++) { + my $pr = $projlist->[$i]; + push @{$categories{ $pr->{'category'} }}, $pr; + } + + return wantarray ? %categories : \%categories; +} + # print 'sort by' <th> element, generating 'sort by $name' replay link # if that order is not selected sub print_sort_th { @@ -4827,70 +5029,15 @@ sub format_sort_th { return $sort_th; } -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); +sub git_project_list_rows { + my ($projlist, $from, $to, $check_forks) = @_; - $order ||= $default_projects_order; $from = 0 unless defined $from; - $to = $#projects if (!defined $to || $#projects < $to); + $to = $#$projlist if (!defined $to || $#$projlist < $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('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 - } + my $pr = $projlist->[$i]; if ($alternate) { print "<tr class=\"dark\">\n"; @@ -4898,11 +5045,17 @@ sub git_project_list_body { print "<tr class=\"light\">\n"; } $alternate ^= 1; + if ($check_forks) { print "<td>"; if ($pr->{'forks'}) { - print "<!-- $pr->{'forks'} -->\n"; - print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+"); + my $nforks = scalar @{$pr->{'forks'}}; + if ($nforks > 0) { + print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"), + -title => "$nforks forks"}, "+"); + } else { + print $cgi->span({-title => "$nforks forks"}, "+"); + } } print "</td>\n"; } @@ -4923,6 +5076,84 @@ sub git_project_list_body { "</td>\n" . "</tr>\n"; } +} + +sub git_project_list_body { + # actually uses global variable $project + my ($projlist, $order, $from, $to, $extra, $no_header) = @_; + my @projects = @$projlist; + + my $check_forks = gitweb_check_feature('forks'); + my $show_ctags = gitweb_check_feature('ctags'); + my $tagfilter = $show_ctags ? $cgi->param('by_tag') : undef; + $check_forks = undef + if ($tagfilter || $searchtext); + + # filtering out forks before filling info allows to do less work + @projects = filter_forks_from_projects_list(\@projects) + if ($check_forks); + @projects = fill_project_list_info(\@projects); + # searching projects require filling to be run before it + @projects = search_projects_list(\@projects, + 'searchtext' => $searchtext, + 'tagfilter' => $tagfilter) + if ($tagfilter || $searchtext); + + $order ||= $default_projects_order; + $from = 0 unless defined $from; + $to = $#projects if (!defined $to || $#projects < $to); + + # short circuit + if ($from > $to) { + print "<center>\n". + "<b>No such projects found</b><br />\n". + "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n". + "</center>\n<br />\n"; + return; + } + + @projects = sort_projects_list(\@projects, $order); + + if ($show_ctags) { + my $ctags = git_gather_all_ctags(\@projects); + 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('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"; + } + + if ($projects_list_group_categories) { + # only display categories with projects in the $from-$to window + @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to]; + my %categories = build_projlist_by_category(\@projects, $from, $to); + foreach my $cat (sort keys %categories) { + unless ($cat eq "") { + print "<tr>\n"; + if ($check_forks) { + print "<td></td>\n"; + } + print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n"; + print "</tr>\n"; + } + + git_project_list_rows($categories{$cat}, undef, undef, $check_forks); + } + } else { + git_project_list_rows(\@projects, $from, $to, $check_forks); + } + if (defined $extra) { print "<tr>\n"; if ($check_forks) { @@ -5382,7 +5613,10 @@ sub git_forks { } sub git_project_index { - my @projects = git_get_projects_list($project); + my @projects = git_get_projects_list(); + if (!@projects) { + die_error(404, "No projects found"); + } print $cgi->header( -type => 'text/plain', @@ -5424,7 +5658,11 @@ sub git_summary { my $check_forks = gitweb_check_feature('forks'); if ($check_forks) { + # find forks of a project @forklist = git_get_projects_list($project); + # filter out forks of forks + @forklist = filter_forks_from_projects_list(\@forklist) + if (@forklist); } git_header_html(); @@ -5454,13 +5692,14 @@ sub git_summary { my $show_ctags = gitweb_check_feature('ctags'); 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>"; + if (%$ctags) { + # without ability to add tags, don't show if there are none + my $cloud = git_populate_project_tagcloud($ctags); + print "<tr id=\"metadata_ctags\">" . + "<td>content tags</td>" . + "<td>".git_show_project_tagcloud($cloud, 48)."</td>" . + "</tr>\n"; + } } print "</table>\n"; @@ -7345,6 +7584,9 @@ sub git_atom { sub git_opml { my @list = git_get_projects_list(); + if (!@list) { + die_error(404, "No projects found"); + } print $cgi->header( -type => 'text/xml', diff --git a/gitweb/static/gitweb.css b/gitweb/static/gitweb.css index 8dd093563e..7d88509208 100644 --- a/gitweb/static/gitweb.css +++ b/gitweb/static/gitweb.css @@ -295,6 +295,13 @@ td.current_head { text-decoration: underline; } +td.category { + background-color: #d9d8d1; + border-top: 1px solid #000000; + border-left: 1px solid #000000; + font-weight: bold; +} + table.diff_tree span.file_status.new { color: #008000; } |