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