2 # SPDX-License-Identifier: GPL-2.0
4 # (c) 2007, Joe Perches <joe@perches.com>
5 # created from checkpatch.pl
7 # Print selected MAINTAINERS information for
8 # the files modified in a patch or for a file
10 # usage: perl scripts/get_maintainer.pl [OPTIONS] <patch>
11 # perl scripts/get_maintainer.pl [OPTIONS] -f <file>
19 use Getopt
::Long
qw(:config no_auto_abbrev);
23 my $cur_path = fastgetcwd
() . '/';
26 my $email_usename = 1;
27 my $email_maintainer = 1;
28 my $email_reviewer = 1;
31 my $email_moderated_list = 1;
32 my $email_subscriber_list = 0;
33 my $email_git_penguin_chiefs = 0;
35 my $email_git_all_signature_types = 0;
36 my $email_git_blame = 0;
37 my $email_git_blame_signatures = 1;
38 my $email_git_fallback = 1;
39 my $email_git_min_signatures = 1;
40 my $email_git_max_maintainers = 5;
41 my $email_git_min_percent = 5;
42 my $email_git_since = "1-year-ago";
43 my $email_hg_since = "-365";
45 my $email_remove_duplicates = 1;
46 my $email_use_mailmap = 1;
47 my $output_multiline = 1;
48 my $output_separator = ", ";
50 my $output_rolestats = 1;
51 my $output_section_maxlen = 50;
61 my $from_filename = 0;
62 my $pattern_depth = 0;
63 my $self_test = undef;
66 my $find_maintainer_files = 0;
72 my %commit_author_hash;
73 my %commit_signer_hash;
75 my @penguin_chief = ();
76 push(@penguin_chief, "Linus Torvalds:torvalds\@linux-foundation.org");
77 #Andrew wants in on most everything - 2009/01/14
78 #push(@penguin_chief, "Andrew Morton:akpm\@linux-foundation.org");
80 my @penguin_chief_names = ();
81 foreach my $chief (@penguin_chief) {
82 if ($chief =~ m/^(.*):(.*)/) {
85 push(@penguin_chief_names, $chief_name);
88 my $penguin_chiefs = "\(" . join("|", @penguin_chief_names) . "\)";
90 # Signature types of people who are either
91 # a) responsible for the code in question, or
92 # b) familiar enough with it to give relevant feedback
93 my @signature_tags = ();
94 push(@signature_tags, "Signed-off-by:");
95 push(@signature_tags, "Reviewed-by:");
96 push(@signature_tags, "Acked-by:");
98 my $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
100 # rfc822 email address - preloaded methods go here.
101 my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
102 my $rfc822_char = '[\\000-\\377]';
104 # VCS command support: class-like functions and strings
109 "execute_cmd" => \
&git_execute_cmd
,
110 "available" => '(which("git") ne "") && (-e ".git")',
111 "find_signers_cmd" =>
112 "git log --no-color --follow --since=\$email_git_since " .
113 '--numstat --no-merges ' .
114 '--format="GitCommit: %H%n' .
115 'GitAuthor: %an <%ae>%n' .
120 "find_commit_signers_cmd" =>
121 "git log --no-color " .
123 '--format="GitCommit: %H%n' .
124 'GitAuthor: %an <%ae>%n' .
129 "find_commit_author_cmd" =>
130 "git log --no-color " .
132 '--format="GitCommit: %H%n' .
133 'GitAuthor: %an <%ae>%n' .
135 'GitSubject: %s%n"' .
137 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
138 "blame_file_cmd" => "git blame -l \$file",
139 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})",
140 "blame_commit_pattern" => "^([0-9a-f]+) ",
141 "author_pattern" => "^GitAuthor: (.*)",
142 "subject_pattern" => "^GitSubject: (.*)",
143 "stat_pattern" => "^(\\d+)\\t(\\d+)\\t\$file\$",
144 "file_exists_cmd" => "git ls-files \$file",
145 "list_files_cmd" => "git ls-files \$file",
149 "execute_cmd" => \
&hg_execute_cmd
,
150 "available" => '(which("hg") ne "") && (-d ".hg")',
151 "find_signers_cmd" =>
152 "hg log --date=\$email_hg_since " .
153 "--template='HgCommit: {node}\\n" .
154 "HgAuthor: {author}\\n" .
155 "HgSubject: {desc}\\n'" .
157 "find_commit_signers_cmd" =>
159 "--template='HgSubject: {desc}\\n'" .
161 "find_commit_author_cmd" =>
163 "--template='HgCommit: {node}\\n" .
164 "HgAuthor: {author}\\n" .
165 "HgSubject: {desc|firstline}\\n'" .
167 "blame_range_cmd" => "", # not supported
168 "blame_file_cmd" => "hg blame -n \$file",
169 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})",
170 "blame_commit_pattern" => "^([ 0-9a-f]+):",
171 "author_pattern" => "^HgAuthor: (.*)",
172 "subject_pattern" => "^HgSubject: (.*)",
173 "stat_pattern" => "^(\\d+)\t(\\d+)\t\$file\$",
174 "file_exists_cmd" => "hg files \$file",
175 "list_files_cmd" => "hg manifest -R \$file",
178 my $conf = which_conf
(".get_maintainer.conf");
181 open(my $conffile, '<', "$conf")
182 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
184 while (<$conffile>) {
187 $line =~ s/\s*\n?$//g;
191 next if ($line =~ m/^\s*#/);
192 next if ($line =~ m/^\s*$/);
194 my @words = split(" ", $line);
195 foreach my $word (@words) {
196 last if ($word =~ m/^#/);
197 push (@conf_args, $word);
201 unshift(@ARGV, @conf_args) if @conf_args;
204 my @ignore_emails = ();
205 my $ignore_file = which_conf
(".get_maintainer.ignore");
206 if (-f
$ignore_file) {
207 open(my $ignore, '<', "$ignore_file")
208 or warn "$P: Can't find a readable .get_maintainer.ignore file $!\n";
212 $line =~ s/\s*\n?$//;
217 next if ($line =~ m/^\s*$/);
218 if (rfc822_valid
($line)) {
219 push(@ignore_emails, $line);
227 if ($_ =~ /^-{1,2}self-test(?:=|$)/) {
228 die "$P: using --self-test does not allow any other option or argument\n";
235 'git!' => \
$email_git,
236 'git-all-signature-types!' => \
$email_git_all_signature_types,
237 'git-blame!' => \
$email_git_blame,
238 'git-blame-signatures!' => \
$email_git_blame_signatures,
239 'git-fallback!' => \
$email_git_fallback,
240 'git-chief-penguins!' => \
$email_git_penguin_chiefs,
241 'git-min-signatures=i' => \
$email_git_min_signatures,
242 'git-max-maintainers=i' => \
$email_git_max_maintainers,
243 'git-min-percent=i' => \
$email_git_min_percent,
244 'git-since=s' => \
$email_git_since,
245 'hg-since=s' => \
$email_hg_since,
246 'i|interactive!' => \
$interactive,
247 'remove-duplicates!' => \
$email_remove_duplicates,
248 'mailmap!' => \
$email_use_mailmap,
249 'm!' => \
$email_maintainer,
250 'r!' => \
$email_reviewer,
251 'n!' => \
$email_usename,
252 'l!' => \
$email_list,
253 'fixes!' => \
$email_fixes,
254 'moderated!' => \
$email_moderated_list,
255 's!' => \
$email_subscriber_list,
256 'multiline!' => \
$output_multiline,
257 'roles!' => \
$output_roles,
258 'rolestats!' => \
$output_rolestats,
259 'separator=s' => \
$output_separator,
260 'subsystem!' => \
$subsystem,
261 'status!' => \
$status,
265 'letters=s' => \
$letters,
266 'pattern-depth=i' => \
$pattern_depth,
267 'k|keywords!' => \
$keywords,
268 'sections!' => \
$sections,
269 'fe|file-emails!' => \
$file_emails,
270 'f|file' => \
$from_filename,
271 'find-maintainer-files' => \
$find_maintainer_files,
272 'mpath|maintainer-path=s' => \
$maintainer_path,
273 'self-test:s' => \
$self_test,
274 'v|version' => \
$version,
275 'h|help|usage' => \
$help,
277 die "$P: invalid argument - use --help if necessary\n";
286 print("${P} ${V}\n");
290 if (defined $self_test) {
291 read_all_maintainer_files
();
296 if (-t STDIN
&& !@ARGV) {
297 # We're talking to a terminal, but have no command line arguments.
298 die "$P: missing patchfile or -f file - use --help if necessary\n";
301 $output_multiline = 0 if ($output_separator ne ", ");
302 $output_rolestats = 1 if ($interactive);
303 $output_roles = 1 if ($output_rolestats);
305 if ($sections || $letters ne "") {
316 my $selections = $email + $scm + $status + $subsystem + $web;
317 if ($selections == 0) {
318 die "$P: Missing required option: email, scm, status, subsystem or web\n";
323 ($email_maintainer + $email_reviewer +
324 $email_list + $email_subscriber_list +
325 $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
326 die "$P: Please select at least 1 email option\n";
329 if ($tree && !top_of_kernel_tree
($lk_path)) {
330 die "$P: The current directory does not appear to be "
331 . "a linux kernel source tree.\n";
334 ## Read MAINTAINERS for type/value pairs
339 my @self_test_info = ();
341 sub read_maintainer_file
{
344 open (my $maint, '<', "$file")
345 or die "$P: Can't open MAINTAINERS file '$file': $!\n";
351 if ($line =~ m/^([A-Z]):\s*(.*)/) {
355 ##Filename pattern matching
356 if ($type eq "F" || $type eq "X") {
357 $value =~ s@\
.@
\\\
.@g; ##Convert . to \.
358 $value =~ s/\*/\.\*/g; ##Convert * to .*
359 $value =~ s/\?/\./g; ##Convert ? to .
360 ##if pattern is a directory and it lacks a trailing slash, add one
362 $value =~ s@
([^/])$@$1/@
;
364 } elsif ($type eq "K") {
365 $keyword_hash{@typevalue} = $value;
367 push(@typevalue, "$type:$value");
368 } elsif (!(/^\s*$/ || /^\s*\#/)) {
369 push(@typevalue, $line);
371 if (defined $self_test) {
372 push(@self_test_info, {file
=>$file, linenr
=>$i, line
=>$line});
379 sub find_is_maintainer_file
{
381 return if ($file !~ m@
/MAINTAINERS
$@
);
382 $file = $File::Find
::name
;
383 return if (! -f
$file);
384 push(@mfiles, $file);
387 sub find_ignore_git
{
388 return grep { $_ !~ /^\.git$/; } @_;
391 read_all_maintainer_files
();
393 sub read_all_maintainer_files
{
394 my $path = "${lk_path}MAINTAINERS";
395 if (defined $maintainer_path) {
396 $path = $maintainer_path;
397 # Perl Cookbook tilde expansion if necessary
398 $path =~ s@
^~([^/]*)@
$1 ?
(getpwnam($1))[7] : ( $ENV{HOME
} || $ENV{LOGDIR
} || (getpwuid($<))[7])@ex;
402 $path .= '/' if ($path !~ m@
/$@
);
403 if ($find_maintainer_files) {
404 find
( { wanted
=> \
&find_is_maintainer_file
,
405 preprocess
=> \
&find_ignore_git
,
409 opendir(DIR
, "$path") or die $!;
410 my @files = readdir(DIR
);
412 foreach my $file (@files) {
413 push(@mfiles, "$path$file") if ($file !~ /^\./);
416 } elsif (-f
"$path") {
417 push(@mfiles, "$path");
419 die "$P: MAINTAINER file not found '$path'\n";
421 die "$P: No MAINTAINER files found in '$path'\n" if (scalar(@mfiles) == 0);
422 foreach my $file (@mfiles) {
423 read_maintainer_file
("$file");
428 # Read mail address map
441 return if (!$email_use_mailmap || !(-f
"${lk_path}.mailmap"));
443 open(my $mailmap_file, '<', "${lk_path}.mailmap")
444 or warn "$P: Can't open .mailmap: $!\n";
446 while (<$mailmap_file>) {
447 s/#.*$//; #strip comments
448 s/^\s+|\s+$//g; #trim
450 next if (/^\s*$/); #skip empty lines
451 #entries have one of the following formats:
454 # name1 <mail1> <mail2>
455 # name1 <mail1> name2 <mail2>
456 # (see man git-shortlog)
458 if (/^([^<]+)<([^>]+)>$/) {
462 $real_name =~ s/\s+$//;
463 ($real_name, $address) = parse_email
("$real_name <$address>");
464 $mailmap->{names
}->{$address} = $real_name;
466 } elsif (/^<([^>]+)>\s*<([^>]+)>$/) {
467 my $real_address = $1;
468 my $wrong_address = $2;
470 $mailmap->{addresses
}->{$wrong_address} = $real_address;
472 } elsif (/^(.+)<([^>]+)>\s*<([^>]+)>$/) {
474 my $real_address = $2;
475 my $wrong_address = $3;
477 $real_name =~ s/\s+$//;
478 ($real_name, $real_address) =
479 parse_email
("$real_name <$real_address>");
480 $mailmap->{names
}->{$wrong_address} = $real_name;
481 $mailmap->{addresses
}->{$wrong_address} = $real_address;
483 } elsif (/^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$/) {
485 my $real_address = $2;
487 my $wrong_address = $4;
489 $real_name =~ s/\s+$//;
490 ($real_name, $real_address) =
491 parse_email
("$real_name <$real_address>");
493 $wrong_name =~ s/\s+$//;
494 ($wrong_name, $wrong_address) =
495 parse_email
("$wrong_name <$wrong_address>");
497 my $wrong_email = format_email
($wrong_name, $wrong_address, 1);
498 $mailmap->{names
}->{$wrong_email} = $real_name;
499 $mailmap->{addresses
}->{$wrong_email} = $real_address;
502 close($mailmap_file);
505 ## use the filenames on the command line or find the filenames in the patchfiles
508 my @fixes = (); # If a patch description includes Fixes: lines
510 my @keyword_tvi = ();
511 my @file_emails = ();
514 push(@ARGV, "&STDIN");
517 foreach my $file (@ARGV) {
518 if ($file ne "&STDIN") {
519 ##if $file is a directory and it lacks a trailing slash, add one
521 $file =~ s@
([^/])$@$1/@
;
522 } elsif (!(-f
$file)) {
523 die "$P: file '${file}' not found\n";
526 if ($from_filename || ($file ne "&STDIN" && vcs_file_exists
($file))) {
527 $file =~ s/^\Q${cur_path}\E//; #strip any absolute path
528 $file =~ s/^\Q${lk_path}\E//; #or the path to the lk tree
530 if ($file ne "MAINTAINERS" && -f
$file && ($keywords || $file_emails)) {
531 open(my $f, '<', $file)
532 or die "$P: Can't open $file: $!\n";
533 my $text = do { local($/) ; <$f> };
536 foreach my $line (keys %keyword_hash) {
537 if ($text =~ m/$keyword_hash{$line}/x) {
538 push(@keyword_tvi, $line);
543 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;
544 push(@file_emails, clean_file_emails
(@poss_addr));
548 my $file_cnt = @files;
551 open(my $patch, "< $file")
552 or die "$P: Can't open $file: $!\n";
554 # We can check arbitrary information before the patch
555 # like the commit message, mail headers, etc...
556 # This allows us to match arbitrary keywords against any part
557 # of a git format-patch generated file (subject tags, etc...)
559 my $patch_prefix = ""; #Parsing the intro
563 if (m/^ mode change [0-7]+ => [0-7]+ (\S+)\s*$/) {
565 push(@files, $filename);
566 } elsif (m/^rename (?:from|to) (\S+)\s*$/) {
568 push(@files, $filename);
569 } elsif (m/^diff --git a\/(\S
+) b\
/(\S+)\s*$/) {
572 push(@files, $filename1);
573 push(@files, $filename2);
574 } elsif (m/^Fixes:\s+([0-9a-fA-F]{6,40})/) {
575 push(@fixes, $1) if ($email_fixes);
576 } elsif (m/^\+\+\+\s+(\S+)/ or m/^---\s+(\S+)/) {
578 $filename =~ s@
^[^/]*/@@
;
580 $lastfile = $filename;
581 push(@files, $filename);
582 $patch_prefix = "^[+-].*"; #Now parsing the actual patch
583 } elsif (m/^\@\@ -(\d+),(\d+)/) {
584 if ($email_git_blame) {
585 push(@range, "$lastfile:$1:$2");
587 } elsif ($keywords) {
588 foreach my $line (keys %keyword_hash) {
589 if ($patch_line =~ m/${patch_prefix}$keyword_hash{$line}/x) {
590 push(@keyword_tvi, $line);
597 if ($file_cnt == @files) {
598 warn "$P: file '${file}' doesn't appear to be a patch. "
599 . "Add -f to options?\n";
601 @files = sort_and_uniq
(@files);
605 @file_emails = uniq
(@file_emails);
606 @fixes = uniq
(@fixes);
609 my %email_hash_address;
617 my %deduplicate_name_hash = ();
618 my %deduplicate_address_hash = ();
620 my @maintainers = get_maintainers
();
622 @maintainers = merge_email
(@maintainers);
623 output
(@maintainers);
632 @status = uniq
(@status);
637 @subsystem = uniq
(@subsystem);
652 my @section_headers = ();
655 @lsfiles = vcs_list_files
($lk_path);
657 for my $x (@self_test_info) {
660 ## Section header duplication and missing section content
661 if (($self_test eq "" || $self_test =~ /\bsections\b/) &&
662 $x->{line
} =~ /^\S[^:]/ &&
663 defined $self_test_info[$index] &&
664 $self_test_info[$index]->{line
} =~ /^([A-Z]):\s*\S/) {
669 if (grep(m@
^\Q
$x->{line
}\E@
, @section_headers)) {
670 print("$x->{file}:$x->{linenr}: warning: duplicate section header\t$x->{line}\n");
672 push(@section_headers, $x->{line
});
674 my $nextline = $index;
675 while (defined $self_test_info[$nextline] &&
676 $self_test_info[$nextline]->{line
} =~ /^([A-Z]):\s*(\S.*)/) {
682 } elsif ($type eq "F" || $type eq "N") {
684 } elsif ($type eq "M" || $type eq "R" || $type eq "L") {
689 if (!$has_ML && $status !~ /orphan|obsolete/i) {
690 print("$x->{file}:$x->{linenr}: warning: section without email address\t$x->{line}\n");
693 print("$x->{file}:$x->{linenr}: warning: section without status \t$x->{line}\n");
696 print("$x->{file}:$x->{linenr}: warning: section without file pattern\t$x->{line}\n");
700 next if ($x->{line
} !~ /^([A-Z]):\s*(.*)/);
705 ## Filename pattern matching
706 if (($type eq "F" || $type eq "X") &&
707 ($self_test eq "" || $self_test =~ /\bpatterns\b/)) {
708 $value =~ s@\
.@
\\\
.@g; ##Convert . to \.
709 $value =~ s/\*/\.\*/g; ##Convert * to .*
710 $value =~ s/\?/\./g; ##Convert ? to .
711 ##if pattern is a directory and it lacks a trailing slash, add one
713 $value =~ s@
([^/])$@$1/@
;
715 if (!grep(m@
^$value@
, @lsfiles)) {
716 print("$x->{file}:$x->{linenr}: warning: no file matches\t$x->{line}\n");
720 } elsif (($type eq "W" || $type eq "Q" || $type eq "B") &&
721 $value =~ /^https?:/ &&
722 ($self_test eq "" || $self_test =~ /\blinks\b/)) {
723 next if (grep(m@
^\Q
$value\E
$@
, @good_links));
725 if (grep(m@
^\Q
$value\E
$@
, @bad_links)) {
728 my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $value`;
730 push(@good_links, $value);
732 push(@bad_links, $value);
737 print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
741 } elsif ($type eq "T" &&
742 ($self_test eq "" || $self_test =~ /\bscm\b/)) {
743 next if (grep(m@
^\Q
$value\E
$@
, @good_links));
745 if (grep(m@
^\Q
$value\E
$@
, @bad_links)) {
747 } elsif ($value !~ /^(?:git|quilt|hg)\s+\S/) {
748 print("$x->{file}:$x->{linenr}: warning: malformed entry\t$x->{line}\n");
749 } elsif ($value =~ /^git\s+(\S+)(\s+([^\(]+\S+))?/) {
753 my $output = `git ls-remote --exit-code -h "$url" $branch > /dev/null 2>&1`;
755 push(@good_links, $value);
757 push(@bad_links, $value);
760 } elsif ($value =~ /^(?:quilt|hg)\s+(https?:\S+)/) {
762 my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $url`;
764 push(@good_links, $value);
766 push(@bad_links, $value);
771 print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
777 sub ignore_email_address
{
780 foreach my $ignore (@ignore_emails) {
781 return 1 if ($ignore eq $address);
787 sub range_is_maintained
{
788 my ($start, $end) = @_;
790 for (my $i = $start; $i < $end; $i++) {
791 my $line = $typevalue[$i];
792 if ($line =~ m/^([A-Z]):\s*(.*)/) {
796 if ($value =~ /(maintain|support)/i) {
805 sub range_has_maintainer
{
806 my ($start, $end) = @_;
808 for (my $i = $start; $i < $end; $i++) {
809 my $line = $typevalue[$i];
810 if ($line =~ m/^([A-Z]):\s*(.*)/) {
821 sub get_maintainers
{
822 %email_hash_name = ();
823 %email_hash_address = ();
824 %commit_author_hash = ();
825 %commit_signer_hash = ();
833 %deduplicate_name_hash = ();
834 %deduplicate_address_hash = ();
835 if ($email_git_all_signature_types) {
836 $signature_pattern = "(.+?)[Bb][Yy]:";
838 $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
841 # Find responsible parties
843 my %exact_pattern_match_hash = ();
845 foreach my $file (@files) {
848 my $tvi = find_first_section
();
849 while ($tvi < @typevalue) {
850 my $start = find_starting_index
($tvi);
851 my $end = find_ending_index
($tvi);
855 #Do not match excluded file patterns
857 for ($i = $start; $i < $end; $i++) {
858 my $line = $typevalue[$i];
859 if ($line =~ m/^([A-Z]):\s*(.*)/) {
863 if (file_match_pattern
($file, $value)) {
872 for ($i = $start; $i < $end; $i++) {
873 my $line = $typevalue[$i];
874 if ($line =~ m/^([A-Z]):\s*(.*)/) {
878 if (file_match_pattern
($file, $value)) {
879 my $value_pd = ($value =~ tr@
/@@
);
880 my $file_pd = ($file =~ tr@
/@@
);
881 $value_pd++ if (substr($value,-1,1) ne "/");
882 $value_pd = -1 if ($value =~ /^\.\*/);
883 if ($value_pd >= $file_pd &&
884 range_is_maintained
($start, $end) &&
885 range_has_maintainer
($start, $end)) {
886 $exact_pattern_match_hash{$file} = 1;
888 if ($pattern_depth == 0 ||
889 (($file_pd - $value_pd) < $pattern_depth)) {
890 $hash{$tvi} = $value_pd;
893 } elsif ($type eq 'N') {
894 if ($file =~ m/$value/x) {
904 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
905 add_categories
($line);
908 my $start = find_starting_index
($line);
909 my $end = find_ending_index
($line);
910 for ($i = $start; $i < $end; $i++) {
911 my $line = $typevalue[$i];
912 if ($line =~ /^[FX]:/) { ##Restore file patterns
913 $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
914 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ?
915 $line =~ s/\\\./\./g; ##Convert \. to .
916 $line =~ s/\.\*/\*/g; ##Convert .* to *
918 my $count = $line =~ s/^([A-Z]):/$1:\t/g;
919 if ($letters eq "" || (!$count || $letters =~ /$1/i)) {
929 @keyword_tvi = sort_and_uniq
(@keyword_tvi);
930 foreach my $line (@keyword_tvi) {
931 add_categories
($line);
935 foreach my $email (@email_to, @list_to) {
936 $email->[0] = deduplicate_email
($email->[0]);
939 foreach my $file (@files) {
941 ($email_git || ($email_git_fallback &&
942 !$exact_pattern_match_hash{$file}))) {
943 vcs_file_signoffs
($file);
945 if ($email && $email_git_blame) {
946 vcs_file_blame
($file);
951 foreach my $chief (@penguin_chief) {
952 if ($chief =~ m/^(.*):(.*)/) {
955 $email_address = format_email
($1, $2, $email_usename);
956 if ($email_git_penguin_chiefs) {
957 push(@email_to, [$email_address, 'chief penguin']);
959 @email_to = grep($_->[0] !~ /${email_address}/, @email_to);
964 foreach my $email (@file_emails) {
965 my ($name, $address) = parse_email
($email);
967 my $tmp_email = format_email
($name, $address, $email_usename);
968 push_email_address
($tmp_email, '');
969 add_role
($tmp_email, 'in file');
973 foreach my $fix (@fixes) {
974 vcs_add_commit_signers
($fix, "blamed_fixes");
978 if ($email || $email_list) {
980 @to = (@to, @email_to);
983 @to = (@to, @list_to);
988 @to = interactive_get_maintainers
(\
@to);
994 sub file_match_pattern
{
995 my ($file, $pattern) = @_;
996 if (substr($pattern, -1) eq "/") {
997 if ($file =~ m@
^$pattern@
) {
1001 if ($file =~ m@
^$pattern@
) {
1002 my $s1 = ($file =~ tr@
/@@
);
1003 my $s2 = ($pattern =~ tr@
/@@
);
1014 usage: $P [options] patchfile
1015 $P [options] -f file|directory
1018 MAINTAINER field selection options:
1019 --email => print email address(es) if any
1020 --git => include recent git \*-by: signers
1021 --git-all-signature-types => include signers regardless of signature type
1022 or use only ${signature_pattern} signers (default: $email_git_all_signature_types)
1023 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
1024 --git-chief-penguins => include ${penguin_chiefs}
1025 --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
1026 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
1027 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
1028 --git-blame => use git blame to find modified commits for patch or file
1029 --git-blame-signatures => when used with --git-blame, also include all commit signers
1030 --git-since => git history to use (default: $email_git_since)
1031 --hg-since => hg history to use (default: $email_hg_since)
1032 --interactive => display a menu (mostly useful if used with the --git option)
1033 --m => include maintainer(s) if any
1034 --r => include reviewer(s) if any
1035 --n => include name 'Full Name <addr\@domain.tld>'
1036 --l => include list(s) if any
1037 --moderated => include moderated lists(s) if any (default: true)
1038 --s => include subscriber only list(s) if any (default: false)
1039 --remove-duplicates => minimize duplicate email names/addresses
1040 --roles => show roles (status:subsystem, git-signer, list, etc...)
1041 --rolestats => show roles and statistics (commits/total_commits, %)
1042 --file-emails => add email addresses found in -f file (default: 0 (off))
1043 --fixes => for patches, add signatures of commits with 'Fixes: <commit>' (default: 1 (on))
1044 --scm => print SCM tree(s) if any
1045 --status => print status if any
1046 --subsystem => print subsystem name if any
1047 --web => print website(s) if any
1049 Output type options:
1050 --separator [, ] => separator for multiple entries on 1 line
1051 using --separator also sets --nomultiline if --separator is not [, ]
1052 --multiline => print 1 entry per line
1055 --pattern-depth => Number of pattern directory traversals (default: 0 (all))
1056 --keywords => scan patch for keywords (default: $keywords)
1057 --sections => print all of the subsystem sections with pattern matches
1058 --letters => print all matching 'letter' types from all matching sections
1059 --mailmap => use .mailmap file (default: $email_use_mailmap)
1060 --no-tree => run without a kernel tree
1061 --self-test => show potential issues with MAINTAINERS file content
1062 --version => show version
1063 --help => show this help information
1066 [--email --tree --nogit --git-fallback --m --r --n --l --multiline
1067 --pattern-depth=0 --remove-duplicates --rolestats]
1070 Using "-f directory" may give unexpected results:
1071 Used with "--git", git signators for _all_ files in and below
1072 directory are examined as git recurses directories.
1073 Any specified X: (exclude) pattern matches are _not_ ignored.
1074 Used with "--nogit", directory is used as a pattern match,
1075 no individual file within the directory or subdirectory
1077 Used with "--git-blame", does not iterate all files in directory
1078 Using "--git-blame" is slow and may add old committers and authors
1079 that are no longer active maintainers to the output.
1080 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
1081 other automated tools that expect only ["name"] <email address>
1082 may not work because of additional output after <email address>.
1083 Using "--rolestats" and "--git-blame" shows the #/total=% commits,
1084 not the percentage of the entire file authored. # of commits is
1085 not a good measure of amount of code authored. 1 major commit may
1086 contain a thousand lines, 5 trivial commits may modify a single line.
1087 If git is not installed, but mercurial (hg) is installed and an .hg
1088 repository exists, the following options apply to mercurial:
1090 --git-min-signatures, --git-max-maintainers, --git-min-percent, and
1092 Use --hg-since not --git-since to control date selection
1093 File ".get_maintainer.conf", if it exists in the linux kernel source root
1094 directory, can change whatever get_maintainer defaults are desired.
1095 Entries in this file can be any command line argument.
1096 This file is prepended to any additional command line arguments.
1097 Multiple lines and # comments are allowed.
1098 Most options have both positive and negative forms.
1099 The negative forms for --<foo> are --no<foo> and --no-<foo>.
1104 sub top_of_kernel_tree
{
1107 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
1110 if ( (-f
"${lk_path}COPYING")
1111 && (-f
"${lk_path}CREDITS")
1112 && (-f
"${lk_path}Kbuild")
1113 && (-e
"${lk_path}MAINTAINERS")
1114 && (-f
"${lk_path}Makefile")
1115 && (-f
"${lk_path}README")
1116 && (-d
"${lk_path}Documentation")
1117 && (-d
"${lk_path}arch")
1118 && (-d
"${lk_path}include")
1119 && (-d
"${lk_path}drivers")
1120 && (-d
"${lk_path}fs")
1121 && (-d
"${lk_path}init")
1122 && (-d
"${lk_path}ipc")
1123 && (-d
"${lk_path}kernel")
1124 && (-d
"${lk_path}lib")
1125 && (-d
"${lk_path}scripts")) {
1132 my ($formatted_email) = @_;
1137 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
1140 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
1142 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
1146 $name =~ s/^\s+|\s+$//g;
1147 $name =~ s/^\"|\"$//g;
1148 $address =~ s/^\s+|\s+$//g;
1150 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
1151 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
1152 $name = "\"$name\"";
1155 return ($name, $address);
1159 my ($name, $address, $usename) = @_;
1161 my $formatted_email;
1163 $name =~ s/^\s+|\s+$//g;
1164 $name =~ s/^\"|\"$//g;
1165 $address =~ s/^\s+|\s+$//g;
1167 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
1168 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
1169 $name = "\"$name\"";
1173 if ("$name" eq "") {
1174 $formatted_email = "$address";
1176 $formatted_email = "$name <$address>";
1179 $formatted_email = $address;
1182 return $formatted_email;
1185 sub find_first_section
{
1188 while ($index < @typevalue) {
1189 my $tv = $typevalue[$index];
1190 if (($tv =~ m/^([A-Z]):\s*(.*)/)) {
1199 sub find_starting_index
{
1202 while ($index > 0) {
1203 my $tv = $typevalue[$index];
1204 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
1213 sub find_ending_index
{
1216 while ($index < @typevalue) {
1217 my $tv = $typevalue[$index];
1218 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
1227 sub get_subsystem_name
{
1230 my $start = find_starting_index
($index);
1232 my $subsystem = $typevalue[$start];
1233 if ($output_section_maxlen && length($subsystem) > $output_section_maxlen) {
1234 $subsystem = substr($subsystem, 0, $output_section_maxlen - 3);
1235 $subsystem =~ s/\s*$//;
1236 $subsystem = $subsystem . "...";
1241 sub get_maintainer_role
{
1245 my $start = find_starting_index
($index);
1246 my $end = find_ending_index
($index);
1248 my $role = "unknown";
1249 my $subsystem = get_subsystem_name
($index);
1251 for ($i = $start + 1; $i < $end; $i++) {
1252 my $tv = $typevalue[$i];
1253 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1256 if ($ptype eq "S") {
1263 if ($role eq "supported") {
1264 $role = "supporter";
1265 } elsif ($role eq "maintained") {
1266 $role = "maintainer";
1267 } elsif ($role eq "odd fixes") {
1268 $role = "odd fixer";
1269 } elsif ($role eq "orphan") {
1270 $role = "orphan minder";
1271 } elsif ($role eq "obsolete") {
1272 $role = "obsolete minder";
1273 } elsif ($role eq "buried alive in reporters") {
1274 $role = "chief penguin";
1277 return $role . ":" . $subsystem;
1283 my $subsystem = get_subsystem_name
($index);
1285 if ($subsystem eq "THE REST") {
1292 sub add_categories
{
1296 my $start = find_starting_index
($index);
1297 my $end = find_ending_index
($index);
1299 push(@subsystem, $typevalue[$start]);
1301 for ($i = $start + 1; $i < $end; $i++) {
1302 my $tv = $typevalue[$i];
1303 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1306 if ($ptype eq "L") {
1307 my $list_address = $pvalue;
1308 my $list_additional = "";
1309 my $list_role = get_list_role
($i);
1311 if ($list_role ne "") {
1312 $list_role = ":" . $list_role;
1314 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
1316 $list_additional = $2;
1318 if ($list_additional =~ m/subscribers-only/) {
1319 if ($email_subscriber_list) {
1320 if (!$hash_list_to{lc($list_address)}) {
1321 $hash_list_to{lc($list_address)} = 1;
1322 push(@list_to, [$list_address,
1323 "subscriber list${list_role}"]);
1328 if (!$hash_list_to{lc($list_address)}) {
1329 if ($list_additional =~ m/moderated/) {
1330 if ($email_moderated_list) {
1331 $hash_list_to{lc($list_address)} = 1;
1332 push(@list_to, [$list_address,
1333 "moderated list${list_role}"]);
1336 $hash_list_to{lc($list_address)} = 1;
1337 push(@list_to, [$list_address,
1338 "open list${list_role}"]);
1343 } elsif ($ptype eq "M") {
1344 if ($email_maintainer) {
1345 my $role = get_maintainer_role
($i);
1346 push_email_addresses
($pvalue, $role);
1348 } elsif ($ptype eq "R") {
1349 if ($email_reviewer) {
1350 my $subsystem = get_subsystem_name
($i);
1351 push_email_addresses
($pvalue, "reviewer:$subsystem");
1353 } elsif ($ptype eq "T") {
1354 push(@scm, $pvalue);
1355 } elsif ($ptype eq "W") {
1356 push(@web, $pvalue);
1357 } elsif ($ptype eq "S") {
1358 push(@status, $pvalue);
1365 my ($name, $address) = @_;
1367 return 1 if (($name eq "") && ($address eq ""));
1368 return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
1369 return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
1374 sub push_email_address
{
1375 my ($line, $role) = @_;
1377 my ($name, $address) = parse_email
($line);
1379 if ($address eq "") {
1383 if (!$email_remove_duplicates) {
1384 push(@email_to, [format_email
($name, $address, $email_usename), $role]);
1385 } elsif (!email_inuse
($name, $address)) {
1386 push(@email_to, [format_email
($name, $address, $email_usename), $role]);
1387 $email_hash_name{lc($name)}++ if ($name ne "");
1388 $email_hash_address{lc($address)}++;
1394 sub push_email_addresses
{
1395 my ($address, $role) = @_;
1397 my @address_list = ();
1399 if (rfc822_valid
($address)) {
1400 push_email_address
($address, $role);
1401 } elsif (@address_list = rfc822_validlist
($address)) {
1402 my $array_count = shift(@address_list);
1403 while (my $entry = shift(@address_list)) {
1404 push_email_address
($entry, $role);
1407 if (!push_email_address
($address, $role)) {
1408 warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1414 my ($line, $role) = @_;
1416 my ($name, $address) = parse_email
($line);
1417 my $email = format_email
($name, $address, $email_usename);
1419 foreach my $entry (@email_to) {
1420 if ($email_remove_duplicates) {
1421 my ($entry_name, $entry_address) = parse_email
($entry->[0]);
1422 if (($name eq $entry_name || $address eq $entry_address)
1423 && ($role eq "" || !($entry->[1] =~ m/$role/))
1425 if ($entry->[1] eq "") {
1426 $entry->[1] = "$role";
1428 $entry->[1] = "$entry->[1],$role";
1432 if ($email eq $entry->[0]
1433 && ($role eq "" || !($entry->[1] =~ m/$role/))
1435 if ($entry->[1] eq "") {
1436 $entry->[1] = "$role";
1438 $entry->[1] = "$entry->[1],$role";
1448 foreach my $path (split(/:/, $ENV{PATH
})) {
1449 if (-e
"$path/$bin") {
1450 return "$path/$bin";
1460 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1461 if (-e
"$path/$conf") {
1462 return "$path/$conf";
1472 my ($name, $address) = parse_email
($line);
1473 my $email = format_email
($name, $address, 1);
1474 my $real_name = $name;
1475 my $real_address = $address;
1477 if (exists $mailmap->{names
}->{$email} ||
1478 exists $mailmap->{addresses
}->{$email}) {
1479 if (exists $mailmap->{names
}->{$email}) {
1480 $real_name = $mailmap->{names
}->{$email};
1482 if (exists $mailmap->{addresses
}->{$email}) {
1483 $real_address = $mailmap->{addresses
}->{$email};
1486 if (exists $mailmap->{names
}->{$address}) {
1487 $real_name = $mailmap->{names
}->{$address};
1489 if (exists $mailmap->{addresses
}->{$address}) {
1490 $real_address = $mailmap->{addresses
}->{$address};
1493 return format_email
($real_name, $real_address, 1);
1497 my (@addresses) = @_;
1499 my @mapped_emails = ();
1500 foreach my $line (@addresses) {
1501 push(@mapped_emails, mailmap_email
($line));
1503 merge_by_realname
(@mapped_emails) if ($email_use_mailmap);
1504 return @mapped_emails;
1507 sub merge_by_realname
{
1511 foreach my $email (@emails) {
1512 my ($name, $address) = parse_email
($email);
1513 if (exists $address_map{$name}) {
1514 $address = $address_map{$name};
1515 $email = format_email
($name, $address, 1);
1517 $address_map{$name} = $address;
1522 sub git_execute_cmd
{
1526 my $output = `$cmd`;
1527 $output =~ s/^\s*//gm;
1528 @lines = split("\n", $output);
1533 sub hg_execute_cmd
{
1537 my $output = `$cmd`;
1538 @lines = split("\n", $output);
1543 sub extract_formatted_signatures
{
1544 my (@signature_lines) = @_;
1546 my @type = @signature_lines;
1548 s/\s*(.*):.*/$1/ for (@type);
1551 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1553 ## Reformat email addresses (with names) to avoid badly written signatures
1555 foreach my $signer (@signature_lines) {
1556 $signer = deduplicate_email
($signer);
1559 return (\
@type, \
@signature_lines);
1562 sub vcs_find_signers
{
1563 my ($cmd, $file) = @_;
1566 my @signatures = ();
1570 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1572 my $pattern = $VCS_cmds{"commit_pattern"};
1573 my $author_pattern = $VCS_cmds{"author_pattern"};
1574 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1576 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1578 $commits = grep(/$pattern/, @lines); # of commits
1580 @authors = grep(/$author_pattern/, @lines);
1581 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1582 @stats = grep(/$stat_pattern/, @lines);
1584 # print("stats: <@stats>\n");
1586 return (0, \
@signatures, \
@authors, \
@stats) if !@signatures;
1588 save_commits_by_author
(@lines) if ($interactive);
1589 save_commits_by_signer
(@lines) if ($interactive);
1591 if (!$email_git_penguin_chiefs) {
1592 @signatures = grep(!/${penguin_chiefs}/i, @signatures);
1595 my ($author_ref, $authors_ref) = extract_formatted_signatures
(@authors);
1596 my ($types_ref, $signers_ref) = extract_formatted_signatures
(@signatures);
1598 return ($commits, $signers_ref, $authors_ref, \
@stats);
1601 sub vcs_find_author
{
1605 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1607 if (!$email_git_penguin_chiefs) {
1608 @lines = grep(!/${penguin_chiefs}/i, @lines);
1611 return @lines if !@lines;
1614 foreach my $line (@lines) {
1615 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1617 my ($name, $address) = parse_email
($author);
1618 $author = format_email
($name, $address, 1);
1619 push(@authors, $author);
1623 save_commits_by_author
(@lines) if ($interactive);
1624 save_commits_by_signer
(@lines) if ($interactive);
1629 sub vcs_save_commits
{
1634 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1636 foreach my $line (@lines) {
1637 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1650 return @commits if (!(-f
$file));
1652 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1653 my @all_commits = ();
1655 $cmd = $VCS_cmds{"blame_file_cmd"};
1656 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1657 @all_commits = vcs_save_commits
($cmd);
1659 foreach my $file_range_diff (@range) {
1660 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1662 my $diff_start = $2;
1663 my $diff_length = $3;
1664 next if ("$file" ne "$diff_file");
1665 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1666 push(@commits, $all_commits[$i]);
1670 foreach my $file_range_diff (@range) {
1671 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1673 my $diff_start = $2;
1674 my $diff_length = $3;
1675 next if ("$file" ne "$diff_file");
1676 $cmd = $VCS_cmds{"blame_range_cmd"};
1677 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1678 push(@commits, vcs_save_commits
($cmd));
1681 $cmd = $VCS_cmds{"blame_file_cmd"};
1682 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1683 @commits = vcs_save_commits
($cmd);
1686 foreach my $commit (@commits) {
1687 $commit =~ s/^\^//g;
1693 my $printed_novcs = 0;
1695 %VCS_cmds = %VCS_cmds_git;
1696 return 1 if eval $VCS_cmds{"available"};
1697 %VCS_cmds = %VCS_cmds_hg;
1698 return 2 if eval $VCS_cmds{"available"};
1700 if (!$printed_novcs) {
1701 warn("$P: No supported VCS found. Add --nogit to options?\n");
1702 warn("Using a git repository produces better results.\n");
1703 warn("Try Linus Torvalds' latest git repository using:\n");
1704 warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n");
1712 return $vcs_used == 1;
1716 return $vcs_used == 2;
1719 sub vcs_add_commit_signers
{
1720 return if (!vcs_exists
());
1722 my ($commit, $desc) = @_;
1723 my $commit_count = 0;
1724 my $commit_authors_ref;
1725 my $commit_signers_ref;
1727 my @commit_authors = ();
1728 my @commit_signers = ();
1731 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1732 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1734 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers
($cmd, "");
1735 @commit_authors = @
{$commit_authors_ref} if defined $commit_authors_ref;
1736 @commit_signers = @
{$commit_signers_ref} if defined $commit_signers_ref;
1738 foreach my $signer (@commit_signers) {
1739 $signer = deduplicate_email
($signer);
1742 vcs_assign
($desc, 1, @commit_signers);
1745 sub interactive_get_maintainers
{
1746 my ($list_ref) = @_;
1747 my @list = @
$list_ref;
1756 foreach my $entry (@list) {
1757 $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
1758 $selected{$count} = 1;
1759 $authored{$count} = 0;
1760 $signed{$count} = 0;
1766 my $print_options = 0;
1771 printf STDERR
"\n%1s %2s %-65s",
1772 "*", "#", "email/list and role:stats";
1774 ($email_git_fallback && !$maintained) ||
1776 print STDERR
"auth sign";
1779 foreach my $entry (@list) {
1780 my $email = $entry->[0];
1781 my $role = $entry->[1];
1783 $sel = "*" if ($selected{$count});
1784 my $commit_author = $commit_author_hash{$email};
1785 my $commit_signer = $commit_signer_hash{$email};
1788 $authored++ for (@
{$commit_author});
1789 $signed++ for (@
{$commit_signer});
1790 printf STDERR
"%1s %2d %-65s", $sel, $count + 1, $email;
1791 printf STDERR
"%4d %4d", $authored, $signed
1792 if ($authored > 0 || $signed > 0);
1793 printf STDERR
"\n %s\n", $role;
1794 if ($authored{$count}) {
1795 my $commit_author = $commit_author_hash{$email};
1796 foreach my $ref (@
{$commit_author}) {
1797 print STDERR
" Author: @{$ref}[1]\n";
1800 if ($signed{$count}) {
1801 my $commit_signer = $commit_signer_hash{$email};
1802 foreach my $ref (@
{$commit_signer}) {
1803 print STDERR
" @{$ref}[2]: @{$ref}[1]\n";
1810 my $date_ref = \
$email_git_since;
1811 $date_ref = \
$email_hg_since if (vcs_is_hg
());
1812 if ($print_options) {
1817 Version Control options:
1818 g use git history [$email_git]
1819 gf use git-fallback [$email_git_fallback]
1820 b use git blame [$email_git_blame]
1821 bs use blame signatures [$email_git_blame_signatures]
1822 c# minimum commits [$email_git_min_signatures]
1823 %# min percent [$email_git_min_percent]
1824 d# history to use [$$date_ref]
1825 x# max maintainers [$email_git_max_maintainers]
1826 t all signature types [$email_git_all_signature_types]
1827 m use .mailmap [$email_use_mailmap]
1834 tm toggle maintainers
1835 tg toggle git entries
1836 tl toggle open list entries
1837 ts toggle subscriber list entries
1838 f emails in file [$file_emails]
1839 k keywords in file [$keywords]
1840 r remove duplicates [$email_remove_duplicates]
1841 p# pattern match depth [$pattern_depth]
1845 "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1847 my $input = <STDIN
>;
1852 my @wish = split(/[, ]+/, $input);
1853 foreach my $nr (@wish) {
1855 my $sel = substr($nr, 0, 1);
1856 my $str = substr($nr, 1);
1858 $val = $1 if $str =~ /^(\d+)$/;
1863 $output_rolestats = 0;
1866 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1867 $selected{$nr - 1} = !$selected{$nr - 1};
1868 } elsif ($sel eq "*" || $sel eq '^') {
1870 $toggle = 1 if ($sel eq '*');
1871 for (my $i = 0; $i < $count; $i++) {
1872 $selected{$i} = $toggle;
1874 } elsif ($sel eq "0") {
1875 for (my $i = 0; $i < $count; $i++) {
1876 $selected{$i} = !$selected{$i};
1878 } elsif ($sel eq "t") {
1879 if (lc($str) eq "m") {
1880 for (my $i = 0; $i < $count; $i++) {
1881 $selected{$i} = !$selected{$i}
1882 if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
1884 } elsif (lc($str) eq "g") {
1885 for (my $i = 0; $i < $count; $i++) {
1886 $selected{$i} = !$selected{$i}
1887 if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
1889 } elsif (lc($str) eq "l") {
1890 for (my $i = 0; $i < $count; $i++) {
1891 $selected{$i} = !$selected{$i}
1892 if ($list[$i]->[1] =~ /^(open list)/i);
1894 } elsif (lc($str) eq "s") {
1895 for (my $i = 0; $i < $count; $i++) {
1896 $selected{$i} = !$selected{$i}
1897 if ($list[$i]->[1] =~ /^(subscriber list)/i);
1900 } elsif ($sel eq "a") {
1901 if ($val > 0 && $val <= $count) {
1902 $authored{$val - 1} = !$authored{$val - 1};
1903 } elsif ($str eq '*' || $str eq '^') {
1905 $toggle = 1 if ($str eq '*');
1906 for (my $i = 0; $i < $count; $i++) {
1907 $authored{$i} = $toggle;
1910 } elsif ($sel eq "s") {
1911 if ($val > 0 && $val <= $count) {
1912 $signed{$val - 1} = !$signed{$val - 1};
1913 } elsif ($str eq '*' || $str eq '^') {
1915 $toggle = 1 if ($str eq '*');
1916 for (my $i = 0; $i < $count; $i++) {
1917 $signed{$i} = $toggle;
1920 } elsif ($sel eq "o") {
1923 } elsif ($sel eq "g") {
1925 bool_invert
(\
$email_git_fallback);
1927 bool_invert
(\
$email_git);
1930 } elsif ($sel eq "b") {
1932 bool_invert
(\
$email_git_blame_signatures);
1934 bool_invert
(\
$email_git_blame);
1937 } elsif ($sel eq "c") {
1939 $email_git_min_signatures = $val;
1942 } elsif ($sel eq "x") {
1944 $email_git_max_maintainers = $val;
1947 } elsif ($sel eq "%") {
1948 if ($str ne "" && $val >= 0) {
1949 $email_git_min_percent = $val;
1952 } elsif ($sel eq "d") {
1954 $email_git_since = $str;
1955 } elsif (vcs_is_hg
()) {
1956 $email_hg_since = $str;
1959 } elsif ($sel eq "t") {
1960 bool_invert
(\
$email_git_all_signature_types);
1962 } elsif ($sel eq "f") {
1963 bool_invert
(\
$file_emails);
1965 } elsif ($sel eq "r") {
1966 bool_invert
(\
$email_remove_duplicates);
1968 } elsif ($sel eq "m") {
1969 bool_invert
(\
$email_use_mailmap);
1972 } elsif ($sel eq "k") {
1973 bool_invert
(\
$keywords);
1975 } elsif ($sel eq "p") {
1976 if ($str ne "" && $val >= 0) {
1977 $pattern_depth = $val;
1980 } elsif ($sel eq "h" || $sel eq "?") {
1983 Interactive mode allows you to select the various maintainers, submitters,
1984 commit signers and mailing lists that could be CC'd on a patch.
1986 Any *'d entry is selected.
1988 If you have git or hg installed, you can choose to summarize the commit
1989 history of files in the patch. Also, each line of the current file can
1990 be matched to its commit author and that commits signers with blame.
1992 Various knobs exist to control the length of time for active commit
1993 tracking, the maximum number of commit authors and signers to add,
1996 Enter selections at the prompt until you are satisfied that the selected
1997 maintainers are appropriate. You may enter multiple selections separated
1998 by either commas or spaces.
2002 print STDERR
"invalid option: '$nr'\n";
2007 print STDERR
"git-blame can be very slow, please have patience..."
2008 if ($email_git_blame);
2009 goto &get_maintainers
;
2013 #drop not selected entries
2015 my @new_emailto = ();
2016 foreach my $entry (@list) {
2017 if ($selected{$count}) {
2018 push(@new_emailto, $list[$count]);
2022 return @new_emailto;
2026 my ($bool_ref) = @_;
2035 sub deduplicate_email
{
2039 my ($name, $address) = parse_email
($email);
2040 $email = format_email
($name, $address, 1);
2041 $email = mailmap_email
($email);
2043 return $email if (!$email_remove_duplicates);
2045 ($name, $address) = parse_email
($email);
2047 if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
2048 $name = $deduplicate_name_hash{lc($name)}->[0];
2049 $address = $deduplicate_name_hash{lc($name)}->[1];
2051 } elsif ($deduplicate_address_hash{lc($address)}) {
2052 $name = $deduplicate_address_hash{lc($address)}->[0];
2053 $address = $deduplicate_address_hash{lc($address)}->[1];
2057 $deduplicate_name_hash{lc($name)} = [ $name, $address ];
2058 $deduplicate_address_hash{lc($address)} = [ $name, $address ];
2060 $email = format_email
($name, $address, 1);
2061 $email = mailmap_email
($email);
2065 sub save_commits_by_author
{
2072 foreach my $line (@lines) {
2073 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2075 $author = deduplicate_email
($author);
2076 push(@authors, $author);
2078 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
2079 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
2082 for (my $i = 0; $i < @authors; $i++) {
2084 foreach my $ref(@
{$commit_author_hash{$authors[$i]}}) {
2085 if (@
{$ref}[0] eq $commits[$i] &&
2086 @
{$ref}[1] eq $subjects[$i]) {
2092 push(@
{$commit_author_hash{$authors[$i]}},
2093 [ ($commits[$i], $subjects[$i]) ]);
2098 sub save_commits_by_signer
{
2104 foreach my $line (@lines) {
2105 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
2106 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
2107 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
2108 my @signatures = ($line);
2109 my ($types_ref, $signers_ref) = extract_formatted_signatures
(@signatures);
2110 my @types = @
$types_ref;
2111 my @signers = @
$signers_ref;
2113 my $type = $types[0];
2114 my $signer = $signers[0];
2116 $signer = deduplicate_email
($signer);
2119 foreach my $ref(@
{$commit_signer_hash{$signer}}) {
2120 if (@
{$ref}[0] eq $commit &&
2121 @
{$ref}[1] eq $subject &&
2122 @
{$ref}[2] eq $type) {
2128 push(@
{$commit_signer_hash{$signer}},
2129 [ ($commit, $subject, $type) ]);
2136 my ($role, $divisor, @lines) = @_;
2141 return if (@lines <= 0);
2143 if ($divisor <= 0) {
2144 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
2148 @lines = mailmap
(@lines);
2150 return if (@lines <= 0);
2152 @lines = sort(@lines);
2155 $hash{$_}++ for @lines;
2158 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
2159 my $sign_offs = $hash{$line};
2160 my $percent = $sign_offs * 100 / $divisor;
2162 $percent = 100 if ($percent > 100);
2163 next if (ignore_email_address
($line));
2165 last if ($sign_offs < $email_git_min_signatures ||
2166 $count > $email_git_max_maintainers ||
2167 $percent < $email_git_min_percent);
2168 push_email_address
($line, '');
2169 if ($output_rolestats) {
2170 my $fmt_percent = sprintf("%.0f", $percent);
2171 add_role
($line, "$role:$sign_offs/$divisor=$fmt_percent%");
2173 add_role
($line, $role);
2178 sub vcs_file_signoffs
{
2189 $vcs_used = vcs_exists
();
2190 return if (!$vcs_used);
2192 my $cmd = $VCS_cmds{"find_signers_cmd"};
2193 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2195 ($commits, $signers_ref, $authors_ref, $stats_ref) = vcs_find_signers
($cmd, $file);
2197 @signers = @
{$signers_ref} if defined $signers_ref;
2198 @authors = @
{$authors_ref} if defined $authors_ref;
2199 @stats = @
{$stats_ref} if defined $stats_ref;
2201 # print("commits: <$commits>\nsigners:<@signers>\nauthors: <@authors>\nstats: <@stats>\n");
2203 foreach my $signer (@signers) {
2204 $signer = deduplicate_email
($signer);
2207 vcs_assign
("commit_signer", $commits, @signers);
2208 vcs_assign
("authored", $commits, @authors);
2209 if ($#authors == $#stats) {
2210 my $stat_pattern = $VCS_cmds{"stat_pattern"};
2211 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
2215 for (my $i = 0; $i <= $#stats; $i++) {
2216 if ($stats[$i] =~ /$stat_pattern/) {
2221 my @tmp_authors = uniq
(@authors);
2222 foreach my $author (@tmp_authors) {
2223 $author = deduplicate_email
($author);
2225 @tmp_authors = uniq
(@tmp_authors);
2226 my @list_added = ();
2227 my @list_deleted = ();
2228 foreach my $author (@tmp_authors) {
2230 my $auth_deleted = 0;
2231 for (my $i = 0; $i <= $#stats; $i++) {
2232 if ($author eq deduplicate_email
($authors[$i]) &&
2233 $stats[$i] =~ /$stat_pattern/) {
2235 $auth_deleted += $2;
2238 for (my $i = 0; $i < $auth_added; $i++) {
2239 push(@list_added, $author);
2241 for (my $i = 0; $i < $auth_deleted; $i++) {
2242 push(@list_deleted, $author);
2245 vcs_assign
("added_lines", $added, @list_added);
2246 vcs_assign
("removed_lines", $deleted, @list_deleted);
2250 sub vcs_file_blame
{
2254 my @all_commits = ();
2259 $vcs_used = vcs_exists
();
2260 return if (!$vcs_used);
2262 @all_commits = vcs_blame
($file);
2263 @commits = uniq
(@all_commits);
2264 $total_commits = @commits;
2265 $total_lines = @all_commits;
2267 if ($email_git_blame_signatures) {
2270 my $commit_authors_ref;
2271 my $commit_signers_ref;
2273 my @commit_authors = ();
2274 my @commit_signers = ();
2275 my $commit = join(" -r ", @commits);
2278 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2279 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2281 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers
($cmd, $file);
2282 @commit_authors = @
{$commit_authors_ref} if defined $commit_authors_ref;
2283 @commit_signers = @
{$commit_signers_ref} if defined $commit_signers_ref;
2285 push(@signers, @commit_signers);
2287 foreach my $commit (@commits) {
2289 my $commit_authors_ref;
2290 my $commit_signers_ref;
2292 my @commit_authors = ();
2293 my @commit_signers = ();
2296 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2297 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2299 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers
($cmd, $file);
2300 @commit_authors = @
{$commit_authors_ref} if defined $commit_authors_ref;
2301 @commit_signers = @
{$commit_signers_ref} if defined $commit_signers_ref;
2303 push(@signers, @commit_signers);
2308 if ($from_filename) {
2309 if ($output_rolestats) {
2311 if (vcs_is_hg
()) {{ # Double brace for last exit
2313 my @commit_signers = ();
2314 @commits = uniq
(@commits);
2315 @commits = sort(@commits);
2316 my $commit = join(" -r ", @commits);
2319 $cmd = $VCS_cmds{"find_commit_author_cmd"};
2320 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2324 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
2326 if (!$email_git_penguin_chiefs) {
2327 @lines = grep(!/${penguin_chiefs}/i, @lines);
2333 foreach my $line (@lines) {
2334 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2336 $author = deduplicate_email
($author);
2337 push(@authors, $author);
2341 save_commits_by_author
(@lines) if ($interactive);
2342 save_commits_by_signer
(@lines) if ($interactive);
2344 push(@signers, @authors);
2347 foreach my $commit (@commits) {
2349 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
2350 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
2351 my @author = vcs_find_author
($cmd);
2354 my $formatted_author = deduplicate_email
($author[0]);
2356 my $count = grep(/$commit/, @all_commits);
2357 for ($i = 0; $i < $count ; $i++) {
2358 push(@blame_signers, $formatted_author);
2362 if (@blame_signers) {
2363 vcs_assign
("authored lines", $total_lines, @blame_signers);
2366 foreach my $signer (@signers) {
2367 $signer = deduplicate_email
($signer);
2369 vcs_assign
("commits", $total_commits, @signers);
2371 foreach my $signer (@signers) {
2372 $signer = deduplicate_email
($signer);
2374 vcs_assign
("modified commits", $total_commits, @signers);
2378 sub vcs_file_exists
{
2383 my $vcs_used = vcs_exists
();
2384 return 0 if (!$vcs_used);
2386 my $cmd = $VCS_cmds{"file_exists_cmd"};
2387 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2389 $exists = &{$VCS_cmds{"execute_cmd"}}($cmd);
2391 return 0 if ($?
!= 0);
2396 sub vcs_list_files
{
2401 my $vcs_used = vcs_exists
();
2402 return 0 if (!$vcs_used);
2404 my $cmd = $VCS_cmds{"list_files_cmd"};
2405 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2406 @lsfiles = &{$VCS_cmds{"execute_cmd"}}($cmd);
2408 return () if ($?
!= 0);
2417 @parms = grep(!$saw{$_}++, @parms);
2425 @parms = sort @parms;
2426 @parms = grep(!$saw{$_}++, @parms);
2430 sub clean_file_emails
{
2431 my (@file_emails) = @_;
2432 my @fmt_emails = ();
2434 foreach my $email (@file_emails) {
2435 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
2436 my ($name, $address) = parse_email
($email);
2437 if ($name eq '"[,\.]"') {
2441 my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name);
2443 my $first = $nw[@nw - 3];
2444 my $middle = $nw[@nw - 2];
2445 my $last = $nw[@nw - 1];
2447 if (((length($first) == 1 && $first =~ m/[A-Za-z]/) ||
2448 (length($first) == 2 && substr($first, -1) eq ".")) ||
2449 (length($middle) == 1 ||
2450 (length($middle) == 2 && substr($middle, -1) eq "."))) {
2451 $name = "$first $middle $last";
2453 $name = "$middle $last";
2457 if (substr($name, -1) =~ /[,\.]/) {
2458 $name = substr($name, 0, length($name) - 1);
2459 } elsif (substr($name, -2) =~ /[,\.]"/) {
2460 $name = substr($name, 0, length($name) - 2) . '"';
2463 if (substr($name, 0, 1) =~ /[,\.]/) {
2464 $name = substr($name, 1, length($name) - 1);
2465 } elsif (substr($name, 0, 2) =~ /"[,\.]/) {
2466 $name = '"' . substr($name, 2, length($name) - 2);
2469 my $fmt_email = format_email
($name, $address, $email_usename);
2470 push(@fmt_emails, $fmt_email);
2480 my ($address, $role) = @
$_;
2481 if (!$saw{$address}) {
2482 if ($output_roles) {
2483 push(@lines, "$address ($role)");
2485 push(@lines, $address);
2497 if ($output_multiline) {
2498 foreach my $line (@parms) {
2502 print(join($output_separator, @parms));
2510 # Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
2511 # comment. We must allow for rfc822_lwsp (or comments) after each of these.
2512 # This regexp will only work on addresses which have had comments stripped
2513 # and replaced with rfc822_lwsp.
2515 my $specials = '()<>@,;:\\\\".\\[\\]';
2516 my $controls = '\\000-\\037\\177';
2518 my $dtext = "[^\\[\\]\\r\\\\]";
2519 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
2521 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
2523 # Use zero-width assertion to spot the limit of an atom. A simple
2524 # $rfc822_lwsp* causes the regexp engine to hang occasionally.
2525 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
2526 my $word = "(?:$atom|$quoted_string)";
2527 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
2529 my $sub_domain = "(?:$atom|$domain_literal)";
2530 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
2532 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
2534 my $phrase = "$word*";
2535 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
2536 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
2537 my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
2539 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
2540 my $address = "(?:$mailbox|$group)";
2542 return "$rfc822_lwsp*$address";
2545 sub rfc822_strip_comments
{
2547 # Recursively remove comments, and replace with a single space. The simpler
2548 # regexps in the Email Addressing FAQ are imperfect - they will miss escaped
2549 # chars in atoms, for example.
2551 while ($s =~ s
/^((?
:[^"\\]|\\.)*
2552 (?:"(?
:[^"\\]|\\.)*"(?
:[^"\\]|\\.)*)*)
2553 \((?:[^()\\]|\\.)*\)/$1 /osx) {}
2557 # valid: returns true if the parameter is an RFC822 valid address
2560 my $s = rfc822_strip_comments(shift);
2563 $rfc822re = make_rfc822re();
2566 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
2569 # validlist: In scalar context, returns true if the parameter is an RFC822
2570 # valid list of addresses.
2572 # In list context, returns an empty list on failure (an invalid
2573 # address was found); otherwise a list whose first element is the
2574 # number of addresses found and whose remaining elements are the
2575 # addresses. This is needed to disambiguate failure (invalid)
2576 # from success with no addresses found, because an empty string is
2579 sub rfc822_validlist {
2580 my $s = rfc822_strip_comments(shift);
2583 $rfc822re = make_rfc822re();
2585 # * null list items are valid according to the RFC
2586 # * the '1' business is to aid in distinguishing failure from no results
2589 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
2590 $s =~ m/^$rfc822_char*$/) {
2591 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
2594 return wantarray ? (scalar(@r), @r) : 1;
2596 return wantarray ? () : 0;