1#!/usr/bin/perl -w 2# (c) 2007, Joe Perches <joe@perches.com> 3# created from checkpatch.pl 4# 5# Print selected MAINTAINERS information for 6# the files modified in a patch or for a file 7# 8# usage: perl scripts/get_maintainers.pl [OPTIONS] <patch> 9# perl scripts/get_maintainers.pl [OPTIONS] -f <file> 10# 11# Licensed under the terms of the GNU GPL License version 2 12 13use strict; 14 15my $P = $0; 16my $V = '0.16'; 17 18use Getopt::Long qw(:config no_auto_abbrev); 19 20my $lk_path = "./"; 21my $email = 1; 22my $email_usename = 1; 23my $email_maintainer = 1; 24my $email_list = 1; 25my $email_subscriber_list = 0; 26my $email_git = 1; 27my $email_git_penguin_chiefs = 0; 28my $email_git_min_signatures = 1; 29my $email_git_max_maintainers = 5; 30my $email_git_since = "1-year-ago"; 31my $output_multiline = 1; 32my $output_separator = ", "; 33my $scm = 0; 34my $web = 0; 35my $subsystem = 0; 36my $status = 0; 37my $from_filename = 0; 38my $version = 0; 39my $help = 0; 40 41my $exit = 0; 42 43my @penguin_chief = (); 44push(@penguin_chief,"Linus Torvalds:torvalds\@linux-foundation.org"); 45#Andrew wants in on most everything - 2009/01/14 46#push(@penguin_chief,"Andrew Morton:akpm\@linux-foundation.org"); 47 48my @penguin_chief_names = (); 49foreach my $chief (@penguin_chief) { 50 if ($chief =~ m/^(.*):(.*)/) { 51 my $chief_name = $1; 52 my $chief_addr = $2; 53 push(@penguin_chief_names, $chief_name); 54 } 55} 56my $penguin_chiefs = "\(" . join("|",@penguin_chief_names) . "\)"; 57 58# rfc822 email address - preloaded methods go here. 59my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])"; 60my $rfc822_char = '[\\000-\\377]'; 61 62if (!GetOptions( 63 'email!' => \$email, 64 'git!' => \$email_git, 65 'git-chief-penguins!' => \$email_git_penguin_chiefs, 66 'git-min-signatures=i' => \$email_git_min_signatures, 67 'git-max-maintainers=i' => \$email_git_max_maintainers, 68 'git-since=s' => \$email_git_since, 69 'm!' => \$email_maintainer, 70 'n!' => \$email_usename, 71 'l!' => \$email_list, 72 's!' => \$email_subscriber_list, 73 'multiline!' => \$output_multiline, 74 'separator=s' => \$output_separator, 75 'subsystem!' => \$subsystem, 76 'status!' => \$status, 77 'scm!' => \$scm, 78 'web!' => \$web, 79 'f|file' => \$from_filename, 80 'v|version' => \$version, 81 'h|help' => \$help, 82 )) { 83 usage(); 84 die "$P: invalid argument\n"; 85} 86 87if ($help != 0) { 88 usage(); 89 exit 0; 90} 91 92if ($version != 0) { 93 print("${P} ${V}\n"); 94 exit 0; 95} 96 97if ($#ARGV < 0) { 98 usage(); 99 die "$P: argument missing: patchfile or -f file please\n"; 100} 101 102my $selections = $email + $scm + $status + $subsystem + $web; 103if ($selections == 0) { 104 usage(); 105 die "$P: Missing required option: email, scm, status, subsystem or web\n"; 106} 107 108if ($email && ($email_maintainer + $email_list + $email_subscriber_list 109 + $email_git + $email_git_penguin_chiefs) == 0) { 110 usage(); 111 die "$P: Please select at least 1 email option\n"; 112} 113 114if (!top_of_kernel_tree($lk_path)) { 115 die "$P: The current directory does not appear to be " 116 . "a linux kernel source tree.\n"; 117} 118 119## Read MAINTAINERS for type/value pairs 120 121my @typevalue = (); 122open(MAINT, "<${lk_path}MAINTAINERS") || die "$P: Can't open MAINTAINERS\n"; 123while (<MAINT>) { 124 my $line = $_; 125 126 if ($line =~ m/^(\C):\s*(.*)/) { 127 my $type = $1; 128 my $value = $2; 129 130 ##Filename pattern matching 131 if ($type eq "F" || $type eq "X") { 132 $value =~ s@\.@\\\.@g; ##Convert . to \. 133 $value =~ s/\*/\.\*/g; ##Convert * to .* 134 $value =~ s/\?/\./g; ##Convert ? to . 135 } 136 push(@typevalue, "$type:$value"); 137 } elsif (!/^(\s)*$/) { 138 $line =~ s/\n$//g; 139 push(@typevalue, $line); 140 } 141} 142close(MAINT); 143 144## use the filenames on the command line or find the filenames in the patchfiles 145 146my @files = (); 147 148foreach my $file (@ARGV) { 149 next if ((-d $file)); 150 if (!(-f $file)) { 151 die "$P: file '${file}' not found\n"; 152 } 153 if ($from_filename) { 154 push(@files, $file); 155 } else { 156 my $file_cnt = @files; 157 open(PATCH, "<$file") or die "$P: Can't open ${file}\n"; 158 while (<PATCH>) { 159 if (m/^\+\+\+\s+(\S+)/) { 160 my $filename = $1; 161 $filename =~ s@^[^/]*/@@; 162 $filename =~ s@\n@@; 163 push(@files, $filename); 164 } 165 } 166 close(PATCH); 167 if ($file_cnt == @files) { 168 warn "$P: file '${file}' doesn't appear to be a patch. " 169 . "Add -f to options?\n"; 170 } 171 @files = sort_and_uniq(@files); 172 } 173} 174 175my @email_to = (); 176my @list_to = (); 177my @scm = (); 178my @web = (); 179my @subsystem = (); 180my @status = (); 181 182# Find responsible parties 183 184foreach my $file (@files) { 185 186#Do not match excluded file patterns 187 188 my $exclude = 0; 189 foreach my $line (@typevalue) { 190 if ($line =~ m/^(\C):\s*(.*)/) { 191 my $type = $1; 192 my $value = $2; 193 if ($type eq 'X') { 194 if (file_match_pattern($file, $value)) { 195 $exclude = 1; 196 } 197 } 198 } 199 } 200 201 if (!$exclude) { 202 my $tvi = 0; 203 foreach my $line (@typevalue) { 204 if ($line =~ m/^(\C):\s*(.*)/) { 205 my $type = $1; 206 my $value = $2; 207 if ($type eq 'F') { 208 if (file_match_pattern($file, $value)) { 209 add_categories($tvi); 210 } 211 } 212 } 213 $tvi++; 214 } 215 } 216 217 if ($email && $email_git) { 218 recent_git_signoffs($file); 219 } 220 221} 222 223if ($email) { 224 foreach my $chief (@penguin_chief) { 225 if ($chief =~ m/^(.*):(.*)/) { 226 my $email_address; 227 if ($email_usename) { 228 $email_address = format_email($1, $2); 229 } else { 230 $email_address = $2; 231 } 232 if ($email_git_penguin_chiefs) { 233 push(@email_to, $email_address); 234 } else { 235 @email_to = grep(!/${email_address}/, @email_to); 236 } 237 } 238 } 239} 240 241if ($email || $email_list) { 242 my @to = (); 243 if ($email) { 244 @to = (@to, @email_to); 245 } 246 if ($email_list) { 247 @to = (@to, @list_to); 248 } 249 output(uniq(@to)); 250} 251 252if ($scm) { 253 @scm = sort_and_uniq(@scm); 254 output(@scm); 255} 256 257if ($status) { 258 @status = sort_and_uniq(@status); 259 output(@status); 260} 261 262if ($subsystem) { 263 @subsystem = sort_and_uniq(@subsystem); 264 output(@subsystem); 265} 266 267if ($web) { 268 @web = sort_and_uniq(@web); 269 output(@web); 270} 271 272exit($exit); 273 274sub file_match_pattern { 275 my ($file, $pattern) = @_; 276 if (substr($pattern, -1) eq "/") { 277 if ($file =~ m@^$pattern@) { 278 return 1; 279 } 280 } else { 281 if ($file =~ m@^$pattern@) { 282 my $s1 = ($file =~ tr@/@@); 283 my $s2 = ($pattern =~ tr@/@@); 284 if ($s1 == $s2) { 285 return 1; 286 } 287 } 288 } 289 return 0; 290} 291 292sub usage { 293 print <<EOT; 294usage: $P [options] patchfile 295 $P [options] -f file 296version: $V 297 298MAINTAINER field selection options: 299 --email => print email address(es) if any 300 --git => include recent git \*-by: signers 301 --git-chief-penguins => include ${penguin_chiefs} 302 --git-min-signatures => number of signatures required (default: 1) 303 --git-max-maintainers => maximum maintainers to add (default: 5) 304 --git-since => git history to use (default: 1-year-ago) 305 --m => include maintainer(s) if any 306 --n => include name 'Full Name <addr\@domain.tld>' 307 --l => include list(s) if any 308 --s => include subscriber only list(s) if any 309 --scm => print SCM tree(s) if any 310 --status => print status if any 311 --subsystem => print subsystem name if any 312 --web => print website(s) if any 313 314Output type options: 315 --separator [, ] => separator for multiple entries on 1 line 316 --multiline => print 1 entry per line 317 318Default options: 319 [--email --git --m --n --l --multiline] 320 321Other options: 322 --version => show version 323 --help => show this help information 324 325EOT 326} 327 328sub top_of_kernel_tree { 329 my ($lk_path) = @_; 330 331 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") { 332 $lk_path .= "/"; 333 } 334 if ( (-f "${lk_path}COPYING") 335 && (-f "${lk_path}CREDITS") 336 && (-f "${lk_path}Kbuild") 337 && (-f "${lk_path}MAINTAINERS") 338 && (-f "${lk_path}Makefile") 339 && (-f "${lk_path}README") 340 && (-d "${lk_path}Documentation") 341 && (-d "${lk_path}arch") 342 && (-d "${lk_path}include") 343 && (-d "${lk_path}drivers") 344 && (-d "${lk_path}fs") 345 && (-d "${lk_path}init") 346 && (-d "${lk_path}ipc") 347 && (-d "${lk_path}kernel") 348 && (-d "${lk_path}lib") 349 && (-d "${lk_path}scripts")) { 350 return 1; 351 } 352 return 0; 353} 354 355sub format_email { 356 my ($name, $email) = @_; 357 358 $name =~ s/^\s+|\s+$//g; 359 $name =~ s/^\"|\"$//g; 360 $email =~ s/^\s+|\s+$//g; 361 362 my $formatted_email = ""; 363 364 if ($name =~ /[^a-z0-9 \.\-]/i) { ##has "must quote" chars 365 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes 366 $formatted_email = "\"${name}\"\ \<${email}\>"; 367 } else { 368 $formatted_email = "${name} \<${email}\>"; 369 } 370 return $formatted_email; 371} 372 373sub add_categories { 374 my ($index) = @_; 375 376 $index = $index - 1; 377 while ($index >= 0) { 378 my $tv = $typevalue[$index]; 379 if ($tv =~ m/^(\C):\s*(.*)/) { 380 my $ptype = $1; 381 my $pvalue = $2; 382 if ($ptype eq "L") { 383 my $list_address = $pvalue; 384 my $list_additional = ""; 385 if ($list_address =~ m/([^\s]+)\s+(.*)$/) { 386 $list_address = $1; 387 $list_additional = $2; 388 } 389 if ($list_additional =~ m/subscribers-only/) { 390 if ($email_subscriber_list) { 391 push(@list_to, $list_address); 392 } 393 } else { 394 if ($email_list) { 395 push(@list_to, $list_address); 396 } 397 } 398 } elsif ($ptype eq "M") { 399 my $p_used = 0; 400 if ($index >= 0) { 401 my $tv = $typevalue[$index - 1]; 402 if ($tv =~ m/^(\C):\s*(.*)/) { 403 if ($1 eq "P") { 404 if ($email_usename) { 405 push_email_address(format_email($2, $pvalue)); 406 $p_used = 1; 407 } 408 } 409 } 410 } 411 if (!$p_used) { 412 push_email_addresses($pvalue); 413 } 414 } elsif ($ptype eq "T") { 415 push(@scm, $pvalue); 416 } elsif ($ptype eq "W") { 417 push(@web, $pvalue); 418 } elsif ($ptype eq "S") { 419 push(@status, $pvalue); 420 } 421 422 $index--; 423 } else { 424 push(@subsystem,$tv); 425 $index = -1; 426 } 427 } 428} 429 430sub push_email_address { 431 my ($email_address) = @_; 432 433 my $email_name = ""; 434 if ($email_address =~ m/([^<]+)<(.*\@.*)>$/) { 435 $email_name = $1; 436 $email_address = $2; 437 } 438 439 if ($email_maintainer) { 440 if ($email_usename && $email_name) { 441 push(@email_to, format_email($email_name, $email_address)); 442 } else { 443 push(@email_to, $email_address); 444 } 445 } 446} 447 448sub push_email_addresses { 449 my ($address) = @_; 450 451 my @address_list = (); 452 453 if (rfc822_valid($address)) { 454 push_email_address($address); 455 } elsif (@address_list = rfc822_validlist($address)) { 456 my $array_count = shift(@address_list); 457 while (my $entry = shift(@address_list)) { 458 push_email_address($entry); 459 } 460 } else { 461 warn("Invalid MAINTAINERS address: '" . $address . "'\n"); 462 } 463} 464 465sub which { 466 my ($bin) = @_; 467 468 foreach my $path (split(/:/, $ENV{PATH})) { 469 if (-e "$path/$bin") { 470 return "$path/$bin"; 471 } 472 } 473 474 return ""; 475} 476 477sub recent_git_signoffs { 478 my ($file) = @_; 479 480 my $sign_offs = ""; 481 my $cmd = ""; 482 my $output = ""; 483 my $count = 0; 484 my @lines = (); 485 486 if (which("git") eq "") { 487 warn("$P: git not found. Add --nogit to options?\n"); 488 return; 489 } 490 if (!(-d ".git")) { 491 warn("$P: .git directory not found. Use a git repository for better results.\n"); 492 warn("$P: perhaps 'git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git'\n"); 493 return; 494 } 495 496 $cmd = "git log --since=${email_git_since} -- ${file}"; 497 $cmd .= " | grep -Ei \"^[-_ a-z]+by:.*\\\@.*\$\""; 498 if (!$email_git_penguin_chiefs) { 499 $cmd .= " | grep -Ev \"${penguin_chiefs}\""; 500 } 501 $cmd .= " | cut -f2- -d\":\""; 502 $cmd .= " | sort | uniq -c | sort -rn"; 503 504 $output = `${cmd}`; 505 $output =~ s/^\s*//gm; 506 507 @lines = split("\n", $output); 508 foreach my $line (@lines) { 509 if ($line =~ m/([0-9]+)\s+(.*)/) { 510 my $sign_offs = $1; 511 $line = $2; 512 $count++; 513 if ($sign_offs < $email_git_min_signatures || 514 $count > $email_git_max_maintainers) { 515 last; 516 } 517 } else { 518 die("$P: Unexpected git output: ${line}\n"); 519 } 520 if ($line =~ m/(.+)<(.+)>/) { 521 my $git_name = $1; 522 my $git_addr = $2; 523 if ($email_usename) { 524 push(@email_to, format_email($git_name, $git_addr)); 525 } else { 526 push(@email_to, $git_addr); 527 } 528 } elsif ($line =~ m/<(.+)>/) { 529 my $git_addr = $1; 530 push(@email_to, $git_addr); 531 } else { 532 push(@email_to, $line); 533 } 534 } 535} 536 537sub uniq { 538 my @parms = @_; 539 540 my %saw; 541 @parms = grep(!$saw{$_}++, @parms); 542 return @parms; 543} 544 545sub sort_and_uniq { 546 my @parms = @_; 547 548 my %saw; 549 @parms = sort @parms; 550 @parms = grep(!$saw{$_}++, @parms); 551 return @parms; 552} 553 554sub output { 555 my @parms = @_; 556 557 if ($output_multiline) { 558 foreach my $line (@parms) { 559 print("${line}\n"); 560 } 561 } else { 562 print(join($output_separator, @parms)); 563 print("\n"); 564 } 565} 566 567my $rfc822re; 568 569sub make_rfc822re { 570# Basic lexical tokens are specials, domain_literal, quoted_string, atom, and 571# comment. We must allow for rfc822_lwsp (or comments) after each of these. 572# This regexp will only work on addresses which have had comments stripped 573# and replaced with rfc822_lwsp. 574 575 my $specials = '()<>@,;:\\\\".\\[\\]'; 576 my $controls = '\\000-\\037\\177'; 577 578 my $dtext = "[^\\[\\]\\r\\\\]"; 579 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*"; 580 581 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*"; 582 583# Use zero-width assertion to spot the limit of an atom. A simple 584# $rfc822_lwsp* causes the regexp engine to hang occasionally. 585 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))"; 586 my $word = "(?:$atom|$quoted_string)"; 587 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*"; 588 589 my $sub_domain = "(?:$atom|$domain_literal)"; 590 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*"; 591 592 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain"; 593 594 my $phrase = "$word*"; 595 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)"; 596 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*"; 597 my $mailbox = "(?:$addr_spec|$phrase$route_addr)"; 598 599 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*"; 600 my $address = "(?:$mailbox|$group)"; 601 602 return "$rfc822_lwsp*$address"; 603} 604 605sub rfc822_strip_comments { 606 my $s = shift; 607# Recursively remove comments, and replace with a single space. The simpler 608# regexps in the Email Addressing FAQ are imperfect - they will miss escaped 609# chars in atoms, for example. 610 611 while ($s =~ s/^((?:[^"\\]|\\.)* 612 (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*) 613 \((?:[^()\\]|\\.)*\)/$1 /osx) {} 614 return $s; 615} 616 617# valid: returns true if the parameter is an RFC822 valid address 618# 619sub rfc822_valid ($) { 620 my $s = rfc822_strip_comments(shift); 621 622 if (!$rfc822re) { 623 $rfc822re = make_rfc822re(); 624 } 625 626 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/; 627} 628 629# validlist: In scalar context, returns true if the parameter is an RFC822 630# valid list of addresses. 631# 632# In list context, returns an empty list on failure (an invalid 633# address was found); otherwise a list whose first element is the 634# number of addresses found and whose remaining elements are the 635# addresses. This is needed to disambiguate failure (invalid) 636# from success with no addresses found, because an empty string is 637# a valid list. 638 639sub rfc822_validlist ($) { 640 my $s = rfc822_strip_comments(shift); 641 642 if (!$rfc822re) { 643 $rfc822re = make_rfc822re(); 644 } 645 # * null list items are valid according to the RFC 646 # * the '1' business is to aid in distinguishing failure from no results 647 648 my @r; 649 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so && 650 $s =~ m/^$rfc822_char*$/) { 651 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) { 652 push @r, $1; 653 } 654 return wantarray ? (scalar(@r), @r) : 1; 655 } 656 else { 657 return wantarray ? () : 0; 658 } 659} 660