@@ -2651,21 +2651,23 @@ sub git_get_project_url_list {
26512651}
26522652
26532653sub 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+
27562824our $gitweb_project_owner = undef ;
27572825sub 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
47444812sub 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
47844865sub print_sort_th {
@@ -4805,28 +4886,39 @@ sub format_sort_th {
48054886sub 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
53595445sub 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
73207413sub 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