This is a live mirror of the Perl 5 development currently hosted at https://github.com/perl/perl5
1c52829f329500c6f91de2c82c7be7e324ceda81
[perl5.git] / Porting / Maintainers.pm
1 #
2 # Maintainers.pm - show information about maintainers
3 #
4
5 package Maintainers;
6
7 use strict;
8 use warnings;
9
10 use lib "Porting";
11 # Please don't use post 5.008 features as this module is used by
12 # Porting/makemeta, and that in turn has to be run by the perl just built.
13 use 5.008;
14
15 require "Maintainers.pl";
16 use vars qw(%Modules %Maintainers);
17
18 use vars qw(@ISA @EXPORT_OK $VERSION);
19 @ISA = qw(Exporter);
20 @EXPORT_OK = qw(%Modules %Maintainers
21                 get_module_files get_module_pat
22                 show_results process_options files_to_modules
23                 finish_tap_output
24                 reload_manifest);
25 $VERSION = 0.12;
26
27 require Exporter;
28
29 use File::Find;
30 use Getopt::Long;
31
32 my %MANIFEST;
33
34 # (re)read the MANIFEST file, blowing away any previous effort
35
36 sub reload_manifest {
37     %MANIFEST = ();
38
39     my $manifest_path = 'MANIFEST';
40    if (! -e  $manifest_path) {
41         $manifest_path = "../MANIFEST";
42     }
43
44     if (open(my $manfh,  '<', $manifest_path )) {
45         while (<$manfh>) {
46             if (/^(\S+)/) {
47                 $MANIFEST{$1}++;
48             }
49             else {
50                 warn "MANIFEST:$.: malformed line: $_\n";
51             }
52         }
53         close $manfh;
54     } else {
55             die "$0: Failed to open MANIFEST for reading: $!\n";
56     }
57 }
58
59 reload_manifest;
60
61
62 sub get_module_pat {
63     my $m = shift;
64     split ' ', $Modules{$m}{FILES};
65 }
66
67 # expand dir/ or foo* into a full list of files
68 #
69 sub expand_glob {
70     sort { lc $a cmp lc $b }
71         map {
72             -f $_ && $_ !~ /[*?]/ ? # File as-is.
73                 $_ :
74                 -d _ && $_ !~ /[*?]/ ? # Recurse into directories.
75                 do {
76                     my @files;
77                     find(
78                          sub {
79                              push @files, $File::Find::name
80                                  if -f $_ && exists $MANIFEST{$File::Find::name};
81                          }, $_);
82                     @files;
83                 }
84             # Not a glob, but doesn't exist
85             : $_ !~ /[*?{]/ ? $_
86             # The rest are globbable patterns; expand the glob, then
87             # recursively perform directory expansion on any results
88             : expand_glob(glob($_))
89             } @_;
90 }
91
92 sub filter_excluded {
93     my ($m, @files) = @_;
94
95     my $excluded = $Modules{$m}{EXCLUDED};
96     return @files
97         unless $excluded and @$excluded;
98
99     my ($pat) = map { qr/$_/ } join '|' => map {
100         ref $_ ? $_ : qr/\b\Q$_\E$/
101     } @{ $excluded };
102
103     return grep { $_ !~ $pat } @files;
104 }
105
106 sub get_module_files {
107     my $m = shift;
108     return filter_excluded $m => map { expand_glob($_) } get_module_pat($m);
109 }
110
111
112 sub get_maintainer_modules {
113     my $m = shift;
114     sort { lc $a cmp lc $b }
115     grep { $Modules{$_}{MAINTAINER} eq $m }
116     keys %Modules;
117 }
118
119 sub usage {
120     warn <<__EOF__;
121 $0: Usage:
122     --maintainer M | --module M [--files]
123                 List modules or maintainers matching the pattern M.
124                 With --files, list all the files associated with them
125 or
126     --check | --checkmani [commit | file ... | dir ... ]
127                 Check consistency of Maintainers.pl
128                         with a file     checks if it has a maintainer
129                         with a dir      checks all files have a maintainer
130                         with a commit   checks files modified by that commit
131                         no arg          checks for multiple maintainers
132                --checkmani is like --check, but only reports on unclaimed
133                files if they are in MANIFEST
134 or
135     --opened  | file ....
136                 List the module ownership of modified or the listed files
137
138 Matching is case-ignoring regexp, author matching is both by
139 the short id and by the full name and email.  A "module" may
140 not be just a module, it may be a file or files or a subdirectory.
141 The options may be abbreviated to their unique prefixes
142 __EOF__
143     exit(0);
144 }
145
146 my $Maintainer;
147 my $Module;
148 my $Files;
149 my $Check;
150 my $Checkmani;
151 my $Opened;
152 my $TestCounter = 0;
153
154 sub process_options {
155     usage()
156         unless
157             GetOptions(
158                        'maintainer=s'   => \$Maintainer,
159                        'module=s'       => \$Module,
160                        'files'          => \$Files,
161                        'check'          => \$Check,
162                        'checkmani'      => \$Checkmani,
163                        'opened'         => \$Opened,
164                       );
165
166     my @Files;
167
168     if ($Opened) {
169         usage if @ARGV;
170         chomp (@Files = `git ls-files -m --full-name`);
171         die if $?;
172     } elsif (@ARGV == 1 &&
173              $ARGV[0] =~ /^(?:HEAD|[0-9a-f]{4,40})(?:~\d+)?\^*$/) {
174         my $command = "git diff --name-only $ARGV[0]^ $ARGV[0]";
175         chomp (@Files = `$command`);
176         die "'$command' failed: $?" if $?;
177     } else {
178         @Files = @ARGV;
179     }
180
181     usage() if @Files && ($Maintainer || $Module || $Files);
182
183     for my $mean ($Maintainer, $Module) {
184         warn "$0: Did you mean '$0 $mean'?\n"
185             if $mean && -e $mean && $mean ne '.' && !$Files;
186     }
187
188     warn "$0: Did you mean '$0 -mo $Maintainer'?\n"
189         if defined $Maintainer && exists $Modules{$Maintainer};
190
191     warn "$0: Did you mean '$0 -ma $Module'?\n"
192         if defined $Module     && exists $Maintainers{$Module};
193
194     return ($Maintainer, $Module, $Files, @Files);
195 }
196
197 sub files_to_modules {
198     my @Files = @_;
199     my %ModuleByFile;
200
201     for (@Files) { s:^\./:: }
202
203     @ModuleByFile{@Files} = ();
204
205     # First try fast match.
206
207     my %ModuleByPat;
208     for my $module (keys %Modules) {
209         for my $pat (get_module_pat($module)) {
210             $ModuleByPat{$pat} = $module;
211         }
212     }
213     # Expand any globs.
214     my %ExpModuleByPat;
215     for my $pat (keys %ModuleByPat) {
216         if (-e $pat) {
217             $ExpModuleByPat{$pat} = $ModuleByPat{$pat};
218         } else {
219             for my $exp (glob($pat)) {
220                 $ExpModuleByPat{$exp} = $ModuleByPat{$pat};
221             }
222         }
223     }
224     %ModuleByPat = %ExpModuleByPat;
225     for my $file (@Files) {
226         $ModuleByFile{$file} = $ModuleByPat{$file}
227             if exists $ModuleByPat{$file};
228     }
229
230     # If still unresolved files...
231     if (my @ToDo = grep { !defined $ModuleByFile{$_} } keys %ModuleByFile) {
232
233         # Cannot match what isn't there.
234         @ToDo = grep { -e $_ } @ToDo;
235
236         if (@ToDo) {
237             # Try prefix matching.
238
239             # Need to try longest prefixes first, else lib/CPAN may match
240             # lib/CPANPLUS/... and similar
241
242             my @OrderedModuleByPat
243                 = sort {length $b <=> length $a} keys %ModuleByPat;
244
245             # Remove trailing slashes.
246             for (@ToDo) { s|/$|| }
247
248             my %ToDo;
249             @ToDo{@ToDo} = ();
250
251             for my $pat (@OrderedModuleByPat) {
252                 last unless keys %ToDo;
253                 if (-d $pat) {
254                     my @Done;
255                     for my $file (keys %ToDo) {
256                         if ($file =~ m|^$pat|i) {
257                             $ModuleByFile{$file} = $ModuleByPat{$pat};
258                             push @Done, $file;
259                         }
260                     }
261                     delete @ToDo{@Done};
262                 }
263             }
264         }
265     }
266     \%ModuleByFile;
267 }
268 sub show_results {
269     my ($Maintainer, $Module, $Files, @Files) = @_;
270
271     if ($Maintainer) {
272         for my $m (sort keys %Maintainers) {
273             if ($m =~ /$Maintainer/io || $Maintainers{$m} =~ /$Maintainer/io) {
274                 my @modules = get_maintainer_modules($m);
275                 if ($Module) {
276                     @modules = grep { /$Module/io } @modules;
277                 }
278                 if ($Files) {
279                     my @files;
280                     for my $module (@modules) {
281                         push @files, get_module_files($module);
282                     }
283                     printf "%-15s @files\n", $m;
284                 } else {
285                     if ($Module) {
286                         printf "%-15s @modules\n", $m;
287                     } else {
288                         printf "%-15s $Maintainers{$m}\n", $m;
289                     }
290                 }
291             }
292         }
293     } elsif ($Module) {
294         for my $m (sort { lc $a cmp lc $b } keys %Modules) {
295             if ($m =~ /$Module/io) {
296                 if ($Files) {
297                     my @files = get_module_files($m);
298                     printf "%-15s @files\n", $m;
299                 } else {
300                     printf "%-15s %-12s %s\n", $m, $Modules{$m}{MAINTAINER}, $Modules{$m}{UPSTREAM}||'unknown';
301                 }
302             }
303         }
304     } elsif ($Check or $Checkmani) {
305         require Test::More;
306         Test::More->import;
307         if( @Files ) {
308                     missing_maintainers(
309                         $Checkmani
310                             ? sub { -f $_ and exists $MANIFEST{$File::Find::name} }
311                             : sub { /\.(?:[chty]|p[lm]|xs)\z/msx },
312                         @Files
313                     );
314                 } else {
315                     duplicated_maintainers();
316                     superfluous_maintainers();
317                 }
318     } elsif (@Files) {
319         my $ModuleByFile = files_to_modules(@Files);
320         for my $file (@Files) {
321             if (defined $ModuleByFile->{$file}) {
322                 my $module     = $ModuleByFile->{$file};
323                 my $maintainer = $Modules{$ModuleByFile->{$file}}{MAINTAINER};
324                 my $upstream   = $Modules{$module}{UPSTREAM}||'unknown';
325                 printf "%-15s [%-7s] $module $maintainer $Maintainers{$maintainer}\n", $file, $upstream;
326             } else {
327                 printf "%-15s ?\n", $file;
328             }
329         }
330     }
331     elsif ($Opened) {
332         print STDERR "(No files are modified)\n";
333     }
334     else {
335         usage();
336     }
337 }
338
339 my %files;
340
341 sub maintainers_files {
342     %files = ();
343     for my $k (keys %Modules) {
344         for my $f (get_module_files($k)) {
345             ++$files{$f};
346         }
347     }
348 }
349
350 sub duplicated_maintainers {
351     maintainers_files();
352     for my $f (sort keys %files) {
353         cmp_ok($files{$f}, '<=', 1, "File $f appears $files{$f} times in Maintainers.pl");
354     }
355 }
356
357 sub warn_maintainer {
358     my $name = shift;
359     ok($files{$name}, "$name has a maintainer (see Porting/Maintainer.pl)");
360 }
361
362 sub missing_maintainers {
363     my($check, @path) = @_;
364     maintainers_files();
365     my @dir;
366     for my $d (@path) {
367             if( -d $d ) { push @dir, $d } else { warn_maintainer($d) }
368     }
369     find sub { warn_maintainer($File::Find::name) if $check->() }, @dir if @dir;
370 }
371
372 sub superfluous_maintainers {
373     maintainers_files();
374     for my $f (sort keys %files) {
375         ok($MANIFEST{$f}, "File $f has a maintainer and is in MANIFEST");
376     }
377 }
378
379 sub finish_tap_output {
380     done_testing();
381 }
382
383 1;
384