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_section_maxlen = 50;
61 my $keywords_in_file = 0;
63 my $email_file_emails = 0;
64 my $from_filename = 0;
65 my $pattern_depth = 0;
66 my $self_test = undef;
69 my $find_maintainer_files = 0;
76 my @fixes = (); # If a patch description includes Fixes: lines
81 my %commit_author_hash;
82 my %commit_signer_hash;
84 my @penguin_chief = ();
85 push(@penguin_chief, "Linus Torvalds:torvalds\@linux-foundation.org");
86 #Andrew wants in on most everything - 2009/01/14
87 #push(@penguin_chief, "Andrew Morton:akpm\@linux-foundation.org");
89 my @penguin_chief_names = ();
90 foreach my $chief (@penguin_chief) {
91 if ($chief =~ m/^(.*):(.*)/) {
94 push(@penguin_chief_names, $chief_name);
97 my $penguin_chiefs = "\(" . join("|", @penguin_chief_names) . "\)";
99 # Signature types of people who are either
100 # a) responsible for the code in question, or
101 # b) familiar enough with it to give relevant feedback
102 my @signature_tags = ();
103 push(@signature_tags, "Signed-off-by:");
104 push(@signature_tags, "Reviewed-by:");
105 push(@signature_tags, "Acked-by:");
107 my $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
109 # rfc822 email address - preloaded methods go here.
110 my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
111 my $rfc822_char = '[\\000-\\377]';
113 # VCS command support: class-like functions and strings
118 "execute_cmd" => \&git_execute_cmd,
119 "available" => '(which("git") ne "") && (-e ".git")',
120 "find_signers_cmd" =>
121 "git log --no-color --follow --since=\$email_git_since " .
122 '--numstat --no-merges ' .
123 '--format="GitCommit: %H%n' .
124 'GitAuthor: %an <%ae>%n' .
129 "find_commit_signers_cmd" =>
130 "git log --no-color " .
132 '--format="GitCommit: %H%n' .
133 'GitAuthor: %an <%ae>%n' .
138 "find_commit_author_cmd" =>
139 "git log --no-color " .
141 '--format="GitCommit: %H%n' .
142 'GitAuthor: %an <%ae>%n' .
144 'GitSubject: %s%n"' .
146 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
147 "blame_file_cmd" => "git blame -l \$file",
148 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})",
149 "blame_commit_pattern" => "^([0-9a-f]+) ",
150 "author_pattern" => "^GitAuthor: (.*)",
151 "subject_pattern" => "^GitSubject: (.*)",
152 "stat_pattern" => "^(\\d+)\\t(\\d+)\\t\$file\$",
153 "file_exists_cmd" => "git ls-files \$file",
154 "list_files_cmd" => "git ls-files \$file",
158 "execute_cmd" => \&hg_execute_cmd,
159 "available" => '(which("hg") ne "") && (-d ".hg")',
160 "find_signers_cmd" =>
161 "hg log --date=\$email_hg_since " .
162 "--template='HgCommit: {node}\\n" .
163 "HgAuthor: {author}\\n" .
164 "HgSubject: {desc}\\n'" .
166 "find_commit_signers_cmd" =>
168 "--template='HgSubject: {desc}\\n'" .
170 "find_commit_author_cmd" =>
172 "--template='HgCommit: {node}\\n" .
173 "HgAuthor: {author}\\n" .
174 "HgSubject: {desc|firstline}\\n'" .
176 "blame_range_cmd" => "", # not supported
177 "blame_file_cmd" => "hg blame -n \$file",
178 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})",
179 "blame_commit_pattern" => "^([ 0-9a-f]+):",
180 "author_pattern" => "^HgAuthor: (.*)",
181 "subject_pattern" => "^HgSubject: (.*)",
182 "stat_pattern" => "^(\\d+)\t(\\d+)\t\$file\$",
183 "file_exists_cmd" => "hg files \$file",
184 "list_files_cmd" => "hg manifest -R \$file",
187 my $conf = which_conf(".get_maintainer.conf");
190 open(my $conffile, '<', "$conf")
191 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
193 while (<$conffile>) {
196 $line =~ s/\s*\n?$//g;
200 next if ($line =~ m/^\s*#/);
201 next if ($line =~ m/^\s*$/);
203 my @words = split(" ", $line);
204 foreach my $word (@words) {
205 last if ($word =~ m/^#/);
206 push (@conf_args, $word);
210 unshift(@ARGV, @conf_args) if @conf_args;
213 my @ignore_emails = ();
214 my $ignore_file = which_conf(".get_maintainer.ignore");
215 if (-f $ignore_file) {
216 open(my $ignore, '<', "$ignore_file")
217 or warn "$P: Can't find a readable .get_maintainer.ignore file $!\n";
221 $line =~ s/\s*\n?$//;
226 next if ($line =~ m/^\s*$/);
227 if (rfc822_valid($line)) {
228 push(@ignore_emails, $line);
236 if ($_ =~ /^-{1,2}self-test(?:=|$)/) {
237 die "$P: using --self-test does not allow any other option or argument\n";
244 'git!' => \$email_git,
245 'git-all-signature-types!' => \$email_git_all_signature_types,
246 'git-blame!' => \$email_git_blame,
247 'git-blame-signatures!' => \$email_git_blame_signatures,
248 'git-fallback!' => \$email_git_fallback,
249 'git-chief-penguins!' => \$email_git_penguin_chiefs,
250 'git-min-signatures=i' => \$email_git_min_signatures,
251 'git-max-maintainers=i' => \$email_git_max_maintainers,
252 'git-min-percent=i' => \$email_git_min_percent,
253 'git-since=s' => \$email_git_since,
254 'hg-since=s' => \$email_hg_since,
255 'i|interactive!' => \$interactive,
256 'remove-duplicates!' => \$email_remove_duplicates,
257 'mailmap!' => \$email_use_mailmap,
258 'm!' => \$email_maintainer,
259 'r!' => \$email_reviewer,
260 'n!' => \$email_usename,
261 'l!' => \$email_list,
262 'fixes!' => \$email_fixes,
263 'moderated!' => \$email_moderated_list,
264 's!' => \$email_subscriber_list,
265 'multiline!' => \$output_multiline,
266 'roles!' => \$output_roles,
267 'rolestats!' => \$output_rolestats,
268 'separator=s' => \$output_separator,
269 'subsystem!' => \$subsystem,
270 'status!' => \$status,
274 'letters=s' => \$letters,
275 'pattern-depth=i' => \$pattern_depth,
276 'k|keywords!' => \$keywords,
277 'kf|keywords-in-file!' => \$keywords_in_file,
278 'sections!' => \$sections,
279 'fe|file-emails!' => \$email_file_emails,
280 'f|file' => \$from_filename,
281 'find-maintainer-files' => \$find_maintainer_files,
282 'mpath|maintainer-path=s' => \$maintainer_path,
283 'self-test:s' => \$self_test,
284 'v|version' => \$version,
285 'h|help|usage' => \$help,
287 die "$P: invalid argument - use --help if necessary\n";
296 print("${P} ${V}\n");
300 if (defined $self_test) {
301 read_all_maintainer_files();
306 if (-t STDIN && !@ARGV) {
307 # We're talking to a terminal, but have no command line arguments.
308 die "$P: missing patchfile or -f file - use --help if necessary\n";
311 $output_multiline = 0 if ($output_separator ne ", ");
312 $output_rolestats = 1 if ($interactive);
313 $output_roles = 1 if ($output_rolestats);
315 if ($sections || $letters ne "") {
324 $keywords_in_file = 0;
327 my $selections = $email + $scm + $status + $subsystem + $web;
328 if ($selections == 0) {
329 die "$P: Missing required option: email, scm, status, subsystem or web\n";
334 ($email_maintainer + $email_reviewer +
335 $email_list + $email_subscriber_list +
336 $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
337 die "$P: Please select at least 1 email option\n";
340 if ($tree && !top_of_kernel_tree($lk_path)) {
341 die "$P: The current directory does not appear to be "
342 . "a linux kernel source tree.\n";
345 ## Read MAINTAINERS for type/value pairs
350 my @self_test_info = ();
352 sub read_maintainer_file {
355 open (my $maint, '<', "$file")
356 or die "$P: Can't open MAINTAINERS file '$file': $!\n";
362 if ($line =~ m/^([A-Z]):\s*(.*)/) {
366 ##Filename pattern matching
367 if ($type eq "F" || $type eq "X") {
368 $value =~ s@\.@\\\.@g; ##Convert . to \.
369 $value =~ s/\*/\.\*/g; ##Convert * to .*
370 $value =~ s/\?/\./g; ##Convert ? to .
371 ##if pattern is a directory and it lacks a trailing slash, add one
373 $value =~ s@([^/])$@$1/@;
375 } elsif ($type eq "K") {
376 $keyword_hash{@typevalue} = $value;
378 push(@typevalue, "$type:$value");
379 } elsif (!(/^\s*$/ || /^\s*\#/)) {
380 push(@typevalue, $line);
382 if (defined $self_test) {
383 push(@self_test_info, {file=>$file, linenr=>$i, line=>$line});
390 sub find_is_maintainer_file {
392 return if ($file !~ m@/MAINTAINERS$@);
393 $file = $File::Find::name;
394 return if (! -f $file);
395 push(@mfiles, $file);
398 sub find_ignore_git {
399 return grep { $_ !~ /^\.git$/; } @_;
402 read_all_maintainer_files();
404 sub read_all_maintainer_files {
405 my $path = "${lk_path}MAINTAINERS";
406 if (defined $maintainer_path) {
407 $path = $maintainer_path;
408 # Perl Cookbook tilde expansion if necessary
409 $path =~ s@^~([^/]*)@ $1 ? (getpwnam($1))[7] : ( $ENV{HOME} || $ENV{LOGDIR} || (getpwuid($<))[7])@ex;
413 $path .= '/' if ($path !~ m@/$@);
414 if ($find_maintainer_files) {
415 find( { wanted => \&find_is_maintainer_file,
416 preprocess => \&find_ignore_git,
420 opendir(DIR, "$path") or die $!;
421 my @files = readdir(DIR);
423 foreach my $file (@files) {
424 push(@mfiles, "$path$file") if ($file !~ /^\./);
427 } elsif (-f "$path") {
428 push(@mfiles, "$path");
430 die "$P: MAINTAINER file not found '$path'\n";
432 die "$P: No MAINTAINER files found in '$path'\n" if (scalar(@mfiles) == 0);
433 foreach my $file (@mfiles) {
434 read_maintainer_file("$file");
438 sub maintainers_in_file {
441 return if ($file =~ m@\bMAINTAINERS$@);
443 if (-f $file && ($email_file_emails || $file =~ /\.yaml$/)) {
444 open(my $f, '<', $file)
445 or die "$P: Can't open $file: $!\n";
446 my $text = do { local($/) ; <$f> };
449 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;
450 push(@file_emails, clean_file_emails(@poss_addr));
455 # Read mail address map
468 return if (!$email_use_mailmap || !(-f "${lk_path}.mailmap"));
470 open(my $mailmap_file, '<', "${lk_path}.mailmap")
471 or warn "$P: Can't open .mailmap: $!\n";
473 while (<$mailmap_file>) {
474 s/#.*$//; #strip comments
475 s/^\s+|\s+$//g; #trim
477 next if (/^\s*$/); #skip empty lines
478 #entries have one of the following formats:
481 # name1 <mail1> <mail2>
482 # name1 <mail1> name2 <mail2>
483 # (see man git-shortlog)
485 if (/^([^<]+)<([^>]+)>$/) {
489 $real_name =~ s/\s+$//;
490 ($real_name, $address) = parse_email("$real_name <$address>");
491 $mailmap->{names}->{$address} = $real_name;
493 } elsif (/^<([^>]+)>\s*<([^>]+)>$/) {
494 my $real_address = $1;
495 my $wrong_address = $2;
497 $mailmap->{addresses}->{$wrong_address} = $real_address;
499 } elsif (/^(.+)<([^>]+)>\s*<([^>]+)>$/) {
501 my $real_address = $2;
502 my $wrong_address = $3;
504 $real_name =~ s/\s+$//;
505 ($real_name, $real_address) =
506 parse_email("$real_name <$real_address>");
507 $mailmap->{names}->{$wrong_address} = $real_name;
508 $mailmap->{addresses}->{$wrong_address} = $real_address;
510 } elsif (/^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$/) {
512 my $real_address = $2;
514 my $wrong_address = $4;
516 $real_name =~ s/\s+$//;
517 ($real_name, $real_address) =
518 parse_email("$real_name <$real_address>");
520 $wrong_name =~ s/\s+$//;
521 ($wrong_name, $wrong_address) =
522 parse_email("$wrong_name <$wrong_address>");
524 my $wrong_email = format_email($wrong_name, $wrong_address, 1);
525 $mailmap->{names}->{$wrong_email} = $real_name;
526 $mailmap->{addresses}->{$wrong_email} = $real_address;
529 close($mailmap_file);
532 ## use the filenames on the command line or find the filenames in the patchfiles
535 push(@ARGV, "&STDIN");
538 foreach my $file (@ARGV) {
539 if ($file ne "&STDIN") {
540 $file = canonpath($file);
541 ##if $file is a directory and it lacks a trailing slash, add one
543 $file =~ s@([^/])$@$1/@;
544 } elsif (!(-f $file)) {
545 die "$P: file '${file}' not found\n";
548 if ($from_filename && (vcs_exists() && !vcs_file_exists($file))) {
549 warn "$P: file '$file' not found in version control $!\n";
551 if ($from_filename || ($file ne "&STDIN" && vcs_file_exists($file))) {
552 $file =~ s/^\Q${cur_path}\E//; #strip any absolute path
553 $file =~ s/^\Q${lk_path}\E//; #or the path to the lk tree
555 if ($file ne "MAINTAINERS" && -f $file && $keywords && $keywords_in_file) {
556 open(my $f, '<', $file)
557 or die "$P: Can't open $file: $!\n";
558 my $text = do { local($/) ; <$f> };
560 foreach my $line (keys %keyword_hash) {
561 if ($text =~ m/$keyword_hash{$line}/x) {
562 push(@keyword_tvi, $line);
567 my $file_cnt = @files;
570 open(my $patch, "< $file")
571 or die "$P: Can't open $file: $!\n";
573 # We can check arbitrary information before the patch
574 # like the commit message, mail headers, etc...
575 # This allows us to match arbitrary keywords against any part
576 # of a git format-patch generated file (subject tags, etc...)
578 my $patch_prefix = ""; #Parsing the intro
582 if (m/^ mode change [0-7]+ => [0-7]+ (\S+)\s*$/) {
584 push(@files, $filename);
585 } elsif (m/^rename (?:from|to) (\S+)\s*$/) {
587 push(@files, $filename);
588 } elsif (m/^diff --git a\/(\S+) b\/(\S+)\s*$/) {
591 push(@files, $filename1);
592 push(@files, $filename2);
593 } elsif (m/^Fixes:\s+([0-9a-fA-F]{6,40})/) {
594 push(@fixes, $1) if ($email_fixes);
595 } elsif (m/^\+\+\+\s+(\S+)/ or m/^---\s+(\S+)/) {
597 $filename =~ s@^[^/]*/@@;
599 $lastfile = $filename;
600 push(@files, $filename);
601 $patch_prefix = "^[+-].*"; #Now parsing the actual patch
602 } elsif (m/^\@\@ -(\d+),(\d+)/) {
603 if ($email_git_blame) {
604 push(@range, "$lastfile:$1:$2");
606 } elsif ($keywords) {
607 foreach my $line (keys %keyword_hash) {
608 if ($patch_line =~ m/${patch_prefix}$keyword_hash{$line}/x) {
609 push(@keyword_tvi, $line);
616 if ($file_cnt == @files) {
617 warn "$P: file '${file}' doesn't appear to be a patch. "
618 . "Add -f to options?\n";
620 @files = sort_and_uniq(@files);
624 @file_emails = uniq(@file_emails);
625 @fixes = uniq(@fixes);
628 my %email_hash_address;
636 my %deduplicate_name_hash = ();
637 my %deduplicate_address_hash = ();
639 my @maintainers = get_maintainers();
641 @maintainers = merge_email(@maintainers);
642 output(@maintainers);
651 @status = uniq(@status);
656 @subsystem = uniq(@subsystem);
671 my @section_headers = ();
674 @lsfiles = vcs_list_files($lk_path);
676 for my $x (@self_test_info) {
679 ## Section header duplication and missing section content
680 if (($self_test eq "" || $self_test =~ /\bsections\b/) &&
681 $x->{line} =~ /^\S[^:]/ &&
682 defined $self_test_info[$index] &&
683 $self_test_info[$index]->{line} =~ /^([A-Z]):\s*\S/) {
688 if (grep(m@^\Q$x->{line}\E@, @section_headers)) {
689 print("$x->{file}:$x->{linenr}: warning: duplicate section header\t$x->{line}\n");
691 push(@section_headers, $x->{line});
693 my $nextline = $index;
694 while (defined $self_test_info[$nextline] &&
695 $self_test_info[$nextline]->{line} =~ /^([A-Z]):\s*(\S.*)/) {
701 } elsif ($type eq "F" || $type eq "N") {
703 } elsif ($type eq "M" || $type eq "R" || $type eq "L") {
708 if (!$has_ML && $status !~ /orphan|obsolete/i) {
709 print("$x->{file}:$x->{linenr}: warning: section without email address\t$x->{line}\n");
712 print("$x->{file}:$x->{linenr}: warning: section without status \t$x->{line}\n");
715 print("$x->{file}:$x->{linenr}: warning: section without file pattern\t$x->{line}\n");
719 next if ($x->{line} !~ /^([A-Z]):\s*(.*)/);
724 ## Filename pattern matching
725 if (($type eq "F" || $type eq "X") &&
726 ($self_test eq "" || $self_test =~ /\bpatterns\b/)) {
727 $value =~ s@\.@\\\.@g; ##Convert . to \.
728 $value =~ s/\*/\.\*/g; ##Convert * to .*
729 $value =~ s/\?/\./g; ##Convert ? to .
730 ##if pattern is a directory and it lacks a trailing slash, add one
732 $value =~ s@([^/])$@$1/@;
734 if (!grep(m@^$value@, @lsfiles)) {
735 print("$x->{file}:$x->{linenr}: warning: no file matches\t$x->{line}\n");
739 } elsif (($type eq "W" || $type eq "Q" || $type eq "B") &&
740 $value =~ /^https?:/ &&
741 ($self_test eq "" || $self_test =~ /\blinks\b/)) {
742 next if (grep(m@^\Q$value\E$@, @good_links));
744 if (grep(m@^\Q$value\E$@, @bad_links)) {
747 my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $value`;
749 push(@good_links, $value);
751 push(@bad_links, $value);
756 print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
760 } elsif ($type eq "T" &&
761 ($self_test eq "" || $self_test =~ /\bscm\b/)) {
762 next if (grep(m@^\Q$value\E$@, @good_links));
764 if (grep(m@^\Q$value\E$@, @bad_links)) {
766 } elsif ($value !~ /^(?:git|quilt|hg)\s+\S/) {
767 print("$x->{file}:$x->{linenr}: warning: malformed entry\t$x->{line}\n");
768 } elsif ($value =~ /^git\s+(\S+)(\s+([^\(]+\S+))?/) {
772 my $output = `git ls-remote --exit-code -h "$url" $branch > /dev/null 2>&1`;
774 push(@good_links, $value);
776 push(@bad_links, $value);
779 } elsif ($value =~ /^(?:quilt|hg)\s+(https?:\S+)/) {
781 my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $url`;
783 push(@good_links, $value);
785 push(@bad_links, $value);
790 print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
796 sub ignore_email_address {
799 foreach my $ignore (@ignore_emails) {
800 return 1 if ($ignore eq $address);
806 sub range_is_maintained {
807 my ($start, $end) = @_;
809 for (my $i = $start; $i < $end; $i++) {
810 my $line = $typevalue[$i];
811 if ($line =~ m/^([A-Z]):\s*(.*)/) {
815 if ($value =~ /(maintain|support)/i) {
824 sub range_has_maintainer {
825 my ($start, $end) = @_;
827 for (my $i = $start; $i < $end; $i++) {
828 my $line = $typevalue[$i];
829 if ($line =~ m/^([A-Z]):\s*(.*)/) {
840 sub get_maintainers {
841 %email_hash_name = ();
842 %email_hash_address = ();
843 %commit_author_hash = ();
844 %commit_signer_hash = ();
852 %deduplicate_name_hash = ();
853 %deduplicate_address_hash = ();
854 if ($email_git_all_signature_types) {
855 $signature_pattern = "(.+?)[Bb][Yy]:";
857 $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
860 # Find responsible parties
862 my %exact_pattern_match_hash = ();
864 foreach my $file (@files) {
867 my $tvi = find_first_section();
868 while ($tvi < @typevalue) {
869 my $start = find_starting_index($tvi);
870 my $end = find_ending_index($tvi);
874 #Do not match excluded file patterns
876 for ($i = $start; $i < $end; $i++) {
877 my $line = $typevalue[$i];
878 if ($line =~ m/^([A-Z]):\s*(.*)/) {
882 if (file_match_pattern($file, $value)) {
891 for ($i = $start; $i < $end; $i++) {
892 my $line = $typevalue[$i];
893 if ($line =~ m/^([A-Z]):\s*(.*)/) {
897 if (file_match_pattern($file, $value)) {
898 my $value_pd = ($value =~ tr@/@@);
899 my $file_pd = ($file =~ tr@/@@);
900 $value_pd++ if (substr($value,-1,1) ne "/");
901 $value_pd = -1 if ($value =~ /^\.\*/);
902 if ($value_pd >= $file_pd &&
903 range_is_maintained($start, $end) &&
904 range_has_maintainer($start, $end)) {
905 $exact_pattern_match_hash{$file} = 1;
907 if ($pattern_depth == 0 ||
908 (($file_pd - $value_pd) < $pattern_depth)) {
909 $hash{$tvi} = $value_pd;
912 } elsif ($type eq 'N') {
913 if ($file =~ m/$value/x) {
923 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
924 add_categories($line, "");
927 my $start = find_starting_index($line);
928 my $end = find_ending_index($line);
929 for ($i = $start; $i < $end; $i++) {
930 my $line = $typevalue[$i];
931 if ($line =~ /^[FX]:/) { ##Restore file patterns
932 $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
933 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ?
934 $line =~ s/\\\./\./g; ##Convert \. to .
935 $line =~ s/\.\*/\*/g; ##Convert .* to *
937 my $count = $line =~ s/^([A-Z]):/$1:\t/g;
938 if ($letters eq "" || (!$count || $letters =~ /$1/i)) {
946 maintainers_in_file($file);
950 @keyword_tvi = sort_and_uniq(@keyword_tvi);
951 foreach my $line (@keyword_tvi) {
952 add_categories($line, ":Keyword:$keyword_hash{$line}");
956 foreach my $email (@email_to, @list_to) {
957 $email->[0] = deduplicate_email($email->[0]);
960 foreach my $file (@files) {
963 ($email_git_fallback &&
964 $file !~ /MAINTAINERS$/ &&
965 !$exact_pattern_match_hash{$file}))) {
966 vcs_file_signoffs($file);
968 if ($email && $email_git_blame) {
969 vcs_file_blame($file);
974 foreach my $chief (@penguin_chief) {
975 if ($chief =~ m/^(.*):(.*)/) {
978 $email_address = format_email($1, $2, $email_usename);
979 if ($email_git_penguin_chiefs) {
980 push(@email_to, [$email_address, 'chief penguin']);
982 @email_to = grep($_->[0] !~ /${email_address}/, @email_to);
987 foreach my $email (@file_emails) {
988 $email = mailmap_email($email);
989 my ($name, $address) = parse_email($email);
991 my $tmp_email = format_email($name, $address, $email_usename);
992 push_email_address($tmp_email, '');
993 add_role($tmp_email, 'in file');
997 foreach my $fix (@fixes) {
998 vcs_add_commit_signers($fix, "blamed_fixes");
1002 if ($email || $email_list) {
1004 @to = (@to, @email_to);
1007 @to = (@to, @list_to);
1012 @to = interactive_get_maintainers(\@to);
1018 sub file_match_pattern {
1019 my ($file, $pattern) = @_;
1020 if (substr($pattern, -1) eq "/") {
1021 if ($file =~ m@^$pattern@) {
1025 if ($file =~ m@^$pattern@) {
1026 my $s1 = ($file =~ tr@/@@);
1027 my $s2 = ($pattern =~ tr@/@@);
1038 usage: $P [options] patchfile
1039 $P [options] -f file|directory
1042 MAINTAINER field selection options:
1043 --email => print email address(es) if any
1044 --git => include recent git \*-by: signers
1045 --git-all-signature-types => include signers regardless of signature type
1046 or use only ${signature_pattern} signers (default: $email_git_all_signature_types)
1047 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
1048 --git-chief-penguins => include ${penguin_chiefs}
1049 --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
1050 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
1051 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
1052 --git-blame => use git blame to find modified commits for patch or file
1053 --git-blame-signatures => when used with --git-blame, also include all commit signers
1054 --git-since => git history to use (default: $email_git_since)
1055 --hg-since => hg history to use (default: $email_hg_since)
1056 --interactive => display a menu (mostly useful if used with the --git option)
1057 --m => include maintainer(s) if any
1058 --r => include reviewer(s) if any
1059 --n => include name 'Full Name <addr\@domain.tld>'
1060 --l => include list(s) if any
1061 --moderated => include moderated lists(s) if any (default: true)
1062 --s => include subscriber only list(s) if any (default: false)
1063 --remove-duplicates => minimize duplicate email names/addresses
1064 --roles => show roles (status:subsystem, git-signer, list, etc...)
1065 --rolestats => show roles and statistics (commits/total_commits, %)
1066 --file-emails => add email addresses found in -f file (default: 0 (off))
1067 --fixes => for patches, add signatures of commits with 'Fixes: <commit>' (default: 1 (on))
1068 --scm => print SCM tree(s) if any
1069 --status => print status if any
1070 --subsystem => print subsystem name if any
1071 --web => print website(s) if any
1073 Output type options:
1074 --separator [, ] => separator for multiple entries on 1 line
1075 using --separator also sets --nomultiline if --separator is not [, ]
1076 --multiline => print 1 entry per line
1079 --pattern-depth => Number of pattern directory traversals (default: 0 (all))
1080 --keywords => scan patch for keywords (default: $keywords)
1081 --keywords-in-file => scan file for keywords (default: $keywords_in_file)
1082 --sections => print all of the subsystem sections with pattern matches
1083 --letters => print all matching 'letter' types from all matching sections
1084 --mailmap => use .mailmap file (default: $email_use_mailmap)
1085 --no-tree => run without a kernel tree
1086 --self-test => show potential issues with MAINTAINERS file content
1087 --version => show version
1088 --help => show this help information
1091 [--email --tree --nogit --git-fallback --m --r --n --l --multiline
1092 --pattern-depth=0 --remove-duplicates --rolestats --keywords]
1095 Using "-f directory" may give unexpected results:
1096 Used with "--git", git signators for _all_ files in and below
1097 directory are examined as git recurses directories.
1098 Any specified X: (exclude) pattern matches are _not_ ignored.
1099 Used with "--nogit", directory is used as a pattern match,
1100 no individual file within the directory or subdirectory
1102 Used with "--git-blame", does not iterate all files in directory
1103 Using "--git-blame" is slow and may add old committers and authors
1104 that are no longer active maintainers to the output.
1105 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
1106 other automated tools that expect only ["name"] <email address>
1107 may not work because of additional output after <email address>.
1108 Using "--rolestats" and "--git-blame" shows the #/total=% commits,
1109 not the percentage of the entire file authored. # of commits is
1110 not a good measure of amount of code authored. 1 major commit may
1111 contain a thousand lines, 5 trivial commits may modify a single line.
1112 If git is not installed, but mercurial (hg) is installed and an .hg
1113 repository exists, the following options apply to mercurial:
1115 --git-min-signatures, --git-max-maintainers, --git-min-percent, and
1117 Use --hg-since not --git-since to control date selection
1118 File ".get_maintainer.conf", if it exists in the linux kernel source root
1119 directory, can change whatever get_maintainer defaults are desired.
1120 Entries in this file can be any command line argument.
1121 This file is prepended to any additional command line arguments.
1122 Multiple lines and # comments are allowed.
1123 Most options have both positive and negative forms.
1124 The negative forms for --<foo> are --no<foo> and --no-<foo>.
1129 sub top_of_kernel_tree {
1132 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
1135 if ( (-f "${lk_path}COPYING")
1136 && (-f "${lk_path}CREDITS")
1137 && (-f "${lk_path}Kbuild")
1138 && (-e "${lk_path}MAINTAINERS")
1139 && (-f "${lk_path}Makefile")
1140 && (-f "${lk_path}README")
1141 && (-d "${lk_path}Documentation")
1142 && (-d "${lk_path}arch")
1143 && (-d "${lk_path}include")
1144 && (-d "${lk_path}drivers")
1145 && (-d "${lk_path}fs")
1146 && (-d "${lk_path}init")
1147 && (-d "${lk_path}ipc")
1148 && (-d "${lk_path}kernel")
1149 && (-d "${lk_path}lib")
1150 && (-d "${lk_path}scripts")) {
1159 if ($name =~ /[^\w \-]/ai) { ##has "must quote" chars
1160 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
1161 $name = "\"$name\"";
1168 my ($formatted_email) = @_;
1173 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
1176 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
1178 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
1182 $name =~ s/^\s+|\s+$//g;
1183 $name =~ s/^\"|\"$//g;
1184 $name = escape_name($name);
1185 $address =~ s/^\s+|\s+$//g;
1187 return ($name, $address);
1191 my ($name, $address, $usename) = @_;
1193 my $formatted_email;
1195 $name =~ s/^\s+|\s+$//g;
1196 $name =~ s/^\"|\"$//g;
1197 $name = escape_name($name);
1198 $address =~ s/^\s+|\s+$//g;
1201 if ("$name" eq "") {
1202 $formatted_email = "$address";
1204 $formatted_email = "$name <$address>";
1207 $formatted_email = $address;
1210 return $formatted_email;
1213 sub find_first_section {
1216 while ($index < @typevalue) {
1217 my $tv = $typevalue[$index];
1218 if (($tv =~ m/^([A-Z]):\s*(.*)/)) {
1227 sub find_starting_index {
1230 while ($index > 0) {
1231 my $tv = $typevalue[$index];
1232 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
1241 sub find_ending_index {
1244 while ($index < @typevalue) {
1245 my $tv = $typevalue[$index];
1246 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
1255 sub get_subsystem_name {
1258 my $start = find_starting_index($index);
1260 my $subsystem = $typevalue[$start];
1261 if ($output_section_maxlen && length($subsystem) > $output_section_maxlen) {
1262 $subsystem = substr($subsystem, 0, $output_section_maxlen - 3);
1263 $subsystem =~ s/\s*$//;
1264 $subsystem = $subsystem . "...";
1269 sub get_maintainer_role {
1273 my $start = find_starting_index($index);
1274 my $end = find_ending_index($index);
1276 my $role = "unknown";
1277 my $subsystem = get_subsystem_name($index);
1279 for ($i = $start + 1; $i < $end; $i++) {
1280 my $tv = $typevalue[$i];
1281 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1284 if ($ptype eq "S") {
1291 if ($role eq "supported") {
1292 $role = "supporter";
1293 } elsif ($role eq "maintained") {
1294 $role = "maintainer";
1295 } elsif ($role eq "odd fixes") {
1296 $role = "odd fixer";
1297 } elsif ($role eq "orphan") {
1298 $role = "orphan minder";
1299 } elsif ($role eq "obsolete") {
1300 $role = "obsolete minder";
1301 } elsif ($role eq "buried alive in reporters") {
1302 $role = "chief penguin";
1305 return $role . ":" . $subsystem;
1311 my $subsystem = get_subsystem_name($index);
1313 if ($subsystem eq "THE REST") {
1320 sub add_categories {
1321 my ($index, $suffix) = @_;
1324 my $start = find_starting_index($index);
1325 my $end = find_ending_index($index);
1327 push(@subsystem, $typevalue[$start]);
1329 for ($i = $start + 1; $i < $end; $i++) {
1330 my $tv = $typevalue[$i];
1331 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1334 if ($ptype eq "L") {
1335 my $list_address = $pvalue;
1336 my $list_additional = "";
1337 my $list_role = get_list_role($i);
1339 if ($list_role ne "") {
1340 $list_role = ":" . $list_role;
1342 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
1344 $list_additional = $2;
1346 if ($list_additional =~ m/subscribers-only/) {
1347 if ($email_subscriber_list) {
1348 if (!$hash_list_to{lc($list_address)}) {
1349 $hash_list_to{lc($list_address)} = 1;
1350 push(@list_to, [$list_address,
1351 "subscriber list${list_role}" . $suffix]);
1356 if (!$hash_list_to{lc($list_address)}) {
1357 if ($list_additional =~ m/moderated/) {
1358 if ($email_moderated_list) {
1359 $hash_list_to{lc($list_address)} = 1;
1360 push(@list_to, [$list_address,
1361 "moderated list${list_role}" . $suffix]);
1364 $hash_list_to{lc($list_address)} = 1;
1365 push(@list_to, [$list_address,
1366 "open list${list_role}" . $suffix]);
1371 } elsif ($ptype eq "M") {
1372 if ($email_maintainer) {
1373 my $role = get_maintainer_role($i);
1374 push_email_addresses($pvalue, $role . $suffix);
1376 } elsif ($ptype eq "R") {
1377 if ($email_reviewer) {
1378 my $subsystem = get_subsystem_name($i);
1379 push_email_addresses($pvalue, "reviewer:$subsystem" . $suffix);
1381 } elsif ($ptype eq "T") {
1382 push(@scm, $pvalue . $suffix);
1383 } elsif ($ptype eq "W") {
1384 push(@web, $pvalue . $suffix);
1385 } elsif ($ptype eq "S") {
1386 push(@status, $pvalue . $suffix);
1393 my ($name, $address) = @_;
1395 return 1 if (($name eq "") && ($address eq ""));
1396 return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
1397 return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
1402 sub push_email_address {
1403 my ($line, $role) = @_;
1405 my ($name, $address) = parse_email($line);
1407 if ($address eq "") {
1411 if (!$email_remove_duplicates) {
1412 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1413 } elsif (!email_inuse($name, $address)) {
1414 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1415 $email_hash_name{lc($name)}++ if ($name ne "");
1416 $email_hash_address{lc($address)}++;
1422 sub push_email_addresses {
1423 my ($address, $role) = @_;
1425 my @address_list = ();
1427 if (rfc822_valid($address)) {
1428 push_email_address($address, $role);
1429 } elsif (@address_list = rfc822_validlist($address)) {
1430 my $array_count = shift(@address_list);
1431 while (my $entry = shift(@address_list)) {
1432 push_email_address($entry, $role);
1435 if (!push_email_address($address, $role)) {
1436 warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1442 my ($line, $role) = @_;
1444 my ($name, $address) = parse_email($line);
1445 my $email = format_email($name, $address, $email_usename);
1447 foreach my $entry (@email_to) {
1448 if ($email_remove_duplicates) {
1449 my ($entry_name, $entry_address) = parse_email($entry->[0]);
1450 if (($name eq $entry_name || $address eq $entry_address)
1451 && ($role eq "" || !($entry->[1] =~ m/$role/))
1453 if ($entry->[1] eq "") {
1454 $entry->[1] = "$role";
1456 $entry->[1] = "$entry->[1],$role";
1460 if ($email eq $entry->[0]
1461 && ($role eq "" || !($entry->[1] =~ m/$role/))
1463 if ($entry->[1] eq "") {
1464 $entry->[1] = "$role";
1466 $entry->[1] = "$entry->[1],$role";
1476 foreach my $path (split(/:/, $ENV{PATH})) {
1477 if (-e "$path/$bin") {
1478 return "$path/$bin";
1488 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1489 if (-e "$path/$conf") {
1490 return "$path/$conf";
1500 my ($name, $address) = parse_email($line);
1501 my $email = format_email($name, $address, 1);
1502 my $real_name = $name;
1503 my $real_address = $address;
1505 if (exists $mailmap->{names}->{$email} ||
1506 exists $mailmap->{addresses}->{$email}) {
1507 if (exists $mailmap->{names}->{$email}) {
1508 $real_name = $mailmap->{names}->{$email};
1510 if (exists $mailmap->{addresses}->{$email}) {
1511 $real_address = $mailmap->{addresses}->{$email};
1514 if (exists $mailmap->{names}->{$address}) {
1515 $real_name = $mailmap->{names}->{$address};
1517 if (exists $mailmap->{addresses}->{$address}) {
1518 $real_address = $mailmap->{addresses}->{$address};
1521 return format_email($real_name, $real_address, 1);
1525 my (@addresses) = @_;
1527 my @mapped_emails = ();
1528 foreach my $line (@addresses) {
1529 push(@mapped_emails, mailmap_email($line));
1531 merge_by_realname(@mapped_emails) if ($email_use_mailmap);
1532 return @mapped_emails;
1535 sub merge_by_realname {
1539 foreach my $email (@emails) {
1540 my ($name, $address) = parse_email($email);
1541 if (exists $address_map{$name}) {
1542 $address = $address_map{$name};
1543 $email = format_email($name, $address, 1);
1545 $address_map{$name} = $address;
1550 sub git_execute_cmd {
1554 my $output = `$cmd`;
1555 $output =~ s/^\s*//gm;
1556 @lines = split("\n", $output);
1561 sub hg_execute_cmd {
1565 my $output = `$cmd`;
1566 @lines = split("\n", $output);
1571 sub extract_formatted_signatures {
1572 my (@signature_lines) = @_;
1574 my @type = @signature_lines;
1576 s/\s*(.*):.*/$1/ for (@type);
1579 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1581 ## Reformat email addresses (with names) to avoid badly written signatures
1583 foreach my $signer (@signature_lines) {
1584 $signer = deduplicate_email($signer);
1587 return (\@type, \@signature_lines);
1590 sub vcs_find_signers {
1591 my ($cmd, $file) = @_;
1594 my @signatures = ();
1598 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1600 my $pattern = $VCS_cmds{"commit_pattern"};
1601 my $author_pattern = $VCS_cmds{"author_pattern"};
1602 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1604 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1606 $commits = grep(/$pattern/, @lines); # of commits
1608 @authors = grep(/$author_pattern/, @lines);
1609 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1610 @stats = grep(/$stat_pattern/, @lines);
1612 # print("stats: <@stats>\n");
1614 return (0, \@signatures, \@authors, \@stats) if !@signatures;
1616 save_commits_by_author(@lines) if ($interactive);
1617 save_commits_by_signer(@lines) if ($interactive);
1619 if (!$email_git_penguin_chiefs) {
1620 @signatures = grep(!/${penguin_chiefs}/i, @signatures);
1623 my ($author_ref, $authors_ref) = extract_formatted_signatures(@authors);
1624 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1626 return ($commits, $signers_ref, $authors_ref, \@stats);
1629 sub vcs_find_author {
1633 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1635 if (!$email_git_penguin_chiefs) {
1636 @lines = grep(!/${penguin_chiefs}/i, @lines);
1639 return @lines if !@lines;
1642 foreach my $line (@lines) {
1643 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1645 my ($name, $address) = parse_email($author);
1646 $author = format_email($name, $address, 1);
1647 push(@authors, $author);
1651 save_commits_by_author(@lines) if ($interactive);
1652 save_commits_by_signer(@lines) if ($interactive);
1657 sub vcs_save_commits {
1662 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1664 foreach my $line (@lines) {
1665 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1678 return @commits if (!(-f $file));
1680 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1681 my @all_commits = ();
1683 $cmd = $VCS_cmds{"blame_file_cmd"};
1684 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1685 @all_commits = vcs_save_commits($cmd);
1687 foreach my $file_range_diff (@range) {
1688 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1690 my $diff_start = $2;
1691 my $diff_length = $3;
1692 next if ("$file" ne "$diff_file");
1693 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1694 push(@commits, $all_commits[$i]);
1698 foreach my $file_range_diff (@range) {
1699 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1701 my $diff_start = $2;
1702 my $diff_length = $3;
1703 next if ("$file" ne "$diff_file");
1704 $cmd = $VCS_cmds{"blame_range_cmd"};
1705 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1706 push(@commits, vcs_save_commits($cmd));
1709 $cmd = $VCS_cmds{"blame_file_cmd"};
1710 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1711 @commits = vcs_save_commits($cmd);
1714 foreach my $commit (@commits) {
1715 $commit =~ s/^\^//g;
1721 my $printed_novcs = 0;
1723 %VCS_cmds = %VCS_cmds_git;
1724 return 1 if eval $VCS_cmds{"available"};
1725 %VCS_cmds = %VCS_cmds_hg;
1726 return 2 if eval $VCS_cmds{"available"};
1728 if (!$printed_novcs && $email_git) {
1729 warn("$P: No supported VCS found. Add --nogit to options?\n");
1730 warn("Using a git repository produces better results.\n");
1731 warn("Try Linus Torvalds' latest git repository using:\n");
1732 warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n");
1740 return $vcs_used == 1;
1744 return $vcs_used == 2;
1747 sub vcs_add_commit_signers {
1748 return if (!vcs_exists());
1750 my ($commit, $desc) = @_;
1751 my $commit_count = 0;
1752 my $commit_authors_ref;
1753 my $commit_signers_ref;
1755 my @commit_authors = ();
1756 my @commit_signers = ();
1759 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1760 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1762 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, "");
1763 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
1764 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
1766 foreach my $signer (@commit_signers) {
1767 $signer = deduplicate_email($signer);
1770 vcs_assign($desc, 1, @commit_signers);
1773 sub interactive_get_maintainers {
1774 my ($list_ref) = @_;
1775 my @list = @$list_ref;
1784 foreach my $entry (@list) {
1785 $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
1786 $selected{$count} = 1;
1787 $authored{$count} = 0;
1788 $signed{$count} = 0;
1794 my $print_options = 0;
1799 printf STDERR "\n%1s %2s %-65s",
1800 "*", "#", "email/list and role:stats";
1802 ($email_git_fallback && !$maintained) ||
1804 print STDERR "auth sign";
1807 foreach my $entry (@list) {
1808 my $email = $entry->[0];
1809 my $role = $entry->[1];
1811 $sel = "*" if ($selected{$count});
1812 my $commit_author = $commit_author_hash{$email};
1813 my $commit_signer = $commit_signer_hash{$email};
1816 $authored++ for (@{$commit_author});
1817 $signed++ for (@{$commit_signer});
1818 printf STDERR "%1s %2d %-65s", $sel, $count + 1, $email;
1819 printf STDERR "%4d %4d", $authored, $signed
1820 if ($authored > 0 || $signed > 0);
1821 printf STDERR "\n %s\n", $role;
1822 if ($authored{$count}) {
1823 my $commit_author = $commit_author_hash{$email};
1824 foreach my $ref (@{$commit_author}) {
1825 print STDERR " Author: @{$ref}[1]\n";
1828 if ($signed{$count}) {
1829 my $commit_signer = $commit_signer_hash{$email};
1830 foreach my $ref (@{$commit_signer}) {
1831 print STDERR " @{$ref}[2]: @{$ref}[1]\n";
1838 my $date_ref = \$email_git_since;
1839 $date_ref = \$email_hg_since if (vcs_is_hg());
1840 if ($print_options) {
1845 Version Control options:
1846 g use git history [$email_git]
1847 gf use git-fallback [$email_git_fallback]
1848 b use git blame [$email_git_blame]
1849 bs use blame signatures [$email_git_blame_signatures]
1850 c# minimum commits [$email_git_min_signatures]
1851 %# min percent [$email_git_min_percent]
1852 d# history to use [$$date_ref]
1853 x# max maintainers [$email_git_max_maintainers]
1854 t all signature types [$email_git_all_signature_types]
1855 m use .mailmap [$email_use_mailmap]
1862 tm toggle maintainers
1863 tg toggle git entries
1864 tl toggle open list entries
1865 ts toggle subscriber list entries
1866 f emails in file [$email_file_emails]
1867 k keywords in file [$keywords]
1868 r remove duplicates [$email_remove_duplicates]
1869 p# pattern match depth [$pattern_depth]
1873 "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1875 my $input = <STDIN>;
1880 my @wish = split(/[, ]+/, $input);
1881 foreach my $nr (@wish) {
1883 my $sel = substr($nr, 0, 1);
1884 my $str = substr($nr, 1);
1886 $val = $1 if $str =~ /^(\d+)$/;
1891 $output_rolestats = 0;
1894 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1895 $selected{$nr - 1} = !$selected{$nr - 1};
1896 } elsif ($sel eq "*" || $sel eq '^') {
1898 $toggle = 1 if ($sel eq '*');
1899 for (my $i = 0; $i < $count; $i++) {
1900 $selected{$i} = $toggle;
1902 } elsif ($sel eq "0") {
1903 for (my $i = 0; $i < $count; $i++) {
1904 $selected{$i} = !$selected{$i};
1906 } elsif ($sel eq "t") {
1907 if (lc($str) eq "m") {
1908 for (my $i = 0; $i < $count; $i++) {
1909 $selected{$i} = !$selected{$i}
1910 if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
1912 } elsif (lc($str) eq "g") {
1913 for (my $i = 0; $i < $count; $i++) {
1914 $selected{$i} = !$selected{$i}
1915 if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
1917 } elsif (lc($str) eq "l") {
1918 for (my $i = 0; $i < $count; $i++) {
1919 $selected{$i} = !$selected{$i}
1920 if ($list[$i]->[1] =~ /^(open list)/i);
1922 } elsif (lc($str) eq "s") {
1923 for (my $i = 0; $i < $count; $i++) {
1924 $selected{$i} = !$selected{$i}
1925 if ($list[$i]->[1] =~ /^(subscriber list)/i);
1928 } elsif ($sel eq "a") {
1929 if ($val > 0 && $val <= $count) {
1930 $authored{$val - 1} = !$authored{$val - 1};
1931 } elsif ($str eq '*' || $str eq '^') {
1933 $toggle = 1 if ($str eq '*');
1934 for (my $i = 0; $i < $count; $i++) {
1935 $authored{$i} = $toggle;
1938 } elsif ($sel eq "s") {
1939 if ($val > 0 && $val <= $count) {
1940 $signed{$val - 1} = !$signed{$val - 1};
1941 } elsif ($str eq '*' || $str eq '^') {
1943 $toggle = 1 if ($str eq '*');
1944 for (my $i = 0; $i < $count; $i++) {
1945 $signed{$i} = $toggle;
1948 } elsif ($sel eq "o") {
1951 } elsif ($sel eq "g") {
1953 bool_invert(\$email_git_fallback);
1955 bool_invert(\$email_git);
1958 } elsif ($sel eq "b") {
1960 bool_invert(\$email_git_blame_signatures);
1962 bool_invert(\$email_git_blame);
1965 } elsif ($sel eq "c") {
1967 $email_git_min_signatures = $val;
1970 } elsif ($sel eq "x") {
1972 $email_git_max_maintainers = $val;
1975 } elsif ($sel eq "%") {
1976 if ($str ne "" && $val >= 0) {
1977 $email_git_min_percent = $val;
1980 } elsif ($sel eq "d") {
1982 $email_git_since = $str;
1983 } elsif (vcs_is_hg()) {
1984 $email_hg_since = $str;
1987 } elsif ($sel eq "t") {
1988 bool_invert(\$email_git_all_signature_types);
1990 } elsif ($sel eq "f") {
1991 bool_invert(\$email_file_emails);
1993 } elsif ($sel eq "r") {
1994 bool_invert(\$email_remove_duplicates);
1996 } elsif ($sel eq "m") {
1997 bool_invert(\$email_use_mailmap);
2000 } elsif ($sel eq "k") {
2001 bool_invert(\$keywords);
2003 } elsif ($sel eq "p") {
2004 if ($str ne "" && $val >= 0) {
2005 $pattern_depth = $val;
2008 } elsif ($sel eq "h" || $sel eq "?") {
2011 Interactive mode allows you to select the various maintainers, submitters,
2012 commit signers and mailing lists that could be CC'd on a patch.
2014 Any *'d entry is selected.
2016 If you have git or hg installed, you can choose to summarize the commit
2017 history of files in the patch. Also, each line of the current file can
2018 be matched to its commit author and that commits signers with blame.
2020 Various knobs exist to control the length of time for active commit
2021 tracking, the maximum number of commit authors and signers to add,
2024 Enter selections at the prompt until you are satisfied that the selected
2025 maintainers are appropriate. You may enter multiple selections separated
2026 by either commas or spaces.
2030 print STDERR "invalid option: '$nr'\n";
2035 print STDERR "git-blame can be very slow, please have patience..."
2036 if ($email_git_blame);
2037 goto &get_maintainers;
2041 #drop not selected entries
2043 my @new_emailto = ();
2044 foreach my $entry (@list) {
2045 if ($selected{$count}) {
2046 push(@new_emailto, $list[$count]);
2050 return @new_emailto;
2054 my ($bool_ref) = @_;
2063 sub deduplicate_email {
2067 my ($name, $address) = parse_email($email);
2068 $email = format_email($name, $address, 1);
2069 $email = mailmap_email($email);
2071 return $email if (!$email_remove_duplicates);
2073 ($name, $address) = parse_email($email);
2075 if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
2076 $name = $deduplicate_name_hash{lc($name)}->[0];
2077 $address = $deduplicate_name_hash{lc($name)}->[1];
2079 } elsif ($deduplicate_address_hash{lc($address)}) {
2080 $name = $deduplicate_address_hash{lc($address)}->[0];
2081 $address = $deduplicate_address_hash{lc($address)}->[1];
2085 $deduplicate_name_hash{lc($name)} = [ $name, $address ];
2086 $deduplicate_address_hash{lc($address)} = [ $name, $address ];
2088 $email = format_email($name, $address, 1);
2089 $email = mailmap_email($email);
2093 sub save_commits_by_author {
2100 foreach my $line (@lines) {
2101 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2103 $author = deduplicate_email($author);
2104 push(@authors, $author);
2106 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
2107 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
2110 for (my $i = 0; $i < @authors; $i++) {
2112 foreach my $ref(@{$commit_author_hash{$authors[$i]}}) {
2113 if (@{$ref}[0] eq $commits[$i] &&
2114 @{$ref}[1] eq $subjects[$i]) {
2120 push(@{$commit_author_hash{$authors[$i]}},
2121 [ ($commits[$i], $subjects[$i]) ]);
2126 sub save_commits_by_signer {
2132 foreach my $line (@lines) {
2133 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
2134 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
2135 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
2136 my @signatures = ($line);
2137 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
2138 my @types = @$types_ref;
2139 my @signers = @$signers_ref;
2141 my $type = $types[0];
2142 my $signer = $signers[0];
2144 $signer = deduplicate_email($signer);
2147 foreach my $ref(@{$commit_signer_hash{$signer}}) {
2148 if (@{$ref}[0] eq $commit &&
2149 @{$ref}[1] eq $subject &&
2150 @{$ref}[2] eq $type) {
2156 push(@{$commit_signer_hash{$signer}},
2157 [ ($commit, $subject, $type) ]);
2164 my ($role, $divisor, @lines) = @_;
2169 return if (@lines <= 0);
2171 if ($divisor <= 0) {
2172 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
2176 @lines = mailmap(@lines);
2178 return if (@lines <= 0);
2180 @lines = sort(@lines);
2183 $hash{$_}++ for @lines;
2186 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
2187 my $sign_offs = $hash{$line};
2188 my $percent = $sign_offs * 100 / $divisor;
2190 $percent = 100 if ($percent > 100);
2191 next if (ignore_email_address($line));
2193 last if ($sign_offs < $email_git_min_signatures ||
2194 $count > $email_git_max_maintainers ||
2195 $percent < $email_git_min_percent);
2196 push_email_address($line, '');
2197 if ($output_rolestats) {
2198 my $fmt_percent = sprintf("%.0f", $percent);
2199 add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%");
2201 add_role($line, $role);
2206 sub vcs_file_signoffs {
2217 $vcs_used = vcs_exists();
2218 return if (!$vcs_used);
2220 my $cmd = $VCS_cmds{"find_signers_cmd"};
2221 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2223 ($commits, $signers_ref, $authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2225 @signers = @{$signers_ref} if defined $signers_ref;
2226 @authors = @{$authors_ref} if defined $authors_ref;
2227 @stats = @{$stats_ref} if defined $stats_ref;
2229 # print("commits: <$commits>\nsigners:<@signers>\nauthors: <@authors>\nstats: <@stats>\n");
2231 foreach my $signer (@signers) {
2232 $signer = deduplicate_email($signer);
2235 vcs_assign("commit_signer", $commits, @signers);
2236 vcs_assign("authored", $commits, @authors);
2237 if ($#authors == $#stats) {
2238 my $stat_pattern = $VCS_cmds{"stat_pattern"};
2239 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
2243 for (my $i = 0; $i <= $#stats; $i++) {
2244 if ($stats[$i] =~ /$stat_pattern/) {
2249 my @tmp_authors = uniq(@authors);
2250 foreach my $author (@tmp_authors) {
2251 $author = deduplicate_email($author);
2253 @tmp_authors = uniq(@tmp_authors);
2254 my @list_added = ();
2255 my @list_deleted = ();
2256 foreach my $author (@tmp_authors) {
2258 my $auth_deleted = 0;
2259 for (my $i = 0; $i <= $#stats; $i++) {
2260 if ($author eq deduplicate_email($authors[$i]) &&
2261 $stats[$i] =~ /$stat_pattern/) {
2263 $auth_deleted += $2;
2266 for (my $i = 0; $i < $auth_added; $i++) {
2267 push(@list_added, $author);
2269 for (my $i = 0; $i < $auth_deleted; $i++) {
2270 push(@list_deleted, $author);
2273 vcs_assign("added_lines", $added, @list_added);
2274 vcs_assign("removed_lines", $deleted, @list_deleted);
2278 sub vcs_file_blame {
2282 my @all_commits = ();
2287 $vcs_used = vcs_exists();
2288 return if (!$vcs_used);
2290 @all_commits = vcs_blame($file);
2291 @commits = uniq(@all_commits);
2292 $total_commits = @commits;
2293 $total_lines = @all_commits;
2295 if ($email_git_blame_signatures) {
2298 my $commit_authors_ref;
2299 my $commit_signers_ref;
2301 my @commit_authors = ();
2302 my @commit_signers = ();
2303 my $commit = join(" -r ", @commits);
2306 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2307 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2309 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2310 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2311 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2313 push(@signers, @commit_signers);
2315 foreach my $commit (@commits) {
2317 my $commit_authors_ref;
2318 my $commit_signers_ref;
2320 my @commit_authors = ();
2321 my @commit_signers = ();
2324 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2325 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2327 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2328 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2329 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2331 push(@signers, @commit_signers);
2336 if ($from_filename) {
2337 if ($output_rolestats) {
2339 if (vcs_is_hg()) {{ # Double brace for last exit
2341 my @commit_signers = ();
2342 @commits = uniq(@commits);
2343 @commits = sort(@commits);
2344 my $commit = join(" -r ", @commits);
2347 $cmd = $VCS_cmds{"find_commit_author_cmd"};
2348 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2352 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
2354 if (!$email_git_penguin_chiefs) {
2355 @lines = grep(!/${penguin_chiefs}/i, @lines);
2361 foreach my $line (@lines) {
2362 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2364 $author = deduplicate_email($author);
2365 push(@authors, $author);
2369 save_commits_by_author(@lines) if ($interactive);
2370 save_commits_by_signer(@lines) if ($interactive);
2372 push(@signers, @authors);
2375 foreach my $commit (@commits) {
2377 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
2378 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
2379 my @author = vcs_find_author($cmd);
2382 my $formatted_author = deduplicate_email($author[0]);
2384 my $count = grep(/$commit/, @all_commits);
2385 for ($i = 0; $i < $count ; $i++) {
2386 push(@blame_signers, $formatted_author);
2390 if (@blame_signers) {
2391 vcs_assign("authored lines", $total_lines, @blame_signers);
2394 foreach my $signer (@signers) {
2395 $signer = deduplicate_email($signer);
2397 vcs_assign("commits", $total_commits, @signers);
2399 foreach my $signer (@signers) {
2400 $signer = deduplicate_email($signer);
2402 vcs_assign("modified commits", $total_commits, @signers);
2406 sub vcs_file_exists {
2411 my $vcs_used = vcs_exists();
2412 return 0 if (!$vcs_used);
2414 my $cmd = $VCS_cmds{"file_exists_cmd"};
2415 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2417 $exists = &{$VCS_cmds{"execute_cmd"}}($cmd);
2419 return 0 if ($? != 0);
2424 sub vcs_list_files {
2429 my $vcs_used = vcs_exists();
2430 return 0 if (!$vcs_used);
2432 my $cmd = $VCS_cmds{"list_files_cmd"};
2433 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2434 @lsfiles = &{$VCS_cmds{"execute_cmd"}}($cmd);
2436 return () if ($? != 0);
2445 @parms = grep(!$saw{$_}++, @parms);
2453 @parms = sort @parms;
2454 @parms = grep(!$saw{$_}++, @parms);
2458 sub clean_file_emails {
2459 my (@file_emails) = @_;
2460 my @fmt_emails = ();
2462 foreach my $email (@file_emails) {
2463 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
2464 my ($name, $address) = parse_email($email);
2466 # Strip quotes for easier processing, format_email will add them back
2467 $name =~ s/^"(.*)"$/$1/;
2469 # Split into name-like parts and remove stray punctuation particles
2470 my @nw = split(/[^\p{L}\'\,\.\+-]/, $name);
2471 @nw = grep(!/^[\'\,\.\+-]$/, @nw);
2473 # Make a best effort to extract the name, and only the name, by taking
2474 # only the last two names, or in the case of obvious initials, the last
2477 my $first = $nw[@nw - 3];
2478 my $middle = $nw[@nw - 2];
2479 my $last = $nw[@nw - 1];
2481 if (((length($first) == 1 && $first =~ m/\p{L}/) ||
2482 (length($first) == 2 && substr($first, -1) eq ".")) ||
2483 (length($middle) == 1 ||
2484 (length($middle) == 2 && substr($middle, -1) eq "."))) {
2485 $name = "$first $middle $last";
2487 $name = "$middle $last";
2493 if (substr($name, -1) =~ /[,\.]/) {
2494 $name = substr($name, 0, length($name) - 1);
2497 if (substr($name, 0, 1) =~ /[,\.]/) {
2498 $name = substr($name, 1, length($name) - 1);
2501 my $fmt_email = format_email($name, $address, $email_usename);
2502 push(@fmt_emails, $fmt_email);
2512 my ($address, $role) = @$_;
2513 if (!$saw{$address}) {
2514 if ($output_roles) {
2515 push(@lines, "$address ($role)");
2517 push(@lines, $address);
2529 if ($output_multiline) {
2530 foreach my $line (@parms) {
2534 print(join($output_separator, @parms));
2542 # Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
2543 # comment. We must allow for rfc822_lwsp (or comments) after each of these.
2544 # This regexp will only work on addresses which have had comments stripped
2545 # and replaced with rfc822_lwsp.
2547 my $specials = '()<>@,;:\\\\".\\[\\]';
2548 my $controls = '\\000-\\037\\177';
2550 my $dtext = "[^\\[\\]\\r\\\\]";
2551 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
2553 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
2555 # Use zero-width assertion to spot the limit of an atom. A simple
2556 # $rfc822_lwsp* causes the regexp engine to hang occasionally.
2557 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
2558 my $word = "(?:$atom|$quoted_string)";
2559 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
2561 my $sub_domain = "(?:$atom|$domain_literal)";
2562 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
2564 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
2566 my $phrase = "$word*";
2567 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
2568 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
2569 my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
2571 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
2572 my $address = "(?:$mailbox|$group)";
2574 return "$rfc822_lwsp*$address";
2577 sub rfc822_strip_comments {
2579 # Recursively remove comments, and replace with a single space. The simpler
2580 # regexps in the Email Addressing FAQ are imperfect - they will miss escaped
2581 # chars in atoms, for example.
2583 while ($s =~ s/^((?:[^"\\]|\\.)*
2584 (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
2585 \((?:[^()\\]|\\.)*\)/$1 /osx) {}
2589 # valid: returns true if the parameter is an RFC822 valid address
2592 my $s = rfc822_strip_comments(shift);
2595 $rfc822re = make_rfc822re();
2598 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
2601 # validlist: In scalar context, returns true if the parameter is an RFC822
2602 # valid list of addresses.
2604 # In list context, returns an empty list on failure (an invalid
2605 # address was found); otherwise a list whose first element is the
2606 # number of addresses found and whose remaining elements are the
2607 # addresses. This is needed to disambiguate failure (invalid)
2608 # from success with no addresses found, because an empty string is
2611 sub rfc822_validlist {
2612 my $s = rfc822_strip_comments(shift);
2615 $rfc822re = make_rfc822re();
2617 # * null list items are valid according to the RFC
2618 # * the '1' business is to aid in distinguishing failure from no results
2621 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
2622 $s =~ m/^$rfc822_char*$/) {
2623 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
2626 return wantarray ? (scalar(@r), @r) : 1;
2628 return wantarray ? () : 0;