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