Skip to content

Commit 12b1443

Browse files
jnarebgitster
authored andcommitted
gitweb: Restructure projects list generation
Extract filtering out forks (which is done if 'forks' feature is enabled) into filter_forks_from_projects_list subroutine, and searching projects (via projects search form, or via content tags) into search_projects_list subroutine. Both are now run _before_ displaying projects, and not while printing; this allow to know upfront if there were any found projects. Gitweb now can and do print 'No such projects found' if user searches for phrase which does not correspond to any project (any repository). This also would allow splitting projects list into pages, if we so desire. Filtering out forks and marking repository (project) as having forks is now consolidated into one subroutine (special case of handling forks in git_get_projects_list only for $projects_list being file is now removed). Forks handling is also cleaned up and simplified. $pr->{'forks'} now contains un-filled list of forks; we can now also detect situation where the way for having forks is prepared, but there are no forks yet. Sorting projects got also refactored in a very straight way (just moving code) into sort_projects_list subroutine. The interaction between forks, content tags and searching is now made more explicit: searching whether by tag, or via search form turns off fork filtering (gitweb searches also forks, and will show all results). If 'ctags' feature is disabled, then searching by tag is too. The t9500 test now includes some basic test for 'forks' and 'ctags' features; the t9502 includes test checking if gitweb correctly filters out forks. Generating list of projects by scanning given directory is now also a bit simplified wrt. handling filtering; it is byproduct of extracting filtering forks to separate subroutine. While at it we now detect that there are no projects and respond with "404 No projects found" also for 'project_index' and 'opml' actions. Helped-by: Jonathan Nieder <[email protected]> Signed-off-by: Jakub Narebski <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 1c08bf5 commit 12b1443

File tree

3 files changed

+296
-77
lines changed

3 files changed

+296
-77
lines changed

gitweb/gitweb.perl

Lines changed: 173 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2651,21 +2651,23 @@ sub git_get_project_url_list {
26512651
}
26522652

