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