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);
22 use File::Spec::Functions;
23 use open qw(:std :encoding(UTF-8));
25 my $cur_path = fastgetcwd() . '/';
28 my $email_usename = 1;
29 my $email_maintainer = 1;
30 my $email_reviewer = 1;
33 my $email_moderated_list = 1;
34 my $email_subscriber_list = 0;
35 my $email_git_penguin_chiefs = 0;
37 my $email_git_all_signature_types = 0;
38 my $email_git_blame = 0;
39 my $email_git_blame_signatures = 1;
40 my $email_git_fallback = 1;
41 my $email_git_min_signatures = 1;
42 my $email_git_max_maintainers = 5;
43 my $email_git_min_percent = 5;
44 my $email_git_since = "1-year-ago";
45 my $email_hg_since = "-365";
47 my $email_remove_duplicates = 1;
48 my $email_use_mailmap = 1;
49 my $output_multiline = 1;
50 my $output_separator = ", ";
52 my $output_rolestats = 1;
53 my $output_substatus = undef;
54 my $output_section_maxlen = 50;
63 my $keywords_in_file = 0;
65 my $email_file_emails = 0;
66 my $from_filename = 0;
67 my $pattern_depth = 0;
68 my $self_test = undef;
71 my $find_maintainer_files = 0;
78 my @fixes = (); # If a patch description includes Fixes: lines
83 my %commit_author_hash;
84 my %commit_signer_hash;
86 my @penguin_chief = ();
87 push(@penguin_chief, "Linus Torvalds:torvalds\@linux-foundation.org");
88 #Andrew wants in on most everything - 2009/01/14
89 #push(@penguin_chief, "Andrew Morton:akpm\@linux-foundation.org");
91 my @penguin_chief_names = ();
92 foreach my $chief (@penguin_chief) {
93 if ($chief =~ m/^(.*):(.*)/) {
96 push(@penguin_chief_names, $chief_name);
99 my $penguin_chiefs = "\(" . join("|", @penguin_chief_names) . "\)";
101 # Signature types of people who are either
102 # a) responsible for the code in question, or
103 # b) familiar enough with it to give relevant feedback
104 my @signature_tags = ();
105 push(@signature_tags, "Signed-off-by:");
106 push(@signature_tags, "Reviewed-by:");
107 push(@signature_tags, "Acked-by:");
109 my $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
111 # rfc822 email address - preloaded methods go here.
112 my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
113 my $rfc822_char = '[\\000-\\377]';
115 # VCS command support: class-like functions and strings
120 "execute_cmd" => \&git_execute_cmd,
121 "available" => '(which("git") ne "") && (-e ".git")',
122 "find_signers_cmd" =>
123 "git log --no-color --follow --since=\$email_git_since " .
124 '--numstat --no-merges ' .
125 '--format="GitCommit: %H%n' .
126 'GitAuthor: %an <%ae>%n' .
131 "find_commit_signers_cmd" =>
132 "git log --no-color " .
134 '--format="GitCommit: %H%n' .
135 'GitAuthor: %an <%ae>%n' .
140 "find_commit_author_cmd" =>
141 "git log --no-color " .
143 '--format="GitCommit: %H%n' .
144 'GitAuthor: %an <%ae>%n' .
146 'GitSubject: %s%n"' .
148 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
149 "blame_file_cmd" => "git blame -l \$file",
150 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})",
151 "blame_commit_pattern" => "^([0-9a-f]+) ",
152 "author_pattern" => "^GitAuthor: (.*)",
153 "subject_pattern" => "^GitSubject: (.*)",
154 "stat_pattern" => "^(\\d+)\\t(\\d+)\\t\$file\$",
155 "file_exists_cmd" => "git ls-files \$file",
156 "list_files_cmd" => "git ls-files \$file",
160 "execute_cmd" => \&hg_execute_cmd,
161 "available" => '(which("hg") ne "") && (-d ".hg")',
162 "find_signers_cmd" =>
163 "hg log --date=\$email_hg_since " .
164 "--template='HgCommit: {node}\\n" .
165 "HgAuthor: {author}\\n" .
166 "HgSubject: {desc}\\n'" .
168 "find_commit_signers_cmd" =>
170 "--template='HgSubject: {desc}\\n'" .
172 "find_commit_author_cmd" =>
174 "--template='HgCommit: {node}\\n" .
175 "HgAuthor: {author}\\n" .
176 "HgSubject: {desc|firstline}\\n'" .
178 "blame_range_cmd" => "", # not supported
179 "blame_file_cmd" => "hg blame -n \$file",
180 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})",
181 "blame_commit_pattern" => "^([ 0-9a-f]+):",
182 "author_pattern" => "^HgAuthor: (.*)",
183 "subject_pattern" => "^HgSubject: (.*)",
184 "stat_pattern" => "^(\\d+)\t(\\d+)\t\$file\$",
185 "file_exists_cmd" => "hg files \$file",
186 "list_files_cmd" => "hg manifest -R \$file",
189 my $conf = which_conf(".get_maintainer.conf");
192 open(my $conffile, '<', "$conf")
193 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
195 while (<$conffile>) {
198 $line =~ s/\s*\n?$//g;
202 next if ($line =~ m/^\s*#/);
203 next if ($line =~ m/^\s*$/);
205 my @words = split(" ", $line);
206 foreach my $word (@words) {
207 last if ($word =~ m/^#/);
208 push (@conf_args, $word);
212 unshift(@ARGV, @conf_args) if @conf_args;
215 my @ignore_emails = ();
216 my $ignore_file = which_conf(".get_maintainer.ignore");
217 if (-f $ignore_file) {
218 open(my $ignore, '<', "$ignore_file")
219 or warn "$P: Can't find a readable .get_maintainer.ignore file $!\n";
223 $line =~ s/\s*\n?$//;
228 next if ($line =~ m/^\s*$/);
229 if (rfc822_valid($line)) {
230 push(@ignore_emails, $line);
238 if ($_ =~ /^-{1,2}self-test(?:=|$)/) {
239 die "$P: using --self-test does not allow any other option or argument\n";
246 'git!' => \$email_git,
247 'git-all-signature-types!' => \$email_git_all_signature_types,
248 'git-blame!' => \$email_git_blame,
249 'git-blame-signatures!' => \$email_git_blame_signatures,
250 'git-fallback!' => \$email_git_fallback,
251 'git-chief-penguins!' => \$email_git_penguin_chiefs,
252 'git-min-signatures=i' => \$email_git_min_signatures,
253 'git-max-maintainers=i' => \$email_git_max_maintainers,
254 'git-min-percent=i' => \$email_git_min_percent,
255 'git-since=s' => \$email_git_since,
256 'hg-since=s' => \$email_hg_since,
257 'i|interactive!' => \$interactive,
258 'remove-duplicates!' => \$email_remove_duplicates,
259 'mailmap!' => \$email_use_mailmap,
260 'm!' => \$email_maintainer,
261 'r!' => \$email_reviewer,
262 'n!' => \$email_usename,
263 'l!' => \$email_list,
264 'fixes!' => \$email_fixes,
265 'moderated!' => \$email_moderated_list,
266 's!' => \$email_subscriber_list,
267 'multiline!' => \$output_multiline,
268 'roles!' => \$output_roles,
269 'rolestats!' => \$output_rolestats,
270 'separator=s' => \$output_separator,
271 'subsystem!' => \$subsystem,
272 'status!' => \$status,
273 'substatus!' => \$output_substatus,
278 'letters=s' => \$letters,
279 'pattern-depth=i' => \$pattern_depth,
280 'k|keywords!' => \$keywords,
281 'kf|keywords-in-file!' => \$keywords_in_file,
282 'sections!' => \$sections,
283 'fe|file-emails!' => \$email_file_emails,
284 'f|file' => \$from_filename,
285 'find-maintainer-files' => \$find_maintainer_files,
286 'mpath|maintainer-path=s' => \$maintainer_path,
287 'self-test:s' => \$self_test,
288 'v|version' => \$version,
289 'h|help|usage' => \$help,
291 die "$P: invalid argument - use --help if necessary\n";
300 print("${P} ${V}\n");
304 if (defined $self_test) {
305 read_all_maintainer_files();
310 if (-t STDIN && !@ARGV) {
311 # We're talking to a terminal, but have no command line arguments.
312 die "$P: missing patchfile or -f file - use --help if necessary\n";
315 $output_multiline = 0 if ($output_separator ne ", ");
316 $output_rolestats = 1 if ($interactive);
317 $output_roles = 1 if ($output_rolestats);
319 if (!defined $output_substatus) {
320 $output_substatus = $email && $output_roles && -t STDOUT;
323 if ($sections || $letters ne "") {
333 $keywords_in_file = 0;
336 my $selections = $email + $scm + $status + $subsystem + $web + $bug;
337 if ($selections == 0) {
338 die "$P: Missing required option: email, scm, status, subsystem, web or bug\n";
343 ($email_maintainer + $email_reviewer +
344 $email_list + $email_subscriber_list +
345 $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
346 die "$P: Please select at least 1 email option\n";
349 if ($tree && !top_of_kernel_tree($lk_path)) {
350 die "$P: The current directory does not appear to be "
351 . "a linux kernel source tree.\n";
354 ## Read MAINTAINERS for type/value pairs
359 my @self_test_info = ();
361 sub read_maintainer_file {
364 open (my $maint, '<', "$file")
365 or die "$P: Can't open MAINTAINERS file '$file': $!\n";
371 if ($line =~ m/^([A-Z]):\s*(.*)/) {
375 ##Filename pattern matching
376 if ($type eq "F" || $type eq "X") {
377 $value =~ s@\.@\\\.@g; ##Convert . to \.
378 $value =~ s/\*/\.\*/g; ##Convert * to .*
379 $value =~ s/\?/\./g; ##Convert ? to .
380 ##if pattern is a directory and it lacks a trailing slash, add one
382 $value =~ s@([^/])$@$1/@;
384 } elsif ($type eq "K") {
385 $keyword_hash{@typevalue} = $value;
387 push(@typevalue, "$type:$value");
388 } elsif (!(/^\s*$/ || /^\s*\#/)) {
389 push(@typevalue, $line);
391 if (defined $self_test) {
392 push(@self_test_info, {file=>$file, linenr=>$i, line=>$line});
399 sub find_is_maintainer_file {
401 return if ($file !~ m@/MAINTAINERS$@);
402 $file = $File::Find::name;
403 return if (! -f $file);
404 push(@mfiles, $file);
407 sub find_ignore_git {
408 return grep { $_ !~ /^\.git$/; } @_;
411 read_all_maintainer_files();
413 sub read_all_maintainer_files {
414 my $path = "${lk_path}MAINTAINERS";
415 if (defined $maintainer_path) {
416 $path = $maintainer_path;
417 # Perl Cookbook tilde expansion if necessary
418 $path =~ s@^~([^/]*)@ $1 ? (getpwnam($1))[7] : ( $ENV{HOME} || $ENV{LOGDIR} || (getpwuid($<))[7])@ex;
422 $path .= '/' if ($path !~ m@/$@);
423 if ($find_maintainer_files) {
424 find( { wanted => \&find_is_maintainer_file,
425 preprocess => \&find_ignore_git,
429 opendir(DIR, "$path") or die $!;
430 my @files = readdir(DIR);
432 foreach my $file (@files) {
433 push(@mfiles, "$path$file") if ($file !~ /^\./);
436 } elsif (-f "$path") {
437 push(@mfiles, "$path");
439 die "$P: MAINTAINER file not found '$path'\n";
441 die "$P: No MAINTAINER files found in '$path'\n" if (scalar(@mfiles) == 0);
442 foreach my $file (@mfiles) {
443 read_maintainer_file("$file");
447 sub maintainers_in_file {
450 return if ($file =~ m@\bMAINTAINERS$@);
452 if (-f $file && ($email_file_emails || $file =~ /\.yaml$/)) {
453 open(my $f, '<', $file)
454 or die "$P: Can't open $file: $!\n";
455 my $text = do { local($/) ; <$f> };
458 my @poss_addr = $text =~ m$[\p{L}\"\' \,\.\+-]*\s*[\,]*\s*[\(\<\{]{0,1}[A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+\.[A-Za-z0-9]+[\)\>\}]{0,1}$g;
459 push(@file_emails, clean_file_emails(@poss_addr));
464 # Read mail address map
477 return if (!$email_use_mailmap || !(-f "${lk_path}.mailmap"));
479 open(my $mailmap_file, '<', "${lk_path}.mailmap")
480 or warn "$P: Can't open .mailmap: $!\n";
482 while (<$mailmap_file>) {
483 s/#.*$//; #strip comments
484 s/^\s+|\s+$//g; #trim
486 next if (/^\s*$/); #skip empty lines
487 #entries have one of the following formats:
490 # name1 <mail1> <mail2>
491 # name1 <mail1> name2 <mail2>
492 # (see man git-shortlog)
494 if (/^([^<]+)<([^>]+)>$/) {
498 $real_name =~ s/\s+$//;
499 ($real_name, $address) = parse_email("$real_name <$address>");
500 $mailmap->{names}->{$address} = $real_name;
502 } elsif (/^<([^>]+)>\s*<([^>]+)>$/) {
503 my $real_address = $1;
504 my $wrong_address = $2;
506 $mailmap->{addresses}->{$wrong_address} = $real_address;
508 } elsif (/^(.+)<([^>]+)>\s*<([^>]+)>$/) {
510 my $real_address = $2;
511 my $wrong_address = $3;
513 $real_name =~ s/\s+$//;
514 ($real_name, $real_address) =
515 parse_email("$real_name <$real_address>");
516 $mailmap->{names}->{$wrong_address} = $real_name;
517 $mailmap->{addresses}->{$wrong_address} = $real_address;
519 } elsif (/^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$/) {
521 my $real_address = $2;
523 my $wrong_address = $4;
525 $real_name =~ s/\s+$//;
526 ($real_name, $real_address) =
527 parse_email("$real_name <$real_address>");
529 $wrong_name =~ s/\s+$//;
530 ($wrong_name, $wrong_address) =
531 parse_email("$wrong_name <$wrong_address>");
533 my $wrong_email = format_email($wrong_name, $wrong_address, 1);
534 $mailmap->{names}->{$wrong_email} = $real_name;
535 $mailmap->{addresses}->{$wrong_email} = $real_address;
538 close($mailmap_file);
541 ## use the filenames on the command line or find the filenames in the patchfiles
544 push(@ARGV, "&STDIN");
547 foreach my $file (@ARGV) {
548 if ($file ne "&STDIN") {
549 $file = canonpath($file);
550 ##if $file is a directory and it lacks a trailing slash, add one
552 $file =~ s@([^/])$@$1/@;
553 } elsif (!(-f $file)) {
554 die "$P: file '${file}' not found\n";
557 if ($from_filename && (vcs_exists() && !vcs_file_exists($file))) {
558 warn "$P: file '$file' not found in version control $!\n";
560 if ($from_filename || ($file ne "&STDIN" && vcs_file_exists($file))) {
561 $file =~ s/^\Q${cur_path}\E//; #strip any absolute path
562 $file =~ s/^\Q${lk_path}\E//; #or the path to the lk tree
564 if ($file ne "MAINTAINERS" && -f $file && $keywords && $keywords_in_file) {
565 open(my $f, '<', $file)
566 or die "$P: Can't open $file: $!\n";
567 my $text = do { local($/) ; <$f> };
569 foreach my $line (keys %keyword_hash) {
570 if ($text =~ m/$keyword_hash{$line}/x) {
571 push(@keyword_tvi, $line);
576 my $file_cnt = @files;
579 open(my $patch, "< $file")
580 or die "$P: Can't open $file: $!\n";
582 # We can check arbitrary information before the patch
583 # like the commit message, mail headers, etc...
584 # This allows us to match arbitrary keywords against any part
585 # of a git format-patch generated file (subject tags, etc...)
587 my $patch_prefix = ""; #Parsing the intro
591 if (m/^ mode change [0-7]+ => [0-7]+ (\S+)\s*$/) {
593 push(@files, $filename);
594 } elsif (m/^rename (?:from|to) (\S+)\s*$/) {
596 push(@files, $filename);
597 } elsif (m/^diff --git a\/(\S+) b\/(\S+)\s*$/) {
600 push(@files, $filename1);
601 push(@files, $filename2);
602 } elsif (m/^Fixes:\s+([0-9a-fA-F]{6,40})/) {
603 push(@fixes, $1) if ($email_fixes);
604 } elsif (m/^\+\+\+\s+(\S+)/ or m/^---\s+(\S+)/) {
606 $filename =~ s@^[^/]*/@@;
608 $lastfile = $filename;
609 push(@files, $filename);
610 $patch_prefix = "^[+-].*"; #Now parsing the actual patch
611 } elsif (m/^\@\@ -(\d+),(\d+)/) {
612 if ($email_git_blame) {
613 push(@range, "$lastfile:$1:$2");
615 } elsif ($keywords) {
616 foreach my $line (keys %keyword_hash) {
617 if ($patch_line =~ m/${patch_prefix}$keyword_hash{$line}/x) {
618 push(@keyword_tvi, $line);
625 if ($file_cnt == @files) {
626 warn "$P: file '${file}' doesn't appear to be a patch. "
627 . "Add -f to options?\n";
629 @files = sort_and_uniq(@files);
633 @file_emails = uniq(@file_emails);
634 @fixes = uniq(@fixes);
637 my %email_hash_address;
647 my %deduplicate_name_hash = ();
648 my %deduplicate_address_hash = ();
650 my @maintainers = get_maintainers();
652 @maintainers = merge_email(@maintainers);
653 output(@maintainers);
661 if ($output_substatus) {
662 @substatus = uniq(@substatus);
667 @status = uniq(@status);
672 @subsystem = uniq(@subsystem);
692 my @section_headers = ();
695 @lsfiles = vcs_list_files($lk_path);
697 for my $x (@self_test_info) {
700 ## Section header duplication and missing section content
701 if (($self_test eq "" || $self_test =~ /\bsections\b/) &&
702 $x->{line} =~ /^\S[^:]/ &&
703 defined $self_test_info[$index] &&
704 $self_test_info[$index]->{line} =~ /^([A-Z]):\s*\S/) {
709 if (grep(m@^\Q$x->{line}\E@, @section_headers)) {
710 print("$x->{file}:$x->{linenr}: warning: duplicate section header\t$x->{line}\n");
712 push(@section_headers, $x->{line});
714 my $nextline = $index;
715 while (defined $self_test_info[$nextline] &&
716 $self_test_info[$nextline]->{line} =~ /^([A-Z]):\s*(\S.*)/) {
722 } elsif ($type eq "F" || $type eq "N") {
724 } elsif ($type eq "M" || $type eq "R" || $type eq "L") {
729 if (!$has_ML && $status !~ /orphan|obsolete/i) {
730 print("$x->{file}:$x->{linenr}: warning: section without email address\t$x->{line}\n");
733 print("$x->{file}:$x->{linenr}: warning: section without status \t$x->{line}\n");
736 print("$x->{file}:$x->{linenr}: warning: section without file pattern\t$x->{line}\n");
740 next if ($x->{line} !~ /^([A-Z]):\s*(.*)/);
745 ## Filename pattern matching
746 if (($type eq "F" || $type eq "X") &&
747 ($self_test eq "" || $self_test =~ /\bpatterns\b/)) {
748 $value =~ s@\.@\\\.@g; ##Convert . to \.
749 $value =~ s/\*/\.\*/g; ##Convert * to .*
750 $value =~ s/\?/\./g; ##Convert ? to .
751 ##if pattern is a directory and it lacks a trailing slash, add one
753 $value =~ s@([^/])$@$1/@;
755 if (!grep(m@^$value@, @lsfiles)) {
756 print("$x->{file}:$x->{linenr}: warning: no file matches\t$x->{line}\n");
760 } elsif (($type eq "W" || $type eq "Q" || $type eq "B") &&
761 $value =~ /^https?:/ &&
762 ($self_test eq "" || $self_test =~ /\blinks\b/)) {
763 next if (grep(m@^\Q$value\E$@, @good_links));
765 if (grep(m@^\Q$value\E$@, @bad_links)) {
768 my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $value`;
770 push(@good_links, $value);
772 push(@bad_links, $value);
777 print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
781 } elsif ($type eq "T" &&
782 ($self_test eq "" || $self_test =~ /\bscm\b/)) {
783 next if (grep(m@^\Q$value\E$@, @good_links));
785 if (grep(m@^\Q$value\E$@, @bad_links)) {
787 } elsif ($value !~ /^(?:git|quilt|hg)\s+\S/) {
788 print("$x->{file}:$x->{linenr}: warning: malformed entry\t$x->{line}\n");
789 } elsif ($value =~ /^git\s+(\S+)(\s+([^\(]+\S+))?/) {
793 my $output = `git ls-remote --exit-code -h "$url" $branch > /dev/null 2>&1`;
795 push(@good_links, $value);
797 push(@bad_links, $value);
800 } elsif ($value =~ /^(?:quilt|hg)\s+(https?:\S+)/) {
802 my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $url`;
804 push(@good_links, $value);
806 push(@bad_links, $value);
811 print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
817 sub ignore_email_address {
820 foreach my $ignore (@ignore_emails) {
821 return 1 if ($ignore eq $address);
827 sub range_is_maintained {
828 my ($start, $end) = @_;
830 for (my $i = $start; $i < $end; $i++) {
831 my $line = $typevalue[$i];
832 if ($line =~ m/^([A-Z]):\s*(.*)/) {
836 if ($value =~ /(maintain|support)/i) {
845 sub range_has_maintainer {
846 my ($start, $end) = @_;
848 for (my $i = $start; $i < $end; $i++) {
849 my $line = $typevalue[$i];
850 if ($line =~ m/^([A-Z]):\s*(.*)/) {
861 sub get_maintainers {
862 %email_hash_name = ();
863 %email_hash_address = ();
864 %commit_author_hash = ();
865 %commit_signer_hash = ();
875 %deduplicate_name_hash = ();
876 %deduplicate_address_hash = ();
877 if ($email_git_all_signature_types) {
878 $signature_pattern = "(.+?)[Bb][Yy]:";
880 $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
883 # Find responsible parties
885 my %exact_pattern_match_hash = ();
887 foreach my $file (@files) {
890 my $tvi = find_first_section();
891 while ($tvi < @typevalue) {
892 my $start = find_starting_index($tvi);
893 my $end = find_ending_index($tvi);
897 #Do not match excluded file patterns
899 for ($i = $start; $i < $end; $i++) {
900 my $line = $typevalue[$i];
901 if ($line =~ m/^([A-Z]):\s*(.*)/) {
905 if (file_match_pattern($file, $value)) {
914 for ($i = $start; $i < $end; $i++) {
915 my $line = $typevalue[$i];
916 if ($line =~ m/^([A-Z]):\s*(.*)/) {
920 if (file_match_pattern($file, $value)) {
921 my $value_pd = ($value =~ tr@/@@);
922 my $file_pd = ($file =~ tr@/@@);
923 $value_pd++ if (substr($value,-1,1) ne "/");
924 $value_pd = -1 if ($value =~ /^\.\*/);
925 if ($value_pd >= $file_pd &&
926 range_is_maintained($start, $end) &&
927 range_has_maintainer($start, $end)) {
928 $exact_pattern_match_hash{$file} = 1;
930 if ($pattern_depth == 0 ||
931 (($file_pd - $value_pd) < $pattern_depth)) {
932 $hash{$tvi} = $value_pd;
935 } elsif ($type eq 'N') {
936 if ($file =~ m/$value/x) {
946 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
947 add_categories($line, "");
950 my $start = find_starting_index($line);
951 my $end = find_ending_index($line);
952 for ($i = $start; $i < $end; $i++) {
953 my $line = $typevalue[$i];
954 if ($line =~ /^[FX]:/) { ##Restore file patterns
955 $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
956 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ?
957 $line =~ s/\\\./\./g; ##Convert \. to .
958 $line =~ s/\.\*/\*/g; ##Convert .* to *
960 my $count = $line =~ s/^([A-Z]):/$1:\t/g;
961 if ($letters eq "" || (!$count || $letters =~ /$1/i)) {
969 maintainers_in_file($file);
973 @keyword_tvi = sort_and_uniq(@keyword_tvi);
974 foreach my $line (@keyword_tvi) {
975 add_categories($line, ":Keyword:$keyword_hash{$line}");
979 foreach my $email (@email_to, @list_to) {
980 $email->[0] = deduplicate_email($email->[0]);
983 foreach my $file (@files) {
986 ($email_git_fallback &&
987 $file !~ /MAINTAINERS$/ &&
988 !$exact_pattern_match_hash{$file}))) {
989 vcs_file_signoffs($file);
991 if ($email && $email_git_blame) {
992 vcs_file_blame($file);
997 foreach my $chief (@penguin_chief) {
998 if ($chief =~ m/^(.*):(.*)/) {
1001 $email_address = format_email($1, $2, $email_usename);
1002 if ($email_git_penguin_chiefs) {
1003 push(@email_to, [$email_address, 'chief penguin']);
1005 @email_to = grep($_->[0] !~ /${email_address}/, @email_to);
1010 foreach my $email (@file_emails) {
1011 $email = mailmap_email($email);
1012 my ($name, $address) = parse_email($email);
1014 my $tmp_email = format_email($name, $address, $email_usename);
1015 push_email_address($tmp_email, '');
1016 add_role($tmp_email, 'in file');
1020 foreach my $fix (@fixes) {
1021 vcs_add_commit_signers($fix, "blamed_fixes");
1025 if ($email || $email_list) {
1027 @to = (@to, @email_to);
1030 @to = (@to, @list_to);
1035 @to = interactive_get_maintainers(\@to);
1041 sub file_match_pattern {
1042 my ($file, $pattern) = @_;
1043 if (substr($pattern, -1) eq "/") {
1044 if ($file =~ m@^$pattern@) {
1048 if ($file =~ m@^$pattern@) {
1049 my $s1 = ($file =~ tr@/@@);
1050 my $s2 = ($pattern =~ tr@/@@);
1061 usage: $P [options] patchfile
1062 $P [options] -f file|directory
1065 MAINTAINER field selection options:
1066 --email => print email address(es) if any
1067 --git => include recent git \*-by: signers
1068 --git-all-signature-types => include signers regardless of signature type
1069 or use only ${signature_pattern} signers (default: $email_git_all_signature_types)
1070 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
1071 --git-chief-penguins => include ${penguin_chiefs}
1072 --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
1073 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
1074 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
1075 --git-blame => use git blame to find modified commits for patch or file
1076 --git-blame-signatures => when used with --git-blame, also include all commit signers
1077 --git-since => git history to use (default: $email_git_since)
1078 --hg-since => hg history to use (default: $email_hg_since)
1079 --interactive => display a menu (mostly useful if used with the --git option)
1080 --m => include maintainer(s) if any
1081 --r => include reviewer(s) if any
1082 --n => include name 'Full Name <addr\@domain.tld>'
1083 --l => include list(s) if any
1084 --moderated => include moderated lists(s) if any (default: true)
1085 --s => include subscriber only list(s) if any (default: false)
1086 --remove-duplicates => minimize duplicate email names/addresses
1087 --roles => show roles (role:subsystem, git-signer, list, etc...)
1088 --rolestats => show roles and statistics (commits/total_commits, %)
1089 --substatus => show subsystem status if not Maintained (default: match --roles when output is tty)"
1090 --file-emails => add email addresses found in -f file (default: 0 (off))
1091 --fixes => for patches, add signatures of commits with 'Fixes: <commit>' (default: 1 (on))
1092 --scm => print SCM tree(s) if any
1093 --status => print status if any
1094 --subsystem => print subsystem name if any
1095 --web => print website(s) if any
1096 --bug => print bug reporting info if any
1098 Output type options:
1099 --separator [, ] => separator for multiple entries on 1 line
1100 using --separator also sets --nomultiline if --separator is not [, ]
1101 --multiline => print 1 entry per line
1104 --pattern-depth => Number of pattern directory traversals (default: 0 (all))
1105 --keywords => scan patch for keywords (default: $keywords)
1106 --keywords-in-file => scan file for keywords (default: $keywords_in_file)
1107 --sections => print all of the subsystem sections with pattern matches
1108 --letters => print all matching 'letter' types from all matching sections
1109 --mailmap => use .mailmap file (default: $email_use_mailmap)
1110 --no-tree => run without a kernel tree
1111 --self-test => show potential issues with MAINTAINERS file content
1112 --version => show version
1113 --help => show this help information
1116 [--email --tree --nogit --git-fallback --m --r --n --l --multiline
1117 --pattern-depth=0 --remove-duplicates --rolestats --keywords]
1120 Using "-f directory" may give unexpected results:
1121 Used with "--git", git signators for _all_ files in and below
1122 directory are examined as git recurses directories.
1123 Any specified X: (exclude) pattern matches are _not_ ignored.
1124 Used with "--nogit", directory is used as a pattern match,
1125 no individual file within the directory or subdirectory
1127 Used with "--git-blame", does not iterate all files in directory
1128 Using "--git-blame" is slow and may add old committers and authors
1129 that are no longer active maintainers to the output.
1130 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
1131 other automated tools that expect only ["name"] <email address>
1132 may not work because of additional output after <email address>.
1133 Using "--rolestats" and "--git-blame" shows the #/total=% commits,
1134 not the percentage of the entire file authored. # of commits is
1135 not a good measure of amount of code authored. 1 major commit may
1136 contain a thousand lines, 5 trivial commits may modify a single line.
1137 If git is not installed, but mercurial (hg) is installed and an .hg
1138 repository exists, the following options apply to mercurial:
1140 --git-min-signatures, --git-max-maintainers, --git-min-percent, and
1142 Use --hg-since not --git-since to control date selection
1143 File ".get_maintainer.conf", if it exists in the linux kernel source root
1144 directory, can change whatever get_maintainer defaults are desired.
1145 Entries in this file can be any command line argument.
1146 This file is prepended to any additional command line arguments.
1147 Multiple lines and # comments are allowed.
1148 Most options have both positive and negative forms.
1149 The negative forms for --<foo> are --no<foo> and --no-<foo>.
1154 sub top_of_kernel_tree {
1157 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
1160 if ( (-f "${lk_path}COPYING")
1161 && (-f "${lk_path}CREDITS")
1162 && (-f "${lk_path}Kbuild")
1163 && (-e "${lk_path}MAINTAINERS")
1164 && (-f "${lk_path}Makefile")
1165 && (-f "${lk_path}README")
1166 && (-d "${lk_path}Documentation")
1167 && (-d "${lk_path}arch")
1168 && (-d "${lk_path}include")
1169 && (-d "${lk_path}drivers")
1170 && (-d "${lk_path}fs")
1171 && (-d "${lk_path}init")
1172 && (-d "${lk_path}ipc")
1173 && (-d "${lk_path}kernel")
1174 && (-d "${lk_path}lib")
1175 && (-d "${lk_path}scripts")) {
1184 if ($name =~ /[^\w \-]/ai) { ##has "must quote" chars
1185 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
1186 $name = "\"$name\"";
1193 my ($formatted_email) = @_;
1198 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
1201 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
1203 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
1207 $name =~ s/^\s+|\s+$//g;
1208 $name =~ s/^\"|\"$//g;
1209 $name = escape_name($name);
1210 $address =~ s/^\s+|\s+$//g;
1212 return ($name, $address);
1216 my ($name, $address, $usename) = @_;
1218 my $formatted_email;
1220 $name =~ s/^\s+|\s+$//g;
1221 $name =~ s/^\"|\"$//g;
1222 $name = escape_name($name);
1223 $address =~ s/^\s+|\s+$//g;
1226 if ("$name" eq "") {
1227 $formatted_email = "$address";
1229 $formatted_email = "$name <$address>";
1232 $formatted_email = $address;
1235 return $formatted_email;
1238 sub find_first_section {
1241 while ($index < @typevalue) {
1242 my $tv = $typevalue[$index];
1243 if (($tv =~ m/^([A-Z]):\s*(.*)/)) {
1252 sub find_starting_index {
1255 while ($index > 0) {
1256 my $tv = $typevalue[$index];
1257 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
1266 sub find_ending_index {
1269 while ($index < @typevalue) {
1270 my $tv = $typevalue[$index];
1271 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
1280 sub get_subsystem_name {
1283 my $start = find_starting_index($index);
1285 my $subsystem = $typevalue[$start];
1286 if ($output_section_maxlen && length($subsystem) > $output_section_maxlen) {
1287 $subsystem = substr($subsystem, 0, $output_section_maxlen - 3);
1288 $subsystem =~ s/\s*$//;
1289 $subsystem = $subsystem . "...";
1294 sub get_maintainer_role {
1298 my $start = find_starting_index($index);
1299 my $end = find_ending_index($index);
1301 my $role = "maintainer";
1302 my $subsystem = get_subsystem_name($index);
1303 my $status = "unknown";
1305 for ($i = $start + 1; $i < $end; $i++) {
1306 my $tv = $typevalue[$i];
1307 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1310 if ($ptype eq "S") {
1316 $status = lc($status);
1317 if ($status eq "buried alive in reporters") {
1318 $role = "chief penguin";
1321 return $role . ":" . $subsystem;
1327 my $subsystem = get_subsystem_name($index);
1329 if ($subsystem eq "THE REST") {
1336 sub add_categories {
1337 my ($index, $suffix) = @_;
1340 my $start = find_starting_index($index);
1341 my $end = find_ending_index($index);
1343 my $subsystem = $typevalue[$start];
1344 push(@subsystem, $subsystem);
1345 my $status = "Unknown";
1347 for ($i = $start + 1; $i < $end; $i++) {
1348 my $tv = $typevalue[$i];
1349 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1352 if ($ptype eq "L") {
1353 my $list_address = $pvalue;
1354 my $list_additional = "";
1355 my $list_role = get_list_role($i);
1357 if ($list_role ne "") {
1358 $list_role = ":" . $list_role;
1360 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
1362 $list_additional = $2;
1364 if ($list_additional =~ m/subscribers-only/) {
1365 if ($email_subscriber_list) {
1366 if (!$hash_list_to{lc($list_address)}) {
1367 $hash_list_to{lc($list_address)} = 1;
1368 push(@list_to, [$list_address,
1369 "subscriber list${list_role}" . $suffix]);
1374 if (!$hash_list_to{lc($list_address)}) {
1375 if ($list_additional =~ m/moderated/) {
1376 if ($email_moderated_list) {
1377 $hash_list_to{lc($list_address)} = 1;
1378 push(@list_to, [$list_address,
1379 "moderated list${list_role}" . $suffix]);
1382 $hash_list_to{lc($list_address)} = 1;
1383 push(@list_to, [$list_address,
1384 "open list${list_role}" . $suffix]);
1389 } elsif ($ptype eq "M") {
1390 if ($email_maintainer) {
1391 my $role = get_maintainer_role($i);
1392 push_email_addresses($pvalue, $role . $suffix);
1394 } elsif ($ptype eq "R") {
1395 if ($email_reviewer) {
1396 my $subs = get_subsystem_name($i);
1397 push_email_addresses($pvalue, "reviewer:$subs" . $suffix);
1399 } elsif ($ptype eq "T") {
1400 push(@scm, $pvalue . $suffix);
1401 } elsif ($ptype eq "W") {
1402 push(@web, $pvalue . $suffix);
1403 } elsif ($ptype eq "B") {
1404 push(@bug, $pvalue . $suffix);
1405 } elsif ($ptype eq "S") {
1406 push(@status, $pvalue . $suffix);
1412 if ($subsystem ne "THE REST" and $status ne "Maintained") {
1413 push(@substatus, $subsystem . " status: " . $status . $suffix)
1418 my ($name, $address) = @_;
1420 return 1 if (($name eq "") && ($address eq ""));
1421 return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
1422 return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
1427 sub push_email_address {
1428 my ($line, $role) = @_;
1430 my ($name, $address) = parse_email($line);
1432 if ($address eq "") {
1436 if (!$email_remove_duplicates) {
1437 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1438 } elsif (!email_inuse($name, $address)) {
1439 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1440 $email_hash_name{lc($name)}++ if ($name ne "");
1441 $email_hash_address{lc($address)}++;
1447 sub push_email_addresses {
1448 my ($address, $role) = @_;
1450 my @address_list = ();
1452 if (rfc822_valid($address)) {
1453 push_email_address($address, $role);
1454 } elsif (@address_list = rfc822_validlist($address)) {
1455 my $array_count = shift(@address_list);
1456 while (my $entry = shift(@address_list)) {
1457 push_email_address($entry, $role);
1460 if (!push_email_address($address, $role)) {
1461 warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1467 my ($line, $role) = @_;
1469 my ($name, $address) = parse_email($line);
1470 my $email = format_email($name, $address, $email_usename);
1472 foreach my $entry (@email_to) {
1473 if ($email_remove_duplicates) {
1474 my ($entry_name, $entry_address) = parse_email($entry->[0]);
1475 if (($name eq $entry_name || $address eq $entry_address)
1476 && ($role eq "" || !($entry->[1] =~ m/$role/))
1478 if ($entry->[1] eq "") {
1479 $entry->[1] = "$role";
1481 $entry->[1] = "$entry->[1],$role";
1485 if ($email eq $entry->[0]
1486 && ($role eq "" || !($entry->[1] =~ m/$role/))
1488 if ($entry->[1] eq "") {
1489 $entry->[1] = "$role";
1491 $entry->[1] = "$entry->[1],$role";
1501 foreach my $path (split(/:/, $ENV{PATH})) {
1502 if (-e "$path/$bin") {
1503 return "$path/$bin";
1513 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1514 if (-e "$path/$conf") {
1515 return "$path/$conf";
1525 my ($name, $address) = parse_email($line);
1526 my $email = format_email($name, $address, 1);
1527 my $real_name = $name;
1528 my $real_address = $address;
1530 if (exists $mailmap->{names}->{$email} ||
1531 exists $mailmap->{addresses}->{$email}) {
1532 if (exists $mailmap->{names}->{$email}) {
1533 $real_name = $mailmap->{names}->{$email};
1535 if (exists $mailmap->{addresses}->{$email}) {
1536 $real_address = $mailmap->{addresses}->{$email};
1539 if (exists $mailmap->{names}->{$address}) {
1540 $real_name = $mailmap->{names}->{$address};
1542 if (exists $mailmap->{addresses}->{$address}) {
1543 $real_address = $mailmap->{addresses}->{$address};
1546 return format_email($real_name, $real_address, 1);
1550 my (@addresses) = @_;
1552 my @mapped_emails = ();
1553 foreach my $line (@addresses) {
1554 push(@mapped_emails, mailmap_email($line));
1556 merge_by_realname(@mapped_emails) if ($email_use_mailmap);
1557 return @mapped_emails;
1560 sub merge_by_realname {
1564 foreach my $email (@emails) {
1565 my ($name, $address) = parse_email($email);
1566 if (exists $address_map{$name}) {
1567 $address = $address_map{$name};
1568 $email = format_email($name, $address, 1);
1570 $address_map{$name} = $address;
1575 sub git_execute_cmd {
1579 my $output = `$cmd`;
1580 $output =~ s/^\s*//gm;
1581 @lines = split("\n", $output);
1586 sub hg_execute_cmd {
1590 my $output = `$cmd`;
1591 @lines = split("\n", $output);
1596 sub extract_formatted_signatures {
1597 my (@signature_lines) = @_;
1599 my @type = @signature_lines;
1601 s/\s*(.*):.*/$1/ for (@type);
1604 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1606 ## Reformat email addresses (with names) to avoid badly written signatures
1608 foreach my $signer (@signature_lines) {
1609 $signer = deduplicate_email($signer);
1612 return (\@type, \@signature_lines);
1615 sub vcs_find_signers {
1616 my ($cmd, $file) = @_;
1619 my @signatures = ();
1623 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1625 my $pattern = $VCS_cmds{"commit_pattern"};
1626 my $author_pattern = $VCS_cmds{"author_pattern"};
1627 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1629 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1631 $commits = grep(/$pattern/, @lines); # of commits
1633 @authors = grep(/$author_pattern/, @lines);
1634 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1635 @stats = grep(/$stat_pattern/, @lines);
1637 # print("stats: <@stats>\n");
1639 return (0, \@signatures, \@authors, \@stats) if !@signatures;
1641 save_commits_by_author(@lines) if ($interactive);
1642 save_commits_by_signer(@lines) if ($interactive);
1644 if (!$email_git_penguin_chiefs) {
1645 @signatures = grep(!/${penguin_chiefs}/i, @signatures);
1648 my ($author_ref, $authors_ref) = extract_formatted_signatures(@authors);
1649 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1651 return ($commits, $signers_ref, $authors_ref, \@stats);
1654 sub vcs_find_author {
1658 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1660 if (!$email_git_penguin_chiefs) {
1661 @lines = grep(!/${penguin_chiefs}/i, @lines);
1664 return @lines if !@lines;
1667 foreach my $line (@lines) {
1668 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1670 my ($name, $address) = parse_email($author);
1671 $author = format_email($name, $address, 1);
1672 push(@authors, $author);
1676 save_commits_by_author(@lines) if ($interactive);
1677 save_commits_by_signer(@lines) if ($interactive);
1682 sub vcs_save_commits {
1687 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1689 foreach my $line (@lines) {
1690 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1703 return @commits if (!(-f $file));
1705 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1706 my @all_commits = ();
1708 $cmd = $VCS_cmds{"blame_file_cmd"};
1709 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1710 @all_commits = vcs_save_commits($cmd);
1712 foreach my $file_range_diff (@range) {
1713 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1715 my $diff_start = $2;
1716 my $diff_length = $3;
1717 next if ("$file" ne "$diff_file");
1718 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1719 push(@commits, $all_commits[$i]);
1723 foreach my $file_range_diff (@range) {
1724 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1726 my $diff_start = $2;
1727 my $diff_length = $3;
1728 next if ("$file" ne "$diff_file");
1729 $cmd = $VCS_cmds{"blame_range_cmd"};
1730 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1731 push(@commits, vcs_save_commits($cmd));
1734 $cmd = $VCS_cmds{"blame_file_cmd"};
1735 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1736 @commits = vcs_save_commits($cmd);
1739 foreach my $commit (@commits) {
1740 $commit =~ s/^\^//g;
1746 my $printed_novcs = 0;
1748 %VCS_cmds = %VCS_cmds_git;
1749 return 1 if eval $VCS_cmds{"available"};
1750 %VCS_cmds = %VCS_cmds_hg;
1751 return 2 if eval $VCS_cmds{"available"};
1753 if (!$printed_novcs && $email_git) {
1754 warn("$P: No supported VCS found. Add --nogit to options?\n");
1755 warn("Using a git repository produces better results.\n");
1756 warn("Try Linus Torvalds' latest git repository using:\n");
1757 warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n");
1765 return $vcs_used == 1;
1769 return $vcs_used == 2;
1772 sub vcs_add_commit_signers {
1773 return if (!vcs_exists());
1775 my ($commit, $desc) = @_;
1776 my $commit_count = 0;
1777 my $commit_authors_ref;
1778 my $commit_signers_ref;
1780 my @commit_authors = ();
1781 my @commit_signers = ();
1784 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1785 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1787 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, "");
1788 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
1789 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
1791 foreach my $signer (@commit_signers) {
1792 $signer = deduplicate_email($signer);
1795 vcs_assign($desc, 1, @commit_signers);
1798 sub interactive_get_maintainers {
1799 my ($list_ref) = @_;
1800 my @list = @$list_ref;
1809 foreach my $entry (@list) {
1810 $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
1811 $selected{$count} = 1;
1812 $authored{$count} = 0;
1813 $signed{$count} = 0;
1819 my $print_options = 0;
1824 printf STDERR "\n%1s %2s %-65s",
1825 "*", "#", "email/list and role:stats";
1827 ($email_git_fallback && !$maintained) ||
1829 print STDERR "auth sign";
1832 foreach my $entry (@list) {
1833 my $email = $entry->[0];
1834 my $role = $entry->[1];
1836 $sel = "*" if ($selected{$count});
1837 my $commit_author = $commit_author_hash{$email};
1838 my $commit_signer = $commit_signer_hash{$email};
1841 $authored++ for (@{$commit_author});
1842 $signed++ for (@{$commit_signer});
1843 printf STDERR "%1s %2d %-65s", $sel, $count + 1, $email;
1844 printf STDERR "%4d %4d", $authored, $signed
1845 if ($authored > 0 || $signed > 0);
1846 printf STDERR "\n %s\n", $role;
1847 if ($authored{$count}) {
1848 my $commit_author = $commit_author_hash{$email};
1849 foreach my $ref (@{$commit_author}) {
1850 print STDERR " Author: @{$ref}[1]\n";
1853 if ($signed{$count}) {
1854 my $commit_signer = $commit_signer_hash{$email};
1855 foreach my $ref (@{$commit_signer}) {
1856 print STDERR " @{$ref}[2]: @{$ref}[1]\n";
1863 my $date_ref = \$email_git_since;
1864 $date_ref = \$email_hg_since if (vcs_is_hg());
1865 if ($print_options) {
1870 Version Control options:
1871 g use git history [$email_git]
1872 gf use git-fallback [$email_git_fallback]
1873 b use git blame [$email_git_blame]
1874 bs use blame signatures [$email_git_blame_signatures]
1875 c# minimum commits [$email_git_min_signatures]
1876 %# min percent [$email_git_min_percent]
1877 d# history to use [$$date_ref]
1878 x# max maintainers [$email_git_max_maintainers]
1879 t all signature types [$email_git_all_signature_types]
1880 m use .mailmap [$email_use_mailmap]
1887 tm toggle maintainers
1888 tg toggle git entries
1889 tl toggle open list entries
1890 ts toggle subscriber list entries
1891 f emails in file [$email_file_emails]
1892 k keywords in file [$keywords]
1893 r remove duplicates [$email_remove_duplicates]
1894 p# pattern match depth [$pattern_depth]
1898 "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1900 my $input = <STDIN>;
1905 my @wish = split(/[, ]+/, $input);
1906 foreach my $nr (@wish) {
1908 my $sel = substr($nr, 0, 1);
1909 my $str = substr($nr, 1);
1911 $val = $1 if $str =~ /^(\d+)$/;
1916 $output_rolestats = 0;
1918 $output_substatus = 0;
1920 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1921 $selected{$nr - 1} = !$selected{$nr - 1};
1922 } elsif ($sel eq "*" || $sel eq '^') {
1924 $toggle = 1 if ($sel eq '*');
1925 for (my $i = 0; $i < $count; $i++) {
1926 $selected{$i} = $toggle;
1928 } elsif ($sel eq "0") {
1929 for (my $i = 0; $i < $count; $i++) {
1930 $selected{$i} = !$selected{$i};
1932 } elsif ($sel eq "t") {
1933 if (lc($str) eq "m") {
1934 for (my $i = 0; $i < $count; $i++) {
1935 $selected{$i} = !$selected{$i}
1936 if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
1938 } elsif (lc($str) eq "g") {
1939 for (my $i = 0; $i < $count; $i++) {
1940 $selected{$i} = !$selected{$i}
1941 if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
1943 } elsif (lc($str) eq "l") {
1944 for (my $i = 0; $i < $count; $i++) {
1945 $selected{$i} = !$selected{$i}
1946 if ($list[$i]->[1] =~ /^(open list)/i);
1948 } elsif (lc($str) eq "s") {
1949 for (my $i = 0; $i < $count; $i++) {
1950 $selected{$i} = !$selected{$i}
1951 if ($list[$i]->[1] =~ /^(subscriber list)/i);
1954 } elsif ($sel eq "a") {
1955 if ($val > 0 && $val <= $count) {
1956 $authored{$val - 1} = !$authored{$val - 1};
1957 } elsif ($str eq '*' || $str eq '^') {
1959 $toggle = 1 if ($str eq '*');
1960 for (my $i = 0; $i < $count; $i++) {
1961 $authored{$i} = $toggle;
1964 } elsif ($sel eq "s") {
1965 if ($val > 0 && $val <= $count) {
1966 $signed{$val - 1} = !$signed{$val - 1};
1967 } elsif ($str eq '*' || $str eq '^') {
1969 $toggle = 1 if ($str eq '*');
1970 for (my $i = 0; $i < $count; $i++) {
1971 $signed{$i} = $toggle;
1974 } elsif ($sel eq "o") {
1977 } elsif ($sel eq "g") {
1979 bool_invert(\$email_git_fallback);
1981 bool_invert(\$email_git);
1984 } elsif ($sel eq "b") {
1986 bool_invert(\$email_git_blame_signatures);
1988 bool_invert(\$email_git_blame);
1991 } elsif ($sel eq "c") {
1993 $email_git_min_signatures = $val;
1996 } elsif ($sel eq "x") {
1998 $email_git_max_maintainers = $val;
2001 } elsif ($sel eq "%") {
2002 if ($str ne "" && $val >= 0) {
2003 $email_git_min_percent = $val;
2006 } elsif ($sel eq "d") {
2008 $email_git_since = $str;
2009 } elsif (vcs_is_hg()) {
2010 $email_hg_since = $str;
2013 } elsif ($sel eq "t") {
2014 bool_invert(\$email_git_all_signature_types);
2016 } elsif ($sel eq "f") {
2017 bool_invert(\$email_file_emails);
2019 } elsif ($sel eq "r") {
2020 bool_invert(\$email_remove_duplicates);
2022 } elsif ($sel eq "m") {
2023 bool_invert(\$email_use_mailmap);
2026 } elsif ($sel eq "k") {
2027 bool_invert(\$keywords);
2029 } elsif ($sel eq "p") {
2030 if ($str ne "" && $val >= 0) {
2031 $pattern_depth = $val;
2034 } elsif ($sel eq "h" || $sel eq "?") {
2037 Interactive mode allows you to select the various maintainers, submitters,
2038 commit signers and mailing lists that could be CC'd on a patch.
2040 Any *'d entry is selected.
2042 If you have git or hg installed, you can choose to summarize the commit
2043 history of files in the patch. Also, each line of the current file can
2044 be matched to its commit author and that commits signers with blame.
2046 Various knobs exist to control the length of time for active commit
2047 tracking, the maximum number of commit authors and signers to add,
2050 Enter selections at the prompt until you are satisfied that the selected
2051 maintainers are appropriate. You may enter multiple selections separated
2052 by either commas or spaces.
2056 print STDERR "invalid option: '$nr'\n";
2061 print STDERR "git-blame can be very slow, please have patience..."
2062 if ($email_git_blame);
2063 goto &get_maintainers;
2067 #drop not selected entries
2069 my @new_emailto = ();
2070 foreach my $entry (@list) {
2071 if ($selected{$count}) {
2072 push(@new_emailto, $list[$count]);
2076 return @new_emailto;
2080 my ($bool_ref) = @_;
2089 sub deduplicate_email {
2093 my ($name, $address) = parse_email($email);
2094 $email = format_email($name, $address, 1);
2095 $email = mailmap_email($email);
2097 return $email if (!$email_remove_duplicates);
2099 ($name, $address) = parse_email($email);
2101 if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
2102 $name = $deduplicate_name_hash{lc($name)}->[0];
2103 $address = $deduplicate_name_hash{lc($name)}->[1];
2105 } elsif ($deduplicate_address_hash{lc($address)}) {
2106 $name = $deduplicate_address_hash{lc($address)}->[0];
2107 $address = $deduplicate_address_hash{lc($address)}->[1];
2111 $deduplicate_name_hash{lc($name)} = [ $name, $address ];
2112 $deduplicate_address_hash{lc($address)} = [ $name, $address ];
2114 $email = format_email($name, $address, 1);
2115 $email = mailmap_email($email);
2119 sub save_commits_by_author {
2126 foreach my $line (@lines) {
2127 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2129 $author = deduplicate_email($author);
2130 push(@authors, $author);
2132 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
2133 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
2136 for (my $i = 0; $i < @authors; $i++) {
2138 foreach my $ref(@{$commit_author_hash{$authors[$i]}}) {
2139 if (@{$ref}[0] eq $commits[$i] &&
2140 @{$ref}[1] eq $subjects[$i]) {
2146 push(@{$commit_author_hash{$authors[$i]}},
2147 [ ($commits[$i], $subjects[$i]) ]);
2152 sub save_commits_by_signer {
2158 foreach my $line (@lines) {
2159 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
2160 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
2161 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
2162 my @signatures = ($line);
2163 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
2164 my @types = @$types_ref;
2165 my @signers = @$signers_ref;
2167 my $type = $types[0];
2168 my $signer = $signers[0];
2170 $signer = deduplicate_email($signer);
2173 foreach my $ref(@{$commit_signer_hash{$signer}}) {
2174 if (@{$ref}[0] eq $commit &&
2175 @{$ref}[1] eq $subject &&
2176 @{$ref}[2] eq $type) {
2182 push(@{$commit_signer_hash{$signer}},
2183 [ ($commit, $subject, $type) ]);
2190 my ($role, $divisor, @lines) = @_;
2195 return if (@lines <= 0);
2197 if ($divisor <= 0) {
2198 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
2202 @lines = mailmap(@lines);
2204 return if (@lines <= 0);
2206 @lines = sort(@lines);
2209 $hash{$_}++ for @lines;
2212 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
2213 my $sign_offs = $hash{$line};
2214 my $percent = $sign_offs * 100 / $divisor;
2216 $percent = 100 if ($percent > 100);
2217 next if (ignore_email_address($line));
2219 last if ($sign_offs < $email_git_min_signatures ||
2220 $count > $email_git_max_maintainers ||
2221 $percent < $email_git_min_percent);
2222 push_email_address($line, '');
2223 if ($output_rolestats) {
2224 my $fmt_percent = sprintf("%.0f", $percent);
2225 add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%");
2227 add_role($line, $role);
2232 sub vcs_file_signoffs {
2243 $vcs_used = vcs_exists();
2244 return if (!$vcs_used);
2246 my $cmd = $VCS_cmds{"find_signers_cmd"};
2247 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2249 ($commits, $signers_ref, $authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2251 @signers = @{$signers_ref} if defined $signers_ref;
2252 @authors = @{$authors_ref} if defined $authors_ref;
2253 @stats = @{$stats_ref} if defined $stats_ref;
2255 # print("commits: <$commits>\nsigners:<@signers>\nauthors: <@authors>\nstats: <@stats>\n");
2257 foreach my $signer (@signers) {
2258 $signer = deduplicate_email($signer);
2261 vcs_assign("commit_signer", $commits, @signers);
2262 vcs_assign("authored", $commits, @authors);
2263 if ($#authors == $#stats) {
2264 my $stat_pattern = $VCS_cmds{"stat_pattern"};
2265 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
2269 for (my $i = 0; $i <= $#stats; $i++) {
2270 if ($stats[$i] =~ /$stat_pattern/) {
2275 my @tmp_authors = uniq(@authors);
2276 foreach my $author (@tmp_authors) {
2277 $author = deduplicate_email($author);
2279 @tmp_authors = uniq(@tmp_authors);
2280 my @list_added = ();
2281 my @list_deleted = ();
2282 foreach my $author (@tmp_authors) {
2284 my $auth_deleted = 0;
2285 for (my $i = 0; $i <= $#stats; $i++) {
2286 if ($author eq deduplicate_email($authors[$i]) &&
2287 $stats[$i] =~ /$stat_pattern/) {
2289 $auth_deleted += $2;
2292 for (my $i = 0; $i < $auth_added; $i++) {
2293 push(@list_added, $author);
2295 for (my $i = 0; $i < $auth_deleted; $i++) {
2296 push(@list_deleted, $author);
2299 vcs_assign("added_lines", $added, @list_added);
2300 vcs_assign("removed_lines", $deleted, @list_deleted);
2304 sub vcs_file_blame {
2308 my @all_commits = ();
2313 $vcs_used = vcs_exists();
2314 return if (!$vcs_used);
2316 @all_commits = vcs_blame($file);
2317 @commits = uniq(@all_commits);
2318 $total_commits = @commits;
2319 $total_lines = @all_commits;
2321 if ($email_git_blame_signatures) {
2324 my $commit_authors_ref;
2325 my $commit_signers_ref;
2327 my @commit_authors = ();
2328 my @commit_signers = ();
2329 my $commit = join(" -r ", @commits);
2332 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2333 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2335 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2336 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2337 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2339 push(@signers, @commit_signers);
2341 foreach my $commit (@commits) {
2343 my $commit_authors_ref;
2344 my $commit_signers_ref;
2346 my @commit_authors = ();
2347 my @commit_signers = ();
2350 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2351 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2353 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2354 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2355 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2357 push(@signers, @commit_signers);
2362 if ($from_filename) {
2363 if ($output_rolestats) {
2365 if (vcs_is_hg()) {{ # Double brace for last exit
2367 my @commit_signers = ();
2368 @commits = uniq(@commits);
2369 @commits = sort(@commits);
2370 my $commit = join(" -r ", @commits);
2373 $cmd = $VCS_cmds{"find_commit_author_cmd"};
2374 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2378 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
2380 if (!$email_git_penguin_chiefs) {
2381 @lines = grep(!/${penguin_chiefs}/i, @lines);
2387 foreach my $line (@lines) {
2388 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2390 $author = deduplicate_email($author);
2391 push(@authors, $author);
2395 save_commits_by_author(@lines) if ($interactive);
2396 save_commits_by_signer(@lines) if ($interactive);
2398 push(@signers, @authors);
2401 foreach my $commit (@commits) {
2403 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
2404 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
2405 my @author = vcs_find_author($cmd);
2408 my $formatted_author = deduplicate_email($author[0]);
2410 my $count = grep(/$commit/, @all_commits);
2411 for ($i = 0; $i < $count ; $i++) {
2412 push(@blame_signers, $formatted_author);
2416 if (@blame_signers) {
2417 vcs_assign("authored lines", $total_lines, @blame_signers);
2420 foreach my $signer (@signers) {
2421 $signer = deduplicate_email($signer);
2423 vcs_assign("commits", $total_commits, @signers);
2425 foreach my $signer (@signers) {
2426 $signer = deduplicate_email($signer);
2428 vcs_assign("modified commits", $total_commits, @signers);
2432 sub vcs_file_exists {
2437 my $vcs_used = vcs_exists();
2438 return 0 if (!$vcs_used);
2440 my $cmd = $VCS_cmds{"file_exists_cmd"};
2441 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2443 $exists = &{$VCS_cmds{"execute_cmd"}}($cmd);
2445 return 0 if ($? != 0);
2450 sub vcs_list_files {
2455 my $vcs_used = vcs_exists();
2456 return 0 if (!$vcs_used);
2458 my $cmd = $VCS_cmds{"list_files_cmd"};
2459 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2460 @lsfiles = &{$VCS_cmds{"execute_cmd"}}($cmd);
2462 return () if ($? != 0);
2471 @parms = grep(!$saw{$_}++, @parms);
2479 @parms = sort @parms;
2480 @parms = grep(!$saw{$_}++, @parms);
2484 sub clean_file_emails {
2485 my (@file_emails) = @_;
2486 my @fmt_emails = ();
2488 foreach my $email (@file_emails) {
2489 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
2490 my ($name, $address) = parse_email($email);
2492 # Strip quotes for easier processing, format_email will add them back
2493 $name =~ s/^"(.*)"$/$1/;
2495 # Split into name-like parts and remove stray punctuation particles
2496 my @nw = split(/[^\p{L}\'\,\.\+-]/, $name);
2497 @nw = grep(!/^[\'\,\.\+-]$/, @nw);
2499 # Make a best effort to extract the name, and only the name, by taking
2500 # only the last two names, or in the case of obvious initials, the last
2503 my $first = $nw[@nw - 3];
2504 my $middle = $nw[@nw - 2];
2505 my $last = $nw[@nw - 1];
2507 if (((length($first) == 1 && $first =~ m/\p{L}/) ||
2508 (length($first) == 2 && substr($first, -1) eq ".")) ||
2509 (length($middle) == 1 ||
2510 (length($middle) == 2 && substr($middle, -1) eq "."))) {
2511 $name = "$first $middle $last";
2513 $name = "$middle $last";
2519 if (substr($name, -1) =~ /[,\.]/) {
2520 $name = substr($name, 0, length($name) - 1);
2523 if (substr($name, 0, 1) =~ /[,\.]/) {
2524 $name = substr($name, 1, length($name) - 1);
2527 my $fmt_email = format_email($name, $address, $email_usename);
2528 push(@fmt_emails, $fmt_email);
2538 my ($address, $role) = @$_;
2539 if (!$saw{$address}) {
2540 if ($output_roles) {
2541 push(@lines, "$address ($role)");
2543 push(@lines, $address);
2555 if ($output_multiline) {
2556 foreach my $line (@parms) {
2560 print(join($output_separator, @parms));
2568 # Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
2569 # comment. We must allow for rfc822_lwsp (or comments) after each of these.
2570 # This regexp will only work on addresses which have had comments stripped
2571 # and replaced with rfc822_lwsp.
2573 my $specials = '()<>@,;:\\\\".\\[\\]';
2574 my $controls = '\\000-\\037\\177';
2576 my $dtext = "[^\\[\\]\\r\\\\]";
2577 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
2579 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
2581 # Use zero-width assertion to spot the limit of an atom. A simple
2582 # $rfc822_lwsp* causes the regexp engine to hang occasionally.
2583 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
2584 my $word = "(?:$atom|$quoted_string)";
2585 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
2587 my $sub_domain = "(?:$atom|$domain_literal)";
2588 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
2590 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
2592 my $phrase = "$word*";
2593 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
2594 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
2595 my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
2597 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
2598 my $address = "(?:$mailbox|$group)";
2600 return "$rfc822_lwsp*$address";
2603 sub rfc822_strip_comments {
2605 # Recursively remove comments, and replace with a single space. The simpler
2606 # regexps in the Email Addressing FAQ are imperfect - they will miss escaped
2607 # chars in atoms, for example.
2609 while ($s =~ s/^((?:[^"\\]|\\.)*
2610 (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
2611 \((?:[^()\\]|\\.)*\)/$1 /osx) {}
2615 # valid: returns true if the parameter is an RFC822 valid address
2618 my $s = rfc822_strip_comments(shift);
2621 $rfc822re = make_rfc822re();
2624 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
2627 # validlist: In scalar context, returns true if the parameter is an RFC822
2628 # valid list of addresses.
2630 # In list context, returns an empty list on failure (an invalid
2631 # address was found); otherwise a list whose first element is the
2632 # number of addresses found and whose remaining elements are the
2633 # addresses. This is needed to disambiguate failure (invalid)
2634 # from success with no addresses found, because an empty string is
2637 sub rfc822_validlist {
2638 my $s = rfc822_strip_comments(shift);
2641 $rfc822re = make_rfc822re();
2643 # * null list items are valid according to the RFC
2644 # * the '1' business is to aid in distinguishing failure from no results
2647 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
2648 $s =~ m/^$rfc822_char*$/) {
2649 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
2652 return wantarray ? (scalar(@r), @r) : 1;
2654 return wantarray ? () : 0;