1# 2# Maintainers.pm - show information about maintainers 3# 4 5package Maintainers; 6 7use strict; 8use warnings; 9 10use 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. 13use 5.008; 14 15require "Maintainers.pl"; 16use vars qw(%Modules %Maintainers); 17 18use 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.10; 26 27require Exporter; 28 29use File::Find; 30use Getopt::Long; 31 32my %MANIFEST; 33 34# (re)read the MANIFEST file, blowing away any previous effort 35 36sub 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 59reload_manifest; 60 61 62sub 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# 69sub 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 92sub 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 106sub get_module_files { 107 my $m = shift; 108 return filter_excluded $m => map { expand_glob($_) } get_module_pat($m); 109} 110 111 112sub 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 119sub 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 125or 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 134or 135 --opened | file .... 136 List the module ownership of modified or the listed files 137 138Matching is case-ignoring regexp, author matching is both by 139the short id and by the full name and email. A "module" may 140not be just a module, it may be a file or files or a subdirectory. 141The options may be abbreviated to their unique prefixes 142__EOF__ 143 exit(0); 144} 145 146my $Maintainer; 147my $Module; 148my $Files; 149my $Check; 150my $Checkmani; 151my $Opened; 152my $TestCounter = 0; 153 154sub 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 197sub 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} 268sub 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 339my %files; 340 341sub 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 350sub 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 357sub warn_maintainer { 358 my $name = shift; 359 ok($files{$name}, "$name has a maintainer"); 360} 361 362sub 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 372sub 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 379sub finish_tap_output { 380 done_testing(); 381} 382 3831; 384 385