2 # (c) 2007, Joe Perches <joe@perches.com>
3 # created from checkpatch.pl
5 # Print selected MAINTAINERS information for
6 # the files modified in a patch or for a file
8 # usage: perl scripts/get_maintainer.pl [OPTIONS] <patch>
9 # perl scripts/get_maintainer.pl [OPTIONS] -f <file>
11 # SPDX-License-Identifier: GPL-2.0-only
19 use Getopt
::Long
qw(:config no_auto_abbrev);
22 my $cur_path = fastgetcwd
() . '/';
25 my $email_usename = 1;
26 my $email_maintainer = 1;
27 my $email_reviewer = 1;
29 my $email_subscriber_list = 0;
30 my $email_git_penguin_chiefs = 0;
32 my $email_git_all_signature_types = 0;
33 my $email_git_blame = 0;
34 my $email_git_blame_signatures = 1;
35 my $email_git_fallback = 1;
36 my $email_git_min_signatures = 1;
37 my $email_git_max_maintainers = 5;
38 my $email_git_min_percent = 5;
39 my $email_git_since = "1-year-ago";
40 my $email_hg_since = "-365";
42 my $email_remove_duplicates = 1;
43 my $email_use_mailmap = 1;
44 my $output_multiline = 1;
45 my $output_separator = ", ";
47 my $output_rolestats = 1;
48 my $output_section_maxlen = 50;
57 my $from_filename = 0;
58 my $pattern_depth = 0;
66 my %commit_author_hash;
67 my %commit_signer_hash;
69 my @penguin_chief = ();
70 push(@penguin_chief, "Linus Torvalds:torvalds\@linux-foundation.org");
71 #Andrew wants in on most everything - 2009/01/14
72 #push(@penguin_chief, "Andrew Morton:akpm\@linux-foundation.org");
74 my @penguin_chief_names = ();
75 foreach my $chief (@penguin_chief) {
76 if ($chief =~ m/^(.*):(.*)/) {
79 push(@penguin_chief_names, $chief_name);
82 my $penguin_chiefs = "\(" . join("|", @penguin_chief_names) . "\)";
84 # Signature types of people who are either
85 # a) responsible for the code in question, or
86 # b) familiar enough with it to give relevant feedback
87 my @signature_tags = ();
88 push(@signature_tags, "Signed-off-by:");
89 push(@signature_tags, "Reviewed-by:");
90 push(@signature_tags, "Acked-by:");
92 my $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
94 # rfc822 email address - preloaded methods go here.
95 my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
96 my $rfc822_char = '[\\000-\\377]';
98 # VCS command support: class-like functions and strings
103 "execute_cmd" => \
&git_execute_cmd
,
104 "available" => '(which("git") ne "") && (-e ".git")',
105 "find_signers_cmd" =>
106 "git log --no-color --follow --since=\$email_git_since " .
107 '--numstat --no-merges ' .
108 '--format="GitCommit: %H%n' .
109 'GitAuthor: %an <%ae>%n' .
114 "find_commit_signers_cmd" =>
115 "git log --no-color " .
117 '--format="GitCommit: %H%n' .
118 'GitAuthor: %an <%ae>%n' .
123 "find_commit_author_cmd" =>
124 "git log --no-color " .
126 '--format="GitCommit: %H%n' .
127 'GitAuthor: %an <%ae>%n' .
129 'GitSubject: %s%n"' .
131 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
132 "blame_file_cmd" => "git blame -l \$file",
133 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})",
134 "blame_commit_pattern" => "^([0-9a-f]+) ",
135 "author_pattern" => "^GitAuthor: (.*)",
136 "subject_pattern" => "^GitSubject: (.*)",
137 "stat_pattern" => "^(\\d+)\\t(\\d+)\\t\$file\$",
138 "file_exists_cmd" => "git ls-files \$file",
142 "execute_cmd" => \
&hg_execute_cmd
,
143 "available" => '(which("hg") ne "") && (-d ".hg")',
144 "find_signers_cmd" =>
145 "hg log --date=\$email_hg_since " .
146 "--template='HgCommit: {node}\\n" .
147 "HgAuthor: {author}\\n" .
148 "HgSubject: {desc}\\n'" .
150 "find_commit_signers_cmd" =>
152 "--template='HgSubject: {desc}\\n'" .
154 "find_commit_author_cmd" =>
156 "--template='HgCommit: {node}\\n" .
157 "HgAuthor: {author}\\n" .
158 "HgSubject: {desc|firstline}\\n'" .
160 "blame_range_cmd" => "", # not supported
161 "blame_file_cmd" => "hg blame -n \$file",
162 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})",
163 "blame_commit_pattern" => "^([ 0-9a-f]+):",
164 "author_pattern" => "^HgAuthor: (.*)",
165 "subject_pattern" => "^HgSubject: (.*)",
166 "stat_pattern" => "^(\\d+)\t(\\d+)\t\$file\$",
167 "file_exists_cmd" => "hg files \$file",
170 my $conf = which_conf
(".get_maintainer.conf");
173 open(my $conffile, '<', "$conf")
174 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
176 while (<$conffile>) {
179 $line =~ s/\s*\n?$//g;
183 next if ($line =~ m/^\s*#/);
184 next if ($line =~ m/^\s*$/);
186 my @words = split(" ", $line);
187 foreach my $word (@words) {
188 last if ($word =~ m/^#/);
189 push (@conf_args, $word);
193 unshift(@ARGV, @conf_args) if @conf_args;
196 my @ignore_emails = ();
197 my $ignore_file = which_conf
(".get_maintainer.ignore");
198 if (-f
$ignore_file) {
199 open(my $ignore, '<', "$ignore_file")
200 or warn "$P: Can't find a readable .get_maintainer.ignore file $!\n";
204 $line =~ s/\s*\n?$//;
209 next if ($line =~ m/^\s*$/);
210 if (rfc822_valid
($line)) {
211 push(@ignore_emails, $line);
219 'git!' => \
$email_git,
220 'git-all-signature-types!' => \
$email_git_all_signature_types,
221 'git-blame!' => \
$email_git_blame,
222 'git-blame-signatures!' => \
$email_git_blame_signatures,
223 'git-fallback!' => \
$email_git_fallback,
224 'git-chief-penguins!' => \
$email_git_penguin_chiefs,
225 'git-min-signatures=i' => \
$email_git_min_signatures,
226 'git-max-maintainers=i' => \
$email_git_max_maintainers,
227 'git-min-percent=i' => \
$email_git_min_percent,
228 'git-since=s' => \
$email_git_since,
229 'hg-since=s' => \
$email_hg_since,
230 'i|interactive!' => \
$interactive,
231 'remove-duplicates!' => \
$email_remove_duplicates,
232 'mailmap!' => \
$email_use_mailmap,
233 'm!' => \
$email_maintainer,
234 'r!' => \
$email_reviewer,
235 'n!' => \
$email_usename,
236 'l!' => \
$email_list,
237 's!' => \
$email_subscriber_list,
238 'multiline!' => \
$output_multiline,
239 'roles!' => \
$output_roles,
240 'rolestats!' => \
$output_rolestats,
241 'separator=s' => \
$output_separator,
242 'subsystem!' => \
$subsystem,
243 'status!' => \
$status,
246 'letters=s' => \
$letters,
247 'pattern-depth=i' => \
$pattern_depth,
248 'k|keywords!' => \
$keywords,
249 'sections!' => \
$sections,
250 'fe|file-emails!' => \
$file_emails,
251 'f|file' => \
$from_filename,
252 'v|version' => \
$version,
253 'h|help|usage' => \
$help,
255 die "$P: invalid argument - use --help if necessary\n";
264 print("${P} ${V}\n");
268 if (-t STDIN
&& !@ARGV) {
269 # We're talking to a terminal, but have no command line arguments.
270 die "$P: missing patchfile or -f file - use --help if necessary\n";
273 $output_multiline = 0 if ($output_separator ne ", ");
274 $output_rolestats = 1 if ($interactive);
275 $output_roles = 1 if ($output_rolestats);
277 if ($sections || $letters ne "") {
288 my $selections = $email + $scm + $status + $subsystem + $web;
289 if ($selections == 0) {
290 die "$P: Missing required option: email, scm, status, subsystem or web\n";
295 ($email_maintainer + $email_reviewer +
296 $email_list + $email_subscriber_list +
297 $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
298 die "$P: Please select at least 1 email option\n";
301 ## Read MAINTAINERS for type/value pairs
306 open (my $maint, '<', "${lk_path}MAINTAINERS")
307 or die "$P: Can't open MAINTAINERS: $!\n";
311 if ($line =~ m/^([A-Z]):\s*(.*)/) {
315 ##Filename pattern matching
316 if ($type eq "F" || $type eq "X") {
317 $value =~ s@\
.@
\\\
.@g; ##Convert . to \.
318 $value =~ s/\*/\.\*/g; ##Convert * to .*
319 $value =~ s/\?/\./g; ##Convert ? to .
320 ##if pattern is a directory and it lacks a trailing slash, add one
322 $value =~ s@
([^/])$@$1/@
;
324 } elsif ($type eq "K") {
325 $keyword_hash{@typevalue} = $value;
327 push(@typevalue, "$type:$value");
328 } elsif (!/^(\s)*$/) {
330 push(@typevalue, $line);
337 # Read mail address map
350 return if (!$email_use_mailmap || !(-f
"${lk_path}.mailmap"));
352 open(my $mailmap_file, '<', "${lk_path}.mailmap")
353 or warn "$P: Can't open .mailmap: $!\n";
355 while (<$mailmap_file>) {
356 s/#.*$//; #strip comments
357 s/^\s+|\s+$//g; #trim
359 next if (/^\s*$/); #skip empty lines
360 #entries have one of the following formats:
363 # name1 <mail1> <mail2>
364 # name1 <mail1> name2 <mail2>
365 # (see man git-shortlog)
367 if (/^([^<]+)<([^>]+)>$/) {
371 $real_name =~ s/\s+$//;
372 ($real_name, $address) = parse_email
("$real_name <$address>");
373 $mailmap->{names
}->{$address} = $real_name;
375 } elsif (/^<([^>]+)>\s*<([^>]+)>$/) {
376 my $real_address = $1;
377 my $wrong_address = $2;
379 $mailmap->{addresses
}->{$wrong_address} = $real_address;
381 } elsif (/^(.+)<([^>]+)>\s*<([^>]+)>$/) {
383 my $real_address = $2;
384 my $wrong_address = $3;
386 $real_name =~ s/\s+$//;
387 ($real_name, $real_address) =
388 parse_email
("$real_name <$real_address>");
389 $mailmap->{names
}->{$wrong_address} = $real_name;
390 $mailmap->{addresses
}->{$wrong_address} = $real_address;
392 } elsif (/^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$/) {
394 my $real_address = $2;
396 my $wrong_address = $4;
398 $real_name =~ s/\s+$//;
399 ($real_name, $real_address) =
400 parse_email
("$real_name <$real_address>");
402 $wrong_name =~ s/\s+$//;
403 ($wrong_name, $wrong_address) =
404 parse_email
("$wrong_name <$wrong_address>");
406 my $wrong_email = format_email
($wrong_name, $wrong_address, 1);
407 $mailmap->{names
}->{$wrong_email} = $real_name;
408 $mailmap->{addresses
}->{$wrong_email} = $real_address;
411 close($mailmap_file);
414 ## use the filenames on the command line or find the filenames in the patchfiles
418 my @keyword_tvi = ();
419 my @file_emails = ();
422 push(@ARGV, "&STDIN");
425 foreach my $file (@ARGV) {
426 if ($file ne "&STDIN") {
427 ##if $file is a directory and it lacks a trailing slash, add one
429 $file =~ s@
([^/])$@$1/@
;
430 } elsif (!(-f
$file)) {
431 die "$P: file '${file}' not found\n";
434 if ($from_filename || ($file ne "&STDIN" && vcs_file_exists
($file))) {
435 $file =~ s/^\Q${cur_path}\E//; #strip any absolute path
436 $file =~ s/^\Q${lk_path}\E//; #or the path to the lk tree
438 if ($file ne "MAINTAINERS" && -f
$file && ($keywords || $file_emails)) {
439 open(my $f, '<', $file)
440 or die "$P: Can't open $file: $!\n";
441 my $text = do { local($/) ; <$f> };
444 foreach my $line (keys %keyword_hash) {
445 if ($text =~ m/$keyword_hash{$line}/x) {
446 push(@keyword_tvi, $line);
451 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;
452 push(@file_emails, clean_file_emails
(@poss_addr));
456 my $file_cnt = @files;
459 open(my $patch, "< $file")
460 or die "$P: Can't open $file: $!\n";
462 # We can check arbitrary information before the patch
463 # like the commit message, mail headers, etc...
464 # This allows us to match arbitrary keywords against any part
465 # of a git format-patch generated file (subject tags, etc...)
467 my $patch_prefix = ""; #Parsing the intro
471 if (m/^\+\+\+\s+(\S+)/ or m/^---\s+(\S+)/) {
473 $filename =~ s@
^[^/]*/@@
;
475 $lastfile = $filename;
476 push(@files, $filename);
477 $patch_prefix = "^[+-].*"; #Now parsing the actual patch
478 } elsif (m/^\@\@ -(\d+),(\d+)/) {
479 if ($email_git_blame) {
480 push(@range, "$lastfile:$1:$2");
482 } elsif ($keywords) {
483 foreach my $line (keys %keyword_hash) {
484 if ($patch_line =~ m/${patch_prefix}$keyword_hash{$line}/x) {
485 push(@keyword_tvi, $line);
492 if ($file_cnt == @files) {
493 warn "$P: file '${file}' doesn't appear to be a patch. "
494 . "Add -f to options?\n";
496 @files = sort_and_uniq
(@files);
500 @file_emails = uniq
(@file_emails);
503 my %email_hash_address;
511 my %deduplicate_name_hash = ();
512 my %deduplicate_address_hash = ();
514 my @maintainers = get_maintainers
();
517 @maintainers = merge_email
(@maintainers);
518 output
(@maintainers);
527 @status = uniq
(@status);
532 @subsystem = uniq
(@subsystem);
543 sub ignore_email_address
{
546 foreach my $ignore (@ignore_emails) {
547 return 1 if ($ignore eq $address);
553 sub range_is_maintained
{
554 my ($start, $end) = @_;
556 for (my $i = $start; $i < $end; $i++) {
557 my $line = $typevalue[$i];
558 if ($line =~ m/^([A-Z]):\s*(.*)/) {
562 if ($value =~ /(maintain|support)/i) {
571 sub range_has_maintainer
{
572 my ($start, $end) = @_;
574 for (my $i = $start; $i < $end; $i++) {
575 my $line = $typevalue[$i];
576 if ($line =~ m/^([A-Z]):\s*(.*)/) {
587 sub get_maintainers
{
588 %email_hash_name = ();
589 %email_hash_address = ();
590 %commit_author_hash = ();
591 %commit_signer_hash = ();
599 %deduplicate_name_hash = ();
600 %deduplicate_address_hash = ();
601 if ($email_git_all_signature_types) {
602 $signature_pattern = "(.+?)[Bb][Yy]:";
604 $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
607 # Find responsible parties
609 my %exact_pattern_match_hash = ();
611 foreach my $file (@files) {
614 my $tvi = find_first_section
();
615 while ($tvi < @typevalue) {
616 my $start = find_starting_index
($tvi);
617 my $end = find_ending_index
($tvi);
621 #Do not match excluded file patterns
623 for ($i = $start; $i < $end; $i++) {
624 my $line = $typevalue[$i];
625 if ($line =~ m/^([A-Z]):\s*(.*)/) {
629 if (file_match_pattern
($file, $value)) {
638 for ($i = $start; $i < $end; $i++) {
639 my $line = $typevalue[$i];
640 if ($line =~ m/^([A-Z]):\s*(.*)/) {
644 if (file_match_pattern
($file, $value)) {
645 my $value_pd = ($value =~ tr@
/@@
);
646 my $file_pd = ($file =~ tr@
/@@
);
647 $value_pd++ if (substr($value,-1,1) ne "/");
648 $value_pd = -1 if ($value =~ /^\.\*/);
649 if ($value_pd >= $file_pd &&
650 range_is_maintained
($start, $end) &&
651 range_has_maintainer
($start, $end)) {
652 $exact_pattern_match_hash{$file} = 1;
654 if ($pattern_depth == 0 ||
655 (($file_pd - $value_pd) < $pattern_depth)) {
656 $hash{$tvi} = $value_pd;
659 } elsif ($type eq 'N') {
660 if ($file =~ m/$value/x) {
670 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
671 add_categories
($line);
674 my $start = find_starting_index
($line);
675 my $end = find_ending_index
($line);
676 for ($i = $start; $i < $end; $i++) {
677 my $line = $typevalue[$i];
678 if ($line =~ /^[FX]:/) { ##Restore file patterns
679 $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
680 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ?
681 $line =~ s/\\\./\./g; ##Convert \. to .
682 $line =~ s/\.\*/\*/g; ##Convert .* to *
684 my $count = $line =~ s/^([A-Z]):/$1:\t/g;
685 if ($letters eq "" || (!$count || $letters =~ /$1/i)) {
695 @keyword_tvi = sort_and_uniq
(@keyword_tvi);
696 foreach my $line (@keyword_tvi) {
697 add_categories
($line);
701 foreach my $email (@email_to, @list_to) {
702 $email->[0] = deduplicate_email
($email->[0]);
705 foreach my $file (@files) {
707 ($email_git || ($email_git_fallback &&
708 !$exact_pattern_match_hash{$file}))) {
709 vcs_file_signoffs
($file);
711 if ($email && $email_git_blame) {
712 vcs_file_blame
($file);
717 foreach my $chief (@penguin_chief) {
718 if ($chief =~ m/^(.*):(.*)/) {
721 $email_address = format_email
($1, $2, $email_usename);
722 if ($email_git_penguin_chiefs) {
723 push(@email_to, [$email_address, 'chief penguin']);
725 @email_to = grep($_->[0] !~ /${email_address}/, @email_to);
730 foreach my $email (@file_emails) {
731 my ($name, $address) = parse_email
($email);
733 my $tmp_email = format_email
($name, $address, $email_usename);
734 push_email_address
($tmp_email, '');
735 add_role
($tmp_email, 'in file');
740 if ($email || $email_list) {
742 @to = (@to, @email_to);
745 @to = (@to, @list_to);
750 @to = interactive_get_maintainers
(\
@to);
756 sub file_match_pattern
{
757 my ($file, $pattern) = @_;
758 if (substr($pattern, -1) eq "/") {
759 if ($file =~ m@
^$pattern@
) {
763 if ($file =~ m@
^$pattern@
) {
764 my $s1 = ($file =~ tr@
/@@
);
765 my $s2 = ($pattern =~ tr@
/@@
);
776 usage: $P [options] patchfile
777 $P [options] -f file|directory
780 MAINTAINER field selection options:
781 --email => print email address(es) if any
782 --git => include recent git \*-by: signers
783 --git-all-signature-types => include signers regardless of signature type
784 or use only ${signature_pattern} signers (default: $email_git_all_signature_types)
785 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
786 --git-chief-penguins => include ${penguin_chiefs}
787 --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
788 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
789 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
790 --git-blame => use git blame to find modified commits for patch or file
791 --git-blame-signatures => when used with --git-blame, also include all commit signers
792 --git-since => git history to use (default: $email_git_since)
793 --hg-since => hg history to use (default: $email_hg_since)
794 --interactive => display a menu (mostly useful if used with the --git option)
795 --m => include maintainer(s) if any
796 --r => include reviewer(s) if any
797 --n => include name 'Full Name <addr\@domain.tld>'
798 --l => include list(s) if any
799 --s => include subscriber only list(s) if any
800 --remove-duplicates => minimize duplicate email names/addresses
801 --roles => show roles (status:subsystem, git-signer, list, etc...)
802 --rolestats => show roles and statistics (commits/total_commits, %)
803 --file-emails => add email addresses found in -f file (default: 0 (off))
804 --scm => print SCM tree(s) if any
805 --status => print status if any
806 --subsystem => print subsystem name if any
807 --web => print website(s) if any
810 --separator [, ] => separator for multiple entries on 1 line
811 using --separator also sets --nomultiline if --separator is not [, ]
812 --multiline => print 1 entry per line
815 --pattern-depth => Number of pattern directory traversals (default: 0 (all))
816 --keywords => scan patch for keywords (default: $keywords)
817 --sections => print all of the subsystem sections with pattern matches
818 --letters => print all matching 'letter' types from all matching sections
819 --mailmap => use .mailmap file (default: $email_use_mailmap)
820 --version => show version
821 --help => show this help information
824 [--email --nogit --git-fallback --m --r --n --l --multiline --pattern-depth=0
825 --remove-duplicates --rolestats]
828 Using "-f directory" may give unexpected results:
829 Used with "--git", git signators for _all_ files in and below
830 directory are examined as git recurses directories.
831 Any specified X: (exclude) pattern matches are _not_ ignored.
832 Used with "--nogit", directory is used as a pattern match,
833 no individual file within the directory or subdirectory
835 Used with "--git-blame", does not iterate all files in directory
836 Using "--git-blame" is slow and may add old committers and authors
837 that are no longer active maintainers to the output.
838 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
839 other automated tools that expect only ["name"] <email address>
840 may not work because of additional output after <email address>.
841 Using "--rolestats" and "--git-blame" shows the #/total=% commits,
842 not the percentage of the entire file authored. # of commits is
843 not a good measure of amount of code authored. 1 major commit may
844 contain a thousand lines, 5 trivial commits may modify a single line.
845 If git is not installed, but mercurial (hg) is installed and an .hg
846 repository exists, the following options apply to mercurial:
848 --git-min-signatures, --git-max-maintainers, --git-min-percent, and
850 Use --hg-since not --git-since to control date selection
851 File ".get_maintainer.conf", if it exists in the linux kernel source root
852 directory, can change whatever get_maintainer defaults are desired.
853 Entries in this file can be any command line argument.
854 This file is prepended to any additional command line arguments.
855 Multiple lines and # comments are allowed.
856 Most options have both positive and negative forms.
857 The negative forms for --<foo> are --no<foo> and --no-<foo>.
862 sub top_of_kernel_tree
{
865 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
868 if ( (-f
"${lk_path}COPYING")
869 && (-f
"${lk_path}CREDITS")
870 && (-f
"${lk_path}Kbuild")
871 && (-f
"${lk_path}MAINTAINERS")
872 && (-f
"${lk_path}Makefile")
873 && (-f
"${lk_path}README")
874 && (-d
"${lk_path}Documentation")
875 && (-d
"${lk_path}arch")
876 && (-d
"${lk_path}include")
877 && (-d
"${lk_path}drivers")
878 && (-d
"${lk_path}fs")
879 && (-d
"${lk_path}init")
880 && (-d
"${lk_path}ipc")
881 && (-d
"${lk_path}kernel")
882 && (-d
"${lk_path}lib")
883 && (-d
"${lk_path}scripts")) {
890 my ($formatted_email) = @_;
895 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
898 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
900 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
904 $name =~ s/^\s+|\s+$//g;
905 $name =~ s/^\"|\"$//g;
906 $address =~ s/^\s+|\s+$//g;
908 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
909 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
913 return ($name, $address);
917 my ($name, $address, $usename) = @_;
921 $name =~ s/^\s+|\s+$//g;
922 $name =~ s/^\"|\"$//g;
923 $address =~ s/^\s+|\s+$//g;
925 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
926 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
932 $formatted_email = "$address";
934 $formatted_email = "$name <$address>";
937 $formatted_email = $address;
940 return $formatted_email;
943 sub find_first_section
{
946 while ($index < @typevalue) {
947 my $tv = $typevalue[$index];
948 if (($tv =~ m/^([A-Z]):\s*(.*)/)) {
957 sub find_starting_index
{
961 my $tv = $typevalue[$index];
962 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
971 sub find_ending_index
{
974 while ($index < @typevalue) {
975 my $tv = $typevalue[$index];
976 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
985 sub get_subsystem_name
{
988 my $start = find_starting_index
($index);
990 my $subsystem = $typevalue[$start];
991 if ($output_section_maxlen && length($subsystem) > $output_section_maxlen) {
992 $subsystem = substr($subsystem, 0, $output_section_maxlen - 3);
993 $subsystem =~ s/\s*$//;
994 $subsystem = $subsystem . "...";
999 sub get_maintainer_role
{
1003 my $start = find_starting_index
($index);
1004 my $end = find_ending_index
($index);
1006 my $role = "unknown";
1007 my $subsystem = get_subsystem_name
($index);
1009 for ($i = $start + 1; $i < $end; $i++) {
1010 my $tv = $typevalue[$i];
1011 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1014 if ($ptype eq "S") {
1021 if ($role eq "supported") {
1022 $role = "supporter";
1023 } elsif ($role eq "maintained") {
1024 $role = "maintainer";
1025 } elsif ($role eq "odd fixes") {
1026 $role = "odd fixer";
1027 } elsif ($role eq "orphan") {
1028 $role = "orphan minder";
1029 } elsif ($role eq "obsolete") {
1030 $role = "obsolete minder";
1031 } elsif ($role eq "buried alive in reporters") {
1032 $role = "chief penguin";
1035 return $role . ":" . $subsystem;
1041 my $subsystem = get_subsystem_name
($index);
1043 if ($subsystem eq "THE REST") {
1050 sub add_categories
{
1054 my $start = find_starting_index
($index);
1055 my $end = find_ending_index
($index);
1057 push(@subsystem, $typevalue[$start]);
1059 for ($i = $start + 1; $i < $end; $i++) {
1060 my $tv = $typevalue[$i];
1061 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1064 if ($ptype eq "L") {
1065 my $list_address = $pvalue;
1066 my $list_additional = "";
1067 my $list_role = get_list_role
($i);
1069 if ($list_role ne "") {
1070 $list_role = ":" . $list_role;
1072 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
1074 $list_additional = $2;
1076 if ($list_additional =~ m/subscribers-only/) {
1077 if ($email_subscriber_list) {
1078 if (!$hash_list_to{lc($list_address)}) {
1079 $hash_list_to{lc($list_address)} = 1;
1080 push(@list_to, [$list_address,
1081 "subscriber list${list_role}"]);
1086 if (!$hash_list_to{lc($list_address)}) {
1087 $hash_list_to{lc($list_address)} = 1;
1088 if ($list_additional =~ m/moderated/) {
1089 push(@list_to, [$list_address,
1090 "moderated list${list_role}"]);
1092 push(@list_to, [$list_address,
1093 "open list${list_role}"]);
1098 } elsif ($ptype eq "M") {
1099 my ($name, $address) = parse_email
($pvalue);
1102 my $tv = $typevalue[$i - 1];
1103 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1106 $pvalue = format_email
($name, $address, $email_usename);
1111 if ($email_maintainer) {
1112 my $role = get_maintainer_role
($i);
1113 push_email_addresses
($pvalue, $role);
1115 } elsif ($ptype eq "R") {
1116 my ($name, $address) = parse_email
($pvalue);
1119 my $tv = $typevalue[$i - 1];
1120 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1123 $pvalue = format_email
($name, $address, $email_usename);
1128 if ($email_reviewer) {
1129 my $subsystem = get_subsystem_name
($i);
1130 push_email_addresses
($pvalue, "reviewer:$subsystem");
1132 } elsif ($ptype eq "T") {
1133 push(@scm, $pvalue);
1134 } elsif ($ptype eq "W") {
1135 push(@web, $pvalue);
1136 } elsif ($ptype eq "S") {
1137 push(@status, $pvalue);
1144 my ($name, $address) = @_;
1146 return 1 if (($name eq "") && ($address eq ""));
1147 return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
1148 return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
1153 sub push_email_address
{
1154 my ($line, $role) = @_;
1156 my ($name, $address) = parse_email
($line);
1158 if ($address eq "") {
1162 if (!$email_remove_duplicates) {
1163 push(@email_to, [format_email
($name, $address, $email_usename), $role]);
1164 } elsif (!email_inuse
($name, $address)) {
1165 push(@email_to, [format_email
($name, $address, $email_usename), $role]);
1166 $email_hash_name{lc($name)}++ if ($name ne "");
1167 $email_hash_address{lc($address)}++;
1173 sub push_email_addresses
{
1174 my ($address, $role) = @_;
1176 my @address_list = ();
1178 if (rfc822_valid
($address)) {
1179 push_email_address
($address, $role);
1180 } elsif (@address_list = rfc822_validlist
($address)) {
1181 my $array_count = shift(@address_list);
1182 while (my $entry = shift(@address_list)) {
1183 push_email_address
($entry, $role);
1186 if (!push_email_address
($address, $role)) {
1187 warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1193 my ($line, $role) = @_;
1195 my ($name, $address) = parse_email
($line);
1196 my $email = format_email
($name, $address, $email_usename);
1198 foreach my $entry (@email_to) {
1199 if ($email_remove_duplicates) {
1200 my ($entry_name, $entry_address) = parse_email
($entry->[0]);
1201 if (($name eq $entry_name || $address eq $entry_address)
1202 && ($role eq "" || !($entry->[1] =~ m/$role/))
1204 if ($entry->[1] eq "") {
1205 $entry->[1] = "$role";
1207 $entry->[1] = "$entry->[1],$role";
1211 if ($email eq $entry->[0]
1212 && ($role eq "" || !($entry->[1] =~ m/$role/))
1214 if ($entry->[1] eq "") {
1215 $entry->[1] = "$role";
1217 $entry->[1] = "$entry->[1],$role";
1227 foreach my $path (split(/:/, $ENV{PATH
})) {
1228 if (-e
"$path/$bin") {
1229 return "$path/$bin";
1239 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1240 if (-e
"$path/$conf") {
1241 return "$path/$conf";
1251 my ($name, $address) = parse_email
($line);
1252 my $email = format_email
($name, $address, 1);
1253 my $real_name = $name;
1254 my $real_address = $address;
1256 if (exists $mailmap->{names
}->{$email} ||
1257 exists $mailmap->{addresses
}->{$email}) {
1258 if (exists $mailmap->{names
}->{$email}) {
1259 $real_name = $mailmap->{names
}->{$email};
1261 if (exists $mailmap->{addresses
}->{$email}) {
1262 $real_address = $mailmap->{addresses
}->{$email};
1265 if (exists $mailmap->{names
}->{$address}) {
1266 $real_name = $mailmap->{names
}->{$address};
1268 if (exists $mailmap->{addresses
}->{$address}) {
1269 $real_address = $mailmap->{addresses
}->{$address};
1272 return format_email
($real_name, $real_address, 1);
1276 my (@addresses) = @_;
1278 my @mapped_emails = ();
1279 foreach my $line (@addresses) {
1280 push(@mapped_emails, mailmap_email
($line));
1282 merge_by_realname
(@mapped_emails) if ($email_use_mailmap);
1283 return @mapped_emails;
1286 sub merge_by_realname
{
1290 foreach my $email (@emails) {
1291 my ($name, $address) = parse_email
($email);
1292 if (exists $address_map{$name}) {
1293 $address = $address_map{$name};
1294 $email = format_email
($name, $address, 1);
1296 $address_map{$name} = $address;
1301 sub git_execute_cmd
{
1305 my $output = `$cmd`;
1306 $output =~ s/^\s*//gm;
1307 @lines = split("\n", $output);
1312 sub hg_execute_cmd
{
1316 my $output = `$cmd`;
1317 @lines = split("\n", $output);
1322 sub extract_formatted_signatures
{
1323 my (@signature_lines) = @_;
1325 my @type = @signature_lines;
1327 s/\s*(.*):.*/$1/ for (@type);
1330 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1332 ## Reformat email addresses (with names) to avoid badly written signatures
1334 foreach my $signer (@signature_lines) {
1335 $signer = deduplicate_email
($signer);
1338 return (\
@type, \
@signature_lines);
1341 sub vcs_find_signers
{
1342 my ($cmd, $file) = @_;
1345 my @signatures = ();
1349 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1351 my $pattern = $VCS_cmds{"commit_pattern"};
1352 my $author_pattern = $VCS_cmds{"author_pattern"};
1353 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1355 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1357 $commits = grep(/$pattern/, @lines); # of commits
1359 @authors = grep(/$author_pattern/, @lines);
1360 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1361 @stats = grep(/$stat_pattern/, @lines);
1363 # print("stats: <@stats>\n");
1365 return (0, \
@signatures, \
@authors, \
@stats) if !@signatures;
1367 save_commits_by_author
(@lines) if ($interactive);
1368 save_commits_by_signer
(@lines) if ($interactive);
1370 if (!$email_git_penguin_chiefs) {
1371 @signatures = grep(!/${penguin_chiefs}/i, @signatures);
1374 my ($author_ref, $authors_ref) = extract_formatted_signatures
(@authors);
1375 my ($types_ref, $signers_ref) = extract_formatted_signatures
(@signatures);
1377 return ($commits, $signers_ref, $authors_ref, \
@stats);
1380 sub vcs_find_author
{
1384 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1386 if (!$email_git_penguin_chiefs) {
1387 @lines = grep(!/${penguin_chiefs}/i, @lines);
1390 return @lines if !@lines;
1393 foreach my $line (@lines) {
1394 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1396 my ($name, $address) = parse_email
($author);
1397 $author = format_email
($name, $address, 1);
1398 push(@authors, $author);
1402 save_commits_by_author
(@lines) if ($interactive);
1403 save_commits_by_signer
(@lines) if ($interactive);
1408 sub vcs_save_commits
{
1413 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1415 foreach my $line (@lines) {
1416 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1429 return @commits if (!(-f
$file));
1431 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1432 my @all_commits = ();
1434 $cmd = $VCS_cmds{"blame_file_cmd"};
1435 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1436 @all_commits = vcs_save_commits
($cmd);
1438 foreach my $file_range_diff (@range) {
1439 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1441 my $diff_start = $2;
1442 my $diff_length = $3;
1443 next if ("$file" ne "$diff_file");
1444 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1445 push(@commits, $all_commits[$i]);
1449 foreach my $file_range_diff (@range) {
1450 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1452 my $diff_start = $2;
1453 my $diff_length = $3;
1454 next if ("$file" ne "$diff_file");
1455 $cmd = $VCS_cmds{"blame_range_cmd"};
1456 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1457 push(@commits, vcs_save_commits
($cmd));
1460 $cmd = $VCS_cmds{"blame_file_cmd"};
1461 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1462 @commits = vcs_save_commits
($cmd);
1465 foreach my $commit (@commits) {
1466 $commit =~ s/^\^//g;
1472 my $printed_novcs = 0;
1474 %VCS_cmds = %VCS_cmds_git;
1475 return 1 if eval $VCS_cmds{"available"};
1476 %VCS_cmds = %VCS_cmds_hg;
1477 return 2 if eval $VCS_cmds{"available"};
1479 if (!$printed_novcs) {
1480 warn("$P: No supported VCS found. Add --nogit to options?\n");
1481 warn("Using a git repository produces better results.\n");
1482 warn("Try Linus Torvalds' latest git repository using:\n");
1483 warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n");
1491 return $vcs_used == 1;
1495 return $vcs_used == 2;
1498 sub interactive_get_maintainers
{
1499 my ($list_ref) = @_;
1500 my @list = @
$list_ref;
1509 foreach my $entry (@list) {
1510 $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
1511 $selected{$count} = 1;
1512 $authored{$count} = 0;
1513 $signed{$count} = 0;
1519 my $print_options = 0;
1524 printf STDERR
"\n%1s %2s %-65s",
1525 "*", "#", "email/list and role:stats";
1527 ($email_git_fallback && !$maintained) ||
1529 print STDERR
"auth sign";
1532 foreach my $entry (@list) {
1533 my $email = $entry->[0];
1534 my $role = $entry->[1];
1536 $sel = "*" if ($selected{$count});
1537 my $commit_author = $commit_author_hash{$email};
1538 my $commit_signer = $commit_signer_hash{$email};
1541 $authored++ for (@
{$commit_author});
1542 $signed++ for (@
{$commit_signer});
1543 printf STDERR
"%1s %2d %-65s", $sel, $count + 1, $email;
1544 printf STDERR
"%4d %4d", $authored, $signed
1545 if ($authored > 0 || $signed > 0);
1546 printf STDERR
"\n %s\n", $role;
1547 if ($authored{$count}) {
1548 my $commit_author = $commit_author_hash{$email};
1549 foreach my $ref (@
{$commit_author}) {
1550 print STDERR
" Author: @{$ref}[1]\n";
1553 if ($signed{$count}) {
1554 my $commit_signer = $commit_signer_hash{$email};
1555 foreach my $ref (@
{$commit_signer}) {
1556 print STDERR
" @{$ref}[2]: @{$ref}[1]\n";
1563 my $date_ref = \
$email_git_since;
1564 $date_ref = \
$email_hg_since if (vcs_is_hg
());
1565 if ($print_options) {
1570 Version Control options:
1571 g use git history [$email_git]
1572 gf use git-fallback [$email_git_fallback]
1573 b use git blame [$email_git_blame]
1574 bs use blame signatures [$email_git_blame_signatures]
1575 c# minimum commits [$email_git_min_signatures]
1576 %# min percent [$email_git_min_percent]
1577 d# history to use [$$date_ref]
1578 x# max maintainers [$email_git_max_maintainers]
1579 t all signature types [$email_git_all_signature_types]
1580 m use .mailmap [$email_use_mailmap]
1587 tm toggle maintainers
1588 tg toggle git entries
1589 tl toggle open list entries
1590 ts toggle subscriber list entries
1591 f emails in file [$file_emails]
1592 k keywords in file [$keywords]
1593 r remove duplicates [$email_remove_duplicates]
1594 p# pattern match depth [$pattern_depth]
1598 "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1600 my $input = <STDIN
>;
1605 my @wish = split(/[, ]+/, $input);
1606 foreach my $nr (@wish) {
1608 my $sel = substr($nr, 0, 1);
1609 my $str = substr($nr, 1);
1611 $val = $1 if $str =~ /^(\d+)$/;
1616 $output_rolestats = 0;
1619 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1620 $selected{$nr - 1} = !$selected{$nr - 1};
1621 } elsif ($sel eq "*" || $sel eq '^') {
1623 $toggle = 1 if ($sel eq '*');
1624 for (my $i = 0; $i < $count; $i++) {
1625 $selected{$i} = $toggle;
1627 } elsif ($sel eq "0") {
1628 for (my $i = 0; $i < $count; $i++) {
1629 $selected{$i} = !$selected{$i};
1631 } elsif ($sel eq "t") {
1632 if (lc($str) eq "m") {
1633 for (my $i = 0; $i < $count; $i++) {
1634 $selected{$i} = !$selected{$i}
1635 if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
1637 } elsif (lc($str) eq "g") {
1638 for (my $i = 0; $i < $count; $i++) {
1639 $selected{$i} = !$selected{$i}
1640 if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
1642 } elsif (lc($str) eq "l") {
1643 for (my $i = 0; $i < $count; $i++) {
1644 $selected{$i} = !$selected{$i}
1645 if ($list[$i]->[1] =~ /^(open list)/i);
1647 } elsif (lc($str) eq "s") {
1648 for (my $i = 0; $i < $count; $i++) {
1649 $selected{$i} = !$selected{$i}
1650 if ($list[$i]->[1] =~ /^(subscriber list)/i);
1653 } elsif ($sel eq "a") {
1654 if ($val > 0 && $val <= $count) {
1655 $authored{$val - 1} = !$authored{$val - 1};
1656 } elsif ($str eq '*' || $str eq '^') {
1658 $toggle = 1 if ($str eq '*');
1659 for (my $i = 0; $i < $count; $i++) {
1660 $authored{$i} = $toggle;
1663 } elsif ($sel eq "s") {
1664 if ($val > 0 && $val <= $count) {
1665 $signed{$val - 1} = !$signed{$val - 1};
1666 } elsif ($str eq '*' || $str eq '^') {
1668 $toggle = 1 if ($str eq '*');
1669 for (my $i = 0; $i < $count; $i++) {
1670 $signed{$i} = $toggle;
1673 } elsif ($sel eq "o") {
1676 } elsif ($sel eq "g") {
1678 bool_invert
(\
$email_git_fallback);
1680 bool_invert
(\
$email_git);
1683 } elsif ($sel eq "b") {
1685 bool_invert
(\
$email_git_blame_signatures);
1687 bool_invert
(\
$email_git_blame);
1690 } elsif ($sel eq "c") {
1692 $email_git_min_signatures = $val;
1695 } elsif ($sel eq "x") {
1697 $email_git_max_maintainers = $val;
1700 } elsif ($sel eq "%") {
1701 if ($str ne "" && $val >= 0) {
1702 $email_git_min_percent = $val;
1705 } elsif ($sel eq "d") {
1707 $email_git_since = $str;
1708 } elsif (vcs_is_hg
()) {
1709 $email_hg_since = $str;
1712 } elsif ($sel eq "t") {
1713 bool_invert
(\
$email_git_all_signature_types);
1715 } elsif ($sel eq "f") {
1716 bool_invert
(\
$file_emails);
1718 } elsif ($sel eq "r") {
1719 bool_invert
(\
$email_remove_duplicates);
1721 } elsif ($sel eq "m") {
1722 bool_invert
(\
$email_use_mailmap);
1725 } elsif ($sel eq "k") {
1726 bool_invert
(\
$keywords);
1728 } elsif ($sel eq "p") {
1729 if ($str ne "" && $val >= 0) {
1730 $pattern_depth = $val;
1733 } elsif ($sel eq "h" || $sel eq "?") {
1736 Interactive mode allows you to select the various maintainers, submitters,
1737 commit signers and mailing lists that could be CC'd on a patch.
1739 Any *'d entry is selected.
1741 If you have git or hg installed, you can choose to summarize the commit
1742 history of files in the patch. Also, each line of the current file can
1743 be matched to its commit author and that commits signers with blame.
1745 Various knobs exist to control the length of time for active commit
1746 tracking, the maximum number of commit authors and signers to add,
1749 Enter selections at the prompt until you are satisfied that the selected
1750 maintainers are appropriate. You may enter multiple selections separated
1751 by either commas or spaces.
1755 print STDERR
"invalid option: '$nr'\n";
1760 print STDERR
"git-blame can be very slow, please have patience..."
1761 if ($email_git_blame);
1762 goto &get_maintainers
;
1766 #drop not selected entries
1768 my @new_emailto = ();
1769 foreach my $entry (@list) {
1770 if ($selected{$count}) {
1771 push(@new_emailto, $list[$count]);
1775 return @new_emailto;
1779 my ($bool_ref) = @_;
1788 sub deduplicate_email
{
1792 my ($name, $address) = parse_email
($email);
1793 $email = format_email
($name, $address, 1);
1794 $email = mailmap_email
($email);
1796 return $email if (!$email_remove_duplicates);
1798 ($name, $address) = parse_email
($email);
1800 if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
1801 $name = $deduplicate_name_hash{lc($name)}->[0];
1802 $address = $deduplicate_name_hash{lc($name)}->[1];
1804 } elsif ($deduplicate_address_hash{lc($address)}) {
1805 $name = $deduplicate_address_hash{lc($address)}->[0];
1806 $address = $deduplicate_address_hash{lc($address)}->[1];
1810 $deduplicate_name_hash{lc($name)} = [ $name, $address ];
1811 $deduplicate_address_hash{lc($address)} = [ $name, $address ];
1813 $email = format_email
($name, $address, 1);
1814 $email = mailmap_email
($email);
1818 sub save_commits_by_author
{
1825 foreach my $line (@lines) {
1826 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1828 $author = deduplicate_email
($author);
1829 push(@authors, $author);
1831 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1832 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1835 for (my $i = 0; $i < @authors; $i++) {
1837 foreach my $ref(@
{$commit_author_hash{$authors[$i]}}) {
1838 if (@
{$ref}[0] eq $commits[$i] &&
1839 @
{$ref}[1] eq $subjects[$i]) {
1845 push(@
{$commit_author_hash{$authors[$i]}},
1846 [ ($commits[$i], $subjects[$i]) ]);
1851 sub save_commits_by_signer
{
1857 foreach my $line (@lines) {
1858 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1859 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1860 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
1861 my @signatures = ($line);
1862 my ($types_ref, $signers_ref) = extract_formatted_signatures
(@signatures);
1863 my @types = @
$types_ref;
1864 my @signers = @
$signers_ref;
1866 my $type = $types[0];
1867 my $signer = $signers[0];
1869 $signer = deduplicate_email
($signer);
1872 foreach my $ref(@
{$commit_signer_hash{$signer}}) {
1873 if (@
{$ref}[0] eq $commit &&
1874 @
{$ref}[1] eq $subject &&
1875 @
{$ref}[2] eq $type) {
1881 push(@
{$commit_signer_hash{$signer}},
1882 [ ($commit, $subject, $type) ]);
1889 my ($role, $divisor, @lines) = @_;
1894 return if (@lines <= 0);
1896 if ($divisor <= 0) {
1897 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
1901 @lines = mailmap
(@lines);
1903 return if (@lines <= 0);
1905 @lines = sort(@lines);
1908 $hash{$_}++ for @lines;
1911 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
1912 my $sign_offs = $hash{$line};
1913 my $percent = $sign_offs * 100 / $divisor;
1915 $percent = 100 if ($percent > 100);
1916 next if (ignore_email_address
($line));
1918 last if ($sign_offs < $email_git_min_signatures ||
1919 $count > $email_git_max_maintainers ||
1920 $percent < $email_git_min_percent);
1921 push_email_address
($line, '');
1922 if ($output_rolestats) {
1923 my $fmt_percent = sprintf("%.0f", $percent);
1924 add_role
($line, "$role:$sign_offs/$divisor=$fmt_percent%");
1926 add_role
($line, $role);
1931 sub vcs_file_signoffs
{
1942 $vcs_used = vcs_exists
();
1943 return if (!$vcs_used);
1945 my $cmd = $VCS_cmds{"find_signers_cmd"};
1946 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
1948 ($commits, $signers_ref, $authors_ref, $stats_ref) = vcs_find_signers
($cmd, $file);
1950 @signers = @
{$signers_ref} if defined $signers_ref;
1951 @authors = @
{$authors_ref} if defined $authors_ref;
1952 @stats = @
{$stats_ref} if defined $stats_ref;
1954 # print("commits: <$commits>\nsigners:<@signers>\nauthors: <@authors>\nstats: <@stats>\n");
1956 foreach my $signer (@signers) {
1957 $signer = deduplicate_email
($signer);
1960 vcs_assign
("commit_signer", $commits, @signers);
1961 vcs_assign
("authored", $commits, @authors);
1962 if ($#authors == $#stats) {
1963 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1964 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1968 for (my $i = 0; $i <= $#stats; $i++) {
1969 if ($stats[$i] =~ /$stat_pattern/) {
1974 my @tmp_authors = uniq
(@authors);
1975 foreach my $author (@tmp_authors) {
1976 $author = deduplicate_email
($author);
1978 @tmp_authors = uniq
(@tmp_authors);
1979 my @list_added = ();
1980 my @list_deleted = ();
1981 foreach my $author (@tmp_authors) {
1983 my $auth_deleted = 0;
1984 for (my $i = 0; $i <= $#stats; $i++) {
1985 if ($author eq deduplicate_email
($authors[$i]) &&
1986 $stats[$i] =~ /$stat_pattern/) {
1988 $auth_deleted += $2;
1991 for (my $i = 0; $i < $auth_added; $i++) {
1992 push(@list_added, $author);
1994 for (my $i = 0; $i < $auth_deleted; $i++) {
1995 push(@list_deleted, $author);
1998 vcs_assign
("added_lines", $added, @list_added);
1999 vcs_assign
("removed_lines", $deleted, @list_deleted);
2003 sub vcs_file_blame
{
2007 my @all_commits = ();
2012 $vcs_used = vcs_exists
();
2013 return if (!$vcs_used);
2015 @all_commits = vcs_blame
($file);
2016 @commits = uniq
(@all_commits);
2017 $total_commits = @commits;
2018 $total_lines = @all_commits;
2020 if ($email_git_blame_signatures) {
2023 my $commit_authors_ref;
2024 my $commit_signers_ref;
2026 my @commit_authors = ();
2027 my @commit_signers = ();
2028 my $commit = join(" -r ", @commits);
2031 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2032 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2034 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers
($cmd, $file);
2035 @commit_authors = @
{$commit_authors_ref} if defined $commit_authors_ref;
2036 @commit_signers = @
{$commit_signers_ref} if defined $commit_signers_ref;
2038 push(@signers, @commit_signers);
2040 foreach my $commit (@commits) {
2042 my $commit_authors_ref;
2043 my $commit_signers_ref;
2045 my @commit_authors = ();
2046 my @commit_signers = ();
2049 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2050 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2052 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers
($cmd, $file);
2053 @commit_authors = @
{$commit_authors_ref} if defined $commit_authors_ref;
2054 @commit_signers = @
{$commit_signers_ref} if defined $commit_signers_ref;
2056 push(@signers, @commit_signers);
2061 if ($from_filename) {
2062 if ($output_rolestats) {
2064 if (vcs_is_hg
()) {{ # Double brace for last exit
2066 my @commit_signers = ();
2067 @commits = uniq
(@commits);
2068 @commits = sort(@commits);
2069 my $commit = join(" -r ", @commits);
2072 $cmd = $VCS_cmds{"find_commit_author_cmd"};
2073 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2077 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
2079 if (!$email_git_penguin_chiefs) {
2080 @lines = grep(!/${penguin_chiefs}/i, @lines);
2086 foreach my $line (@lines) {
2087 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2089 $author = deduplicate_email
($author);
2090 push(@authors, $author);
2094 save_commits_by_author
(@lines) if ($interactive);
2095 save_commits_by_signer
(@lines) if ($interactive);
2097 push(@signers, @authors);
2100 foreach my $commit (@commits) {
2102 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
2103 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
2104 my @author = vcs_find_author
($cmd);
2107 my $formatted_author = deduplicate_email
($author[0]);
2109 my $count = grep(/$commit/, @all_commits);
2110 for ($i = 0; $i < $count ; $i++) {
2111 push(@blame_signers, $formatted_author);
2115 if (@blame_signers) {
2116 vcs_assign
("authored lines", $total_lines, @blame_signers);
2119 foreach my $signer (@signers) {
2120 $signer = deduplicate_email
($signer);
2122 vcs_assign
("commits", $total_commits, @signers);
2124 foreach my $signer (@signers) {
2125 $signer = deduplicate_email
($signer);
2127 vcs_assign
("modified commits", $total_commits, @signers);
2131 sub vcs_file_exists
{
2136 my $vcs_used = vcs_exists
();
2137 return 0 if (!$vcs_used);
2139 my $cmd = $VCS_cmds{"file_exists_cmd"};
2140 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2142 $exists = &{$VCS_cmds{"execute_cmd"}}($cmd);
2144 return 0 if ($?
!= 0);
2153 @parms = grep(!$saw{$_}++, @parms);
2161 @parms = sort @parms;
2162 @parms = grep(!$saw{$_}++, @parms);
2166 sub clean_file_emails
{
2167 my (@file_emails) = @_;
2168 my @fmt_emails = ();
2170 foreach my $email (@file_emails) {
2171 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
2172 my ($name, $address) = parse_email
($email);
2173 if ($name eq '"[,\.]"') {
2177 my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name);
2179 my $first = $nw[@nw - 3];
2180 my $middle = $nw[@nw - 2];
2181 my $last = $nw[@nw - 1];
2183 if (((length($first) == 1 && $first =~ m/[A-Za-z]/) ||
2184 (length($first) == 2 && substr($first, -1) eq ".")) ||
2185 (length($middle) == 1 ||
2186 (length($middle) == 2 && substr($middle, -1) eq "."))) {
2187 $name = "$first $middle $last";
2189 $name = "$middle $last";
2193 if (substr($name, -1) =~ /[,\.]/) {
2194 $name = substr($name, 0, length($name) - 1);
2195 } elsif (substr($name, -2) =~ /[,\.]"/) {
2196 $name = substr($name, 0, length($name) - 2) . '"';
2199 if (substr($name, 0, 1) =~ /[,\.]/) {
2200 $name = substr($name, 1, length($name) - 1);
2201 } elsif (substr($name, 0, 2) =~ /"[,\.]/) {
2202 $name = '"' . substr($name, 2, length($name) - 2);
2205 my $fmt_email = format_email
($name, $address, $email_usename);
2206 push(@fmt_emails, $fmt_email);
2216 my ($address, $role) = @
$_;
2217 if (!$saw{$address}) {
2218 if ($output_roles) {
2219 push(@lines, "$address ($role)");
2221 push(@lines, $address);
2233 if ($output_multiline) {
2234 foreach my $line (@parms) {
2238 print(join($output_separator, @parms));
2246 # Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
2247 # comment. We must allow for rfc822_lwsp (or comments) after each of these.
2248 # This regexp will only work on addresses which have had comments stripped
2249 # and replaced with rfc822_lwsp.
2251 my $specials = '()<>@,;:\\\\".\\[\\]';
2252 my $controls = '\\000-\\037\\177';
2254 my $dtext = "[^\\[\\]\\r\\\\]";
2255 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
2257 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
2259 # Use zero-width assertion to spot the limit of an atom. A simple
2260 # $rfc822_lwsp* causes the regexp engine to hang occasionally.
2261 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
2262 my $word = "(?:$atom|$quoted_string)";
2263 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
2265 my $sub_domain = "(?:$atom|$domain_literal)";
2266 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
2268 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
2270 my $phrase = "$word*";
2271 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
2272 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
2273 my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
2275 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
2276 my $address = "(?:$mailbox|$group)";
2278 return "$rfc822_lwsp*$address";
2281 sub rfc822_strip_comments
{
2283 # Recursively remove comments, and replace with a single space. The simpler
2284 # regexps in the Email Addressing FAQ are imperfect - they will miss escaped
2285 # chars in atoms, for example.
2287 while ($s =~ s
/^((?
:[^"\\]|\\.)*
2288 (?:"(?
:[^"\\]|\\.)*"(?
:[^"\\]|\\.)*)*)
2289 \((?:[^()\\]|\\.)*\)/$1 /osx) {}
2293 # valid: returns true if the parameter is an RFC822 valid address
2296 my $s = rfc822_strip_comments(shift);
2299 $rfc822re = make_rfc822re();
2302 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
2305 # validlist: In scalar context, returns true if the parameter is an RFC822
2306 # valid list of addresses.
2308 # In list context, returns an empty list on failure (an invalid
2309 # address was found); otherwise a list whose first element is the
2310 # number of addresses found and whose remaining elements are the
2311 # addresses. This is needed to disambiguate failure (invalid)
2312 # from success with no addresses found, because an empty string is
2315 sub rfc822_validlist {
2316 my $s = rfc822_strip_comments(shift);
2319 $rfc822re = make_rfc822re();
2321 # * null list items are valid according to the RFC
2322 # * the '1' business is to aid in distinguishing failure from no results
2325 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
2326 $s =~ m/^$rfc822_char*$/) {
2327 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
2330 return wantarray ? (scalar(@r), @r) : 1;
2332 return wantarray ? () : 0;