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_maintainer.pl [OPTIONS] <patch> 9# perl scripts/get_maintainer.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.26'; 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_penguin_chiefs = 0; 27my $email_git = 0; 28my $email_git_all_signature_types = 0; 29my $email_git_blame = 0; 30my $email_git_blame_signatures = 1; 31my $email_git_fallback = 1; 32my $email_git_min_signatures = 1; 33my $email_git_max_maintainers = 5; 34my $email_git_min_percent = 5; 35my $email_git_since = "1-year-ago"; 36my $email_hg_since = "-365"; 37my $interactive = 0; 38my $email_remove_duplicates = 1; 39my $email_use_mailmap = 1; 40my $output_multiline = 1; 41my $output_separator = ", "; 42my $output_roles = 0; 43my $output_rolestats = 1; 44my $scm = 0; 45my $web = 0; 46my $subsystem = 0; 47my $status = 0; 48my $keywords = 1; 49my $sections = 0; 50my $file_emails = 0; 51my $from_filename = 0; 52my $pattern_depth = 0; 53my $version = 0; 54my $help = 0; 55 56my $vcs_used = 0; 57 58my $exit = 0; 59 60my %commit_author_hash; 61my %commit_signer_hash; 62 63my @penguin_chief = (); 64push(@penguin_chief, "Linus Torvalds:torvalds\@linux-foundation.org"); 65#Andrew wants in on most everything - 2009/01/14 66#push(@penguin_chief, "Andrew Morton:akpm\@linux-foundation.org"); 67 68my @penguin_chief_names = (); 69foreach my $chief (@penguin_chief) { 70 if ($chief =~ m/^(.*):(.*)/) { 71 my $chief_name = $1; 72 my $chief_addr = $2; 73 push(@penguin_chief_names, $chief_name); 74 } 75} 76my $penguin_chiefs = "\(" . join("|", @penguin_chief_names) . "\)"; 77 78# Signature types of people who are either 79# a) responsible for the code in question, or 80# b) familiar enough with it to give relevant feedback 81my @signature_tags = (); 82push(@signature_tags, "Signed-off-by:"); 83push(@signature_tags, "Reviewed-by:"); 84push(@signature_tags, "Acked-by:"); 85 86# rfc822 email address - preloaded methods go here. 87my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])"; 88my $rfc822_char = '[\\000-\\377]'; 89 90# VCS command support: class-like functions and strings 91 92my %VCS_cmds; 93 94my %VCS_cmds_git = ( 95 "execute_cmd" => \&git_execute_cmd, 96 "available" => '(which("git") ne "") && (-d ".git")', 97 "find_signers_cmd" => 98 "git log --no-color --since=\$email_git_since " . 99 '--format="GitCommit: %H%n' . 100 'GitAuthor: %an <%ae>%n' . 101 'GitDate: %aD%n' . 102 'GitSubject: %s%n' . 103 '%b%n"' . 104 " -- \$file", 105 "find_commit_signers_cmd" => 106 "git log --no-color " . 107 '--format="GitCommit: %H%n' . 108 'GitAuthor: %an <%ae>%n' . 109 'GitDate: %aD%n' . 110 'GitSubject: %s%n' . 111 '%b%n"' . 112 " -1 \$commit", 113 "find_commit_author_cmd" => 114 "git log --no-color " . 115 '--format="GitCommit: %H%n' . 116 'GitAuthor: %an <%ae>%n' . 117 'GitDate: %aD%n' . 118 'GitSubject: %s%n"' . 119 " -1 \$commit", 120 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file", 121 "blame_file_cmd" => "git blame -l \$file", 122 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})", 123 "blame_commit_pattern" => "^([0-9a-f]+) ", 124 "author_pattern" => "^GitAuthor: (.*)", 125 "subject_pattern" => "^GitSubject: (.*)", 126); 127 128my %VCS_cmds_hg = ( 129 "execute_cmd" => \&hg_execute_cmd, 130 "available" => '(which("hg") ne "") && (-d ".hg")', 131 "find_signers_cmd" => 132 "hg log --date=\$email_hg_since " . 133 "--template='HgCommit: {node}\\n" . 134 "HgAuthor: {author}\\n" . 135 "HgSubject: {desc}\\n'" . 136 " -- \$file", 137 "find_commit_signers_cmd" => 138 "hg log " . 139 "--template='HgSubject: {desc}\\n'" . 140 " -r \$commit", 141 "find_commit_author_cmd" => 142 "hg log " . 143 "--template='HgCommit: {node}\\n" . 144 "HgAuthor: {author}\\n" . 145 "HgSubject: {desc|firstline}\\n'" . 146 " -r \$commit", 147 "blame_range_cmd" => "", # not supported 148 "blame_file_cmd" => "hg blame -n \$file", 149 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})", 150 "blame_commit_pattern" => "^([ 0-9a-f]+):", 151 "author_pattern" => "^HgAuthor: (.*)", 152 "subject_pattern" => "^HgSubject: (.*)", 153); 154 155my $conf = which_conf(".get_maintainer.conf"); 156if (-f $conf) { 157 my @conf_args; 158 open(my $conffile, '<', "$conf") 159 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n"; 160 161 while (<$conffile>) { 162 my $line = $_; 163 164 $line =~ s/\s*\n?$//g; 165 $line =~ s/^\s*//g; 166 $line =~ s/\s+/ /g; 167 168 next if ($line =~ m/^\s*#/); 169 next if ($line =~ m/^\s*$/); 170 171 my @words = split(" ", $line); 172 foreach my $word (@words) { 173 last if ($word =~ m/^#/); 174 push (@conf_args, $word); 175 } 176 } 177 close($conffile); 178 unshift(@ARGV, @conf_args) if @conf_args; 179} 180 181if (!GetOptions( 182 'email!' => \$email, 183 'git!' => \$email_git, 184 'git-all-signature-types!' => \$email_git_all_signature_types, 185 'git-blame!' => \$email_git_blame, 186 'git-blame-signatures!' => \$email_git_blame_signatures, 187 'git-fallback!' => \$email_git_fallback, 188 'git-chief-penguins!' => \$email_git_penguin_chiefs, 189 'git-min-signatures=i' => \$email_git_min_signatures, 190 'git-max-maintainers=i' => \$email_git_max_maintainers, 191 'git-min-percent=i' => \$email_git_min_percent, 192 'git-since=s' => \$email_git_since, 193 'hg-since=s' => \$email_hg_since, 194 'i|interactive!' => \$interactive, 195 'remove-duplicates!' => \$email_remove_duplicates, 196 'mailmap!' => \$email_use_mailmap, 197 'm!' => \$email_maintainer, 198 'n!' => \$email_usename, 199 'l!' => \$email_list, 200 's!' => \$email_subscriber_list, 201 'multiline!' => \$output_multiline, 202 'roles!' => \$output_roles, 203 'rolestats!' => \$output_rolestats, 204 'separator=s' => \$output_separator, 205 'subsystem!' => \$subsystem, 206 'status!' => \$status, 207 'scm!' => \$scm, 208 'web!' => \$web, 209 'pattern-depth=i' => \$pattern_depth, 210 'k|keywords!' => \$keywords, 211 'sections!' => \$sections, 212 'fe|file-emails!' => \$file_emails, 213 'f|file' => \$from_filename, 214 'v|version' => \$version, 215 'h|help|usage' => \$help, 216 )) { 217 die "$P: invalid argument - use --help if necessary\n"; 218} 219 220if ($help != 0) { 221 usage(); 222 exit 0; 223} 224 225if ($version != 0) { 226 print("${P} ${V}\n"); 227 exit 0; 228} 229 230if (-t STDIN && !@ARGV) { 231 # We're talking to a terminal, but have no command line arguments. 232 die "$P: missing patchfile or -f file - use --help if necessary\n"; 233} 234 235$output_multiline = 0 if ($output_separator ne ", "); 236$output_rolestats = 1 if ($interactive); 237$output_roles = 1 if ($output_rolestats); 238 239if ($sections) { 240 $email = 0; 241 $email_list = 0; 242 $scm = 0; 243 $status = 0; 244 $subsystem = 0; 245 $web = 0; 246 $keywords = 0; 247 $interactive = 0; 248} else { 249 my $selections = $email + $scm + $status + $subsystem + $web; 250 if ($selections == 0) { 251 die "$P: Missing required option: email, scm, status, subsystem or web\n"; 252 } 253} 254 255if ($email && 256 ($email_maintainer + $email_list + $email_subscriber_list + 257 $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) { 258 die "$P: Please select at least 1 email option\n"; 259} 260 261if (!top_of_kernel_tree($lk_path)) { 262 die "$P: The current directory does not appear to be " 263 . "a linux kernel source tree.\n"; 264} 265 266## Read MAINTAINERS for type/value pairs 267 268my @typevalue = (); 269my %keyword_hash; 270 271open (my $maint, '<', "${lk_path}MAINTAINERS") 272 or die "$P: Can't open MAINTAINERS: $!\n"; 273while (<$maint>) { 274 my $line = $_; 275 276 if ($line =~ m/^(\C):\s*(.*)/) { 277 my $type = $1; 278 my $value = $2; 279 280 ##Filename pattern matching 281 if ($type eq "F" || $type eq "X") { 282 $value =~ s@\.@\\\.@g; ##Convert . to \. 283 $value =~ s/\*/\.\*/g; ##Convert * to .* 284 $value =~ s/\?/\./g; ##Convert ? to . 285 ##if pattern is a directory and it lacks a trailing slash, add one 286 if ((-d $value)) { 287 $value =~ s@([^/])$@$1/@; 288 } 289 } elsif ($type eq "K") { 290 $keyword_hash{@typevalue} = $value; 291 } 292 push(@typevalue, "$type:$value"); 293 } elsif (!/^(\s)*$/) { 294 $line =~ s/\n$//g; 295 push(@typevalue, $line); 296 } 297} 298close($maint); 299 300 301# 302# Read mail address map 303# 304 305my $mailmap; 306 307read_mailmap(); 308 309sub read_mailmap { 310 $mailmap = { 311 names => {}, 312 addresses => {} 313 }; 314 315 return if (!$email_use_mailmap || !(-f "${lk_path}.mailmap")); 316 317 open(my $mailmap_file, '<', "${lk_path}.mailmap") 318 or warn "$P: Can't open .mailmap: $!\n"; 319 320 while (<$mailmap_file>) { 321 s/#.*$//; #strip comments 322 s/^\s+|\s+$//g; #trim 323 324 next if (/^\s*$/); #skip empty lines 325 #entries have one of the following formats: 326 # name1 <mail1> 327 # <mail1> <mail2> 328 # name1 <mail1> <mail2> 329 # name1 <mail1> name2 <mail2> 330 # (see man git-shortlog) 331 if (/^(.+)<(.+)>$/) { 332 my $real_name = $1; 333 my $address = $2; 334 335 $real_name =~ s/\s+$//; 336 ($real_name, $address) = parse_email("$real_name <$address>"); 337 $mailmap->{names}->{$address} = $real_name; 338 339 } elsif (/^<([^\s]+)>\s*<([^\s]+)>$/) { 340 my $real_address = $1; 341 my $wrong_address = $2; 342 343 $mailmap->{addresses}->{$wrong_address} = $real_address; 344 345 } elsif (/^(.+)<([^\s]+)>\s*<([^\s]+)>$/) { 346 my $real_name = $1; 347 my $real_address = $2; 348 my $wrong_address = $3; 349 350 $real_name =~ s/\s+$//; 351 ($real_name, $real_address) = 352 parse_email("$real_name <$real_address>"); 353 $mailmap->{names}->{$wrong_address} = $real_name; 354 $mailmap->{addresses}->{$wrong_address} = $real_address; 355 356 } elsif (/^(.+)<([^\s]+)>\s*([^\s].*)<([^\s]+)>$/) { 357 my $real_name = $1; 358 my $real_address = $2; 359 my $wrong_name = $3; 360 my $wrong_address = $4; 361 362 $real_name =~ s/\s+$//; 363 ($real_name, $real_address) = 364 parse_email("$real_name <$real_address>"); 365 366 $wrong_name =~ s/\s+$//; 367 ($wrong_name, $wrong_address) = 368 parse_email("$wrong_name <$wrong_address>"); 369 370 my $wrong_email = format_email($wrong_name, $wrong_address, 1); 371 $mailmap->{names}->{$wrong_email} = $real_name; 372 $mailmap->{addresses}->{$wrong_email} = $real_address; 373 } 374 } 375 close($mailmap_file); 376} 377 378## use the filenames on the command line or find the filenames in the patchfiles 379 380my @files = (); 381my @range = (); 382my @keyword_tvi = (); 383my @file_emails = (); 384 385if (!@ARGV) { 386 push(@ARGV, "&STDIN"); 387} 388 389foreach my $file (@ARGV) { 390 if ($file ne "&STDIN") { 391 ##if $file is a directory and it lacks a trailing slash, add one 392 if ((-d $file)) { 393 $file =~ s@([^/])$@$1/@; 394 } elsif (!(-f $file)) { 395 die "$P: file '${file}' not found\n"; 396 } 397 } 398 if ($from_filename) { 399 push(@files, $file); 400 if ($file ne "MAINTAINERS" && -f $file && ($keywords || $file_emails)) { 401 open(my $f, '<', $file) 402 or die "$P: Can't open $file: $!\n"; 403 my $text = do { local($/) ; <$f> }; 404 close($f); 405 if ($keywords) { 406 foreach my $line (keys %keyword_hash) { 407 if ($text =~ m/$keyword_hash{$line}/x) { 408 push(@keyword_tvi, $line); 409 } 410 } 411 } 412 if ($file_emails) { 413 my @poss_addr = $text =~ m$[A-Za-zÀ-ÿ\"\' \,\.\+-]*\s*[\,]*\s*[\(\<\{]{0,1}[A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+\.[A-Za-z0-9]+[\)\>\}]{0,1}$g; 414 push(@file_emails, clean_file_emails(@poss_addr)); 415 } 416 } 417 } else { 418 my $file_cnt = @files; 419 my $lastfile; 420 421 open(my $patch, "< $file") 422 or die "$P: Can't open $file: $!\n"; 423 424 # We can check arbitrary information before the patch 425 # like the commit message, mail headers, etc... 426 # This allows us to match arbitrary keywords against any part 427 # of a git format-patch generated file (subject tags, etc...) 428 429 my $patch_prefix = ""; #Parsing the intro 430 431 while (<$patch>) { 432 my $patch_line = $_; 433 if (m/^\+\+\+\s+(\S+)/) { 434 my $filename = $1; 435 $filename =~ s@^[^/]*/@@; 436 $filename =~ s@\n@@; 437 $lastfile = $filename; 438 push(@files, $filename); 439 $patch_prefix = "^[+-].*"; #Now parsing the actual patch 440 } elsif (m/^\@\@ -(\d+),(\d+)/) { 441 if ($email_git_blame) { 442 push(@range, "$lastfile:$1:$2"); 443 } 444 } elsif ($keywords) { 445 foreach my $line (keys %keyword_hash) { 446 if ($patch_line =~ m/${patch_prefix}$keyword_hash{$line}/x) { 447 push(@keyword_tvi, $line); 448 } 449 } 450 } 451 } 452 close($patch); 453 454 if ($file_cnt == @files) { 455 warn "$P: file '${file}' doesn't appear to be a patch. " 456 . "Add -f to options?\n"; 457 } 458 @files = sort_and_uniq(@files); 459 } 460} 461 462@file_emails = uniq(@file_emails); 463 464my %email_hash_name; 465my %email_hash_address; 466my @email_to = (); 467my %hash_list_to; 468my @list_to = (); 469my @scm = (); 470my @web = (); 471my @subsystem = (); 472my @status = (); 473my %deduplicate_name_hash = (); 474my %deduplicate_address_hash = (); 475my $signature_pattern; 476 477my @maintainers = get_maintainers(); 478 479if (@maintainers) { 480 @maintainers = merge_email(@maintainers); 481 output(@maintainers); 482} 483 484if ($scm) { 485 @scm = uniq(@scm); 486 output(@scm); 487} 488 489if ($status) { 490 @status = uniq(@status); 491 output(@status); 492} 493 494if ($subsystem) { 495 @subsystem = uniq(@subsystem); 496 output(@subsystem); 497} 498 499if ($web) { 500 @web = uniq(@web); 501 output(@web); 502} 503 504exit($exit); 505 506sub range_is_maintained { 507 my ($start, $end) = @_; 508 509 for (my $i = $start; $i < $end; $i++) { 510 my $line = $typevalue[$i]; 511 if ($line =~ m/^(\C):\s*(.*)/) { 512 my $type = $1; 513 my $value = $2; 514 if ($type eq 'S') { 515 if ($value =~ /(maintain|support)/i) { 516 return 1; 517 } 518 } 519 } 520 } 521 return 0; 522} 523 524sub range_has_maintainer { 525 my ($start, $end) = @_; 526 527 for (my $i = $start; $i < $end; $i++) { 528 my $line = $typevalue[$i]; 529 if ($line =~ m/^(\C):\s*(.*)/) { 530 my $type = $1; 531 my $value = $2; 532 if ($type eq 'M') { 533 return 1; 534 } 535 } 536 } 537 return 0; 538} 539 540sub get_maintainers { 541 %email_hash_name = (); 542 %email_hash_address = (); 543 %commit_author_hash = (); 544 %commit_signer_hash = (); 545 @email_to = (); 546 %hash_list_to = (); 547 @list_to = (); 548 @scm = (); 549 @web = (); 550 @subsystem = (); 551 @status = (); 552 %deduplicate_name_hash = (); 553 %deduplicate_address_hash = (); 554 if ($email_git_all_signature_types) { 555 $signature_pattern = "(.+?)[Bb][Yy]:"; 556 } else { 557 $signature_pattern = "\(" . join("|", @signature_tags) . "\)"; 558 } 559 560 # Find responsible parties 561 562 my %exact_pattern_match_hash = (); 563 564 foreach my $file (@files) { 565 566 my %hash; 567 my $tvi = find_first_section(); 568 while ($tvi < @typevalue) { 569 my $start = find_starting_index($tvi); 570 my $end = find_ending_index($tvi); 571 my $exclude = 0; 572 my $i; 573 574 #Do not match excluded file patterns 575 576 for ($i = $start; $i < $end; $i++) { 577 my $line = $typevalue[$i]; 578 if ($line =~ m/^(\C):\s*(.*)/) { 579 my $type = $1; 580 my $value = $2; 581 if ($type eq 'X') { 582 if (file_match_pattern($file, $value)) { 583 $exclude = 1; 584 last; 585 } 586 } 587 } 588 } 589 590 if (!$exclude) { 591 for ($i = $start; $i < $end; $i++) { 592 my $line = $typevalue[$i]; 593 if ($line =~ m/^(\C):\s*(.*)/) { 594 my $type = $1; 595 my $value = $2; 596 if ($type eq 'F') { 597 if (file_match_pattern($file, $value)) { 598 my $value_pd = ($value =~ tr@/@@); 599 my $file_pd = ($file =~ tr@/@@); 600 $value_pd++ if (substr($value,-1,1) ne "/"); 601 $value_pd = -1 if ($value =~ /^\.\*/); 602 if ($value_pd >= $file_pd && 603 range_is_maintained($start, $end) && 604 range_has_maintainer($start, $end)) { 605 $exact_pattern_match_hash{$file} = 1; 606 } 607 if ($pattern_depth == 0 || 608 (($file_pd - $value_pd) < $pattern_depth)) { 609 $hash{$tvi} = $value_pd; 610 } 611 } 612 } 613 } 614 } 615 } 616 $tvi = $end + 1; 617 } 618 619 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) { 620 add_categories($line); 621 if ($sections) { 622 my $i; 623 my $start = find_starting_index($line); 624 my $end = find_ending_index($line); 625 for ($i = $start; $i < $end; $i++) { 626 my $line = $typevalue[$i]; 627 if ($line =~ /^[FX]:/) { ##Restore file patterns 628 $line =~ s/([^\\])\.([^\*])/$1\?$2/g; 629 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ? 630 $line =~ s/\\\./\./g; ##Convert \. to . 631 $line =~ s/\.\*/\*/g; ##Convert .* to * 632 } 633 $line =~ s/^([A-Z]):/$1:\t/g; 634 print("$line\n"); 635 } 636 print("\n"); 637 } 638 } 639 } 640 641 if ($keywords) { 642 @keyword_tvi = sort_and_uniq(@keyword_tvi); 643 foreach my $line (@keyword_tvi) { 644 add_categories($line); 645 } 646 } 647 648 foreach my $email (@email_to, @list_to) { 649 $email->[0] = deduplicate_email($email->[0]); 650 } 651 652 foreach my $file (@files) { 653 if ($email && 654 ($email_git || ($email_git_fallback && 655 !$exact_pattern_match_hash{$file}))) { 656 vcs_file_signoffs($file); 657 } 658 if ($email && $email_git_blame) { 659 vcs_file_blame($file); 660 } 661 } 662 663 if ($email) { 664 foreach my $chief (@penguin_chief) { 665 if ($chief =~ m/^(.*):(.*)/) { 666 my $email_address; 667 668 $email_address = format_email($1, $2, $email_usename); 669 if ($email_git_penguin_chiefs) { 670 push(@email_to, [$email_address, 'chief penguin']); 671 } else { 672 @email_to = grep($_->[0] !~ /${email_address}/, @email_to); 673 } 674 } 675 } 676 677 foreach my $email (@file_emails) { 678 my ($name, $address) = parse_email($email); 679 680 my $tmp_email = format_email($name, $address, $email_usename); 681 push_email_address($tmp_email, ''); 682 add_role($tmp_email, 'in file'); 683 } 684 } 685 686 my @to = (); 687 if ($email || $email_list) { 688 if ($email) { 689 @to = (@to, @email_to); 690 } 691 if ($email_list) { 692 @to = (@to, @list_to); 693 } 694 } 695 696 if ($interactive) { 697 @to = interactive_get_maintainers(\@to); 698 } 699 700 return @to; 701} 702 703sub file_match_pattern { 704 my ($file, $pattern) = @_; 705 if (substr($pattern, -1) eq "/") { 706 if ($file =~ m@^$pattern@) { 707 return 1; 708 } 709 } else { 710 if ($file =~ m@^$pattern@) { 711 my $s1 = ($file =~ tr@/@@); 712 my $s2 = ($pattern =~ tr@/@@); 713 if ($s1 == $s2) { 714 return 1; 715 } 716 } 717 } 718 return 0; 719} 720 721sub usage { 722 print <<EOT; 723usage: $P [options] patchfile 724 $P [options] -f file|directory 725version: $V 726 727MAINTAINER field selection options: 728 --email => print email address(es) if any 729 --git => include recent git \*-by: signers 730 --git-all-signature-types => include signers regardless of signature type 731 or use only ${signature_pattern} signers (default: $email_git_all_signature_types) 732 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback) 733 --git-chief-penguins => include ${penguin_chiefs} 734 --git-min-signatures => number of signatures required (default: $email_git_min_signatures) 735 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers) 736 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent) 737 --git-blame => use git blame to find modified commits for patch or file 738 --git-since => git history to use (default: $email_git_since) 739 --hg-since => hg history to use (default: $email_hg_since) 740 --interactive => display a menu (mostly useful if used with the --git option) 741 --m => include maintainer(s) if any 742 --n => include name 'Full Name <addr\@domain.tld>' 743 --l => include list(s) if any 744 --s => include subscriber only list(s) if any 745 --remove-duplicates => minimize duplicate email names/addresses 746 --roles => show roles (status:subsystem, git-signer, list, etc...) 747 --rolestats => show roles and statistics (commits/total_commits, %) 748 --file-emails => add email addresses found in -f file (default: 0 (off)) 749 --scm => print SCM tree(s) if any 750 --status => print status if any 751 --subsystem => print subsystem name if any 752 --web => print website(s) if any 753 754Output type options: 755 --separator [, ] => separator for multiple entries on 1 line 756 using --separator also sets --nomultiline if --separator is not [, ] 757 --multiline => print 1 entry per line 758 759Other options: 760 --pattern-depth => Number of pattern directory traversals (default: 0 (all)) 761 --keywords => scan patch for keywords (default: $keywords) 762 --sections => print all of the subsystem sections with pattern matches 763 --mailmap => use .mailmap file (default: $email_use_mailmap) 764 --version => show version 765 --help => show this help information 766 767Default options: 768 [--email --nogit --git-fallback --m --n --l --multiline -pattern-depth=0 769 --remove-duplicates --rolestats] 770 771Notes: 772 Using "-f directory" may give unexpected results: 773 Used with "--git", git signators for _all_ files in and below 774 directory are examined as git recurses directories. 775 Any specified X: (exclude) pattern matches are _not_ ignored. 776 Used with "--nogit", directory is used as a pattern match, 777 no individual file within the directory or subdirectory 778 is matched. 779 Used with "--git-blame", does not iterate all files in directory 780 Using "--git-blame" is slow and may add old committers and authors 781 that are no longer active maintainers to the output. 782 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any 783 other automated tools that expect only ["name"] <email address> 784 may not work because of additional output after <email address>. 785 Using "--rolestats" and "--git-blame" shows the #/total=% commits, 786 not the percentage of the entire file authored. # of commits is 787 not a good measure of amount of code authored. 1 major commit may 788 contain a thousand lines, 5 trivial commits may modify a single line. 789 If git is not installed, but mercurial (hg) is installed and an .hg 790 repository exists, the following options apply to mercurial: 791 --git, 792 --git-min-signatures, --git-max-maintainers, --git-min-percent, and 793 --git-blame 794 Use --hg-since not --git-since to control date selection 795 File ".get_maintainer.conf", if it exists in the linux kernel source root 796 directory, can change whatever get_maintainer defaults are desired. 797 Entries in this file can be any command line argument. 798 This file is prepended to any additional command line arguments. 799 Multiple lines and # comments are allowed. 800EOT 801} 802 803sub top_of_kernel_tree { 804 my ($lk_path) = @_; 805 806 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") { 807 $lk_path .= "/"; 808 } 809 if ( (-f "${lk_path}COPYING") 810 && (-f "${lk_path}CREDITS") 811 && (-f "${lk_path}Kbuild") 812 && (-f "${lk_path}MAINTAINERS") 813 && (-f "${lk_path}Makefile") 814 && (-f "${lk_path}README") 815 && (-d "${lk_path}Documentation") 816 && (-d "${lk_path}arch") 817 && (-d "${lk_path}include") 818 && (-d "${lk_path}drivers") 819 && (-d "${lk_path}fs") 820 && (-d "${lk_path}init") 821 && (-d "${lk_path}ipc") 822 && (-d "${lk_path}kernel") 823 && (-d "${lk_path}lib") 824 && (-d "${lk_path}scripts")) { 825 return 1; 826 } 827 return 0; 828} 829 830sub parse_email { 831 my ($formatted_email) = @_; 832 833 my $name = ""; 834 my $address = ""; 835 836 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) { 837 $name = $1; 838 $address = $2; 839 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) { 840 $address = $1; 841 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) { 842 $address = $1; 843 } 844 845 $name =~ s/^\s+|\s+$//g; 846 $name =~ s/^\"|\"$//g; 847 $address =~ s/^\s+|\s+$//g; 848 849 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars 850 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes 851 $name = "\"$name\""; 852 } 853 854 return ($name, $address); 855} 856 857sub format_email { 858 my ($name, $address, $usename) = @_; 859 860 my $formatted_email; 861 862 $name =~ s/^\s+|\s+$//g; 863 $name =~ s/^\"|\"$//g; 864 $address =~ s/^\s+|\s+$//g; 865 866 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars 867 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes 868 $name = "\"$name\""; 869 } 870 871 if ($usename) { 872 if ("$name" eq "") { 873 $formatted_email = "$address"; 874 } else { 875 $formatted_email = "$name <$address>"; 876 } 877 } else { 878 $formatted_email = $address; 879 } 880 881 return $formatted_email; 882} 883 884sub find_first_section { 885 my $index = 0; 886 887 while ($index < @typevalue) { 888 my $tv = $typevalue[$index]; 889 if (($tv =~ m/^(\C):\s*(.*)/)) { 890 last; 891 } 892 $index++; 893 } 894 895 return $index; 896} 897 898sub find_starting_index { 899 my ($index) = @_; 900 901 while ($index > 0) { 902 my $tv = $typevalue[$index]; 903 if (!($tv =~ m/^(\C):\s*(.*)/)) { 904 last; 905 } 906 $index--; 907 } 908 909 return $index; 910} 911 912sub find_ending_index { 913 my ($index) = @_; 914 915 while ($index < @typevalue) { 916 my $tv = $typevalue[$index]; 917 if (!($tv =~ m/^(\C):\s*(.*)/)) { 918 last; 919 } 920 $index++; 921 } 922 923 return $index; 924} 925 926sub get_maintainer_role { 927 my ($index) = @_; 928 929 my $i; 930 my $start = find_starting_index($index); 931 my $end = find_ending_index($index); 932 933 my $role; 934 my $subsystem = $typevalue[$start]; 935 if (length($subsystem) > 20) { 936 $subsystem = substr($subsystem, 0, 17); 937 $subsystem =~ s/\s*$//; 938 $subsystem = $subsystem . "..."; 939 } 940 941 for ($i = $start + 1; $i < $end; $i++) { 942 my $tv = $typevalue[$i]; 943 if ($tv =~ m/^(\C):\s*(.*)/) { 944 my $ptype = $1; 945 my $pvalue = $2; 946 if ($ptype eq "S") { 947 $role = $pvalue; 948 } 949 } 950 } 951 952 $role = lc($role); 953 if ($role eq "supported") { 954 $role = "supporter"; 955 } elsif ($role eq "maintained") { 956 $role = "maintainer"; 957 } elsif ($role eq "odd fixes") { 958 $role = "odd fixer"; 959 } elsif ($role eq "orphan") { 960 $role = "orphan minder"; 961 } elsif ($role eq "obsolete") { 962 $role = "obsolete minder"; 963 } elsif ($role eq "buried alive in reporters") { 964 $role = "chief penguin"; 965 } 966 967 return $role . ":" . $subsystem; 968} 969 970sub get_list_role { 971 my ($index) = @_; 972 973 my $i; 974 my $start = find_starting_index($index); 975 my $end = find_ending_index($index); 976 977 my $subsystem = $typevalue[$start]; 978 if (length($subsystem) > 20) { 979 $subsystem = substr($subsystem, 0, 17); 980 $subsystem =~ s/\s*$//; 981 $subsystem = $subsystem . "..."; 982 } 983 984 if ($subsystem eq "THE REST") { 985 $subsystem = ""; 986 } 987 988 return $subsystem; 989} 990 991sub add_categories { 992 my ($index) = @_; 993 994 my $i; 995 my $start = find_starting_index($index); 996 my $end = find_ending_index($index); 997 998 push(@subsystem, $typevalue[$start]); 999 1000 for ($i = $start + 1; $i < $end; $i++) { 1001 my $tv = $typevalue[$i]; 1002 if ($tv =~ m/^(\C):\s*(.*)/) { 1003 my $ptype = $1; 1004 my $pvalue = $2; 1005 if ($ptype eq "L") { 1006 my $list_address = $pvalue; 1007 my $list_additional = ""; 1008 my $list_role = get_list_role($i); 1009 1010 if ($list_role ne "") { 1011 $list_role = ":" . $list_role; 1012 } 1013 if ($list_address =~ m/([^\s]+)\s+(.*)$/) { 1014 $list_address = $1; 1015 $list_additional = $2; 1016 } 1017 if ($list_additional =~ m/subscribers-only/) { 1018 if ($email_subscriber_list) { 1019 if (!$hash_list_to{lc($list_address)}) { 1020 $hash_list_to{lc($list_address)} = 1; 1021 push(@list_to, [$list_address, 1022 "subscriber list${list_role}"]); 1023 } 1024 } 1025 } else { 1026 if ($email_list) { 1027 if (!$hash_list_to{lc($list_address)}) { 1028 $hash_list_to{lc($list_address)} = 1; 1029 push(@list_to, [$list_address, 1030 "open list${list_role}"]); 1031 } 1032 } 1033 } 1034 } elsif ($ptype eq "M") { 1035 my ($name, $address) = parse_email($pvalue); 1036 if ($name eq "") { 1037 if ($i > 0) { 1038 my $tv = $typevalue[$i - 1]; 1039 if ($tv =~ m/^(\C):\s*(.*)/) { 1040 if ($1 eq "P") { 1041 $name = $2; 1042 $pvalue = format_email($name, $address, $email_usename); 1043 } 1044 } 1045 } 1046 } 1047 if ($email_maintainer) { 1048 my $role = get_maintainer_role($i); 1049 push_email_addresses($pvalue, $role); 1050 } 1051 } elsif ($ptype eq "T") { 1052 push(@scm, $pvalue); 1053 } elsif ($ptype eq "W") { 1054 push(@web, $pvalue); 1055 } elsif ($ptype eq "S") { 1056 push(@status, $pvalue); 1057 } 1058 } 1059 } 1060} 1061 1062sub email_inuse { 1063 my ($name, $address) = @_; 1064 1065 return 1 if (($name eq "") && ($address eq "")); 1066 return 1 if (($name ne "") && exists($email_hash_name{lc($name)})); 1067 return 1 if (($address ne "") && exists($email_hash_address{lc($address)})); 1068 1069 return 0; 1070} 1071 1072sub push_email_address { 1073 my ($line, $role) = @_; 1074 1075 my ($name, $address) = parse_email($line); 1076 1077 if ($address eq "") { 1078 return 0; 1079 } 1080 1081 if (!$email_remove_duplicates) { 1082 push(@email_to, [format_email($name, $address, $email_usename), $role]); 1083 } elsif (!email_inuse($name, $address)) { 1084 push(@email_to, [format_email($name, $address, $email_usename), $role]); 1085 $email_hash_name{lc($name)}++ if ($name ne ""); 1086 $email_hash_address{lc($address)}++; 1087 } 1088 1089 return 1; 1090} 1091 1092sub push_email_addresses { 1093 my ($address, $role) = @_; 1094 1095 my @address_list = (); 1096 1097 if (rfc822_valid($address)) { 1098 push_email_address($address, $role); 1099 } elsif (@address_list = rfc822_validlist($address)) { 1100 my $array_count = shift(@address_list); 1101 while (my $entry = shift(@address_list)) { 1102 push_email_address($entry, $role); 1103 } 1104 } else { 1105 if (!push_email_address($address, $role)) { 1106 warn("Invalid MAINTAINERS address: '" . $address . "'\n"); 1107 } 1108 } 1109} 1110 1111sub add_role { 1112 my ($line, $role) = @_; 1113 1114 my ($name, $address) = parse_email($line); 1115 my $email = format_email($name, $address, $email_usename); 1116 1117 foreach my $entry (@email_to) { 1118 if ($email_remove_duplicates) { 1119 my ($entry_name, $entry_address) = parse_email($entry->[0]); 1120 if (($name eq $entry_name || $address eq $entry_address) 1121 && ($role eq "" || !($entry->[1] =~ m/$role/)) 1122 ) { 1123 if ($entry->[1] eq "") { 1124 $entry->[1] = "$role"; 1125 } else { 1126 $entry->[1] = "$entry->[1],$role"; 1127 } 1128 } 1129 } else { 1130 if ($email eq $entry->[0] 1131 && ($role eq "" || !($entry->[1] =~ m/$role/)) 1132 ) { 1133 if ($entry->[1] eq "") { 1134 $entry->[1] = "$role"; 1135 } else { 1136 $entry->[1] = "$entry->[1],$role"; 1137 } 1138 } 1139 } 1140 } 1141} 1142 1143sub which { 1144 my ($bin) = @_; 1145 1146 foreach my $path (split(/:/, $ENV{PATH})) { 1147 if (-e "$path/$bin") { 1148 return "$path/$bin"; 1149 } 1150 } 1151 1152 return ""; 1153} 1154 1155sub which_conf { 1156 my ($conf) = @_; 1157 1158 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) { 1159 if (-e "$path/$conf") { 1160 return "$path/$conf"; 1161 } 1162 } 1163 1164 return ""; 1165} 1166 1167sub mailmap_email { 1168 my ($line) = @_; 1169 1170 my ($name, $address) = parse_email($line); 1171 my $email = format_email($name, $address, 1); 1172 my $real_name = $name; 1173 my $real_address = $address; 1174 1175 if (exists $mailmap->{names}->{$email} || 1176 exists $mailmap->{addresses}->{$email}) { 1177 if (exists $mailmap->{names}->{$email}) { 1178 $real_name = $mailmap->{names}->{$email}; 1179 } 1180 if (exists $mailmap->{addresses}->{$email}) { 1181 $real_address = $mailmap->{addresses}->{$email}; 1182 } 1183 } else { 1184 if (exists $mailmap->{names}->{$address}) { 1185 $real_name = $mailmap->{names}->{$address}; 1186 } 1187 if (exists $mailmap->{addresses}->{$address}) { 1188 $real_address = $mailmap->{addresses}->{$address}; 1189 } 1190 } 1191 return format_email($real_name, $real_address, 1); 1192} 1193 1194sub mailmap { 1195 my (@addresses) = @_; 1196 1197 my @mapped_emails = (); 1198 foreach my $line (@addresses) { 1199 push(@mapped_emails, mailmap_email($line)); 1200 } 1201 merge_by_realname(@mapped_emails) if ($email_use_mailmap); 1202 return @mapped_emails; 1203} 1204 1205sub merge_by_realname { 1206 my %address_map; 1207 my (@emails) = @_; 1208 1209 foreach my $email (@emails) { 1210 my ($name, $address) = parse_email($email); 1211 if (exists $address_map{$name}) { 1212 $address = $address_map{$name}; 1213 $email = format_email($name, $address, 1); 1214 } else { 1215 $address_map{$name} = $address; 1216 } 1217 } 1218} 1219 1220sub git_execute_cmd { 1221 my ($cmd) = @_; 1222 my @lines = (); 1223 1224 my $output = `$cmd`; 1225 $output =~ s/^\s*//gm; 1226 @lines = split("\n", $output); 1227 1228 return @lines; 1229} 1230 1231sub hg_execute_cmd { 1232 my ($cmd) = @_; 1233 my @lines = (); 1234 1235 my $output = `$cmd`; 1236 @lines = split("\n", $output); 1237 1238 return @lines; 1239} 1240 1241sub extract_formatted_signatures { 1242 my (@signature_lines) = @_; 1243 1244 my @type = @signature_lines; 1245 1246 s/\s*(.*):.*/$1/ for (@type); 1247 1248 # cut -f2- -d":" 1249 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines); 1250 1251## Reformat email addresses (with names) to avoid badly written signatures 1252 1253 foreach my $signer (@signature_lines) { 1254 $signer = deduplicate_email($signer); 1255 } 1256 1257 return (\@type, \@signature_lines); 1258} 1259 1260sub vcs_find_signers { 1261 my ($cmd) = @_; 1262 my $commits; 1263 my @lines = (); 1264 my @signatures = (); 1265 1266 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd); 1267 1268 my $pattern = $VCS_cmds{"commit_pattern"}; 1269 1270 $commits = grep(/$pattern/, @lines); # of commits 1271 1272 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines); 1273 1274 return (0, @signatures) if !@signatures; 1275 1276 save_commits_by_author(@lines) if ($interactive); 1277 save_commits_by_signer(@lines) if ($interactive); 1278 1279 if (!$email_git_penguin_chiefs) { 1280 @signatures = grep(!/${penguin_chiefs}/i, @signatures); 1281 } 1282 1283 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures); 1284 1285 return ($commits, @$signers_ref); 1286} 1287 1288sub vcs_find_author { 1289 my ($cmd) = @_; 1290 my @lines = (); 1291 1292 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd); 1293 1294 if (!$email_git_penguin_chiefs) { 1295 @lines = grep(!/${penguin_chiefs}/i, @lines); 1296 } 1297 1298 return @lines if !@lines; 1299 1300 my @authors = (); 1301 foreach my $line (@lines) { 1302 if ($line =~ m/$VCS_cmds{"author_pattern"}/) { 1303 my $author = $1; 1304 my ($name, $address) = parse_email($author); 1305 $author = format_email($name, $address, 1); 1306 push(@authors, $author); 1307 } 1308 } 1309 1310 save_commits_by_author(@lines) if ($interactive); 1311 save_commits_by_signer(@lines) if ($interactive); 1312 1313 return @authors; 1314} 1315 1316sub vcs_save_commits { 1317 my ($cmd) = @_; 1318 my @lines = (); 1319 my @commits = (); 1320 1321 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd); 1322 1323 foreach my $line (@lines) { 1324 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) { 1325 push(@commits, $1); 1326 } 1327 } 1328 1329 return @commits; 1330} 1331 1332sub vcs_blame { 1333 my ($file) = @_; 1334 my $cmd; 1335 my @commits = (); 1336 1337 return @commits if (!(-f $file)); 1338 1339 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") { 1340 my @all_commits = (); 1341 1342 $cmd = $VCS_cmds{"blame_file_cmd"}; 1343 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd 1344 @all_commits = vcs_save_commits($cmd); 1345 1346 foreach my $file_range_diff (@range) { 1347 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/)); 1348 my $diff_file = $1; 1349 my $diff_start = $2; 1350 my $diff_length = $3; 1351 next if ("$file" ne "$diff_file"); 1352 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) { 1353 push(@commits, $all_commits[$i]); 1354 } 1355 } 1356 } elsif (@range) { 1357 foreach my $file_range_diff (@range) { 1358 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/)); 1359 my $diff_file = $1; 1360 my $diff_start = $2; 1361 my $diff_length = $3; 1362 next if ("$file" ne "$diff_file"); 1363 $cmd = $VCS_cmds{"blame_range_cmd"}; 1364 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd 1365 push(@commits, vcs_save_commits($cmd)); 1366 } 1367 } else { 1368 $cmd = $VCS_cmds{"blame_file_cmd"}; 1369 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd 1370 @commits = vcs_save_commits($cmd); 1371 } 1372 1373 foreach my $commit (@commits) { 1374 $commit =~ s/^\^//g; 1375 } 1376 1377 return @commits; 1378} 1379 1380my $printed_novcs = 0; 1381sub vcs_exists { 1382 %VCS_cmds = %VCS_cmds_git; 1383 return 1 if eval $VCS_cmds{"available"}; 1384 %VCS_cmds = %VCS_cmds_hg; 1385 return 2 if eval $VCS_cmds{"available"}; 1386 %VCS_cmds = (); 1387 if (!$printed_novcs) { 1388 warn("$P: No supported VCS found. Add --nogit to options?\n"); 1389 warn("Using a git repository produces better results.\n"); 1390 warn("Try Linus Torvalds' latest git repository using:\n"); 1391 warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git\n"); 1392 $printed_novcs = 1; 1393 } 1394 return 0; 1395} 1396 1397sub vcs_is_git { 1398 vcs_exists(); 1399 return $vcs_used == 1; 1400} 1401 1402sub vcs_is_hg { 1403 return $vcs_used == 2; 1404} 1405 1406sub interactive_get_maintainers { 1407 my ($list_ref) = @_; 1408 my @list = @$list_ref; 1409 1410 vcs_exists(); 1411 1412 my %selected; 1413 my %authored; 1414 my %signed; 1415 my $count = 0; 1416 my $maintained = 0; 1417 foreach my $entry (@list) { 1418 $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i); 1419 $selected{$count} = 1; 1420 $authored{$count} = 0; 1421 $signed{$count} = 0; 1422 $count++; 1423 } 1424 1425 #menu loop 1426 my $done = 0; 1427 my $print_options = 0; 1428 my $redraw = 1; 1429 while (!$done) { 1430 $count = 0; 1431 if ($redraw) { 1432 printf STDERR "\n%1s %2s %-65s", 1433 "*", "#", "email/list and role:stats"; 1434 if ($email_git || 1435 ($email_git_fallback && !$maintained) || 1436 $email_git_blame) { 1437 print STDERR "auth sign"; 1438 } 1439 print STDERR "\n"; 1440 foreach my $entry (@list) { 1441 my $email = $entry->[0]; 1442 my $role = $entry->[1]; 1443 my $sel = ""; 1444 $sel = "*" if ($selected{$count}); 1445 my $commit_author = $commit_author_hash{$email}; 1446 my $commit_signer = $commit_signer_hash{$email}; 1447 my $authored = 0; 1448 my $signed = 0; 1449 $authored++ for (@{$commit_author}); 1450 $signed++ for (@{$commit_signer}); 1451 printf STDERR "%1s %2d %-65s", $sel, $count + 1, $email; 1452 printf STDERR "%4d %4d", $authored, $signed 1453 if ($authored > 0 || $signed > 0); 1454 printf STDERR "\n %s\n", $role; 1455 if ($authored{$count}) { 1456 my $commit_author = $commit_author_hash{$email}; 1457 foreach my $ref (@{$commit_author}) { 1458 print STDERR " Author: @{$ref}[1]\n"; 1459 } 1460 } 1461 if ($signed{$count}) { 1462 my $commit_signer = $commit_signer_hash{$email}; 1463 foreach my $ref (@{$commit_signer}) { 1464 print STDERR " @{$ref}[2]: @{$ref}[1]\n"; 1465 } 1466 } 1467 1468 $count++; 1469 } 1470 } 1471 my $date_ref = \$email_git_since; 1472 $date_ref = \$email_hg_since if (vcs_is_hg()); 1473 if ($print_options) { 1474 $print_options = 0; 1475 if (vcs_exists()) { 1476 print STDERR <<EOT 1477 1478Version Control options: 1479g use git history [$email_git] 1480gf use git-fallback [$email_git_fallback] 1481b use git blame [$email_git_blame] 1482bs use blame signatures [$email_git_blame_signatures] 1483c# minimum commits [$email_git_min_signatures] 1484%# min percent [$email_git_min_percent] 1485d# history to use [$$date_ref] 1486x# max maintainers [$email_git_max_maintainers] 1487t all signature types [$email_git_all_signature_types] 1488m use .mailmap [$email_use_mailmap] 1489EOT 1490 } 1491 print STDERR <<EOT 1492 1493Additional options: 14940 toggle all 1495tm toggle maintainers 1496tg toggle git entries 1497tl toggle open list entries 1498ts toggle subscriber list entries 1499f emails in file [$file_emails] 1500k keywords in file [$keywords] 1501r remove duplicates [$email_remove_duplicates] 1502p# pattern match depth [$pattern_depth] 1503EOT 1504 } 1505 print STDERR 1506"\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): "; 1507 1508 my $input = <STDIN>; 1509 chomp($input); 1510 1511 $redraw = 1; 1512 my $rerun = 0; 1513 my @wish = split(/[, ]+/, $input); 1514 foreach my $nr (@wish) { 1515 $nr = lc($nr); 1516 my $sel = substr($nr, 0, 1); 1517 my $str = substr($nr, 1); 1518 my $val = 0; 1519 $val = $1 if $str =~ /^(\d+)$/; 1520 1521 if ($sel eq "y") { 1522 $interactive = 0; 1523 $done = 1; 1524 $output_rolestats = 0; 1525 $output_roles = 0; 1526 last; 1527 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) { 1528 $selected{$nr - 1} = !$selected{$nr - 1}; 1529 } elsif ($sel eq "*" || $sel eq '^') { 1530 my $toggle = 0; 1531 $toggle = 1 if ($sel eq '*'); 1532 for (my $i = 0; $i < $count; $i++) { 1533 $selected{$i} = $toggle; 1534 } 1535 } elsif ($sel eq "0") { 1536 for (my $i = 0; $i < $count; $i++) { 1537 $selected{$i} = !$selected{$i}; 1538 } 1539 } elsif ($sel eq "t") { 1540 if (lc($str) eq "m") { 1541 for (my $i = 0; $i < $count; $i++) { 1542 $selected{$i} = !$selected{$i} 1543 if ($list[$i]->[1] =~ /^(maintainer|supporter)/i); 1544 } 1545 } elsif (lc($str) eq "g") { 1546 for (my $i = 0; $i < $count; $i++) { 1547 $selected{$i} = !$selected{$i} 1548 if ($list[$i]->[1] =~ /^(author|commit|signer)/i); 1549 } 1550 } elsif (lc($str) eq "l") { 1551 for (my $i = 0; $i < $count; $i++) { 1552 $selected{$i} = !$selected{$i} 1553 if ($list[$i]->[1] =~ /^(open list)/i); 1554 } 1555 } elsif (lc($str) eq "s") { 1556 for (my $i = 0; $i < $count; $i++) { 1557 $selected{$i} = !$selected{$i} 1558 if ($list[$i]->[1] =~ /^(subscriber list)/i); 1559 } 1560 } 1561 } elsif ($sel eq "a") { 1562 if ($val > 0 && $val <= $count) { 1563 $authored{$val - 1} = !$authored{$val - 1}; 1564 } elsif ($str eq '*' || $str eq '^') { 1565 my $toggle = 0; 1566 $toggle = 1 if ($str eq '*'); 1567 for (my $i = 0; $i < $count; $i++) { 1568 $authored{$i} = $toggle; 1569 } 1570 } 1571 } elsif ($sel eq "s") { 1572 if ($val > 0 && $val <= $count) { 1573 $signed{$val - 1} = !$signed{$val - 1}; 1574 } elsif ($str eq '*' || $str eq '^') { 1575 my $toggle = 0; 1576 $toggle = 1 if ($str eq '*'); 1577 for (my $i = 0; $i < $count; $i++) { 1578 $signed{$i} = $toggle; 1579 } 1580 } 1581 } elsif ($sel eq "o") { 1582 $print_options = 1; 1583 $redraw = 1; 1584 } elsif ($sel eq "g") { 1585 if ($str eq "f") { 1586 bool_invert(\$email_git_fallback); 1587 } else { 1588 bool_invert(\$email_git); 1589 } 1590 $rerun = 1; 1591 } elsif ($sel eq "b") { 1592 if ($str eq "s") { 1593 bool_invert(\$email_git_blame_signatures); 1594 } else { 1595 bool_invert(\$email_git_blame); 1596 } 1597 $rerun = 1; 1598 } elsif ($sel eq "c") { 1599 if ($val > 0) { 1600 $email_git_min_signatures = $val; 1601 $rerun = 1; 1602 } 1603 } elsif ($sel eq "x") { 1604 if ($val > 0) { 1605 $email_git_max_maintainers = $val; 1606 $rerun = 1; 1607 } 1608 } elsif ($sel eq "%") { 1609 if ($str ne "" && $val >= 0) { 1610 $email_git_min_percent = $val; 1611 $rerun = 1; 1612 } 1613 } elsif ($sel eq "d") { 1614 if (vcs_is_git()) { 1615 $email_git_since = $str; 1616 } elsif (vcs_is_hg()) { 1617 $email_hg_since = $str; 1618 } 1619 $rerun = 1; 1620 } elsif ($sel eq "t") { 1621 bool_invert(\$email_git_all_signature_types); 1622 $rerun = 1; 1623 } elsif ($sel eq "f") { 1624 bool_invert(\$file_emails); 1625 $rerun = 1; 1626 } elsif ($sel eq "r") { 1627 bool_invert(\$email_remove_duplicates); 1628 $rerun = 1; 1629 } elsif ($sel eq "m") { 1630 bool_invert(\$email_use_mailmap); 1631 read_mailmap(); 1632 $rerun = 1; 1633 } elsif ($sel eq "k") { 1634 bool_invert(\$keywords); 1635 $rerun = 1; 1636 } elsif ($sel eq "p") { 1637 if ($str ne "" && $val >= 0) { 1638 $pattern_depth = $val; 1639 $rerun = 1; 1640 } 1641 } elsif ($sel eq "h" || $sel eq "?") { 1642 print STDERR <<EOT 1643 1644Interactive mode allows you to select the various maintainers, submitters, 1645commit signers and mailing lists that could be CC'd on a patch. 1646 1647Any *'d entry is selected. 1648 1649If you have git or hg installed, you can choose to summarize the commit 1650history of files in the patch. Also, each line of the current file can 1651be matched to its commit author and that commits signers with blame. 1652 1653Various knobs exist to control the length of time for active commit 1654tracking, the maximum number of commit authors and signers to add, 1655and such. 1656 1657Enter selections at the prompt until you are satisfied that the selected 1658maintainers are appropriate. You may enter multiple selections separated 1659by either commas or spaces. 1660 1661EOT 1662 } else { 1663 print STDERR "invalid option: '$nr'\n"; 1664 $redraw = 0; 1665 } 1666 } 1667 if ($rerun) { 1668 print STDERR "git-blame can be very slow, please have patience..." 1669 if ($email_git_blame); 1670 goto &get_maintainers; 1671 } 1672 } 1673 1674 #drop not selected entries 1675 $count = 0; 1676 my @new_emailto = (); 1677 foreach my $entry (@list) { 1678 if ($selected{$count}) { 1679 push(@new_emailto, $list[$count]); 1680 } 1681 $count++; 1682 } 1683 return @new_emailto; 1684} 1685 1686sub bool_invert { 1687 my ($bool_ref) = @_; 1688 1689 if ($$bool_ref) { 1690 $$bool_ref = 0; 1691 } else { 1692 $$bool_ref = 1; 1693 } 1694} 1695 1696sub deduplicate_email { 1697 my ($email) = @_; 1698 1699 my $matched = 0; 1700 my ($name, $address) = parse_email($email); 1701 $email = format_email($name, $address, 1); 1702 $email = mailmap_email($email); 1703 1704 return $email if (!$email_remove_duplicates); 1705 1706 ($name, $address) = parse_email($email); 1707 1708 if ($name ne "" && $deduplicate_name_hash{lc($name)}) { 1709 $name = $deduplicate_name_hash{lc($name)}->[0]; 1710 $address = $deduplicate_name_hash{lc($name)}->[1]; 1711 $matched = 1; 1712 } elsif ($deduplicate_address_hash{lc($address)}) { 1713 $name = $deduplicate_address_hash{lc($address)}->[0]; 1714 $address = $deduplicate_address_hash{lc($address)}->[1]; 1715 $matched = 1; 1716 } 1717 if (!$matched) { 1718 $deduplicate_name_hash{lc($name)} = [ $name, $address ]; 1719 $deduplicate_address_hash{lc($address)} = [ $name, $address ]; 1720 } 1721 $email = format_email($name, $address, 1); 1722 $email = mailmap_email($email); 1723 return $email; 1724} 1725 1726sub save_commits_by_author { 1727 my (@lines) = @_; 1728 1729 my @authors = (); 1730 my @commits = (); 1731 my @subjects = (); 1732 1733 foreach my $line (@lines) { 1734 if ($line =~ m/$VCS_cmds{"author_pattern"}/) { 1735 my $author = $1; 1736 $author = deduplicate_email($author); 1737 push(@authors, $author); 1738 } 1739 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/); 1740 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/); 1741 } 1742 1743 for (my $i = 0; $i < @authors; $i++) { 1744 my $exists = 0; 1745 foreach my $ref(@{$commit_author_hash{$authors[$i]}}) { 1746 if (@{$ref}[0] eq $commits[$i] && 1747 @{$ref}[1] eq $subjects[$i]) { 1748 $exists = 1; 1749 last; 1750 } 1751 } 1752 if (!$exists) { 1753 push(@{$commit_author_hash{$authors[$i]}}, 1754 [ ($commits[$i], $subjects[$i]) ]); 1755 } 1756 } 1757} 1758 1759sub save_commits_by_signer { 1760 my (@lines) = @_; 1761 1762 my $commit = ""; 1763 my $subject = ""; 1764 1765 foreach my $line (@lines) { 1766 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/); 1767 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/); 1768 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) { 1769 my @signatures = ($line); 1770 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures); 1771 my @types = @$types_ref; 1772 my @signers = @$signers_ref; 1773 1774 my $type = $types[0]; 1775 my $signer = $signers[0]; 1776 1777 $signer = deduplicate_email($signer); 1778 1779 my $exists = 0; 1780 foreach my $ref(@{$commit_signer_hash{$signer}}) { 1781 if (@{$ref}[0] eq $commit && 1782 @{$ref}[1] eq $subject && 1783 @{$ref}[2] eq $type) { 1784 $exists = 1; 1785 last; 1786 } 1787 } 1788 if (!$exists) { 1789 push(@{$commit_signer_hash{$signer}}, 1790 [ ($commit, $subject, $type) ]); 1791 } 1792 } 1793 } 1794} 1795 1796sub vcs_assign { 1797 my ($role, $divisor, @lines) = @_; 1798 1799 my %hash; 1800 my $count = 0; 1801 1802 return if (@lines <= 0); 1803 1804 if ($divisor <= 0) { 1805 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n"); 1806 $divisor = 1; 1807 } 1808 1809 @lines = mailmap(@lines); 1810 1811 return if (@lines <= 0); 1812 1813 @lines = sort(@lines); 1814 1815 # uniq -c 1816 $hash{$_}++ for @lines; 1817 1818 # sort -rn 1819 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) { 1820 my $sign_offs = $hash{$line}; 1821 my $percent = $sign_offs * 100 / $divisor; 1822 1823 $percent = 100 if ($percent > 100); 1824 $count++; 1825 last if ($sign_offs < $email_git_min_signatures || 1826 $count > $email_git_max_maintainers || 1827 $percent < $email_git_min_percent); 1828 push_email_address($line, ''); 1829 if ($output_rolestats) { 1830 my $fmt_percent = sprintf("%.0f", $percent); 1831 add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%"); 1832 } else { 1833 add_role($line, $role); 1834 } 1835 } 1836} 1837 1838sub vcs_file_signoffs { 1839 my ($file) = @_; 1840 1841 my @signers = (); 1842 my $commits; 1843 1844 $vcs_used = vcs_exists(); 1845 return if (!$vcs_used); 1846 1847 my $cmd = $VCS_cmds{"find_signers_cmd"}; 1848 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd 1849 1850 ($commits, @signers) = vcs_find_signers($cmd); 1851 1852 foreach my $signer (@signers) { 1853 $signer = deduplicate_email($signer); 1854 } 1855 1856 vcs_assign("commit_signer", $commits, @signers); 1857} 1858 1859sub vcs_file_blame { 1860 my ($file) = @_; 1861 1862 my @signers = (); 1863 my @all_commits = (); 1864 my @commits = (); 1865 my $total_commits; 1866 my $total_lines; 1867 1868 $vcs_used = vcs_exists(); 1869 return if (!$vcs_used); 1870 1871 @all_commits = vcs_blame($file); 1872 @commits = uniq(@all_commits); 1873 $total_commits = @commits; 1874 $total_lines = @all_commits; 1875 1876 if ($email_git_blame_signatures) { 1877 if (vcs_is_hg()) { 1878 my $commit_count; 1879 my @commit_signers = (); 1880 my $commit = join(" -r ", @commits); 1881 my $cmd; 1882 1883 $cmd = $VCS_cmds{"find_commit_signers_cmd"}; 1884 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd 1885 1886 ($commit_count, @commit_signers) = vcs_find_signers($cmd); 1887 1888 push(@signers, @commit_signers); 1889 } else { 1890 foreach my $commit (@commits) { 1891 my $commit_count; 1892 my @commit_signers = (); 1893 my $cmd; 1894 1895 $cmd = $VCS_cmds{"find_commit_signers_cmd"}; 1896 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd 1897 1898 ($commit_count, @commit_signers) = vcs_find_signers($cmd); 1899 1900 push(@signers, @commit_signers); 1901 } 1902 } 1903 } 1904 1905 if ($from_filename) { 1906 if ($output_rolestats) { 1907 my @blame_signers; 1908 if (vcs_is_hg()) {{ # Double brace for last exit 1909 my $commit_count; 1910 my @commit_signers = (); 1911 @commits = uniq(@commits); 1912 @commits = sort(@commits); 1913 my $commit = join(" -r ", @commits); 1914 my $cmd; 1915 1916 $cmd = $VCS_cmds{"find_commit_author_cmd"}; 1917 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd 1918 1919 my @lines = (); 1920 1921 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd); 1922 1923 if (!$email_git_penguin_chiefs) { 1924 @lines = grep(!/${penguin_chiefs}/i, @lines); 1925 } 1926 1927 last if !@lines; 1928 1929 my @authors = (); 1930 foreach my $line (@lines) { 1931 if ($line =~ m/$VCS_cmds{"author_pattern"}/) { 1932 my $author = $1; 1933 $author = deduplicate_email($author); 1934 push(@authors, $author); 1935 } 1936 } 1937 1938 save_commits_by_author(@lines) if ($interactive); 1939 save_commits_by_signer(@lines) if ($interactive); 1940 1941 push(@signers, @authors); 1942 }} 1943 else { 1944 foreach my $commit (@commits) { 1945 my $i; 1946 my $cmd = $VCS_cmds{"find_commit_author_cmd"}; 1947 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd 1948 my @author = vcs_find_author($cmd); 1949 next if !@author; 1950 1951 my $formatted_author = deduplicate_email($author[0]); 1952 1953 my $count = grep(/$commit/, @all_commits); 1954 for ($i = 0; $i < $count ; $i++) { 1955 push(@blame_signers, $formatted_author); 1956 } 1957 } 1958 } 1959 if (@blame_signers) { 1960 vcs_assign("authored lines", $total_lines, @blame_signers); 1961 } 1962 } 1963 foreach my $signer (@signers) { 1964 $signer = deduplicate_email($signer); 1965 } 1966 vcs_assign("commits", $total_commits, @signers); 1967 } else { 1968 foreach my $signer (@signers) { 1969 $signer = deduplicate_email($signer); 1970 } 1971 vcs_assign("modified commits", $total_commits, @signers); 1972 } 1973} 1974 1975sub uniq { 1976 my (@parms) = @_; 1977 1978 my %saw; 1979 @parms = grep(!$saw{$_}++, @parms); 1980 return @parms; 1981} 1982 1983sub sort_and_uniq { 1984 my (@parms) = @_; 1985 1986 my %saw; 1987 @parms = sort @parms; 1988 @parms = grep(!$saw{$_}++, @parms); 1989 return @parms; 1990} 1991 1992sub clean_file_emails { 1993 my (@file_emails) = @_; 1994 my @fmt_emails = (); 1995 1996 foreach my $email (@file_emails) { 1997 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g; 1998 my ($name, $address) = parse_email($email); 1999 if ($name eq '"[,\.]"') { 2000 $name = ""; 2001 } 2002 2003 my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name); 2004 if (@nw > 2) { 2005 my $first = $nw[@nw - 3]; 2006 my $middle = $nw[@nw - 2]; 2007 my $last = $nw[@nw - 1]; 2008 2009 if (((length($first) == 1 && $first =~ m/[A-Za-z]/) || 2010 (length($first) == 2 && substr($first, -1) eq ".")) || 2011 (length($middle) == 1 || 2012 (length($middle) == 2 && substr($middle, -1) eq "."))) { 2013 $name = "$first $middle $last"; 2014 } else { 2015 $name = "$middle $last"; 2016 } 2017 } 2018 2019 if (substr($name, -1) =~ /[,\.]/) { 2020 $name = substr($name, 0, length($name) - 1); 2021 } elsif (substr($name, -2) =~ /[,\.]"/) { 2022 $name = substr($name, 0, length($name) - 2) . '"'; 2023 } 2024 2025 if (substr($name, 0, 1) =~ /[,\.]/) { 2026 $name = substr($name, 1, length($name) - 1); 2027 } elsif (substr($name, 0, 2) =~ /"[,\.]/) { 2028 $name = '"' . substr($name, 2, length($name) - 2); 2029 } 2030 2031 my $fmt_email = format_email($name, $address, $email_usename); 2032 push(@fmt_emails, $fmt_email); 2033 } 2034 return @fmt_emails; 2035} 2036 2037sub merge_email { 2038 my @lines; 2039 my %saw; 2040 2041 for (@_) { 2042 my ($address, $role) = @$_; 2043 if (!$saw{$address}) { 2044 if ($output_roles) { 2045 push(@lines, "$address ($role)"); 2046 } else { 2047 push(@lines, $address); 2048 } 2049 $saw{$address} = 1; 2050 } 2051 } 2052 2053 return @lines; 2054} 2055 2056sub output { 2057 my (@parms) = @_; 2058 2059 if ($output_multiline) { 2060 foreach my $line (@parms) { 2061 print("${line}\n"); 2062 } 2063 } else { 2064 print(join($output_separator, @parms)); 2065 print("\n"); 2066 } 2067} 2068 2069my $rfc822re; 2070 2071sub make_rfc822re { 2072# Basic lexical tokens are specials, domain_literal, quoted_string, atom, and 2073# comment. We must allow for rfc822_lwsp (or comments) after each of these. 2074# This regexp will only work on addresses which have had comments stripped 2075# and replaced with rfc822_lwsp. 2076 2077 my $specials = '()<>@,;:\\\\".\\[\\]'; 2078 my $controls = '\\000-\\037\\177'; 2079 2080 my $dtext = "[^\\[\\]\\r\\\\]"; 2081 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*"; 2082 2083 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*"; 2084 2085# Use zero-width assertion to spot the limit of an atom. A simple 2086# $rfc822_lwsp* causes the regexp engine to hang occasionally. 2087 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))"; 2088 my $word = "(?:$atom|$quoted_string)"; 2089 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*"; 2090 2091 my $sub_domain = "(?:$atom|$domain_literal)"; 2092 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*"; 2093 2094 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain"; 2095 2096 my $phrase = "$word*"; 2097 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)"; 2098 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*"; 2099 my $mailbox = "(?:$addr_spec|$phrase$route_addr)"; 2100 2101 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*"; 2102 my $address = "(?:$mailbox|$group)"; 2103 2104 return "$rfc822_lwsp*$address"; 2105} 2106 2107sub rfc822_strip_comments { 2108 my $s = shift; 2109# Recursively remove comments, and replace with a single space. The simpler 2110# regexps in the Email Addressing FAQ are imperfect - they will miss escaped 2111# chars in atoms, for example. 2112 2113 while ($s =~ s/^((?:[^"\\]|\\.)* 2114 (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*) 2115 \((?:[^()\\]|\\.)*\)/$1 /osx) {} 2116 return $s; 2117} 2118 2119# valid: returns true if the parameter is an RFC822 valid address 2120# 2121sub rfc822_valid { 2122 my $s = rfc822_strip_comments(shift); 2123 2124 if (!$rfc822re) { 2125 $rfc822re = make_rfc822re(); 2126 } 2127 2128 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/; 2129} 2130 2131# validlist: In scalar context, returns true if the parameter is an RFC822 2132# valid list of addresses. 2133# 2134# In list context, returns an empty list on failure (an invalid 2135# address was found); otherwise a list whose first element is the 2136# number of addresses found and whose remaining elements are the 2137# addresses. This is needed to disambiguate failure (invalid) 2138# from success with no addresses found, because an empty string is 2139# a valid list. 2140 2141sub rfc822_validlist { 2142 my $s = rfc822_strip_comments(shift); 2143 2144 if (!$rfc822re) { 2145 $rfc822re = make_rfc822re(); 2146 } 2147 # * null list items are valid according to the RFC 2148 # * the '1' business is to aid in distinguishing failure from no results 2149 2150 my @r; 2151 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so && 2152 $s =~ m/^$rfc822_char*$/) { 2153 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) { 2154 push(@r, $1); 2155 } 2156 return wantarray ? (scalar(@r), @r) : 1; 2157 } 2158 return wantarray ? () : 0; 2159} 2160