ARC: export "abort" for modules
[linux/fpc-iii.git] / scripts / get_maintainer.pl
blobaed4511f0304e4922ff6770a3d573777b4f235ed
1 #!/usr/bin/perl -w
2 # (c) 2007, Joe Perches <joe@perches.com>
3 # created from checkpatch.pl
5 # Print selected MAINTAINERS information for
6 # the files modified in a patch or for a file
8 # usage: perl scripts/get_maintainer.pl [OPTIONS] <patch>
9 # perl scripts/get_maintainer.pl [OPTIONS] -f <file>
11 # Licensed under the terms of the GNU GPL License version 2
13 use strict;
15 my $P = $0;
16 my $V = '0.26';
18 use Getopt::Long qw(:config no_auto_abbrev);
19 use Cwd;
21 my $cur_path = fastgetcwd() . '/';
22 my $lk_path = "./";
23 my $email = 1;
24 my $email_usename = 1;
25 my $email_maintainer = 1;
26 my $email_reviewer = 1;
27 my $email_list = 1;
28 my $email_subscriber_list = 0;
29 my $email_git_penguin_chiefs = 0;
30 my $email_git = 0;
31 my $email_git_all_signature_types = 0;
32 my $email_git_blame = 0;
33 my $email_git_blame_signatures = 1;
34 my $email_git_fallback = 1;
35 my $email_git_min_signatures = 1;
36 my $email_git_max_maintainers = 5;
37 my $email_git_min_percent = 5;
38 my $email_git_since = "1-year-ago";
39 my $email_hg_since = "-365";
40 my $interactive = 0;
41 my $email_remove_duplicates = 1;
42 my $email_use_mailmap = 1;
43 my $output_multiline = 1;
44 my $output_separator = ", ";
45 my $output_roles = 0;
46 my $output_rolestats = 1;
47 my $output_section_maxlen = 50;
48 my $scm = 0;
49 my $web = 0;
50 my $subsystem = 0;
51 my $status = 0;
52 my $keywords = 1;
53 my $sections = 0;
54 my $file_emails = 0;
55 my $from_filename = 0;
56 my $pattern_depth = 0;
57 my $version = 0;
58 my $help = 0;
60 my $vcs_used = 0;
62 my $exit = 0;
64 my %commit_author_hash;
65 my %commit_signer_hash;
67 my @penguin_chief = ();
68 push(@penguin_chief, "Linus Torvalds:torvalds\@linux-foundation.org");
69 #Andrew wants in on most everything - 2009/01/14
70 #push(@penguin_chief, "Andrew Morton:akpm\@linux-foundation.org");
72 my @penguin_chief_names = ();
73 foreach my $chief (@penguin_chief) {
74 if ($chief =~ m/^(.*):(.*)/) {
75 my $chief_name = $1;
76 my $chief_addr = $2;
77 push(@penguin_chief_names, $chief_name);
80 my $penguin_chiefs = "\(" . join("|", @penguin_chief_names) . "\)";
82 # Signature types of people who are either
83 # a) responsible for the code in question, or
84 # b) familiar enough with it to give relevant feedback
85 my @signature_tags = ();
86 push(@signature_tags, "Signed-off-by:");
87 push(@signature_tags, "Reviewed-by:");
88 push(@signature_tags, "Acked-by:");
90 my $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
92 # rfc822 email address - preloaded methods go here.
93 my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
94 my $rfc822_char = '[\\000-\\377]';
96 # VCS command support: class-like functions and strings
98 my %VCS_cmds;
100 my %VCS_cmds_git = (
101 "execute_cmd" => \&git_execute_cmd,
102 "available" => '(which("git") ne "") && (-e ".git")',
103 "find_signers_cmd" =>
104 "git log --no-color --follow --since=\$email_git_since " .
105 '--numstat --no-merges ' .
106 '--format="GitCommit: %H%n' .
107 'GitAuthor: %an <%ae>%n' .
108 'GitDate: %aD%n' .
109 'GitSubject: %s%n' .
110 '%b%n"' .
111 " -- \$file",
112 "find_commit_signers_cmd" =>
113 "git log --no-color " .
114 '--numstat ' .
115 '--format="GitCommit: %H%n' .
116 'GitAuthor: %an <%ae>%n' .
117 'GitDate: %aD%n' .
118 'GitSubject: %s%n' .
119 '%b%n"' .
120 " -1 \$commit",
121 "find_commit_author_cmd" =>
122 "git log --no-color " .
123 '--numstat ' .
124 '--format="GitCommit: %H%n' .
125 'GitAuthor: %an <%ae>%n' .
126 'GitDate: %aD%n' .
127 'GitSubject: %s%n"' .
128 " -1 \$commit",
129 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
130 "blame_file_cmd" => "git blame -l \$file",
131 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})",
132 "blame_commit_pattern" => "^([0-9a-f]+) ",
133 "author_pattern" => "^GitAuthor: (.*)",
134 "subject_pattern" => "^GitSubject: (.*)",
135 "stat_pattern" => "^(\\d+)\\t(\\d+)\\t\$file\$",
136 "file_exists_cmd" => "git ls-files \$file",
139 my %VCS_cmds_hg = (
140 "execute_cmd" => \&hg_execute_cmd,
141 "available" => '(which("hg") ne "") && (-d ".hg")',
142 "find_signers_cmd" =>
143 "hg log --date=\$email_hg_since " .
144 "--template='HgCommit: {node}\\n" .
145 "HgAuthor: {author}\\n" .
146 "HgSubject: {desc}\\n'" .
147 " -- \$file",
148 "find_commit_signers_cmd" =>
149 "hg log " .
150 "--template='HgSubject: {desc}\\n'" .
151 " -r \$commit",
152 "find_commit_author_cmd" =>
153 "hg log " .
154 "--template='HgCommit: {node}\\n" .
155 "HgAuthor: {author}\\n" .
156 "HgSubject: {desc|firstline}\\n'" .
157 " -r \$commit",
158 "blame_range_cmd" => "", # not supported
159 "blame_file_cmd" => "hg blame -n \$file",
160 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})",
161 "blame_commit_pattern" => "^([ 0-9a-f]+):",
162 "author_pattern" => "^HgAuthor: (.*)",
163 "subject_pattern" => "^HgSubject: (.*)",
164 "stat_pattern" => "^(\\d+)\t(\\d+)\t\$file\$",
165 "file_exists_cmd" => "hg files \$file",
168 my $conf = which_conf(".get_maintainer.conf");
169 if (-f $conf) {
170 my @conf_args;
171 open(my $conffile, '<', "$conf")
172 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
174 while (<$conffile>) {
175 my $line = $_;
177 $line =~ s/\s*\n?$//g;
178 $line =~ s/^\s*//g;
179 $line =~ s/\s+/ /g;
181 next if ($line =~ m/^\s*#/);
182 next if ($line =~ m/^\s*$/);
184 my @words = split(" ", $line);
185 foreach my $word (@words) {
186 last if ($word =~ m/^#/);
187 push (@conf_args, $word);
190 close($conffile);
191 unshift(@ARGV, @conf_args) if @conf_args;
194 my @ignore_emails = ();
195 my $ignore_file = which_conf(".get_maintainer.ignore");
196 if (-f $ignore_file) {
197 open(my $ignore, '<', "$ignore_file")
198 or warn "$P: Can't find a readable .get_maintainer.ignore file $!\n";
199 while (<$ignore>) {
200 my $line = $_;
202 $line =~ s/\s*\n?$//;
203 $line =~ s/^\s*//;
204 $line =~ s/\s+$//;
205 $line =~ s/#.*$//;
207 next if ($line =~ m/^\s*$/);
208 if (rfc822_valid($line)) {
209 push(@ignore_emails, $line);
212 close($ignore);
215 if (!GetOptions(
216 'email!' => \$email,
217 'git!' => \$email_git,
218 'git-all-signature-types!' => \$email_git_all_signature_types,
219 'git-blame!' => \$email_git_blame,
220 'git-blame-signatures!' => \$email_git_blame_signatures,
221 'git-fallback!' => \$email_git_fallback,
222 'git-chief-penguins!' => \$email_git_penguin_chiefs,
223 'git-min-signatures=i' => \$email_git_min_signatures,
224 'git-max-maintainers=i' => \$email_git_max_maintainers,
225 'git-min-percent=i' => \$email_git_min_percent,
226 'git-since=s' => \$email_git_since,
227 'hg-since=s' => \$email_hg_since,
228 'i|interactive!' => \$interactive,
229 'remove-duplicates!' => \$email_remove_duplicates,
230 'mailmap!' => \$email_use_mailmap,
231 'm!' => \$email_maintainer,
232 'r!' => \$email_reviewer,
233 'n!' => \$email_usename,
234 'l!' => \$email_list,
235 's!' => \$email_subscriber_list,
236 'multiline!' => \$output_multiline,
237 'roles!' => \$output_roles,
238 'rolestats!' => \$output_rolestats,
239 'separator=s' => \$output_separator,
240 'subsystem!' => \$subsystem,
241 'status!' => \$status,
242 'scm!' => \$scm,
243 'web!' => \$web,
244 'pattern-depth=i' => \$pattern_depth,
245 'k|keywords!' => \$keywords,
246 'sections!' => \$sections,
247 'fe|file-emails!' => \$file_emails,
248 'f|file' => \$from_filename,
249 'v|version' => \$version,
250 'h|help|usage' => \$help,
251 )) {
252 die "$P: invalid argument - use --help if necessary\n";
255 if ($help != 0) {
256 usage();
257 exit 0;
260 if ($version != 0) {
261 print("${P} ${V}\n");
262 exit 0;
265 if (-t STDIN && !@ARGV) {
266 # We're talking to a terminal, but have no command line arguments.
267 die "$P: missing patchfile or -f file - use --help if necessary\n";
270 $output_multiline = 0 if ($output_separator ne ", ");
271 $output_rolestats = 1 if ($interactive);
272 $output_roles = 1 if ($output_rolestats);
274 if ($sections) {
275 $email = 0;
276 $email_list = 0;
277 $scm = 0;
278 $status = 0;
279 $subsystem = 0;
280 $web = 0;
281 $keywords = 0;
282 $interactive = 0;
283 } else {
284 my $selections = $email + $scm + $status + $subsystem + $web;
285 if ($selections == 0) {
286 die "$P: Missing required option: email, scm, status, subsystem or web\n";
290 if ($email &&
291 ($email_maintainer + $email_reviewer +
292 $email_list + $email_subscriber_list +
293 $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
294 die "$P: Please select at least 1 email option\n";
297 if (!top_of_kernel_tree($lk_path)) {
298 die "$P: The current directory does not appear to be "
299 . "a linux kernel source tree.\n";
302 ## Read MAINTAINERS for type/value pairs
304 my @typevalue = ();
305 my %keyword_hash;
307 open (my $maint, '<', "${lk_path}MAINTAINERS")
308 or die "$P: Can't open MAINTAINERS: $!\n";
309 while (<$maint>) {
310 my $line = $_;
312 if ($line =~ m/^([A-Z]):\s*(.*)/) {
313 my $type = $1;
314 my $value = $2;
316 ##Filename pattern matching
317 if ($type eq "F" || $type eq "X") {
318 $value =~ s@\.@\\\.@g; ##Convert . to \.
319 $value =~ s/\*/\.\*/g; ##Convert * to .*
320 $value =~ s/\?/\./g; ##Convert ? to .
321 ##if pattern is a directory and it lacks a trailing slash, add one
322 if ((-d $value)) {
323 $value =~ s@([^/])$@$1/@;
325 } elsif ($type eq "K") {
326 $keyword_hash{@typevalue} = $value;
328 push(@typevalue, "$type:$value");
329 } elsif (!/^(\s)*$/) {
330 $line =~ s/\n$//g;
331 push(@typevalue, $line);
334 close($maint);
338 # Read mail address map
341 my $mailmap;
343 read_mailmap();
345 sub read_mailmap {
346 $mailmap = {
347 names => {},
348 addresses => {}
351 return if (!$email_use_mailmap || !(-f "${lk_path}.mailmap"));
353 open(my $mailmap_file, '<', "${lk_path}.mailmap")
354 or warn "$P: Can't open .mailmap: $!\n";
356 while (<$mailmap_file>) {
357 s/#.*$//; #strip comments
358 s/^\s+|\s+$//g; #trim
360 next if (/^\s*$/); #skip empty lines
361 #entries have one of the following formats:
362 # name1 <mail1>
363 # <mail1> <mail2>
364 # name1 <mail1> <mail2>
365 # name1 <mail1> name2 <mail2>
366 # (see man git-shortlog)
368 if (/^([^<]+)<([^>]+)>$/) {
369 my $real_name = $1;
370 my $address = $2;
372 $real_name =~ s/\s+$//;
373 ($real_name, $address) = parse_email("$real_name <$address>");
374 $mailmap->{names}->{$address} = $real_name;
376 } elsif (/^<([^>]+)>\s*<([^>]+)>$/) {
377 my $real_address = $1;
378 my $wrong_address = $2;
380 $mailmap->{addresses}->{$wrong_address} = $real_address;
382 } elsif (/^(.+)<([^>]+)>\s*<([^>]+)>$/) {
383 my $real_name = $1;
384 my $real_address = $2;
385 my $wrong_address = $3;
387 $real_name =~ s/\s+$//;
388 ($real_name, $real_address) =
389 parse_email("$real_name <$real_address>");
390 $mailmap->{names}->{$wrong_address} = $real_name;
391 $mailmap->{addresses}->{$wrong_address} = $real_address;
393 } elsif (/^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$/) {
394 my $real_name = $1;
395 my $real_address = $2;
396 my $wrong_name = $3;
397 my $wrong_address = $4;
399 $real_name =~ s/\s+$//;
400 ($real_name, $real_address) =
401 parse_email("$real_name <$real_address>");
403 $wrong_name =~ s/\s+$//;
404 ($wrong_name, $wrong_address) =
405 parse_email("$wrong_name <$wrong_address>");
407 my $wrong_email = format_email($wrong_name, $wrong_address, 1);
408 $mailmap->{names}->{$wrong_email} = $real_name;
409 $mailmap->{addresses}->{$wrong_email} = $real_address;
412 close($mailmap_file);
415 ## use the filenames on the command line or find the filenames in the patchfiles
417 my @files = ();
418 my @range = ();
419 my @keyword_tvi = ();
420 my @file_emails = ();
422 if (!@ARGV) {
423 push(@ARGV, "&STDIN");
426 foreach my $file (@ARGV) {
427 if ($file ne "&STDIN") {
428 ##if $file is a directory and it lacks a trailing slash, add one
429 if ((-d $file)) {
430 $file =~ s@([^/])$@$1/@;
431 } elsif (!(-f $file)) {
432 die "$P: file '${file}' not found\n";
435 if ($from_filename || ($file ne "&STDIN" && vcs_file_exists($file))) {
436 $file =~ s/^\Q${cur_path}\E//; #strip any absolute path
437 $file =~ s/^\Q${lk_path}\E//; #or the path to the lk tree
438 push(@files, $file);
439 if ($file ne "MAINTAINERS" && -f $file && ($keywords || $file_emails)) {
440 open(my $f, '<', $file)
441 or die "$P: Can't open $file: $!\n";
442 my $text = do { local($/) ; <$f> };
443 close($f);
444 if ($keywords) {
445 foreach my $line (keys %keyword_hash) {
446 if ($text =~ m/$keyword_hash{$line}/x) {
447 push(@keyword_tvi, $line);
451 if ($file_emails) {
452 my @poss_addr = $text =~ m$[A-Za--ÿ\"\' \,\.\+-]*\s*[\,]*\s*[\(\<\{]{0,1}[A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+\.[A-Za-z0-9]+[\)\>\}]{0,1}$g;
453 push(@file_emails, clean_file_emails(@poss_addr));
456 } else {
457 my $file_cnt = @files;
458 my $lastfile;
460 open(my $patch, "< $file")
461 or die "$P: Can't open $file: $!\n";
463 # We can check arbitrary information before the patch
464 # like the commit message, mail headers, etc...
465 # This allows us to match arbitrary keywords against any part
466 # of a git format-patch generated file (subject tags, etc...)
468 my $patch_prefix = ""; #Parsing the intro
470 while (<$patch>) {
471 my $patch_line = $_;
472 if (m/^\+\+\+\s+(\S+)/ or m/^---\s+(\S+)/) {
473 my $filename = $1;
474 $filename =~ s@^[^/]*/@@;
475 $filename =~ s@\n@@;
476 $lastfile = $filename;
477 push(@files, $filename);
478 $patch_prefix = "^[+-].*"; #Now parsing the actual patch
479 } elsif (m/^\@\@ -(\d+),(\d+)/) {
480 if ($email_git_blame) {
481 push(@range, "$lastfile:$1:$2");
483 } elsif ($keywords) {
484 foreach my $line (keys %keyword_hash) {
485 if ($patch_line =~ m/${patch_prefix}$keyword_hash{$line}/x) {
486 push(@keyword_tvi, $line);
491 close($patch);
493 if ($file_cnt == @files) {
494 warn "$P: file '${file}' doesn't appear to be a patch. "
495 . "Add -f to options?\n";
497 @files = sort_and_uniq(@files);
501 @file_emails = uniq(@file_emails);
503 my %email_hash_name;
504 my %email_hash_address;
505 my @email_to = ();
506 my %hash_list_to;
507 my @list_to = ();
508 my @scm = ();
509 my @web = ();
510 my @subsystem = ();
511 my @status = ();
512 my %deduplicate_name_hash = ();
513 my %deduplicate_address_hash = ();
515 my @maintainers = get_maintainers();
517 if (@maintainers) {
518 @maintainers = merge_email(@maintainers);
519 output(@maintainers);
522 if ($scm) {
523 @scm = uniq(@scm);
524 output(@scm);
527 if ($status) {
528 @status = uniq(@status);
529 output(@status);
532 if ($subsystem) {
533 @subsystem = uniq(@subsystem);
534 output(@subsystem);
537 if ($web) {
538 @web = uniq(@web);
539 output(@web);
542 exit($exit);
544 sub ignore_email_address {
545 my ($address) = @_;
547 foreach my $ignore (@ignore_emails) {
548 return 1 if ($ignore eq $address);
551 return 0;
554 sub range_is_maintained {
555 my ($start, $end) = @_;
557 for (my $i = $start; $i < $end; $i++) {
558 my $line = $typevalue[$i];
559 if ($line =~ m/^([A-Z]):\s*(.*)/) {
560 my $type = $1;
561 my $value = $2;
562 if ($type eq 'S') {
563 if ($value =~ /(maintain|support)/i) {
564 return 1;
569 return 0;
572 sub range_has_maintainer {
573 my ($start, $end) = @_;
575 for (my $i = $start; $i < $end; $i++) {
576 my $line = $typevalue[$i];
577 if ($line =~ m/^([A-Z]):\s*(.*)/) {
578 my $type = $1;
579 my $value = $2;
580 if ($type eq 'M') {
581 return 1;
585 return 0;
588 sub get_maintainers {
589 %email_hash_name = ();
590 %email_hash_address = ();
591 %commit_author_hash = ();
592 %commit_signer_hash = ();
593 @email_to = ();
594 %hash_list_to = ();
595 @list_to = ();
596 @scm = ();
597 @web = ();
598 @subsystem = ();
599 @status = ();
600 %deduplicate_name_hash = ();
601 %deduplicate_address_hash = ();
602 if ($email_git_all_signature_types) {
603 $signature_pattern = "(.+?)[Bb][Yy]:";
604 } else {
605 $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
608 # Find responsible parties
610 my %exact_pattern_match_hash = ();
612 foreach my $file (@files) {
614 my %hash;
615 my $tvi = find_first_section();
616 while ($tvi < @typevalue) {
617 my $start = find_starting_index($tvi);
618 my $end = find_ending_index($tvi);
619 my $exclude = 0;
620 my $i;
622 #Do not match excluded file patterns
624 for ($i = $start; $i < $end; $i++) {
625 my $line = $typevalue[$i];
626 if ($line =~ m/^([A-Z]):\s*(.*)/) {
627 my $type = $1;
628 my $value = $2;
629 if ($type eq 'X') {
630 if (file_match_pattern($file, $value)) {
631 $exclude = 1;
632 last;
638 if (!$exclude) {
639 for ($i = $start; $i < $end; $i++) {
640 my $line = $typevalue[$i];
641 if ($line =~ m/^([A-Z]):\s*(.*)/) {
642 my $type = $1;
643 my $value = $2;
644 if ($type eq 'F') {
645 if (file_match_pattern($file, $value)) {
646 my $value_pd = ($value =~ tr@/@@);
647 my $file_pd = ($file =~ tr@/@@);
648 $value_pd++ if (substr($value,-1,1) ne "/");
649 $value_pd = -1 if ($value =~ /^\.\*/);
650 if ($value_pd >= $file_pd &&
651 range_is_maintained($start, $end) &&
652 range_has_maintainer($start, $end)) {
653 $exact_pattern_match_hash{$file} = 1;
655 if ($pattern_depth == 0 ||
656 (($file_pd - $value_pd) < $pattern_depth)) {
657 $hash{$tvi} = $value_pd;
660 } elsif ($type eq 'N') {
661 if ($file =~ m/$value/x) {
662 $hash{$tvi} = 0;
668 $tvi = $end + 1;
671 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
672 add_categories($line);
673 if ($sections) {
674 my $i;
675 my $start = find_starting_index($line);
676 my $end = find_ending_index($line);
677 for ($i = $start; $i < $end; $i++) {
678 my $line = $typevalue[$i];
679 if ($line =~ /^[FX]:/) { ##Restore file patterns
680 $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
681 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ?
682 $line =~ s/\\\./\./g; ##Convert \. to .
683 $line =~ s/\.\*/\*/g; ##Convert .* to *
685 $line =~ s/^([A-Z]):/$1:\t/g;
686 print("$line\n");
688 print("\n");
693 if ($keywords) {
694 @keyword_tvi = sort_and_uniq(@keyword_tvi);
695 foreach my $line (@keyword_tvi) {
696 add_categories($line);
700 foreach my $email (@email_to, @list_to) {
701 $email->[0] = deduplicate_email($email->[0]);
704 foreach my $file (@files) {
705 if ($email &&
706 ($email_git || ($email_git_fallback &&
707 !$exact_pattern_match_hash{$file}))) {
708 vcs_file_signoffs($file);
710 if ($email && $email_git_blame) {
711 vcs_file_blame($file);
715 if ($email) {
716 foreach my $chief (@penguin_chief) {
717 if ($chief =~ m/^(.*):(.*)/) {
718 my $email_address;
720 $email_address = format_email($1, $2, $email_usename);
721 if ($email_git_penguin_chiefs) {
722 push(@email_to, [$email_address, 'chief penguin']);
723 } else {
724 @email_to = grep($_->[0] !~ /${email_address}/, @email_to);
729 foreach my $email (@file_emails) {
730 my ($name, $address) = parse_email($email);
732 my $tmp_email = format_email($name, $address, $email_usename);
733 push_email_address($tmp_email, '');
734 add_role($tmp_email, 'in file');
738 my @to = ();
739 if ($email || $email_list) {
740 if ($email) {
741 @to = (@to, @email_to);
743 if ($email_list) {
744 @to = (@to, @list_to);
748 if ($interactive) {
749 @to = interactive_get_maintainers(\@to);
752 return @to;
755 sub file_match_pattern {
756 my ($file, $pattern) = @_;
757 if (substr($pattern, -1) eq "/") {
758 if ($file =~ m@^$pattern@) {
759 return 1;
761 } else {
762 if ($file =~ m@^$pattern@) {
763 my $s1 = ($file =~ tr@/@@);
764 my $s2 = ($pattern =~ tr@/@@);
765 if ($s1 == $s2) {
766 return 1;
770 return 0;
773 sub usage {
774 print <<EOT;
775 usage: $P [options] patchfile
776 $P [options] -f file|directory
777 version: $V
779 MAINTAINER field selection options:
780 --email => print email address(es) if any
781 --git => include recent git \*-by: signers
782 --git-all-signature-types => include signers regardless of signature type
783 or use only ${signature_pattern} signers (default: $email_git_all_signature_types)
784 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
785 --git-chief-penguins => include ${penguin_chiefs}
786 --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
787 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
788 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
789 --git-blame => use git blame to find modified commits for patch or file
790 --git-blame-signatures => when used with --git-blame, also include all commit signers
791 --git-since => git history to use (default: $email_git_since)
792 --hg-since => hg history to use (default: $email_hg_since)
793 --interactive => display a menu (mostly useful if used with the --git option)
794 --m => include maintainer(s) if any
795 --r => include reviewer(s) if any
796 --n => include name 'Full Name <addr\@domain.tld>'
797 --l => include list(s) if any
798 --s => include subscriber only list(s) if any
799 --remove-duplicates => minimize duplicate email names/addresses
800 --roles => show roles (status:subsystem, git-signer, list, etc...)
801 --rolestats => show roles and statistics (commits/total_commits, %)
802 --file-emails => add email addresses found in -f file (default: 0 (off))
803 --scm => print SCM tree(s) if any
804 --status => print status if any
805 --subsystem => print subsystem name if any
806 --web => print website(s) if any
808 Output type options:
809 --separator [, ] => separator for multiple entries on 1 line
810 using --separator also sets --nomultiline if --separator is not [, ]
811 --multiline => print 1 entry per line
813 Other options:
814 --pattern-depth => Number of pattern directory traversals (default: 0 (all))
815 --keywords => scan patch for keywords (default: $keywords)
816 --sections => print all of the subsystem sections with pattern matches
817 --mailmap => use .mailmap file (default: $email_use_mailmap)
818 --version => show version
819 --help => show this help information
821 Default options:
822 [--email --nogit --git-fallback --m --r --n --l --multiline --pattern-depth=0
823 --remove-duplicates --rolestats]
825 Notes:
826 Using "-f directory" may give unexpected results:
827 Used with "--git", git signators for _all_ files in and below
828 directory are examined as git recurses directories.
829 Any specified X: (exclude) pattern matches are _not_ ignored.
830 Used with "--nogit", directory is used as a pattern match,
831 no individual file within the directory or subdirectory
832 is matched.
833 Used with "--git-blame", does not iterate all files in directory
834 Using "--git-blame" is slow and may add old committers and authors
835 that are no longer active maintainers to the output.
836 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
837 other automated tools that expect only ["name"] <email address>
838 may not work because of additional output after <email address>.
839 Using "--rolestats" and "--git-blame" shows the #/total=% commits,
840 not the percentage of the entire file authored. # of commits is
841 not a good measure of amount of code authored. 1 major commit may
842 contain a thousand lines, 5 trivial commits may modify a single line.
843 If git is not installed, but mercurial (hg) is installed and an .hg
844 repository exists, the following options apply to mercurial:
845 --git,
846 --git-min-signatures, --git-max-maintainers, --git-min-percent, and
847 --git-blame
848 Use --hg-since not --git-since to control date selection
849 File ".get_maintainer.conf", if it exists in the linux kernel source root
850 directory, can change whatever get_maintainer defaults are desired.
851 Entries in this file can be any command line argument.
852 This file is prepended to any additional command line arguments.
853 Multiple lines and # comments are allowed.
854 Most options have both positive and negative forms.
855 The negative forms for --<foo> are --no<foo> and --no-<foo>.
860 sub top_of_kernel_tree {
861 my ($lk_path) = @_;
863 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
864 $lk_path .= "/";
866 if ( (-f "${lk_path}COPYING")
867 && (-f "${lk_path}CREDITS")
868 && (-f "${lk_path}Kbuild")
869 && (-f "${lk_path}MAINTAINERS")
870 && (-f "${lk_path}Makefile")
871 && (-f "${lk_path}README")
872 && (-d "${lk_path}Documentation")
873 && (-d "${lk_path}arch")
874 && (-d "${lk_path}include")
875 && (-d "${lk_path}drivers")
876 && (-d "${lk_path}fs")
877 && (-d "${lk_path}init")
878 && (-d "${lk_path}ipc")
879 && (-d "${lk_path}kernel")
880 && (-d "${lk_path}lib")
881 && (-d "${lk_path}scripts")) {
882 return 1;
884 return 0;
887 sub parse_email {
888 my ($formatted_email) = @_;
890 my $name = "";
891 my $address = "";
893 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
894 $name = $1;
895 $address = $2;
896 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
897 $address = $1;
898 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
899 $address = $1;
902 $name =~ s/^\s+|\s+$//g;
903 $name =~ s/^\"|\"$//g;
904 $address =~ s/^\s+|\s+$//g;
906 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
907 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
908 $name = "\"$name\"";
911 return ($name, $address);
914 sub format_email {
915 my ($name, $address, $usename) = @_;
917 my $formatted_email;
919 $name =~ s/^\s+|\s+$//g;
920 $name =~ s/^\"|\"$//g;
921 $address =~ s/^\s+|\s+$//g;
923 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
924 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
925 $name = "\"$name\"";
928 if ($usename) {
929 if ("$name" eq "") {
930 $formatted_email = "$address";
931 } else {
932 $formatted_email = "$name <$address>";
934 } else {
935 $formatted_email = $address;
938 return $formatted_email;
941 sub find_first_section {
942 my $index = 0;
944 while ($index < @typevalue) {
945 my $tv = $typevalue[$index];
946 if (($tv =~ m/^([A-Z]):\s*(.*)/)) {
947 last;
949 $index++;
952 return $index;
955 sub find_starting_index {
956 my ($index) = @_;
958 while ($index > 0) {
959 my $tv = $typevalue[$index];
960 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
961 last;
963 $index--;
966 return $index;
969 sub find_ending_index {
970 my ($index) = @_;
972 while ($index < @typevalue) {
973 my $tv = $typevalue[$index];
974 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
975 last;
977 $index++;
980 return $index;
983 sub get_subsystem_name {
984 my ($index) = @_;
986 my $start = find_starting_index($index);
988 my $subsystem = $typevalue[$start];
989 if ($output_section_maxlen && length($subsystem) > $output_section_maxlen) {
990 $subsystem = substr($subsystem, 0, $output_section_maxlen - 3);
991 $subsystem =~ s/\s*$//;
992 $subsystem = $subsystem . "...";
994 return $subsystem;
997 sub get_maintainer_role {
998 my ($index) = @_;
1000 my $i;
1001 my $start = find_starting_index($index);
1002 my $end = find_ending_index($index);
1004 my $role = "unknown";
1005 my $subsystem = get_subsystem_name($index);
1007 for ($i = $start + 1; $i < $end; $i++) {
1008 my $tv = $typevalue[$i];
1009 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1010 my $ptype = $1;
1011 my $pvalue = $2;
1012 if ($ptype eq "S") {
1013 $role = $pvalue;
1018 $role = lc($role);
1019 if ($role eq "supported") {
1020 $role = "supporter";
1021 } elsif ($role eq "maintained") {
1022 $role = "maintainer";
1023 } elsif ($role eq "odd fixes") {
1024 $role = "odd fixer";
1025 } elsif ($role eq "orphan") {
1026 $role = "orphan minder";
1027 } elsif ($role eq "obsolete") {
1028 $role = "obsolete minder";
1029 } elsif ($role eq "buried alive in reporters") {
1030 $role = "chief penguin";
1033 return $role . ":" . $subsystem;
1036 sub get_list_role {
1037 my ($index) = @_;
1039 my $subsystem = get_subsystem_name($index);
1041 if ($subsystem eq "THE REST") {
1042 $subsystem = "";
1045 return $subsystem;
1048 sub add_categories {
1049 my ($index) = @_;
1051 my $i;
1052 my $start = find_starting_index($index);
1053 my $end = find_ending_index($index);
1055 push(@subsystem, $typevalue[$start]);
1057 for ($i = $start + 1; $i < $end; $i++) {
1058 my $tv = $typevalue[$i];
1059 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1060 my $ptype = $1;
1061 my $pvalue = $2;
1062 if ($ptype eq "L") {
1063 my $list_address = $pvalue;
1064 my $list_additional = "";
1065 my $list_role = get_list_role($i);
1067 if ($list_role ne "") {
1068 $list_role = ":" . $list_role;
1070 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
1071 $list_address = $1;
1072 $list_additional = $2;
1074 if ($list_additional =~ m/subscribers-only/) {
1075 if ($email_subscriber_list) {
1076 if (!$hash_list_to{lc($list_address)}) {
1077 $hash_list_to{lc($list_address)} = 1;
1078 push(@list_to, [$list_address,
1079 "subscriber list${list_role}"]);
1082 } else {
1083 if ($email_list) {
1084 if (!$hash_list_to{lc($list_address)}) {
1085 $hash_list_to{lc($list_address)} = 1;
1086 if ($list_additional =~ m/moderated/) {
1087 push(@list_to, [$list_address,
1088 "moderated list${list_role}"]);
1089 } else {
1090 push(@list_to, [$list_address,
1091 "open list${list_role}"]);
1096 } elsif ($ptype eq "M") {
1097 my ($name, $address) = parse_email($pvalue);
1098 if ($name eq "") {
1099 if ($i > 0) {
1100 my $tv = $typevalue[$i - 1];
1101 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1102 if ($1 eq "P") {
1103 $name = $2;
1104 $pvalue = format_email($name, $address, $email_usename);
1109 if ($email_maintainer) {
1110 my $role = get_maintainer_role($i);
1111 push_email_addresses($pvalue, $role);
1113 } elsif ($ptype eq "R") {
1114 my ($name, $address) = parse_email($pvalue);
1115 if ($name eq "") {
1116 if ($i > 0) {
1117 my $tv = $typevalue[$i - 1];
1118 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1119 if ($1 eq "P") {
1120 $name = $2;
1121 $pvalue = format_email($name, $address, $email_usename);
1126 if ($email_reviewer) {
1127 my $subsystem = get_subsystem_name($i);
1128 push_email_addresses($pvalue, "reviewer:$subsystem");
1130 } elsif ($ptype eq "T") {
1131 push(@scm, $pvalue);
1132 } elsif ($ptype eq "W") {
1133 push(@web, $pvalue);
1134 } elsif ($ptype eq "S") {
1135 push(@status, $pvalue);
1141 sub email_inuse {
1142 my ($name, $address) = @_;
1144 return 1 if (($name eq "") && ($address eq ""));
1145 return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
1146 return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
1148 return 0;
1151 sub push_email_address {
1152 my ($line, $role) = @_;
1154 my ($name, $address) = parse_email($line);
1156 if ($address eq "") {
1157 return 0;
1160 if (!$email_remove_duplicates) {
1161 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1162 } elsif (!email_inuse($name, $address)) {
1163 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1164 $email_hash_name{lc($name)}++ if ($name ne "");
1165 $email_hash_address{lc($address)}++;
1168 return 1;
1171 sub push_email_addresses {
1172 my ($address, $role) = @_;
1174 my @address_list = ();
1176 if (rfc822_valid($address)) {
1177 push_email_address($address, $role);
1178 } elsif (@address_list = rfc822_validlist($address)) {
1179 my $array_count = shift(@address_list);
1180 while (my $entry = shift(@address_list)) {
1181 push_email_address($entry, $role);
1183 } else {
1184 if (!push_email_address($address, $role)) {
1185 warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1190 sub add_role {
1191 my ($line, $role) = @_;
1193 my ($name, $address) = parse_email($line);
1194 my $email = format_email($name, $address, $email_usename);
1196 foreach my $entry (@email_to) {
1197 if ($email_remove_duplicates) {
1198 my ($entry_name, $entry_address) = parse_email($entry->[0]);
1199 if (($name eq $entry_name || $address eq $entry_address)
1200 && ($role eq "" || !($entry->[1] =~ m/$role/))
1202 if ($entry->[1] eq "") {
1203 $entry->[1] = "$role";
1204 } else {
1205 $entry->[1] = "$entry->[1],$role";
1208 } else {
1209 if ($email eq $entry->[0]
1210 && ($role eq "" || !($entry->[1] =~ m/$role/))
1212 if ($entry->[1] eq "") {
1213 $entry->[1] = "$role";
1214 } else {
1215 $entry->[1] = "$entry->[1],$role";
1222 sub which {
1223 my ($bin) = @_;
1225 foreach my $path (split(/:/, $ENV{PATH})) {
1226 if (-e "$path/$bin") {
1227 return "$path/$bin";
1231 return "";
1234 sub which_conf {
1235 my ($conf) = @_;
1237 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1238 if (-e "$path/$conf") {
1239 return "$path/$conf";
1243 return "";
1246 sub mailmap_email {
1247 my ($line) = @_;
1249 my ($name, $address) = parse_email($line);
1250 my $email = format_email($name, $address, 1);
1251 my $real_name = $name;
1252 my $real_address = $address;
1254 if (exists $mailmap->{names}->{$email} ||
1255 exists $mailmap->{addresses}->{$email}) {
1256 if (exists $mailmap->{names}->{$email}) {
1257 $real_name = $mailmap->{names}->{$email};
1259 if (exists $mailmap->{addresses}->{$email}) {
1260 $real_address = $mailmap->{addresses}->{$email};
1262 } else {
1263 if (exists $mailmap->{names}->{$address}) {
1264 $real_name = $mailmap->{names}->{$address};
1266 if (exists $mailmap->{addresses}->{$address}) {
1267 $real_address = $mailmap->{addresses}->{$address};
1270 return format_email($real_name, $real_address, 1);
1273 sub mailmap {
1274 my (@addresses) = @_;
1276 my @mapped_emails = ();
1277 foreach my $line (@addresses) {
1278 push(@mapped_emails, mailmap_email($line));
1280 merge_by_realname(@mapped_emails) if ($email_use_mailmap);
1281 return @mapped_emails;
1284 sub merge_by_realname {
1285 my %address_map;
1286 my (@emails) = @_;
1288 foreach my $email (@emails) {
1289 my ($name, $address) = parse_email($email);
1290 if (exists $address_map{$name}) {
1291 $address = $address_map{$name};
1292 $email = format_email($name, $address, 1);
1293 } else {
1294 $address_map{$name} = $address;
1299 sub git_execute_cmd {
1300 my ($cmd) = @_;
1301 my @lines = ();
1303 my $output = `$cmd`;
1304 $output =~ s/^\s*//gm;
1305 @lines = split("\n", $output);
1307 return @lines;
1310 sub hg_execute_cmd {
1311 my ($cmd) = @_;
1312 my @lines = ();
1314 my $output = `$cmd`;
1315 @lines = split("\n", $output);
1317 return @lines;
1320 sub extract_formatted_signatures {
1321 my (@signature_lines) = @_;
1323 my @type = @signature_lines;
1325 s/\s*(.*):.*/$1/ for (@type);
1327 # cut -f2- -d":"
1328 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1330 ## Reformat email addresses (with names) to avoid badly written signatures
1332 foreach my $signer (@signature_lines) {
1333 $signer = deduplicate_email($signer);
1336 return (\@type, \@signature_lines);
1339 sub vcs_find_signers {
1340 my ($cmd, $file) = @_;
1341 my $commits;
1342 my @lines = ();
1343 my @signatures = ();
1344 my @authors = ();
1345 my @stats = ();
1347 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1349 my $pattern = $VCS_cmds{"commit_pattern"};
1350 my $author_pattern = $VCS_cmds{"author_pattern"};
1351 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1353 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1355 $commits = grep(/$pattern/, @lines); # of commits
1357 @authors = grep(/$author_pattern/, @lines);
1358 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1359 @stats = grep(/$stat_pattern/, @lines);
1361 # print("stats: <@stats>\n");
1363 return (0, \@signatures, \@authors, \@stats) if !@signatures;
1365 save_commits_by_author(@lines) if ($interactive);
1366 save_commits_by_signer(@lines) if ($interactive);
1368 if (!$email_git_penguin_chiefs) {
1369 @signatures = grep(!/${penguin_chiefs}/i, @signatures);
1372 my ($author_ref, $authors_ref) = extract_formatted_signatures(@authors);
1373 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1375 return ($commits, $signers_ref, $authors_ref, \@stats);
1378 sub vcs_find_author {
1379 my ($cmd) = @_;
1380 my @lines = ();
1382 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1384 if (!$email_git_penguin_chiefs) {
1385 @lines = grep(!/${penguin_chiefs}/i, @lines);
1388 return @lines if !@lines;
1390 my @authors = ();
1391 foreach my $line (@lines) {
1392 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1393 my $author = $1;
1394 my ($name, $address) = parse_email($author);
1395 $author = format_email($name, $address, 1);
1396 push(@authors, $author);
1400 save_commits_by_author(@lines) if ($interactive);
1401 save_commits_by_signer(@lines) if ($interactive);
1403 return @authors;
1406 sub vcs_save_commits {
1407 my ($cmd) = @_;
1408 my @lines = ();
1409 my @commits = ();
1411 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1413 foreach my $line (@lines) {
1414 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1415 push(@commits, $1);
1419 return @commits;
1422 sub vcs_blame {
1423 my ($file) = @_;
1424 my $cmd;
1425 my @commits = ();
1427 return @commits if (!(-f $file));
1429 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1430 my @all_commits = ();
1432 $cmd = $VCS_cmds{"blame_file_cmd"};
1433 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1434 @all_commits = vcs_save_commits($cmd);
1436 foreach my $file_range_diff (@range) {
1437 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1438 my $diff_file = $1;
1439 my $diff_start = $2;
1440 my $diff_length = $3;
1441 next if ("$file" ne "$diff_file");
1442 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1443 push(@commits, $all_commits[$i]);
1446 } elsif (@range) {
1447 foreach my $file_range_diff (@range) {
1448 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1449 my $diff_file = $1;
1450 my $diff_start = $2;
1451 my $diff_length = $3;
1452 next if ("$file" ne "$diff_file");
1453 $cmd = $VCS_cmds{"blame_range_cmd"};
1454 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1455 push(@commits, vcs_save_commits($cmd));
1457 } else {
1458 $cmd = $VCS_cmds{"blame_file_cmd"};
1459 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1460 @commits = vcs_save_commits($cmd);
1463 foreach my $commit (@commits) {
1464 $commit =~ s/^\^//g;
1467 return @commits;
1470 my $printed_novcs = 0;
1471 sub vcs_exists {
1472 %VCS_cmds = %VCS_cmds_git;
1473 return 1 if eval $VCS_cmds{"available"};
1474 %VCS_cmds = %VCS_cmds_hg;
1475 return 2 if eval $VCS_cmds{"available"};
1476 %VCS_cmds = ();
1477 if (!$printed_novcs) {
1478 warn("$P: No supported VCS found. Add --nogit to options?\n");
1479 warn("Using a git repository produces better results.\n");
1480 warn("Try Linus Torvalds' latest git repository using:\n");
1481 warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n");
1482 $printed_novcs = 1;
1484 return 0;
1487 sub vcs_is_git {
1488 vcs_exists();
1489 return $vcs_used == 1;
1492 sub vcs_is_hg {
1493 return $vcs_used == 2;
1496 sub interactive_get_maintainers {
1497 my ($list_ref) = @_;
1498 my @list = @$list_ref;
1500 vcs_exists();
1502 my %selected;
1503 my %authored;
1504 my %signed;
1505 my $count = 0;
1506 my $maintained = 0;
1507 foreach my $entry (@list) {
1508 $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
1509 $selected{$count} = 1;
1510 $authored{$count} = 0;
1511 $signed{$count} = 0;
1512 $count++;
1515 #menu loop
1516 my $done = 0;
1517 my $print_options = 0;
1518 my $redraw = 1;
1519 while (!$done) {
1520 $count = 0;
1521 if ($redraw) {
1522 printf STDERR "\n%1s %2s %-65s",
1523 "*", "#", "email/list and role:stats";
1524 if ($email_git ||
1525 ($email_git_fallback && !$maintained) ||
1526 $email_git_blame) {
1527 print STDERR "auth sign";
1529 print STDERR "\n";
1530 foreach my $entry (@list) {
1531 my $email = $entry->[0];
1532 my $role = $entry->[1];
1533 my $sel = "";
1534 $sel = "*" if ($selected{$count});
1535 my $commit_author = $commit_author_hash{$email};
1536 my $commit_signer = $commit_signer_hash{$email};
1537 my $authored = 0;
1538 my $signed = 0;
1539 $authored++ for (@{$commit_author});
1540 $signed++ for (@{$commit_signer});
1541 printf STDERR "%1s %2d %-65s", $sel, $count + 1, $email;
1542 printf STDERR "%4d %4d", $authored, $signed
1543 if ($authored > 0 || $signed > 0);
1544 printf STDERR "\n %s\n", $role;
1545 if ($authored{$count}) {
1546 my $commit_author = $commit_author_hash{$email};
1547 foreach my $ref (@{$commit_author}) {
1548 print STDERR " Author: @{$ref}[1]\n";
1551 if ($signed{$count}) {
1552 my $commit_signer = $commit_signer_hash{$email};
1553 foreach my $ref (@{$commit_signer}) {
1554 print STDERR " @{$ref}[2]: @{$ref}[1]\n";
1558 $count++;
1561 my $date_ref = \$email_git_since;
1562 $date_ref = \$email_hg_since if (vcs_is_hg());
1563 if ($print_options) {
1564 $print_options = 0;
1565 if (vcs_exists()) {
1566 print STDERR <<EOT
1568 Version Control options:
1569 g use git history [$email_git]
1570 gf use git-fallback [$email_git_fallback]
1571 b use git blame [$email_git_blame]
1572 bs use blame signatures [$email_git_blame_signatures]
1573 c# minimum commits [$email_git_min_signatures]
1574 %# min percent [$email_git_min_percent]
1575 d# history to use [$$date_ref]
1576 x# max maintainers [$email_git_max_maintainers]
1577 t all signature types [$email_git_all_signature_types]
1578 m use .mailmap [$email_use_mailmap]
1581 print STDERR <<EOT
1583 Additional options:
1584 0 toggle all
1585 tm toggle maintainers
1586 tg toggle git entries
1587 tl toggle open list entries
1588 ts toggle subscriber list entries
1589 f emails in file [$file_emails]
1590 k keywords in file [$keywords]
1591 r remove duplicates [$email_remove_duplicates]
1592 p# pattern match depth [$pattern_depth]
1595 print STDERR
1596 "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1598 my $input = <STDIN>;
1599 chomp($input);
1601 $redraw = 1;
1602 my $rerun = 0;
1603 my @wish = split(/[, ]+/, $input);
1604 foreach my $nr (@wish) {
1605 $nr = lc($nr);
1606 my $sel = substr($nr, 0, 1);
1607 my $str = substr($nr, 1);
1608 my $val = 0;
1609 $val = $1 if $str =~ /^(\d+)$/;
1611 if ($sel eq "y") {
1612 $interactive = 0;
1613 $done = 1;
1614 $output_rolestats = 0;
1615 $output_roles = 0;
1616 last;
1617 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1618 $selected{$nr - 1} = !$selected{$nr - 1};
1619 } elsif ($sel eq "*" || $sel eq '^') {
1620 my $toggle = 0;
1621 $toggle = 1 if ($sel eq '*');
1622 for (my $i = 0; $i < $count; $i++) {
1623 $selected{$i} = $toggle;
1625 } elsif ($sel eq "0") {
1626 for (my $i = 0; $i < $count; $i++) {
1627 $selected{$i} = !$selected{$i};
1629 } elsif ($sel eq "t") {
1630 if (lc($str) eq "m") {
1631 for (my $i = 0; $i < $count; $i++) {
1632 $selected{$i} = !$selected{$i}
1633 if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
1635 } elsif (lc($str) eq "g") {
1636 for (my $i = 0; $i < $count; $i++) {
1637 $selected{$i} = !$selected{$i}
1638 if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
1640 } elsif (lc($str) eq "l") {
1641 for (my $i = 0; $i < $count; $i++) {
1642 $selected{$i} = !$selected{$i}
1643 if ($list[$i]->[1] =~ /^(open list)/i);
1645 } elsif (lc($str) eq "s") {
1646 for (my $i = 0; $i < $count; $i++) {
1647 $selected{$i} = !$selected{$i}
1648 if ($list[$i]->[1] =~ /^(subscriber list)/i);
1651 } elsif ($sel eq "a") {
1652 if ($val > 0 && $val <= $count) {
1653 $authored{$val - 1} = !$authored{$val - 1};
1654 } elsif ($str eq '*' || $str eq '^') {
1655 my $toggle = 0;
1656 $toggle = 1 if ($str eq '*');
1657 for (my $i = 0; $i < $count; $i++) {
1658 $authored{$i} = $toggle;
1661 } elsif ($sel eq "s") {
1662 if ($val > 0 && $val <= $count) {
1663 $signed{$val - 1} = !$signed{$val - 1};
1664 } elsif ($str eq '*' || $str eq '^') {
1665 my $toggle = 0;
1666 $toggle = 1 if ($str eq '*');
1667 for (my $i = 0; $i < $count; $i++) {
1668 $signed{$i} = $toggle;
1671 } elsif ($sel eq "o") {
1672 $print_options = 1;
1673 $redraw = 1;
1674 } elsif ($sel eq "g") {
1675 if ($str eq "f") {
1676 bool_invert(\$email_git_fallback);
1677 } else {
1678 bool_invert(\$email_git);
1680 $rerun = 1;
1681 } elsif ($sel eq "b") {
1682 if ($str eq "s") {
1683 bool_invert(\$email_git_blame_signatures);
1684 } else {
1685 bool_invert(\$email_git_blame);
1687 $rerun = 1;
1688 } elsif ($sel eq "c") {
1689 if ($val > 0) {
1690 $email_git_min_signatures = $val;
1691 $rerun = 1;
1693 } elsif ($sel eq "x") {
1694 if ($val > 0) {
1695 $email_git_max_maintainers = $val;
1696 $rerun = 1;
1698 } elsif ($sel eq "%") {
1699 if ($str ne "" && $val >= 0) {
1700 $email_git_min_percent = $val;
1701 $rerun = 1;
1703 } elsif ($sel eq "d") {
1704 if (vcs_is_git()) {
1705 $email_git_since = $str;
1706 } elsif (vcs_is_hg()) {
1707 $email_hg_since = $str;
1709 $rerun = 1;
1710 } elsif ($sel eq "t") {
1711 bool_invert(\$email_git_all_signature_types);
1712 $rerun = 1;
1713 } elsif ($sel eq "f") {
1714 bool_invert(\$file_emails);
1715 $rerun = 1;
1716 } elsif ($sel eq "r") {
1717 bool_invert(\$email_remove_duplicates);
1718 $rerun = 1;
1719 } elsif ($sel eq "m") {
1720 bool_invert(\$email_use_mailmap);
1721 read_mailmap();
1722 $rerun = 1;
1723 } elsif ($sel eq "k") {
1724 bool_invert(\$keywords);
1725 $rerun = 1;
1726 } elsif ($sel eq "p") {
1727 if ($str ne "" && $val >= 0) {
1728 $pattern_depth = $val;
1729 $rerun = 1;
1731 } elsif ($sel eq "h" || $sel eq "?") {
1732 print STDERR <<EOT
1734 Interactive mode allows you to select the various maintainers, submitters,
1735 commit signers and mailing lists that could be CC'd on a patch.
1737 Any *'d entry is selected.
1739 If you have git or hg installed, you can choose to summarize the commit
1740 history of files in the patch. Also, each line of the current file can
1741 be matched to its commit author and that commits signers with blame.
1743 Various knobs exist to control the length of time for active commit
1744 tracking, the maximum number of commit authors and signers to add,
1745 and such.
1747 Enter selections at the prompt until you are satisfied that the selected
1748 maintainers are appropriate. You may enter multiple selections separated
1749 by either commas or spaces.
1752 } else {
1753 print STDERR "invalid option: '$nr'\n";
1754 $redraw = 0;
1757 if ($rerun) {
1758 print STDERR "git-blame can be very slow, please have patience..."
1759 if ($email_git_blame);
1760 goto &get_maintainers;
1764 #drop not selected entries
1765 $count = 0;
1766 my @new_emailto = ();
1767 foreach my $entry (@list) {
1768 if ($selected{$count}) {
1769 push(@new_emailto, $list[$count]);
1771 $count++;
1773 return @new_emailto;
1776 sub bool_invert {
1777 my ($bool_ref) = @_;
1779 if ($$bool_ref) {
1780 $$bool_ref = 0;
1781 } else {
1782 $$bool_ref = 1;
1786 sub deduplicate_email {
1787 my ($email) = @_;
1789 my $matched = 0;
1790 my ($name, $address) = parse_email($email);
1791 $email = format_email($name, $address, 1);
1792 $email = mailmap_email($email);
1794 return $email if (!$email_remove_duplicates);
1796 ($name, $address) = parse_email($email);
1798 if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
1799 $name = $deduplicate_name_hash{lc($name)}->[0];
1800 $address = $deduplicate_name_hash{lc($name)}->[1];
1801 $matched = 1;
1802 } elsif ($deduplicate_address_hash{lc($address)}) {
1803 $name = $deduplicate_address_hash{lc($address)}->[0];
1804 $address = $deduplicate_address_hash{lc($address)}->[1];
1805 $matched = 1;
1807 if (!$matched) {
1808 $deduplicate_name_hash{lc($name)} = [ $name, $address ];
1809 $deduplicate_address_hash{lc($address)} = [ $name, $address ];
1811 $email = format_email($name, $address, 1);
1812 $email = mailmap_email($email);
1813 return $email;
1816 sub save_commits_by_author {
1817 my (@lines) = @_;
1819 my @authors = ();
1820 my @commits = ();
1821 my @subjects = ();
1823 foreach my $line (@lines) {
1824 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1825 my $author = $1;
1826 $author = deduplicate_email($author);
1827 push(@authors, $author);
1829 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1830 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1833 for (my $i = 0; $i < @authors; $i++) {
1834 my $exists = 0;
1835 foreach my $ref(@{$commit_author_hash{$authors[$i]}}) {
1836 if (@{$ref}[0] eq $commits[$i] &&
1837 @{$ref}[1] eq $subjects[$i]) {
1838 $exists = 1;
1839 last;
1842 if (!$exists) {
1843 push(@{$commit_author_hash{$authors[$i]}},
1844 [ ($commits[$i], $subjects[$i]) ]);
1849 sub save_commits_by_signer {
1850 my (@lines) = @_;
1852 my $commit = "";
1853 my $subject = "";
1855 foreach my $line (@lines) {
1856 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1857 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1858 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
1859 my @signatures = ($line);
1860 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1861 my @types = @$types_ref;
1862 my @signers = @$signers_ref;
1864 my $type = $types[0];
1865 my $signer = $signers[0];
1867 $signer = deduplicate_email($signer);
1869 my $exists = 0;
1870 foreach my $ref(@{$commit_signer_hash{$signer}}) {
1871 if (@{$ref}[0] eq $commit &&
1872 @{$ref}[1] eq $subject &&
1873 @{$ref}[2] eq $type) {
1874 $exists = 1;
1875 last;
1878 if (!$exists) {
1879 push(@{$commit_signer_hash{$signer}},
1880 [ ($commit, $subject, $type) ]);
1886 sub vcs_assign {
1887 my ($role, $divisor, @lines) = @_;
1889 my %hash;
1890 my $count = 0;
1892 return if (@lines <= 0);
1894 if ($divisor <= 0) {
1895 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
1896 $divisor = 1;
1899 @lines = mailmap(@lines);
1901 return if (@lines <= 0);
1903 @lines = sort(@lines);
1905 # uniq -c
1906 $hash{$_}++ for @lines;
1908 # sort -rn
1909 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
1910 my $sign_offs = $hash{$line};
1911 my $percent = $sign_offs * 100 / $divisor;
1913 $percent = 100 if ($percent > 100);
1914 next if (ignore_email_address($line));
1915 $count++;
1916 last if ($sign_offs < $email_git_min_signatures ||
1917 $count > $email_git_max_maintainers ||
1918 $percent < $email_git_min_percent);
1919 push_email_address($line, '');
1920 if ($output_rolestats) {
1921 my $fmt_percent = sprintf("%.0f", $percent);
1922 add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%");
1923 } else {
1924 add_role($line, $role);
1929 sub vcs_file_signoffs {
1930 my ($file) = @_;
1932 my $authors_ref;
1933 my $signers_ref;
1934 my $stats_ref;
1935 my @authors = ();
1936 my @signers = ();
1937 my @stats = ();
1938 my $commits;
1940 $vcs_used = vcs_exists();
1941 return if (!$vcs_used);
1943 my $cmd = $VCS_cmds{"find_signers_cmd"};
1944 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
1946 ($commits, $signers_ref, $authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
1948 @signers = @{$signers_ref} if defined $signers_ref;
1949 @authors = @{$authors_ref} if defined $authors_ref;
1950 @stats = @{$stats_ref} if defined $stats_ref;
1952 # print("commits: <$commits>\nsigners:<@signers>\nauthors: <@authors>\nstats: <@stats>\n");
1954 foreach my $signer (@signers) {
1955 $signer = deduplicate_email($signer);
1958 vcs_assign("commit_signer", $commits, @signers);
1959 vcs_assign("authored", $commits, @authors);
1960 if ($#authors == $#stats) {
1961 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1962 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1964 my $added = 0;
1965 my $deleted = 0;
1966 for (my $i = 0; $i <= $#stats; $i++) {
1967 if ($stats[$i] =~ /$stat_pattern/) {
1968 $added += $1;
1969 $deleted += $2;
1972 my @tmp_authors = uniq(@authors);
1973 foreach my $author (@tmp_authors) {
1974 $author = deduplicate_email($author);
1976 @tmp_authors = uniq(@tmp_authors);
1977 my @list_added = ();
1978 my @list_deleted = ();
1979 foreach my $author (@tmp_authors) {
1980 my $auth_added = 0;
1981 my $auth_deleted = 0;
1982 for (my $i = 0; $i <= $#stats; $i++) {
1983 if ($author eq deduplicate_email($authors[$i]) &&
1984 $stats[$i] =~ /$stat_pattern/) {
1985 $auth_added += $1;
1986 $auth_deleted += $2;
1989 for (my $i = 0; $i < $auth_added; $i++) {
1990 push(@list_added, $author);
1992 for (my $i = 0; $i < $auth_deleted; $i++) {
1993 push(@list_deleted, $author);
1996 vcs_assign("added_lines", $added, @list_added);
1997 vcs_assign("removed_lines", $deleted, @list_deleted);
2001 sub vcs_file_blame {
2002 my ($file) = @_;
2004 my @signers = ();
2005 my @all_commits = ();
2006 my @commits = ();
2007 my $total_commits;
2008 my $total_lines;
2010 $vcs_used = vcs_exists();
2011 return if (!$vcs_used);
2013 @all_commits = vcs_blame($file);
2014 @commits = uniq(@all_commits);
2015 $total_commits = @commits;
2016 $total_lines = @all_commits;
2018 if ($email_git_blame_signatures) {
2019 if (vcs_is_hg()) {
2020 my $commit_count;
2021 my $commit_authors_ref;
2022 my $commit_signers_ref;
2023 my $stats_ref;
2024 my @commit_authors = ();
2025 my @commit_signers = ();
2026 my $commit = join(" -r ", @commits);
2027 my $cmd;
2029 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2030 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2032 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2033 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2034 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2036 push(@signers, @commit_signers);
2037 } else {
2038 foreach my $commit (@commits) {
2039 my $commit_count;
2040 my $commit_authors_ref;
2041 my $commit_signers_ref;
2042 my $stats_ref;
2043 my @commit_authors = ();
2044 my @commit_signers = ();
2045 my $cmd;
2047 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2048 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2050 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2051 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2052 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2054 push(@signers, @commit_signers);
2059 if ($from_filename) {
2060 if ($output_rolestats) {
2061 my @blame_signers;
2062 if (vcs_is_hg()) {{ # Double brace for last exit
2063 my $commit_count;
2064 my @commit_signers = ();
2065 @commits = uniq(@commits);
2066 @commits = sort(@commits);
2067 my $commit = join(" -r ", @commits);
2068 my $cmd;
2070 $cmd = $VCS_cmds{"find_commit_author_cmd"};
2071 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2073 my @lines = ();
2075 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
2077 if (!$email_git_penguin_chiefs) {
2078 @lines = grep(!/${penguin_chiefs}/i, @lines);
2081 last if !@lines;
2083 my @authors = ();
2084 foreach my $line (@lines) {
2085 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2086 my $author = $1;
2087 $author = deduplicate_email($author);
2088 push(@authors, $author);
2092 save_commits_by_author(@lines) if ($interactive);
2093 save_commits_by_signer(@lines) if ($interactive);
2095 push(@signers, @authors);
2097 else {
2098 foreach my $commit (@commits) {
2099 my $i;
2100 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
2101 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
2102 my @author = vcs_find_author($cmd);
2103 next if !@author;
2105 my $formatted_author = deduplicate_email($author[0]);
2107 my $count = grep(/$commit/, @all_commits);
2108 for ($i = 0; $i < $count ; $i++) {
2109 push(@blame_signers, $formatted_author);
2113 if (@blame_signers) {
2114 vcs_assign("authored lines", $total_lines, @blame_signers);
2117 foreach my $signer (@signers) {
2118 $signer = deduplicate_email($signer);
2120 vcs_assign("commits", $total_commits, @signers);
2121 } else {
2122 foreach my $signer (@signers) {
2123 $signer = deduplicate_email($signer);
2125 vcs_assign("modified commits", $total_commits, @signers);
2129 sub vcs_file_exists {
2130 my ($file) = @_;
2132 my $exists;
2134 my $vcs_used = vcs_exists();
2135 return 0 if (!$vcs_used);
2137 my $cmd = $VCS_cmds{"file_exists_cmd"};
2138 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2139 $cmd .= " 2>&1";
2140 $exists = &{$VCS_cmds{"execute_cmd"}}($cmd);
2142 return 0 if ($? != 0);
2144 return $exists;
2147 sub uniq {
2148 my (@parms) = @_;
2150 my %saw;
2151 @parms = grep(!$saw{$_}++, @parms);
2152 return @parms;
2155 sub sort_and_uniq {
2156 my (@parms) = @_;
2158 my %saw;
2159 @parms = sort @parms;
2160 @parms = grep(!$saw{$_}++, @parms);
2161 return @parms;
2164 sub clean_file_emails {
2165 my (@file_emails) = @_;
2166 my @fmt_emails = ();
2168 foreach my $email (@file_emails) {
2169 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
2170 my ($name, $address) = parse_email($email);
2171 if ($name eq '"[,\.]"') {
2172 $name = "";
2175 my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name);
2176 if (@nw > 2) {
2177 my $first = $nw[@nw - 3];
2178 my $middle = $nw[@nw - 2];
2179 my $last = $nw[@nw - 1];
2181 if (((length($first) == 1 && $first =~ m/[A-Za-z]/) ||
2182 (length($first) == 2 && substr($first, -1) eq ".")) ||
2183 (length($middle) == 1 ||
2184 (length($middle) == 2 && substr($middle, -1) eq "."))) {
2185 $name = "$first $middle $last";
2186 } else {
2187 $name = "$middle $last";
2191 if (substr($name, -1) =~ /[,\.]/) {
2192 $name = substr($name, 0, length($name) - 1);
2193 } elsif (substr($name, -2) =~ /[,\.]"/) {
2194 $name = substr($name, 0, length($name) - 2) . '"';
2197 if (substr($name, 0, 1) =~ /[,\.]/) {
2198 $name = substr($name, 1, length($name) - 1);
2199 } elsif (substr($name, 0, 2) =~ /"[,\.]/) {
2200 $name = '"' . substr($name, 2, length($name) - 2);
2203 my $fmt_email = format_email($name, $address, $email_usename);
2204 push(@fmt_emails, $fmt_email);
2206 return @fmt_emails;
2209 sub merge_email {
2210 my @lines;
2211 my %saw;
2213 for (@_) {
2214 my ($address, $role) = @$_;
2215 if (!$saw{$address}) {
2216 if ($output_roles) {
2217 push(@lines, "$address ($role)");
2218 } else {
2219 push(@lines, $address);
2221 $saw{$address} = 1;
2225 return @lines;
2228 sub output {
2229 my (@parms) = @_;
2231 if ($output_multiline) {
2232 foreach my $line (@parms) {
2233 print("${line}\n");
2235 } else {
2236 print(join($output_separator, @parms));
2237 print("\n");
2241 my $rfc822re;
2243 sub make_rfc822re {
2244 # Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
2245 # comment. We must allow for rfc822_lwsp (or comments) after each of these.
2246 # This regexp will only work on addresses which have had comments stripped
2247 # and replaced with rfc822_lwsp.
2249 my $specials = '()<>@,;:\\\\".\\[\\]';
2250 my $controls = '\\000-\\037\\177';
2252 my $dtext = "[^\\[\\]\\r\\\\]";
2253 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
2255 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
2257 # Use zero-width assertion to spot the limit of an atom. A simple
2258 # $rfc822_lwsp* causes the regexp engine to hang occasionally.
2259 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
2260 my $word = "(?:$atom|$quoted_string)";
2261 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
2263 my $sub_domain = "(?:$atom|$domain_literal)";
2264 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
2266 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
2268 my $phrase = "$word*";
2269 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
2270 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
2271 my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
2273 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
2274 my $address = "(?:$mailbox|$group)";
2276 return "$rfc822_lwsp*$address";
2279 sub rfc822_strip_comments {
2280 my $s = shift;
2281 # Recursively remove comments, and replace with a single space. The simpler
2282 # regexps in the Email Addressing FAQ are imperfect - they will miss escaped
2283 # chars in atoms, for example.
2285 while ($s =~ s/^((?:[^"\\]|\\.)*
2286 (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
2287 \((?:[^()\\]|\\.)*\)/$1 /osx) {}
2288 return $s;
2291 # valid: returns true if the parameter is an RFC822 valid address
2293 sub rfc822_valid {
2294 my $s = rfc822_strip_comments(shift);
2296 if (!$rfc822re) {
2297 $rfc822re = make_rfc822re();
2300 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
2303 # validlist: In scalar context, returns true if the parameter is an RFC822
2304 # valid list of addresses.
2306 # In list context, returns an empty list on failure (an invalid
2307 # address was found); otherwise a list whose first element is the
2308 # number of addresses found and whose remaining elements are the
2309 # addresses. This is needed to disambiguate failure (invalid)
2310 # from success with no addresses found, because an empty string is
2311 # a valid list.
2313 sub rfc822_validlist {
2314 my $s = rfc822_strip_comments(shift);
2316 if (!$rfc822re) {
2317 $rfc822re = make_rfc822re();
2319 # * null list items are valid according to the RFC
2320 # * the '1' business is to aid in distinguishing failure from no results
2322 my @r;
2323 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
2324 $s =~ m/^$rfc822_char*$/) {
2325 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
2326 push(@r, $1);
2328 return wantarray ? (scalar(@r), @r) : 1;
2330 return wantarray ? () : 0;