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