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;
24 my $cur_path = fastgetcwd() . '/';
27 my $email_usename = 1;
28 my $email_maintainer = 1;
29 my $email_reviewer = 1;
32 my $email_moderated_list = 1;
33 my $email_subscriber_list = 0;
34 my $email_git_penguin_chiefs = 0;
36 my $email_git_all_signature_types = 0;
37 my $email_git_blame = 0;
38 my $email_git_blame_signatures = 1;
39 my $email_git_fallback = 1;
40 my $email_git_min_signatures = 1;
41 my $email_git_max_maintainers = 5;
42 my $email_git_min_percent = 5;
43 my $email_git_since = "1-year-ago";
44 my $email_hg_since = "-365";
46 my $email_remove_duplicates = 1;
47 my $email_use_mailmap = 1;
48 my $output_multiline = 1;
49 my $output_separator = ", ";
51 my $output_rolestats = 1;
52 my $output_section_maxlen = 50;
61 my $email_file_emails = 0;
62 my $from_filename = 0;
63 my $pattern_depth = 0;
64 my $self_test = undef;
67 my $find_maintainer_files = 0;
74 my @fixes = (); # If a patch description includes Fixes: lines
79 my %commit_author_hash;
80 my %commit_signer_hash;
82 my @penguin_chief = ();
83 push(@penguin_chief, "Linus Torvalds:torvalds\@linux-foundation.org");
84 #Andrew wants in on most everything - 2009/01/14
85 #push(@penguin_chief, "Andrew Morton:akpm\@linux-foundation.org");
87 my @penguin_chief_names = ();
88 foreach my $chief (@penguin_chief) {
89 if ($chief =~ m/^(.*):(.*)/) {
92 push(@penguin_chief_names, $chief_name);
95 my $penguin_chiefs = "\(" . join("|", @penguin_chief_names) . "\)";
97 # Signature types of people who are either
98 # a) responsible for the code in question, or
99 # b) familiar enough with it to give relevant feedback
100 my @signature_tags = ();
101 push(@signature_tags, "Signed-off-by:");
102 push(@signature_tags, "Reviewed-by:");
103 push(@signature_tags, "Acked-by:");
105 my $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
107 # rfc822 email address - preloaded methods go here.
108 my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
109 my $rfc822_char = '[\\000-\\377]';
111 # VCS command support: class-like functions and strings
116 "execute_cmd" => \&git_execute_cmd,
117 "available" => '(which("git") ne "") && (-e ".git")',
118 "find_signers_cmd" =>
119 "git log --no-color --follow --since=\$email_git_since " .
120 '--numstat --no-merges ' .
121 '--format="GitCommit: %H%n' .
122 'GitAuthor: %an <%ae>%n' .
127 "find_commit_signers_cmd" =>
128 "git log --no-color " .
130 '--format="GitCommit: %H%n' .
131 'GitAuthor: %an <%ae>%n' .
136 "find_commit_author_cmd" =>
137 "git log --no-color " .
139 '--format="GitCommit: %H%n' .
140 'GitAuthor: %an <%ae>%n' .
142 'GitSubject: %s%n"' .
144 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
145 "blame_file_cmd" => "git blame -l \$file",
146 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})",
147 "blame_commit_pattern" => "^([0-9a-f]+) ",
148 "author_pattern" => "^GitAuthor: (.*)",
149 "subject_pattern" => "^GitSubject: (.*)",
150 "stat_pattern" => "^(\\d+)\\t(\\d+)\\t\$file\$",
151 "file_exists_cmd" => "git ls-files \$file",
152 "list_files_cmd" => "git ls-files \$file",
156 "execute_cmd" => \&hg_execute_cmd,
157 "available" => '(which("hg") ne "") && (-d ".hg")',
158 "find_signers_cmd" =>
159 "hg log --date=\$email_hg_since " .
160 "--template='HgCommit: {node}\\n" .
161 "HgAuthor: {author}\\n" .
162 "HgSubject: {desc}\\n'" .
164 "find_commit_signers_cmd" =>
166 "--template='HgSubject: {desc}\\n'" .
168 "find_commit_author_cmd" =>
170 "--template='HgCommit: {node}\\n" .
171 "HgAuthor: {author}\\n" .
172 "HgSubject: {desc|firstline}\\n'" .
174 "blame_range_cmd" => "", # not supported
175 "blame_file_cmd" => "hg blame -n \$file",
176 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})",
177 "blame_commit_pattern" => "^([ 0-9a-f]+):",
178 "author_pattern" => "^HgAuthor: (.*)",
179 "subject_pattern" => "^HgSubject: (.*)",
180 "stat_pattern" => "^(\\d+)\t(\\d+)\t\$file\$",
181 "file_exists_cmd" => "hg files \$file",
182 "list_files_cmd" => "hg manifest -R \$file",
185 my $conf = which_conf(".get_maintainer.conf");
188 open(my $conffile, '<', "$conf")
189 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
191 while (<$conffile>) {
194 $line =~ s/\s*\n?$//g;
198 next if ($line =~ m/^\s*#/);
199 next if ($line =~ m/^\s*$/);
201 my @words = split(" ", $line);
202 foreach my $word (@words) {
203 last if ($word =~ m/^#/);
204 push (@conf_args, $word);
208 unshift(@ARGV, @conf_args) if @conf_args;
211 my @ignore_emails = ();
212 my $ignore_file = which_conf(".get_maintainer.ignore");
213 if (-f $ignore_file) {
214 open(my $ignore, '<', "$ignore_file")
215 or warn "$P: Can't find a readable .get_maintainer.ignore file $!\n";
219 $line =~ s/\s*\n?$//;
224 next if ($line =~ m/^\s*$/);
225 if (rfc822_valid($line)) {
226 push(@ignore_emails, $line);
234 if ($_ =~ /^-{1,2}self-test(?:=|$)/) {
235 die "$P: using --self-test does not allow any other option or argument\n";
242 'git!' => \$email_git,
243 'git-all-signature-types!' => \$email_git_all_signature_types,
244 'git-blame!' => \$email_git_blame,
245 'git-blame-signatures!' => \$email_git_blame_signatures,
246 'git-fallback!' => \$email_git_fallback,
247 'git-chief-penguins!' => \$email_git_penguin_chiefs,
248 'git-min-signatures=i' => \$email_git_min_signatures,
249 'git-max-maintainers=i' => \$email_git_max_maintainers,
250 'git-min-percent=i' => \$email_git_min_percent,
251 'git-since=s' => \$email_git_since,
252 'hg-since=s' => \$email_hg_since,
253 'i|interactive!' => \$interactive,
254 'remove-duplicates!' => \$email_remove_duplicates,
255 'mailmap!' => \$email_use_mailmap,
256 'm!' => \$email_maintainer,
257 'r!' => \$email_reviewer,
258 'n!' => \$email_usename,
259 'l!' => \$email_list,
260 'fixes!' => \$email_fixes,
261 'moderated!' => \$email_moderated_list,
262 's!' => \$email_subscriber_list,
263 'multiline!' => \$output_multiline,
264 'roles!' => \$output_roles,
265 'rolestats!' => \$output_rolestats,
266 'separator=s' => \$output_separator,
267 'subsystem!' => \$subsystem,
268 'status!' => \$status,
272 'letters=s' => \$letters,
273 'pattern-depth=i' => \$pattern_depth,
274 'k|keywords!' => \$keywords,
275 'sections!' => \$sections,
276 'fe|file-emails!' => \$email_file_emails,
277 'f|file' => \$from_filename,
278 'find-maintainer-files' => \$find_maintainer_files,
279 'mpath|maintainer-path=s' => \$maintainer_path,
280 'self-test:s' => \$self_test,
281 'v|version' => \$version,
282 'h|help|usage' => \$help,
284 die "$P: invalid argument - use --help if necessary\n";
293 print("${P} ${V}\n");
297 if (defined $self_test) {
298 read_all_maintainer_files();
303 if (-t STDIN && !@ARGV) {
304 # We're talking to a terminal, but have no command line arguments.
305 die "$P: missing patchfile or -f file - use --help if necessary\n";
308 $output_multiline = 0 if ($output_separator ne ", ");
309 $output_rolestats = 1 if ($interactive);
310 $output_roles = 1 if ($output_rolestats);
312 if ($sections || $letters ne "") {
323 my $selections = $email + $scm + $status + $subsystem + $web;
324 if ($selections == 0) {
325 die "$P: Missing required option: email, scm, status, subsystem or web\n";
330 ($email_maintainer + $email_reviewer +
331 $email_list + $email_subscriber_list +
332 $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
333 die "$P: Please select at least 1 email option\n";
336 if ($tree && !top_of_kernel_tree($lk_path)) {
337 die "$P: The current directory does not appear to be "
338 . "a linux kernel source tree.\n";
341 ## Read MAINTAINERS for type/value pairs
346 my @self_test_info = ();
348 sub read_maintainer_file {
351 open (my $maint, '<', "$file")
352 or die "$P: Can't open MAINTAINERS file '$file': $!\n";
358 if ($line =~ m/^([A-Z]):\s*(.*)/) {
362 ##Filename pattern matching
363 if ($type eq "F" || $type eq "X") {
364 $value =~ s@\.@\\\.@g; ##Convert . to \.
365 $value =~ s/\*/\.\*/g; ##Convert * to .*
366 $value =~ s/\?/\./g; ##Convert ? to .
367 ##if pattern is a directory and it lacks a trailing slash, add one
369 $value =~ s@([^/])$@$1/@;
371 } elsif ($type eq "K") {
372 $keyword_hash{@typevalue} = $value;
374 push(@typevalue, "$type:$value");
375 } elsif (!(/^\s*$/ || /^\s*\#/)) {
376 push(@typevalue, $line);
378 if (defined $self_test) {
379 push(@self_test_info, {file=>$file, linenr=>$i, line=>$line});
386 sub find_is_maintainer_file {
388 return if ($file !~ m@/MAINTAINERS$@);
389 $file = $File::Find::name;
390 return if (! -f $file);
391 push(@mfiles, $file);
394 sub find_ignore_git {
395 return grep { $_ !~ /^\.git$/; } @_;
398 read_all_maintainer_files();
400 sub read_all_maintainer_files {
401 my $path = "${lk_path}MAINTAINERS";
402 if (defined $maintainer_path) {
403 $path = $maintainer_path;
404 # Perl Cookbook tilde expansion if necessary
405 $path =~ s@^~([^/]*)@ $1 ? (getpwnam($1))[7] : ( $ENV{HOME} || $ENV{LOGDIR} || (getpwuid($<))[7])@ex;
409 $path .= '/' if ($path !~ m@/$@);
410 if ($find_maintainer_files) {
411 find( { wanted => \&find_is_maintainer_file,
412 preprocess => \&find_ignore_git,
416 opendir(DIR, "$path") or die $!;
417 my @files = readdir(DIR);
419 foreach my $file (@files) {
420 push(@mfiles, "$path$file") if ($file !~ /^\./);
423 } elsif (-f "$path") {
424 push(@mfiles, "$path");
426 die "$P: MAINTAINER file not found '$path'\n";
428 die "$P: No MAINTAINER files found in '$path'\n" if (scalar(@mfiles) == 0);
429 foreach my $file (@mfiles) {
430 read_maintainer_file("$file");
434 sub maintainers_in_file {
437 return if ($file =~ m@\bMAINTAINERS$@);
439 if (-f $file && ($email_file_emails || $file =~ /\.yaml$/)) {
440 open(my $f, '<', $file)
441 or die "$P: Can't open $file: $!\n";
442 my $text = do { local($/) ; <$f> };
445 my @poss_addr = $text =~ m$[A-Za-zÀ-ÿ\"\' \,\.\+-]*\s*[\,]*\s*[\(\<\{]{0,1}[A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+\.[A-Za-z0-9]+[\)\>\}]{0,1}$g;
446 push(@file_emails, clean_file_emails(@poss_addr));
451 # Read mail address map
464 return if (!$email_use_mailmap || !(-f "${lk_path}.mailmap"));
466 open(my $mailmap_file, '<', "${lk_path}.mailmap")
467 or warn "$P: Can't open .mailmap: $!\n";
469 while (<$mailmap_file>) {
470 s/#.*$//; #strip comments
471 s/^\s+|\s+$//g; #trim
473 next if (/^\s*$/); #skip empty lines
474 #entries have one of the following formats:
477 # name1 <mail1> <mail2>
478 # name1 <mail1> name2 <mail2>
479 # (see man git-shortlog)
481 if (/^([^<]+)<([^>]+)>$/) {
485 $real_name =~ s/\s+$//;
486 ($real_name, $address) = parse_email("$real_name <$address>");
487 $mailmap->{names}->{$address} = $real_name;
489 } elsif (/^<([^>]+)>\s*<([^>]+)>$/) {
490 my $real_address = $1;
491 my $wrong_address = $2;
493 $mailmap->{addresses}->{$wrong_address} = $real_address;
495 } elsif (/^(.+)<([^>]+)>\s*<([^>]+)>$/) {
497 my $real_address = $2;
498 my $wrong_address = $3;
500 $real_name =~ s/\s+$//;
501 ($real_name, $real_address) =
502 parse_email("$real_name <$real_address>");
503 $mailmap->{names}->{$wrong_address} = $real_name;
504 $mailmap->{addresses}->{$wrong_address} = $real_address;
506 } elsif (/^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$/) {
508 my $real_address = $2;
510 my $wrong_address = $4;
512 $real_name =~ s/\s+$//;
513 ($real_name, $real_address) =
514 parse_email("$real_name <$real_address>");
516 $wrong_name =~ s/\s+$//;
517 ($wrong_name, $wrong_address) =
518 parse_email("$wrong_name <$wrong_address>");
520 my $wrong_email = format_email($wrong_name, $wrong_address, 1);
521 $mailmap->{names}->{$wrong_email} = $real_name;
522 $mailmap->{addresses}->{$wrong_email} = $real_address;
525 close($mailmap_file);
528 ## use the filenames on the command line or find the filenames in the patchfiles
531 push(@ARGV, "&STDIN");
534 foreach my $file (@ARGV) {
535 if ($file ne "&STDIN") {
536 $file = canonpath($file);
537 ##if $file is a directory and it lacks a trailing slash, add one
539 $file =~ s@([^/])$@$1/@;
540 } elsif (!(-f $file)) {
541 die "$P: file '${file}' not found\n";
544 if ($from_filename && (vcs_exists() && !vcs_file_exists($file))) {
545 warn "$P: file '$file' not found in version control $!\n";
547 if ($from_filename || ($file ne "&STDIN" && vcs_file_exists($file))) {
548 $file =~ s/^\Q${cur_path}\E//; #strip any absolute path
549 $file =~ s/^\Q${lk_path}\E//; #or the path to the lk tree
551 if ($file ne "MAINTAINERS" && -f $file && $keywords) {
552 open(my $f, '<', $file)
553 or die "$P: Can't open $file: $!\n";
554 my $text = do { local($/) ; <$f> };
557 foreach my $line (keys %keyword_hash) {
558 if ($text =~ m/$keyword_hash{$line}/x) {
559 push(@keyword_tvi, $line);
565 my $file_cnt = @files;
568 open(my $patch, "< $file")
569 or die "$P: Can't open $file: $!\n";
571 # We can check arbitrary information before the patch
572 # like the commit message, mail headers, etc...
573 # This allows us to match arbitrary keywords against any part
574 # of a git format-patch generated file (subject tags, etc...)
576 my $patch_prefix = ""; #Parsing the intro
580 if (m/^ mode change [0-7]+ => [0-7]+ (\S+)\s*$/) {
582 push(@files, $filename);
583 } elsif (m/^rename (?:from|to) (\S+)\s*$/) {
585 push(@files, $filename);
586 } elsif (m/^diff --git a\/(\S+) b\/(\S+)\s*$/) {
589 push(@files, $filename1);
590 push(@files, $filename2);
591 } elsif (m/^Fixes:\s+([0-9a-fA-F]{6,40})/) {
592 push(@fixes, $1) if ($email_fixes);
593 } elsif (m/^\+\+\+\s+(\S+)/ or m/^---\s+(\S+)/) {
595 $filename =~ s@^[^/]*/@@;
597 $lastfile = $filename;
598 push(@files, $filename);
599 $patch_prefix = "^[+-].*"; #Now parsing the actual patch
600 } elsif (m/^\@\@ -(\d+),(\d+)/) {
601 if ($email_git_blame) {
602 push(@range, "$lastfile:$1:$2");
604 } elsif ($keywords) {
605 foreach my $line (keys %keyword_hash) {
606 if ($patch_line =~ m/${patch_prefix}$keyword_hash{$line}/x) {
607 push(@keyword_tvi, $line);
614 if ($file_cnt == @files) {
615 warn "$P: file '${file}' doesn't appear to be a patch. "
616 . "Add -f to options?\n";
618 @files = sort_and_uniq(@files);
622 @file_emails = uniq(@file_emails);
623 @fixes = uniq(@fixes);
626 my %email_hash_address;
634 my %deduplicate_name_hash = ();
635 my %deduplicate_address_hash = ();
637 my @maintainers = get_maintainers();
639 @maintainers = merge_email(@maintainers);
640 output(@maintainers);
649 @status = uniq(@status);
654 @subsystem = uniq(@subsystem);
669 my @section_headers = ();
672 @lsfiles = vcs_list_files($lk_path);
674 for my $x (@self_test_info) {
677 ## Section header duplication and missing section content
678 if (($self_test eq "" || $self_test =~ /\bsections\b/) &&
679 $x->{line} =~ /^\S[^:]/ &&
680 defined $self_test_info[$index] &&
681 $self_test_info[$index]->{line} =~ /^([A-Z]):\s*\S/) {
686 if (grep(m@^\Q$x->{line}\E@, @section_headers)) {
687 print("$x->{file}:$x->{linenr}: warning: duplicate section header\t$x->{line}\n");
689 push(@section_headers, $x->{line});
691 my $nextline = $index;
692 while (defined $self_test_info[$nextline] &&
693 $self_test_info[$nextline]->{line} =~ /^([A-Z]):\s*(\S.*)/) {
699 } elsif ($type eq "F" || $type eq "N") {
701 } elsif ($type eq "M" || $type eq "R" || $type eq "L") {
706 if (!$has_ML && $status !~ /orphan|obsolete/i) {
707 print("$x->{file}:$x->{linenr}: warning: section without email address\t$x->{line}\n");
710 print("$x->{file}:$x->{linenr}: warning: section without status \t$x->{line}\n");
713 print("$x->{file}:$x->{linenr}: warning: section without file pattern\t$x->{line}\n");
717 next if ($x->{line} !~ /^([A-Z]):\s*(.*)/);
722 ## Filename pattern matching
723 if (($type eq "F" || $type eq "X") &&
724 ($self_test eq "" || $self_test =~ /\bpatterns\b/)) {
725 $value =~ s@\.@\\\.@g; ##Convert . to \.
726 $value =~ s/\*/\.\*/g; ##Convert * to .*
727 $value =~ s/\?/\./g; ##Convert ? to .
728 ##if pattern is a directory and it lacks a trailing slash, add one
730 $value =~ s@([^/])$@$1/@;
732 if (!grep(m@^$value@, @lsfiles)) {
733 print("$x->{file}:$x->{linenr}: warning: no file matches\t$x->{line}\n");
737 } elsif (($type eq "W" || $type eq "Q" || $type eq "B") &&
738 $value =~ /^https?:/ &&
739 ($self_test eq "" || $self_test =~ /\blinks\b/)) {
740 next if (grep(m@^\Q$value\E$@, @good_links));
742 if (grep(m@^\Q$value\E$@, @bad_links)) {
745 my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $value`;
747 push(@good_links, $value);
749 push(@bad_links, $value);
754 print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
758 } elsif ($type eq "T" &&
759 ($self_test eq "" || $self_test =~ /\bscm\b/)) {
760 next if (grep(m@^\Q$value\E$@, @good_links));
762 if (grep(m@^\Q$value\E$@, @bad_links)) {
764 } elsif ($value !~ /^(?:git|quilt|hg)\s+\S/) {
765 print("$x->{file}:$x->{linenr}: warning: malformed entry\t$x->{line}\n");
766 } elsif ($value =~ /^git\s+(\S+)(\s+([^\(]+\S+))?/) {
770 my $output = `git ls-remote --exit-code -h "$url" $branch > /dev/null 2>&1`;
772 push(@good_links, $value);
774 push(@bad_links, $value);
777 } elsif ($value =~ /^(?:quilt|hg)\s+(https?:\S+)/) {
779 my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $url`;
781 push(@good_links, $value);
783 push(@bad_links, $value);
788 print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
794 sub ignore_email_address {
797 foreach my $ignore (@ignore_emails) {
798 return 1 if ($ignore eq $address);
804 sub range_is_maintained {
805 my ($start, $end) = @_;
807 for (my $i = $start; $i < $end; $i++) {
808 my $line = $typevalue[$i];
809 if ($line =~ m/^([A-Z]):\s*(.*)/) {
813 if ($value =~ /(maintain|support)/i) {
822 sub range_has_maintainer {
823 my ($start, $end) = @_;
825 for (my $i = $start; $i < $end; $i++) {
826 my $line = $typevalue[$i];
827 if ($line =~ m/^([A-Z]):\s*(.*)/) {
838 sub get_maintainers {
839 %email_hash_name = ();
840 %email_hash_address = ();
841 %commit_author_hash = ();
842 %commit_signer_hash = ();
850 %deduplicate_name_hash = ();
851 %deduplicate_address_hash = ();
852 if ($email_git_all_signature_types) {
853 $signature_pattern = "(.+?)[Bb][Yy]:";
855 $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
858 # Find responsible parties
860 my %exact_pattern_match_hash = ();
862 foreach my $file (@files) {
865 my $tvi = find_first_section();
866 while ($tvi < @typevalue) {
867 my $start = find_starting_index($tvi);
868 my $end = find_ending_index($tvi);
872 #Do not match excluded file patterns
874 for ($i = $start; $i < $end; $i++) {
875 my $line = $typevalue[$i];
876 if ($line =~ m/^([A-Z]):\s*(.*)/) {
880 if (file_match_pattern($file, $value)) {
889 for ($i = $start; $i < $end; $i++) {
890 my $line = $typevalue[$i];
891 if ($line =~ m/^([A-Z]):\s*(.*)/) {
895 if (file_match_pattern($file, $value)) {
896 my $value_pd = ($value =~ tr@/@@);
897 my $file_pd = ($file =~ tr@/@@);
898 $value_pd++ if (substr($value,-1,1) ne "/");
899 $value_pd = -1 if ($value =~ /^\.\*/);
900 if ($value_pd >= $file_pd &&
901 range_is_maintained($start, $end) &&
902 range_has_maintainer($start, $end)) {
903 $exact_pattern_match_hash{$file} = 1;
905 if ($pattern_depth == 0 ||
906 (($file_pd - $value_pd) < $pattern_depth)) {
907 $hash{$tvi} = $value_pd;
910 } elsif ($type eq 'N') {
911 if ($file =~ m/$value/x) {
921 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
922 add_categories($line);
925 my $start = find_starting_index($line);
926 my $end = find_ending_index($line);
927 for ($i = $start; $i < $end; $i++) {
928 my $line = $typevalue[$i];
929 if ($line =~ /^[FX]:/) { ##Restore file patterns
930 $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
931 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ?
932 $line =~ s/\\\./\./g; ##Convert \. to .
933 $line =~ s/\.\*/\*/g; ##Convert .* to *
935 my $count = $line =~ s/^([A-Z]):/$1:\t/g;
936 if ($letters eq "" || (!$count || $letters =~ /$1/i)) {
944 maintainers_in_file($file);
948 @keyword_tvi = sort_and_uniq(@keyword_tvi);
949 foreach my $line (@keyword_tvi) {
950 add_categories($line);
954 foreach my $email (@email_to, @list_to) {
955 $email->[0] = deduplicate_email($email->[0]);
958 foreach my $file (@files) {
961 ($email_git_fallback &&
962 $file !~ /MAINTAINERS$/ &&
963 !$exact_pattern_match_hash{$file}))) {
964 vcs_file_signoffs($file);
966 if ($email && $email_git_blame) {
967 vcs_file_blame($file);
972 foreach my $chief (@penguin_chief) {
973 if ($chief =~ m/^(.*):(.*)/) {
976 $email_address = format_email($1, $2, $email_usename);
977 if ($email_git_penguin_chiefs) {
978 push(@email_to, [$email_address, 'chief penguin']);
980 @email_to = grep($_->[0] !~ /${email_address}/, @email_to);
985 foreach my $email (@file_emails) {
986 my ($name, $address) = parse_email($email);
988 my $tmp_email = format_email($name, $address, $email_usename);
989 push_email_address($tmp_email, '');
990 add_role($tmp_email, 'in file');
994 foreach my $fix (@fixes) {
995 vcs_add_commit_signers($fix, "blamed_fixes");
999 if ($email || $email_list) {
1001 @to = (@to, @email_to);
1004 @to = (@to, @list_to);
1009 @to = interactive_get_maintainers(\@to);
1015 sub file_match_pattern {
1016 my ($file, $pattern) = @_;
1017 if (substr($pattern, -1) eq "/") {
1018 if ($file =~ m@^$pattern@) {
1022 if ($file =~ m@^$pattern@) {
1023 my $s1 = ($file =~ tr@/@@);
1024 my $s2 = ($pattern =~ tr@/@@);
1035 usage: $P [options] patchfile
1036 $P [options] -f file|directory
1039 MAINTAINER field selection options:
1040 --email => print email address(es) if any
1041 --git => include recent git \*-by: signers
1042 --git-all-signature-types => include signers regardless of signature type
1043 or use only ${signature_pattern} signers (default: $email_git_all_signature_types)
1044 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
1045 --git-chief-penguins => include ${penguin_chiefs}
1046 --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
1047 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
1048 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
1049 --git-blame => use git blame to find modified commits for patch or file
1050 --git-blame-signatures => when used with --git-blame, also include all commit signers
1051 --git-since => git history to use (default: $email_git_since)
1052 --hg-since => hg history to use (default: $email_hg_since)
1053 --interactive => display a menu (mostly useful if used with the --git option)
1054 --m => include maintainer(s) if any
1055 --r => include reviewer(s) if any
1056 --n => include name 'Full Name <addr\@domain.tld>'
1057 --l => include list(s) if any
1058 --moderated => include moderated lists(s) if any (default: true)
1059 --s => include subscriber only list(s) if any (default: false)
1060 --remove-duplicates => minimize duplicate email names/addresses
1061 --roles => show roles (status:subsystem, git-signer, list, etc...)
1062 --rolestats => show roles and statistics (commits/total_commits, %)
1063 --file-emails => add email addresses found in -f file (default: 0 (off))
1064 --fixes => for patches, add signatures of commits with 'Fixes: <commit>' (default: 1 (on))
1065 --scm => print SCM tree(s) if any
1066 --status => print status if any
1067 --subsystem => print subsystem name if any
1068 --web => print website(s) if any
1070 Output type options:
1071 --separator [, ] => separator for multiple entries on 1 line
1072 using --separator also sets --nomultiline if --separator is not [, ]
1073 --multiline => print 1 entry per line
1076 --pattern-depth => Number of pattern directory traversals (default: 0 (all))
1077 --keywords => scan patch for keywords (default: $keywords)
1078 --sections => print all of the subsystem sections with pattern matches
1079 --letters => print all matching 'letter' types from all matching sections
1080 --mailmap => use .mailmap file (default: $email_use_mailmap)
1081 --no-tree => run without a kernel tree
1082 --self-test => show potential issues with MAINTAINERS file content
1083 --version => show version
1084 --help => show this help information
1087 [--email --tree --nogit --git-fallback --m --r --n --l --multiline
1088 --pattern-depth=0 --remove-duplicates --rolestats]
1091 Using "-f directory" may give unexpected results:
1092 Used with "--git", git signators for _all_ files in and below
1093 directory are examined as git recurses directories.
1094 Any specified X: (exclude) pattern matches are _not_ ignored.
1095 Used with "--nogit", directory is used as a pattern match,
1096 no individual file within the directory or subdirectory
1098 Used with "--git-blame", does not iterate all files in directory
1099 Using "--git-blame" is slow and may add old committers and authors
1100 that are no longer active maintainers to the output.
1101 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
1102 other automated tools that expect only ["name"] <email address>
1103 may not work because of additional output after <email address>.
1104 Using "--rolestats" and "--git-blame" shows the #/total=% commits,
1105 not the percentage of the entire file authored. # of commits is
1106 not a good measure of amount of code authored. 1 major commit may
1107 contain a thousand lines, 5 trivial commits may modify a single line.
1108 If git is not installed, but mercurial (hg) is installed and an .hg
1109 repository exists, the following options apply to mercurial:
1111 --git-min-signatures, --git-max-maintainers, --git-min-percent, and
1113 Use --hg-since not --git-since to control date selection
1114 File ".get_maintainer.conf", if it exists in the linux kernel source root
1115 directory, can change whatever get_maintainer defaults are desired.
1116 Entries in this file can be any command line argument.
1117 This file is prepended to any additional command line arguments.
1118 Multiple lines and # comments are allowed.
1119 Most options have both positive and negative forms.
1120 The negative forms for --<foo> are --no<foo> and --no-<foo>.
1125 sub top_of_kernel_tree {
1128 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
1131 if ( (-f "${lk_path}COPYING")
1132 && (-f "${lk_path}CREDITS")
1133 && (-f "${lk_path}Kbuild")
1134 && (-e "${lk_path}MAINTAINERS")
1135 && (-f "${lk_path}Makefile")
1136 && (-f "${lk_path}README")
1137 && (-d "${lk_path}Documentation")
1138 && (-d "${lk_path}arch")
1139 && (-d "${lk_path}include")
1140 && (-d "${lk_path}drivers")
1141 && (-d "${lk_path}fs")
1142 && (-d "${lk_path}init")
1143 && (-d "${lk_path}ipc")
1144 && (-d "${lk_path}kernel")
1145 && (-d "${lk_path}lib")
1146 && (-d "${lk_path}scripts")) {
1153 my ($formatted_email) = @_;
1158 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
1161 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
1163 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
1167 $name =~ s/^\s+|\s+$//g;
1168 $name =~ s/^\"|\"$//g;
1169 $address =~ s/^\s+|\s+$//g;
1171 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
1172 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
1173 $name = "\"$name\"";
1176 return ($name, $address);
1180 my ($name, $address, $usename) = @_;
1182 my $formatted_email;
1184 $name =~ s/^\s+|\s+$//g;
1185 $name =~ s/^\"|\"$//g;
1186 $address =~ s/^\s+|\s+$//g;
1188 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
1189 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
1190 $name = "\"$name\"";
1194 if ("$name" eq "") {
1195 $formatted_email = "$address";
1197 $formatted_email = "$name <$address>";
1200 $formatted_email = $address;
1203 return $formatted_email;
1206 sub find_first_section {
1209 while ($index < @typevalue) {
1210 my $tv = $typevalue[$index];
1211 if (($tv =~ m/^([A-Z]):\s*(.*)/)) {
1220 sub find_starting_index {
1223 while ($index > 0) {
1224 my $tv = $typevalue[$index];
1225 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
1234 sub find_ending_index {
1237 while ($index < @typevalue) {
1238 my $tv = $typevalue[$index];
1239 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
1248 sub get_subsystem_name {
1251 my $start = find_starting_index($index);
1253 my $subsystem = $typevalue[$start];
1254 if ($output_section_maxlen && length($subsystem) > $output_section_maxlen) {
1255 $subsystem = substr($subsystem, 0, $output_section_maxlen - 3);
1256 $subsystem =~ s/\s*$//;
1257 $subsystem = $subsystem . "...";
1262 sub get_maintainer_role {
1266 my $start = find_starting_index($index);
1267 my $end = find_ending_index($index);
1269 my $role = "unknown";
1270 my $subsystem = get_subsystem_name($index);
1272 for ($i = $start + 1; $i < $end; $i++) {
1273 my $tv = $typevalue[$i];
1274 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1277 if ($ptype eq "S") {
1284 if ($role eq "supported") {
1285 $role = "supporter";
1286 } elsif ($role eq "maintained") {
1287 $role = "maintainer";
1288 } elsif ($role eq "odd fixes") {
1289 $role = "odd fixer";
1290 } elsif ($role eq "orphan") {
1291 $role = "orphan minder";
1292 } elsif ($role eq "obsolete") {
1293 $role = "obsolete minder";
1294 } elsif ($role eq "buried alive in reporters") {
1295 $role = "chief penguin";
1298 return $role . ":" . $subsystem;
1304 my $subsystem = get_subsystem_name($index);
1306 if ($subsystem eq "THE REST") {
1313 sub add_categories {
1317 my $start = find_starting_index($index);
1318 my $end = find_ending_index($index);
1320 push(@subsystem, $typevalue[$start]);
1322 for ($i = $start + 1; $i < $end; $i++) {
1323 my $tv = $typevalue[$i];
1324 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1327 if ($ptype eq "L") {
1328 my $list_address = $pvalue;
1329 my $list_additional = "";
1330 my $list_role = get_list_role($i);
1332 if ($list_role ne "") {
1333 $list_role = ":" . $list_role;
1335 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
1337 $list_additional = $2;
1339 if ($list_additional =~ m/subscribers-only/) {
1340 if ($email_subscriber_list) {
1341 if (!$hash_list_to{lc($list_address)}) {
1342 $hash_list_to{lc($list_address)} = 1;
1343 push(@list_to, [$list_address,
1344 "subscriber list${list_role}"]);
1349 if (!$hash_list_to{lc($list_address)}) {
1350 if ($list_additional =~ m/moderated/) {
1351 if ($email_moderated_list) {
1352 $hash_list_to{lc($list_address)} = 1;
1353 push(@list_to, [$list_address,
1354 "moderated list${list_role}"]);
1357 $hash_list_to{lc($list_address)} = 1;
1358 push(@list_to, [$list_address,
1359 "open list${list_role}"]);
1364 } elsif ($ptype eq "M") {
1365 if ($email_maintainer) {
1366 my $role = get_maintainer_role($i);
1367 push_email_addresses($pvalue, $role);
1369 } elsif ($ptype eq "R") {
1370 if ($email_reviewer) {
1371 my $subsystem = get_subsystem_name($i);
1372 push_email_addresses($pvalue, "reviewer:$subsystem");
1374 } elsif ($ptype eq "T") {
1375 push(@scm, $pvalue);
1376 } elsif ($ptype eq "W") {
1377 push(@web, $pvalue);
1378 } elsif ($ptype eq "S") {
1379 push(@status, $pvalue);
1386 my ($name, $address) = @_;
1388 return 1 if (($name eq "") && ($address eq ""));
1389 return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
1390 return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
1395 sub push_email_address {
1396 my ($line, $role) = @_;
1398 my ($name, $address) = parse_email($line);
1400 if ($address eq "") {
1404 if (!$email_remove_duplicates) {
1405 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1406 } elsif (!email_inuse($name, $address)) {
1407 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1408 $email_hash_name{lc($name)}++ if ($name ne "");
1409 $email_hash_address{lc($address)}++;
1415 sub push_email_addresses {
1416 my ($address, $role) = @_;
1418 my @address_list = ();
1420 if (rfc822_valid($address)) {
1421 push_email_address($address, $role);
1422 } elsif (@address_list = rfc822_validlist($address)) {
1423 my $array_count = shift(@address_list);
1424 while (my $entry = shift(@address_list)) {
1425 push_email_address($entry, $role);
1428 if (!push_email_address($address, $role)) {
1429 warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1435 my ($line, $role) = @_;
1437 my ($name, $address) = parse_email($line);
1438 my $email = format_email($name, $address, $email_usename);
1440 foreach my $entry (@email_to) {
1441 if ($email_remove_duplicates) {
1442 my ($entry_name, $entry_address) = parse_email($entry->[0]);
1443 if (($name eq $entry_name || $address eq $entry_address)
1444 && ($role eq "" || !($entry->[1] =~ m/$role/))
1446 if ($entry->[1] eq "") {
1447 $entry->[1] = "$role";
1449 $entry->[1] = "$entry->[1],$role";
1453 if ($email eq $entry->[0]
1454 && ($role eq "" || !($entry->[1] =~ m/$role/))
1456 if ($entry->[1] eq "") {
1457 $entry->[1] = "$role";
1459 $entry->[1] = "$entry->[1],$role";
1469 foreach my $path (split(/:/, $ENV{PATH})) {
1470 if (-e "$path/$bin") {
1471 return "$path/$bin";
1481 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1482 if (-e "$path/$conf") {
1483 return "$path/$conf";
1493 my ($name, $address) = parse_email($line);
1494 my $email = format_email($name, $address, 1);
1495 my $real_name = $name;
1496 my $real_address = $address;
1498 if (exists $mailmap->{names}->{$email} ||
1499 exists $mailmap->{addresses}->{$email}) {
1500 if (exists $mailmap->{names}->{$email}) {
1501 $real_name = $mailmap->{names}->{$email};
1503 if (exists $mailmap->{addresses}->{$email}) {
1504 $real_address = $mailmap->{addresses}->{$email};
1507 if (exists $mailmap->{names}->{$address}) {
1508 $real_name = $mailmap->{names}->{$address};
1510 if (exists $mailmap->{addresses}->{$address}) {
1511 $real_address = $mailmap->{addresses}->{$address};
1514 return format_email($real_name, $real_address, 1);
1518 my (@addresses) = @_;
1520 my @mapped_emails = ();
1521 foreach my $line (@addresses) {
1522 push(@mapped_emails, mailmap_email($line));
1524 merge_by_realname(@mapped_emails) if ($email_use_mailmap);
1525 return @mapped_emails;
1528 sub merge_by_realname {
1532 foreach my $email (@emails) {
1533 my ($name, $address) = parse_email($email);
1534 if (exists $address_map{$name}) {
1535 $address = $address_map{$name};
1536 $email = format_email($name, $address, 1);
1538 $address_map{$name} = $address;
1543 sub git_execute_cmd {
1547 my $output = `$cmd`;
1548 $output =~ s/^\s*//gm;
1549 @lines = split("\n", $output);
1554 sub hg_execute_cmd {
1558 my $output = `$cmd`;
1559 @lines = split("\n", $output);
1564 sub extract_formatted_signatures {
1565 my (@signature_lines) = @_;
1567 my @type = @signature_lines;
1569 s/\s*(.*):.*/$1/ for (@type);
1572 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1574 ## Reformat email addresses (with names) to avoid badly written signatures
1576 foreach my $signer (@signature_lines) {
1577 $signer = deduplicate_email($signer);
1580 return (\@type, \@signature_lines);
1583 sub vcs_find_signers {
1584 my ($cmd, $file) = @_;
1587 my @signatures = ();
1591 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1593 my $pattern = $VCS_cmds{"commit_pattern"};
1594 my $author_pattern = $VCS_cmds{"author_pattern"};
1595 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1597 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1599 $commits = grep(/$pattern/, @lines); # of commits
1601 @authors = grep(/$author_pattern/, @lines);
1602 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1603 @stats = grep(/$stat_pattern/, @lines);
1605 # print("stats: <@stats>\n");
1607 return (0, \@signatures, \@authors, \@stats) if !@signatures;
1609 save_commits_by_author(@lines) if ($interactive);
1610 save_commits_by_signer(@lines) if ($interactive);
1612 if (!$email_git_penguin_chiefs) {
1613 @signatures = grep(!/${penguin_chiefs}/i, @signatures);
1616 my ($author_ref, $authors_ref) = extract_formatted_signatures(@authors);
1617 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1619 return ($commits, $signers_ref, $authors_ref, \@stats);
1622 sub vcs_find_author {
1626 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1628 if (!$email_git_penguin_chiefs) {
1629 @lines = grep(!/${penguin_chiefs}/i, @lines);
1632 return @lines if !@lines;
1635 foreach my $line (@lines) {
1636 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1638 my ($name, $address) = parse_email($author);
1639 $author = format_email($name, $address, 1);
1640 push(@authors, $author);
1644 save_commits_by_author(@lines) if ($interactive);
1645 save_commits_by_signer(@lines) if ($interactive);
1650 sub vcs_save_commits {
1655 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1657 foreach my $line (@lines) {
1658 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1671 return @commits if (!(-f $file));
1673 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1674 my @all_commits = ();
1676 $cmd = $VCS_cmds{"blame_file_cmd"};
1677 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1678 @all_commits = vcs_save_commits($cmd);
1680 foreach my $file_range_diff (@range) {
1681 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1683 my $diff_start = $2;
1684 my $diff_length = $3;
1685 next if ("$file" ne "$diff_file");
1686 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1687 push(@commits, $all_commits[$i]);
1691 foreach my $file_range_diff (@range) {
1692 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1694 my $diff_start = $2;
1695 my $diff_length = $3;
1696 next if ("$file" ne "$diff_file");
1697 $cmd = $VCS_cmds{"blame_range_cmd"};
1698 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1699 push(@commits, vcs_save_commits($cmd));
1702 $cmd = $VCS_cmds{"blame_file_cmd"};
1703 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1704 @commits = vcs_save_commits($cmd);
1707 foreach my $commit (@commits) {
1708 $commit =~ s/^\^//g;
1714 my $printed_novcs = 0;
1716 %VCS_cmds = %VCS_cmds_git;
1717 return 1 if eval $VCS_cmds{"available"};
1718 %VCS_cmds = %VCS_cmds_hg;
1719 return 2 if eval $VCS_cmds{"available"};
1721 if (!$printed_novcs) {
1722 warn("$P: No supported VCS found. Add --nogit to options?\n");
1723 warn("Using a git repository produces better results.\n");
1724 warn("Try Linus Torvalds' latest git repository using:\n");
1725 warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n");
1733 return $vcs_used == 1;
1737 return $vcs_used == 2;
1740 sub vcs_add_commit_signers {
1741 return if (!vcs_exists());
1743 my ($commit, $desc) = @_;
1744 my $commit_count = 0;
1745 my $commit_authors_ref;
1746 my $commit_signers_ref;
1748 my @commit_authors = ();
1749 my @commit_signers = ();
1752 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1753 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1755 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, "");
1756 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
1757 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
1759 foreach my $signer (@commit_signers) {
1760 $signer = deduplicate_email($signer);
1763 vcs_assign($desc, 1, @commit_signers);
1766 sub interactive_get_maintainers {
1767 my ($list_ref) = @_;
1768 my @list = @$list_ref;
1777 foreach my $entry (@list) {
1778 $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
1779 $selected{$count} = 1;
1780 $authored{$count} = 0;
1781 $signed{$count} = 0;
1787 my $print_options = 0;
1792 printf STDERR "\n%1s %2s %-65s",
1793 "*", "#", "email/list and role:stats";
1795 ($email_git_fallback && !$maintained) ||
1797 print STDERR "auth sign";
1800 foreach my $entry (@list) {
1801 my $email = $entry->[0];
1802 my $role = $entry->[1];
1804 $sel = "*" if ($selected{$count});
1805 my $commit_author = $commit_author_hash{$email};
1806 my $commit_signer = $commit_signer_hash{$email};
1809 $authored++ for (@{$commit_author});
1810 $signed++ for (@{$commit_signer});
1811 printf STDERR "%1s %2d %-65s", $sel, $count + 1, $email;
1812 printf STDERR "%4d %4d", $authored, $signed
1813 if ($authored > 0 || $signed > 0);
1814 printf STDERR "\n %s\n", $role;
1815 if ($authored{$count}) {
1816 my $commit_author = $commit_author_hash{$email};
1817 foreach my $ref (@{$commit_author}) {
1818 print STDERR " Author: @{$ref}[1]\n";
1821 if ($signed{$count}) {
1822 my $commit_signer = $commit_signer_hash{$email};
1823 foreach my $ref (@{$commit_signer}) {
1824 print STDERR " @{$ref}[2]: @{$ref}[1]\n";
1831 my $date_ref = \$email_git_since;
1832 $date_ref = \$email_hg_since if (vcs_is_hg());
1833 if ($print_options) {
1838 Version Control options:
1839 g use git history [$email_git]
1840 gf use git-fallback [$email_git_fallback]
1841 b use git blame [$email_git_blame]
1842 bs use blame signatures [$email_git_blame_signatures]
1843 c# minimum commits [$email_git_min_signatures]
1844 %# min percent [$email_git_min_percent]
1845 d# history to use [$$date_ref]
1846 x# max maintainers [$email_git_max_maintainers]
1847 t all signature types [$email_git_all_signature_types]
1848 m use .mailmap [$email_use_mailmap]
1855 tm toggle maintainers
1856 tg toggle git entries
1857 tl toggle open list entries
1858 ts toggle subscriber list entries
1859 f emails in file [$email_file_emails]
1860 k keywords in file [$keywords]
1861 r remove duplicates [$email_remove_duplicates]
1862 p# pattern match depth [$pattern_depth]
1866 "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1868 my $input = <STDIN>;
1873 my @wish = split(/[, ]+/, $input);
1874 foreach my $nr (@wish) {
1876 my $sel = substr($nr, 0, 1);
1877 my $str = substr($nr, 1);
1879 $val = $1 if $str =~ /^(\d+)$/;
1884 $output_rolestats = 0;
1887 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1888 $selected{$nr - 1} = !$selected{$nr - 1};
1889 } elsif ($sel eq "*" || $sel eq '^') {
1891 $toggle = 1 if ($sel eq '*');
1892 for (my $i = 0; $i < $count; $i++) {
1893 $selected{$i} = $toggle;
1895 } elsif ($sel eq "0") {
1896 for (my $i = 0; $i < $count; $i++) {
1897 $selected{$i} = !$selected{$i};
1899 } elsif ($sel eq "t") {
1900 if (lc($str) eq "m") {
1901 for (my $i = 0; $i < $count; $i++) {
1902 $selected{$i} = !$selected{$i}
1903 if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
1905 } elsif (lc($str) eq "g") {
1906 for (my $i = 0; $i < $count; $i++) {
1907 $selected{$i} = !$selected{$i}
1908 if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
1910 } elsif (lc($str) eq "l") {
1911 for (my $i = 0; $i < $count; $i++) {
1912 $selected{$i} = !$selected{$i}
1913 if ($list[$i]->[1] =~ /^(open list)/i);
1915 } elsif (lc($str) eq "s") {
1916 for (my $i = 0; $i < $count; $i++) {
1917 $selected{$i} = !$selected{$i}
1918 if ($list[$i]->[1] =~ /^(subscriber list)/i);
1921 } elsif ($sel eq "a") {
1922 if ($val > 0 && $val <= $count) {
1923 $authored{$val - 1} = !$authored{$val - 1};
1924 } elsif ($str eq '*' || $str eq '^') {
1926 $toggle = 1 if ($str eq '*');
1927 for (my $i = 0; $i < $count; $i++) {
1928 $authored{$i} = $toggle;
1931 } elsif ($sel eq "s") {
1932 if ($val > 0 && $val <= $count) {
1933 $signed{$val - 1} = !$signed{$val - 1};
1934 } elsif ($str eq '*' || $str eq '^') {
1936 $toggle = 1 if ($str eq '*');
1937 for (my $i = 0; $i < $count; $i++) {
1938 $signed{$i} = $toggle;
1941 } elsif ($sel eq "o") {
1944 } elsif ($sel eq "g") {
1946 bool_invert(\$email_git_fallback);
1948 bool_invert(\$email_git);
1951 } elsif ($sel eq "b") {
1953 bool_invert(\$email_git_blame_signatures);
1955 bool_invert(\$email_git_blame);
1958 } elsif ($sel eq "c") {
1960 $email_git_min_signatures = $val;
1963 } elsif ($sel eq "x") {
1965 $email_git_max_maintainers = $val;
1968 } elsif ($sel eq "%") {
1969 if ($str ne "" && $val >= 0) {
1970 $email_git_min_percent = $val;
1973 } elsif ($sel eq "d") {
1975 $email_git_since = $str;
1976 } elsif (vcs_is_hg()) {
1977 $email_hg_since = $str;
1980 } elsif ($sel eq "t") {
1981 bool_invert(\$email_git_all_signature_types);
1983 } elsif ($sel eq "f") {
1984 bool_invert(\$email_file_emails);
1986 } elsif ($sel eq "r") {
1987 bool_invert(\$email_remove_duplicates);
1989 } elsif ($sel eq "m") {
1990 bool_invert(\$email_use_mailmap);
1993 } elsif ($sel eq "k") {
1994 bool_invert(\$keywords);
1996 } elsif ($sel eq "p") {
1997 if ($str ne "" && $val >= 0) {
1998 $pattern_depth = $val;
2001 } elsif ($sel eq "h" || $sel eq "?") {
2004 Interactive mode allows you to select the various maintainers, submitters,
2005 commit signers and mailing lists that could be CC'd on a patch.
2007 Any *'d entry is selected.
2009 If you have git or hg installed, you can choose to summarize the commit
2010 history of files in the patch. Also, each line of the current file can
2011 be matched to its commit author and that commits signers with blame.
2013 Various knobs exist to control the length of time for active commit
2014 tracking, the maximum number of commit authors and signers to add,
2017 Enter selections at the prompt until you are satisfied that the selected
2018 maintainers are appropriate. You may enter multiple selections separated
2019 by either commas or spaces.
2023 print STDERR "invalid option: '$nr'\n";
2028 print STDERR "git-blame can be very slow, please have patience..."
2029 if ($email_git_blame);
2030 goto &get_maintainers;
2034 #drop not selected entries
2036 my @new_emailto = ();
2037 foreach my $entry (@list) {
2038 if ($selected{$count}) {
2039 push(@new_emailto, $list[$count]);
2043 return @new_emailto;
2047 my ($bool_ref) = @_;
2056 sub deduplicate_email {
2060 my ($name, $address) = parse_email($email);
2061 $email = format_email($name, $address, 1);
2062 $email = mailmap_email($email);
2064 return $email if (!$email_remove_duplicates);
2066 ($name, $address) = parse_email($email);
2068 if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
2069 $name = $deduplicate_name_hash{lc($name)}->[0];
2070 $address = $deduplicate_name_hash{lc($name)}->[1];
2072 } elsif ($deduplicate_address_hash{lc($address)}) {
2073 $name = $deduplicate_address_hash{lc($address)}->[0];
2074 $address = $deduplicate_address_hash{lc($address)}->[1];
2078 $deduplicate_name_hash{lc($name)} = [ $name, $address ];
2079 $deduplicate_address_hash{lc($address)} = [ $name, $address ];
2081 $email = format_email($name, $address, 1);
2082 $email = mailmap_email($email);
2086 sub save_commits_by_author {
2093 foreach my $line (@lines) {
2094 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2096 $author = deduplicate_email($author);
2097 push(@authors, $author);
2099 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
2100 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
2103 for (my $i = 0; $i < @authors; $i++) {
2105 foreach my $ref(@{$commit_author_hash{$authors[$i]}}) {
2106 if (@{$ref}[0] eq $commits[$i] &&
2107 @{$ref}[1] eq $subjects[$i]) {
2113 push(@{$commit_author_hash{$authors[$i]}},
2114 [ ($commits[$i], $subjects[$i]) ]);
2119 sub save_commits_by_signer {
2125 foreach my $line (@lines) {
2126 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
2127 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
2128 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
2129 my @signatures = ($line);
2130 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
2131 my @types = @$types_ref;
2132 my @signers = @$signers_ref;
2134 my $type = $types[0];
2135 my $signer = $signers[0];
2137 $signer = deduplicate_email($signer);
2140 foreach my $ref(@{$commit_signer_hash{$signer}}) {
2141 if (@{$ref}[0] eq $commit &&
2142 @{$ref}[1] eq $subject &&
2143 @{$ref}[2] eq $type) {
2149 push(@{$commit_signer_hash{$signer}},
2150 [ ($commit, $subject, $type) ]);
2157 my ($role, $divisor, @lines) = @_;
2162 return if (@lines <= 0);
2164 if ($divisor <= 0) {
2165 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
2169 @lines = mailmap(@lines);
2171 return if (@lines <= 0);
2173 @lines = sort(@lines);
2176 $hash{$_}++ for @lines;
2179 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
2180 my $sign_offs = $hash{$line};
2181 my $percent = $sign_offs * 100 / $divisor;
2183 $percent = 100 if ($percent > 100);
2184 next if (ignore_email_address($line));
2186 last if ($sign_offs < $email_git_min_signatures ||
2187 $count > $email_git_max_maintainers ||
2188 $percent < $email_git_min_percent);
2189 push_email_address($line, '');
2190 if ($output_rolestats) {
2191 my $fmt_percent = sprintf("%.0f", $percent);
2192 add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%");
2194 add_role($line, $role);
2199 sub vcs_file_signoffs {
2210 $vcs_used = vcs_exists();
2211 return if (!$vcs_used);
2213 my $cmd = $VCS_cmds{"find_signers_cmd"};
2214 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2216 ($commits, $signers_ref, $authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2218 @signers = @{$signers_ref} if defined $signers_ref;
2219 @authors = @{$authors_ref} if defined $authors_ref;
2220 @stats = @{$stats_ref} if defined $stats_ref;
2222 # print("commits: <$commits>\nsigners:<@signers>\nauthors: <@authors>\nstats: <@stats>\n");
2224 foreach my $signer (@signers) {
2225 $signer = deduplicate_email($signer);
2228 vcs_assign("commit_signer", $commits, @signers);
2229 vcs_assign("authored", $commits, @authors);
2230 if ($#authors == $#stats) {
2231 my $stat_pattern = $VCS_cmds{"stat_pattern"};
2232 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
2236 for (my $i = 0; $i <= $#stats; $i++) {
2237 if ($stats[$i] =~ /$stat_pattern/) {
2242 my @tmp_authors = uniq(@authors);
2243 foreach my $author (@tmp_authors) {
2244 $author = deduplicate_email($author);
2246 @tmp_authors = uniq(@tmp_authors);
2247 my @list_added = ();
2248 my @list_deleted = ();
2249 foreach my $author (@tmp_authors) {
2251 my $auth_deleted = 0;
2252 for (my $i = 0; $i <= $#stats; $i++) {
2253 if ($author eq deduplicate_email($authors[$i]) &&
2254 $stats[$i] =~ /$stat_pattern/) {
2256 $auth_deleted += $2;
2259 for (my $i = 0; $i < $auth_added; $i++) {
2260 push(@list_added, $author);
2262 for (my $i = 0; $i < $auth_deleted; $i++) {
2263 push(@list_deleted, $author);
2266 vcs_assign("added_lines", $added, @list_added);
2267 vcs_assign("removed_lines", $deleted, @list_deleted);
2271 sub vcs_file_blame {
2275 my @all_commits = ();
2280 $vcs_used = vcs_exists();
2281 return if (!$vcs_used);
2283 @all_commits = vcs_blame($file);
2284 @commits = uniq(@all_commits);
2285 $total_commits = @commits;
2286 $total_lines = @all_commits;
2288 if ($email_git_blame_signatures) {
2291 my $commit_authors_ref;
2292 my $commit_signers_ref;
2294 my @commit_authors = ();
2295 my @commit_signers = ();
2296 my $commit = join(" -r ", @commits);
2299 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2300 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2302 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2303 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2304 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2306 push(@signers, @commit_signers);
2308 foreach my $commit (@commits) {
2310 my $commit_authors_ref;
2311 my $commit_signers_ref;
2313 my @commit_authors = ();
2314 my @commit_signers = ();
2317 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2318 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2320 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2321 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2322 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2324 push(@signers, @commit_signers);
2329 if ($from_filename) {
2330 if ($output_rolestats) {
2332 if (vcs_is_hg()) {{ # Double brace for last exit
2334 my @commit_signers = ();
2335 @commits = uniq(@commits);
2336 @commits = sort(@commits);
2337 my $commit = join(" -r ", @commits);
2340 $cmd = $VCS_cmds{"find_commit_author_cmd"};
2341 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2345 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
2347 if (!$email_git_penguin_chiefs) {
2348 @lines = grep(!/${penguin_chiefs}/i, @lines);
2354 foreach my $line (@lines) {
2355 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2357 $author = deduplicate_email($author);
2358 push(@authors, $author);
2362 save_commits_by_author(@lines) if ($interactive);
2363 save_commits_by_signer(@lines) if ($interactive);
2365 push(@signers, @authors);
2368 foreach my $commit (@commits) {
2370 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
2371 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
2372 my @author = vcs_find_author($cmd);
2375 my $formatted_author = deduplicate_email($author[0]);
2377 my $count = grep(/$commit/, @all_commits);
2378 for ($i = 0; $i < $count ; $i++) {
2379 push(@blame_signers, $formatted_author);
2383 if (@blame_signers) {
2384 vcs_assign("authored lines", $total_lines, @blame_signers);
2387 foreach my $signer (@signers) {
2388 $signer = deduplicate_email($signer);
2390 vcs_assign("commits", $total_commits, @signers);
2392 foreach my $signer (@signers) {
2393 $signer = deduplicate_email($signer);
2395 vcs_assign("modified commits", $total_commits, @signers);
2399 sub vcs_file_exists {
2404 my $vcs_used = vcs_exists();
2405 return 0 if (!$vcs_used);
2407 my $cmd = $VCS_cmds{"file_exists_cmd"};
2408 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2410 $exists = &{$VCS_cmds{"execute_cmd"}}($cmd);
2412 return 0 if ($? != 0);
2417 sub vcs_list_files {
2422 my $vcs_used = vcs_exists();
2423 return 0 if (!$vcs_used);
2425 my $cmd = $VCS_cmds{"list_files_cmd"};
2426 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2427 @lsfiles = &{$VCS_cmds{"execute_cmd"}}($cmd);
2429 return () if ($? != 0);
2438 @parms = grep(!$saw{$_}++, @parms);
2446 @parms = sort @parms;
2447 @parms = grep(!$saw{$_}++, @parms);
2451 sub clean_file_emails {
2452 my (@file_emails) = @_;
2453 my @fmt_emails = ();
2455 foreach my $email (@file_emails) {
2456 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
2457 my ($name, $address) = parse_email($email);
2458 if ($name eq '"[,\.]"') {
2462 my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name);
2464 my $first = $nw[@nw - 3];
2465 my $middle = $nw[@nw - 2];
2466 my $last = $nw[@nw - 1];
2468 if (((length($first) == 1 && $first =~ m/[A-Za-z]/) ||
2469 (length($first) == 2 && substr($first, -1) eq ".")) ||
2470 (length($middle) == 1 ||
2471 (length($middle) == 2 && substr($middle, -1) eq "."))) {
2472 $name = "$first $middle $last";
2474 $name = "$middle $last";
2478 if (substr($name, -1) =~ /[,\.]/) {
2479 $name = substr($name, 0, length($name) - 1);
2480 } elsif (substr($name, -2) =~ /[,\.]"/) {
2481 $name = substr($name, 0, length($name) - 2) . '"';
2484 if (substr($name, 0, 1) =~ /[,\.]/) {
2485 $name = substr($name, 1, length($name) - 1);
2486 } elsif (substr($name, 0, 2) =~ /"[,\.]/) {
2487 $name = '"' . substr($name, 2, length($name) - 2);
2490 my $fmt_email = format_email($name, $address, $email_usename);
2491 push(@fmt_emails, $fmt_email);
2501 my ($address, $role) = @$_;
2502 if (!$saw{$address}) {
2503 if ($output_roles) {
2504 push(@lines, "$address ($role)");
2506 push(@lines, $address);
2518 if ($output_multiline) {
2519 foreach my $line (@parms) {
2523 print(join($output_separator, @parms));
2531 # Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
2532 # comment. We must allow for rfc822_lwsp (or comments) after each of these.
2533 # This regexp will only work on addresses which have had comments stripped
2534 # and replaced with rfc822_lwsp.
2536 my $specials = '()<>@,;:\\\\".\\[\\]';
2537 my $controls = '\\000-\\037\\177';
2539 my $dtext = "[^\\[\\]\\r\\\\]";
2540 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
2542 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
2544 # Use zero-width assertion to spot the limit of an atom. A simple
2545 # $rfc822_lwsp* causes the regexp engine to hang occasionally.
2546 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
2547 my $word = "(?:$atom|$quoted_string)";
2548 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
2550 my $sub_domain = "(?:$atom|$domain_literal)";
2551 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
2553 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
2555 my $phrase = "$word*";
2556 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
2557 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
2558 my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
2560 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
2561 my $address = "(?:$mailbox|$group)";
2563 return "$rfc822_lwsp*$address";
2566 sub rfc822_strip_comments {
2568 # Recursively remove comments, and replace with a single space. The simpler
2569 # regexps in the Email Addressing FAQ are imperfect - they will miss escaped
2570 # chars in atoms, for example.
2572 while ($s =~ s/^((?:[^"\\]|\\.)*
2573 (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
2574 \((?:[^()\\]|\\.)*\)/$1 /osx) {}
2578 # valid: returns true if the parameter is an RFC822 valid address
2581 my $s = rfc822_strip_comments(shift);
2584 $rfc822re = make_rfc822re();
2587 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
2590 # validlist: In scalar context, returns true if the parameter is an RFC822
2591 # valid list of addresses.
2593 # In list context, returns an empty list on failure (an invalid
2594 # address was found); otherwise a list whose first element is the
2595 # number of addresses found and whose remaining elements are the
2596 # addresses. This is needed to disambiguate failure (invalid)
2597 # from success with no addresses found, because an empty string is
2600 sub rfc822_validlist {
2601 my $s = rfc822_strip_comments(shift);
2604 $rfc822re = make_rfc822re();
2606 # * null list items are valid according to the RFC
2607 # * the '1' business is to aid in distinguishing failure from no results
2610 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
2611 $s =~ m/^$rfc822_char*$/) {
2612 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
2615 return wantarray ? (scalar(@r), @r) : 1;
2617 return wantarray ? () : 0;