2651
}
2652
2653
sub git_get_projects_list {
2654
- my ($filter) = @_;
2654
+ my $filter = shift || '';
2655
my @list;
2656
2657
- $filter ||= '';
2657
$filter =~ s/\.git$//;
2658
2660
- my $check_forks = gitweb_check_feature('forks');
2661
-
2659
if (-d $projects_list) {
2660
# search in directory
2664
- my $dir = $projects_list . ($filter ? "/$filter" : '');
2661
+ my $dir = $projects_list;
2662
# remove the trailing "/"
2663
$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
+ }
2671
2672
File::Find::find({
2673
follow_fast => 1, # follow symbolic links
2682
# only directories can be git repositories
2683
return unless (-d $_);
2684
# don't traverse too deep (Find is super slow on os x)
2685
+ # $project_maxdepth excludes depth of $projectroot
2686
if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2687
$File::Find::prune = 1;
2688
return;
2689
}
2690
2688
- my $subdir = substr($File::Find::name, $pfxlen + 1);
2691
+ my $path = substr($File::Find::name, $pfxlen + 1);
2692
# we check related file in $projectroot
2690
- my $path = ($filter ? "$filter/" : '') . $subdir;
2693
if (check_export_ok("$projectroot/$path")) {
2694
push @list, { path => $path };
2695
$File::Find::prune = 1;
2702
# 'git%2Fgit.git Linus+Torvalds'
2703
# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2704
# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2703
- my %paths;
2705
open my $fd, '<', $projects_list or return;
2706
PROJECT:
2707
while (my $line = <$fd>) {
2712
if (!defined $path) {
2713
next;
2714
}
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;
2718
}
2719
if (check_export_ok("$projectroot/$path")) {
2720
my $pr = {
2722
owner => to_utf8($owner),
2723
};
2724
push @list, $pr;
2747
- (my $forks_path = $path) =~ s/\.git$//;
2748
- $paths{$forks_path}++;
2725
}
2726
}
2727
close $fd;
2729
return @list;
2730
}
2731
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
+
2824
our $gitweb_project_owner = undef;
2825
sub git_get_project_list_from_file {
2826
4810
# project in the list, removing invalid projects from returned list
4811
# NOTE: modifies $projlist, but does not remove entries from it
4812
sub fill_project_list_info {
4745
- my ($projlist, $check_forks) = @_;
4813
+ my $projlist = shift;
4814
my @projects;
4815
4816
my $show_ctags = gitweb_check_feature('ctags');
4830
if (!defined $pr->{'owner'}) {
4831
$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4832
}
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'});
4835
}
4775
- $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4836
push @projects, $pr;
4837
}
4838
4839
return @projects;
4840
}
4841
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
+
4863
# print 'sort by' <th> element, generating 'sort by $name' replay link
4864
# if that order is not selected
4865
sub print_sort_th {
4886
sub git_project_list_body {
4887
# actually uses global variable $project
4888
my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4889
+ my @projects = @$projlist;
4890
4891
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);
4906
4907
$order ||= $default_projects_order;
4908
$from = 0 unless defined $from;
4909
$to = $#projects if (!defined $to || $#projects < $to);
4910
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;
4918
}
4919
4829
- my $show_ctags = gitweb_check_feature('ctags');
4920
+ @projects = sort_projects_list(\@projects, $order);
4921
+
4922
if ($show_ctags) {
4923
my %ctags;
4924
foreach my $p (@projects) {
4944
"</tr>\n";
4945
}
4946
my $alternate = 1;
4855
- my $tagfilter = $cgi->param('by_tag');
4947
for (my $i = $from; $i <= $to; $i++) {
4948
my $pr = $projects[$i];
4949
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
-
4950
if ($alternate) {
4951
print "<tr class=\"dark\">\n";
4952
} else {
4953
print "<tr class=\"light\">\n";
4954
}
4955
$alternate ^= 1;
4956
+
4957
if ($check_forks) {
4958
print "<td>";
4959
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
+ }
4967
}
4968
print "</td>\n";
4969
}
5443
}
5444
5445
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
+ }
5450
5451
print $cgi->header(
5452
-type => 'text/plain',
5488
my $check_forks = gitweb_check_feature('forks');
5489
5490
if ($check_forks) {
5491
+ # find forks of a project
5492
@forklist = git_get_projects_list($project);
5493
+ # filter out forks of forks
5494
+ @forklist = filter_forks_from_projects_list(\@forklist)
5495
+ if (@forklist);
5496
}
5497
5498
git_header_html();
7412
7413
sub git_opml {
7414
my @list = git_get_projects_list();
7415
+ if (!@list) {
7416
+ die_error(404, "No projects found");
7417
+ }
7418
7419
print $cgi->header(
7420
-type => 'text/xml',