26532653
sub git_get_projects_list {
2654-
my ($filter) = @_;
2654+
my $filter = shift || '';
26552655
my @list;
26562656

2657-
$filter ||= '';
26582657
$filter =~ s/\.git$//;
26592658

2660-
my $check_forks = gitweb_check_feature('forks');
2661-
26622659
if (-d $projects_list) {
26632660
# search in directory
2664-
my $dir = $projects_list . ($filter ? "/$filter" : '');
2661+
my $dir = $projects_list;
26652662
# remove the trailing "/"
26662663
$dir =~ s!/+$!!;
2667-
my $pfxlen = length("$dir");
2668-
my $pfxdepth = ($dir =~ tr!/!!);
2664+
my $pfxlen = length("$projects_list");
2665+
my $pfxdepth = ($projects_list =~ tr!/!!);
2666+
# when filtering, search only given subdirectory
2667+
if ($filter) {
2668+
$dir .= "/$filter";
2669+
$dir =~ s!/+$!!;
2670+
}
26692671

26702672
File::Find::find({
26712673
follow_fast => 1, # follow symbolic links
@@ -2680,14 +2682,14 @@ sub git_get_projects_list {
26802682
# only directories can be git repositories
26812683
return unless (-d $_);
26822684
# don't traverse too deep (Find is super slow on os x)
2685+
# $project_maxdepth excludes depth of $projectroot
26832686
if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
26842687
$File::Find::prune = 1;
26852688
return;
26862689
}
26872690

2688-
my $subdir = substr($File::Find::name, $pfxlen + 1);
2691+
my $path = substr($File::Find::name, $pfxlen + 1);
26892692
# we check related file in $projectroot
2690-
my $path = ($filter ? "$filter/" : '') . $subdir;
26912693
if (check_export_ok("$projectroot/$path")) {
26922694
push @list, { path => $path };
26932695
$File::Find::prune = 1;
@@ -2700,7 +2702,6 @@ sub git_get_projects_list {
27002702
# 'git%2Fgit.git Linus+Torvalds'
27012703
# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
27022704
# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2703-
my %paths;
27042705
open my $fd, '<', $projects_list or return;
27052706
PROJECT:
27062707
while (my $line = <$fd>) {
@@ -2711,48 +2712,115 @@ sub git_get_projects_list {
27112712
if (!defined $path) {
27122713
next;
27132714
}
2714-
if ($filter ne '') {
2715-
# looking for forks;
2716-
my $pfx = substr($path, 0, length($filter));
2717-
if ($pfx ne $filter) {
2718-
next PROJECT;
2719-
}
2720-
my $sfx = substr($path, length($filter));
2721-
if ($sfx !~ /^\/.*\.git$/) {
2722-
next PROJECT;
2723-
}
2724-
} elsif ($check_forks) {
2725-
PATH:
2726-
foreach my $filter (keys %paths) {
2727-
# looking for forks;
2728-
my $pfx = substr($path, 0, length($filter));
2729-
if ($pfx ne $filter) {
2730-
next PATH;
2731-
}
2732-
my $sfx = substr($path, length($filter));
2733-
if ($sfx !~ /^\/.*\.git$/) {
2734-
next PATH;
2735-
}
2736-
# is a fork, don't include it in
2737-
# the list
2738-
next PROJECT;
2739-
}
2715+
# if $filter is rpovided, check if $path begins with $filter
2716+
if ($filter && $path !~ m!^\Q$filter\E/!) {
2717+
next;
27402718
}
27412719
if (check_export_ok("$projectroot/$path")) {
27422720
my $pr = {
27432721
path => $path,
27442722
owner => to_utf8($owner),
27452723
};
27462724
push @list, $pr;
2747-
(my $forks_path = $path) =~ s/\.git$//;
2748-
$paths{$forks_path}++;
27492725
}
27502726
}
27512727
close $fd;
27522728
}
27532729
return @list;
27542730
}
27552731

2732+
# written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
2733+
# as side effects it sets 'forks' field to list of forks for forked projects
2734+
sub filter_forks_from_projects_list {
2735+
my $projects = shift;
2736+
2737+
my %trie; # prefix tree of directories (path components)
2738+
# generate trie out of those directories that might contain forks
2739+
foreach my $pr (@$projects) {
2740+
my $path = $pr->{'path'};
2741+
$path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
2742+
next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
2743+
next unless ($path); # skip '.git' repository: tests, git-instaweb
2744+
next unless (-d $path); # containing directory exists
2745+
$pr->{'forks'} = []; # there can be 0 or more forks of project
2746+
2747+
# add to trie
2748+
my @dirs = split('/', $path);
2749+
# walk the trie, until either runs out of components or out of trie
2750+
my $ref = \%trie;
2751+
while (scalar @dirs &&
2752+
exists($ref->{$dirs[0]})) {
2753+
$ref = $ref->{shift @dirs};
2754+
}
2755+
# create rest of trie structure from rest of components
2756+
foreach my $dir (@dirs) {
2757+
$ref = $ref->{$dir} = {};
2758+
}
2759+
# create end marker, store $pr as a data
2760+
$ref->{''} = $pr if (!exists $ref->{''});
2761+
}
2762+
2763+
# filter out forks, by finding shortest prefix match for paths
2764+
my @filtered;
2765+
PROJECT:
2766+
foreach my $pr (@$projects) {
2767+
# trie lookup
2768+
my $ref = \%trie;
2769+
DIR:
2770+
foreach my $dir (split('/', $pr->{'path'})) {
2771+
if (exists $ref->{''}) {
2772+
# found [shortest] prefix, is a fork - skip it
2773+
push @{$ref->{''}{'forks'}}, $pr;
2774+
next PROJECT;
2775+
}
2776+
if (!exists $ref->{$dir}) {
2777+
# not in trie, cannot have prefix, not a fork
2778+
push @filtered, $pr;
2779+
next PROJECT;
2780+
}
2781+
# If the dir is there, we just walk one step down the trie.
2782+
$ref = $ref->{$dir};
2783+
}
2784+
# we ran out of trie
2785+
# (shouldn't happen: it's either no match, or end marker)
2786+
push @filtered, $pr;
2787+
}
2788+
2789+
return @filtered;
2790+
}
2791+
2792+
# note: fill_project_list_info must be run first,
2793+
# for 'descr_long' and 'ctags' to be filled
2794+
sub search_projects_list {
2795+
my ($projlist, %opts) = @_;
2796+
my $tagfilter = $opts{'tagfilter'};
2797+
my $searchtext = $opts{'searchtext'};
2798+
2799+
return @$projlist
2800+
unless ($tagfilter || $searchtext);
2801+
2802+
my @projects;
2803+
PROJECT:
2804+
foreach my $pr (@$projlist) {
2805+
2806+
if ($tagfilter) {
2807+
next unless ref($pr->{'ctags'}) eq 'HASH';
2808+
next unless
2809+
grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
2810+
}
2811+
2812+
if ($searchtext) {
2813+
next unless
2814+
$pr->{'path'} =~ /$searchtext/ ||
2815+
$pr->{'descr_long'} =~ /$searchtext/;
2816+
}
2817+
2818+
push @projects, $pr;
2819+
}
2820+
2821+
return @projects;
2822+
}
2823+
27562824
our $gitweb_project_owner = undef;
27572825
sub git_get_project_list_from_file {
27582826

@@ -4742,7 +4810,7 @@ sub git_patchset_body {
47424810
# project in the list, removing invalid projects from returned list
47434811
# NOTE: modifies $projlist, but does not remove entries from it
47444812
sub fill_project_list_info {
4745-
my ($projlist, $check_forks) = @_;
4813+
my $projlist = shift;
47464814
my @projects;
47474815

47484816
my $show_ctags = gitweb_check_feature('ctags');
@@ -4762,23 +4830,36 @@ sub fill_project_list_info {
47624830
if (!defined $pr->{'owner'}) {
47634831
$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
47644832
}
4765-
if ($check_forks) {
4766-
my $pname = $pr->{'path'};
4767-
if (($pname =~ s/\.git$//) &&
4768-
($pname !~ /\/$/) &&
4769-
(-d "$projectroot/$pname")) {
4770-
$pr->{'forks'} = "-d $projectroot/$pname";
4771-
} else {
4772-
$pr->{'forks'} = 0;
4773-
}
4833+
if ($show_ctags) {
4834+
$pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
47744835
}
4775-
$show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
47764836
push @projects, $pr;
47774837
}
47784838

47794839
return @projects;
47804840
}
47814841

4842+
sub sort_projects_list {
4843+
my ($projlist, $order) = @_;
4844+
my @projects;
4845+
4846+
my %order_info = (
4847+
project => { key => 'path', type => 'str' },
4848+
descr => { key => 'descr_long', type => 'str' },
4849+
owner => { key => 'owner', type => 'str' },
4850+
age => { key => 'age', type => 'num' }
4851+
);
4852+
my $oi = $order_info{$order};
4853+
return @$projlist unless defined $oi;
4854+
if ($oi->{'type'} eq 'str') {
4855+
@projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist;
4856+
} else {
4857+
@projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist;
4858+
}
4859+
4860+
return @projects;
4861+
}
4862+
47824863
# print 'sort by' <th> element, generating 'sort by $name' replay link
47834864
# if that order is not selected
47844865
sub print_sort_th {
@@ -4805,28 +4886,39 @@ sub format_sort_th {
48054886
sub git_project_list_body {
48064887
# actually uses global variable $project
48074888
my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4889+
my @projects = @$projlist;
48084890

48094891
my $check_forks = gitweb_check_feature('forks');
4810-
my @projects = fill_project_list_info($projlist, $check_forks);
4892+
my $show_ctags = gitweb_check_feature('ctags');
4893+
my $tagfilter = $show_ctags ? $cgi->param('by_tag') : undef;
4894+
$check_forks = undef
4895+
if ($tagfilter || $searchtext);
4896+
4897+
# filtering out forks before filling info allows to do less work
4898+
@projects = filter_forks_from_projects_list(\@projects)
4899+
if ($check_forks);
4900+
@projects = fill_project_list_info(\@projects);
4901+
# searching projects require filling to be run before it
4902+
@projects = search_projects_list(\@projects,
4903+
'searchtext' => $searchtext,
4904+
'tagfilter' => $tagfilter)
4905+
if ($tagfilter || $searchtext);
48114906

48124907
$order ||= $default_projects_order;
48134908
$from = 0 unless defined $from;
48144909
$to = $#projects if (!defined $to || $#projects < $to);
48154910

4816-
my %order_info = (
4817-
project => { key => 'path', type => 'str' },
4818-
descr => { key => 'descr_long', type => 'str' },
4819-
owner => { key => 'owner', type => 'str' },
4820-
age => { key => 'age', type => 'num' }
4821-
);
4822-
my $oi = $order_info{$order};
4823-
if ($oi->{'type'} eq 'str') {
4824-
@projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4825-
} else {
4826-
@projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4911+
# short circuit
4912+
if ($from > $to) {
4913+
print "<center>\n".
4914+
"<b>No such projects found</b><br />\n".
4915+
"Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
4916+
"</center>\n<br />\n";
4917+
return;
48274918
}
48284919

4829-
my $show_ctags = gitweb_check_feature('ctags');
4920+
@projects = sort_projects_list(\@projects, $order);
4921+
48304922
if ($show_ctags) {
48314923
my %ctags;
48324924
foreach my $p (@projects) {
@@ -4852,32 +4944,26 @@ sub git_project_list_body {
48524944
"</tr>\n";
48534945
}
48544946
my $alternate = 1;
4855-
my $tagfilter = $cgi->param('by_tag');
48564947
for (my $i = $from; $i <= $to; $i++) {
48574948
my $pr = $projects[$i];
48584949

4859-
next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4860-
next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4861-
and not $pr->{'descr_long'} =~ /$searchtext/;
4862-
# Weed out forks or non-matching entries of search
4863-
if ($check_forks) {
4864-
my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4865-
$forkbase="^$forkbase" if $forkbase;
4866-
next if not $searchtext and not $tagfilter and $show_ctags
4867-
and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4868-
}
4869-
48704950
if ($alternate) {
48714951
print "<tr class=\"dark\">\n";
48724952
} else {
48734953
print "<tr class=\"light\">\n";
48744954
}
48754955
$alternate ^= 1;
4956+
48764957
if ($check_forks) {
48774958
print "<td>";
48784959
if ($pr->{'forks'}) {
4879-
print "<!-- $pr->{'forks'} -->\n";
4880-
print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4960+
my $nforks = scalar @{$pr->{'forks'}};
4961+
if ($nforks > 0) {
4962+
print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
4963+
-title => "$nforks forks"}, "+");
4964+
} else {
4965+
print $cgi->span({-title => "$nforks forks"}, "+");
4966+
}
48814967
}
48824968
print "</td>\n";
48834969
}
@@ -5357,7 +5443,10 @@ sub git_forks {
53575443
}
53585444

53595445
sub git_project_index {
5360-
my @projects = git_get_projects_list($project);
5446+
my @projects = git_get_projects_list();
5447+
if (!@projects) {
5448+
die_error(404, "No projects found");
5449+
}
53615450

53625451
print $cgi->header(
53635452
-type => 'text/plain',
@@ -5399,7 +5488,11 @@ sub git_summary {
53995488
my $check_forks = gitweb_check_feature('forks');
54005489

54015490
if ($check_forks) {
5491+
# find forks of a project
54025492
@forklist = git_get_projects_list($project);
5493+
# filter out forks of forks
5494+
@forklist = filter_forks_from_projects_list(\@forklist)
5495+
if (@forklist);
54035496
}
54045497

54055498
git_header_html();
@@ -7319,6 +7412,9 @@ sub git_atom {
73197412

73207413
sub git_opml {
73217414
my @list = git_get_projects_list();
7415+
if (!@list) {
7416+
die_error(404, "No projects found");
7417+
}
73227418

73237419
print $cgi->header(
73247420
-type => 'text/xml',

0 commit comments

Comments
 (0)