2 # SPDX-License-Identifier: GPL-2.0
4 # (c) 2007, Joe Perches <joe@perches.com>
5 # created from checkpatch.pl
7 # Print selected MAINTAINERS information for
8 # the files modified in a patch or for a file
10 # usage: perl scripts/get_maintainer.pl [OPTIONS] <patch>
11 # perl scripts/get_maintainer.pl [OPTIONS] -f <file>
19 use Getopt::Long qw(:config no_auto_abbrev);
23 my $cur_path = fastgetcwd() . '/';
26 my $email_usename = 1;
27 my $email_maintainer = 1;
28 my $email_reviewer = 1;
31 my $email_moderated_list = 1;
32 my $email_subscriber_list = 0;
33 my $email_git_penguin_chiefs = 0;
35 my $email_git_all_signature_types = 0;
36 my $email_git_blame = 0;
37 my $email_git_blame_signatures = 1;
38 my $email_git_fallback = 1;
39 my $email_git_min_signatures = 1;
40 my $email_git_max_maintainers = 5;
41 my $email_git_min_percent = 5;
42 my $email_git_since = "1-year-ago";
43 my $email_hg_since = "-365";
45 my $email_remove_duplicates = 1;
46 my $email_use_mailmap = 1;
47 my $output_multiline = 1;
48 my $output_separator = ", ";
50 my $output_rolestats = 1;
51 my $output_section_maxlen = 50;
60 my $email_file_emails = 0;
61 my $from_filename = 0;
62 my $pattern_depth = 0;
63 my $self_test = undef;
66 my $find_maintainer_files = 0;
73 my @fixes = (); # If a patch description includes Fixes: lines
78 my %commit_author_hash;
79 my %commit_signer_hash;
81 my @penguin_chief = ();
82 push(@penguin_chief, "Linus Torvalds:torvalds\@linux-foundation.org");
83 #Andrew wants in on most everything - 2009/01/14
84 #push(@penguin_chief, "Andrew Morton:akpm\@linux-foundation.org");
86 my @penguin_chief_names = ();
87 foreach my $chief (@penguin_chief) {
88 if ($chief =~ m/^(.*):(.*)/) {
91 push(@penguin_chief_names, $chief_name);
94 my $penguin_chiefs = "\(" . join("|", @penguin_chief_names) . "\)";
96 # Signature types of people who are either
97 # a) responsible for the code in question, or
98 # b) familiar enough with it to give relevant feedback
99 my @signature_tags = ();
100 push(@signature_tags, "Signed-off-by:");
101 push(@signature_tags, "Reviewed-by:");
102 push(@signature_tags, "Acked-by:");
104 my $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
106 # rfc822 email address - preloaded methods go here.
107 my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
108 my $rfc822_char = '[\\000-\\377]';
110 # VCS command support: class-like functions and strings
115 "execute_cmd" => \&git_execute_cmd,
116 "available" => '(which("git") ne "") && (-e ".git")',
117 "find_signers_cmd" =>
118 "git log --no-color --follow --since=\$email_git_since " .
119 '--numstat --no-merges ' .
120 '--format="GitCommit: %H%n' .
121 'GitAuthor: %an <%ae>%n' .
126 "find_commit_signers_cmd" =>
127 "git log --no-color " .
129 '--format="GitCommit: %H%n' .
130 'GitAuthor: %an <%ae>%n' .
135 "find_commit_author_cmd" =>
136 "git log --no-color " .
138 '--format="GitCommit: %H%n' .
139 'GitAuthor: %an <%ae>%n' .
141 'GitSubject: %s%n"' .
143 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
144 "blame_file_cmd" => "git blame -l \$file",
145 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})",
146 "blame_commit_pattern" => "^([0-9a-f]+) ",
147 "author_pattern" => "^GitAuthor: (.*)",
148 "subject_pattern" => "^GitSubject: (.*)",
149 "stat_pattern" => "^(\\d+)\\t(\\d+)\\t\$file\$",
150 "file_exists_cmd" => "git ls-files \$file",
151 "list_files_cmd" => "git ls-files \$file",
155 "execute_cmd" => \&hg_execute_cmd,
156 "available" => '(which("hg") ne "") && (-d ".hg")',
157 "find_signers_cmd" =>
158 "hg log --date=\$email_hg_since " .
159 "--template='HgCommit: {node}\\n" .
160 "HgAuthor: {author}\\n" .
161 "HgSubject: {desc}\\n'" .
163 "find_commit_signers_cmd" =>
165 "--template='HgSubject: {desc}\\n'" .
167 "find_commit_author_cmd" =>
169 "--template='HgCommit: {node}\\n" .
170 "HgAuthor: {author}\\n" .
171 "HgSubject: {desc|firstline}\\n'" .
173 "blame_range_cmd" => "", # not supported
174 "blame_file_cmd" => "hg blame -n \$file",
175 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})",
176 "blame_commit_pattern" => "^([ 0-9a-f]+):",
177 "author_pattern" => "^HgAuthor: (.*)",
178 "subject_pattern" => "^HgSubject: (.*)",
179 "stat_pattern" => "^(\\d+)\t(\\d+)\t\$file\$",
180 "file_exists_cmd" => "hg files \$file",
181 "list_files_cmd" => "hg manifest -R \$file",
184 my $conf = which_conf(".get_maintainer.conf");
187 open(my $conffile, '<', "$conf")
188 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
190 while (<$conffile>) {
193 $line =~ s/\s*\n?$//g;
197 next if ($line =~ m/^\s*#/);
198 next if ($line =~ m/^\s*$/);
200 my @words = split(" ", $line);
201 foreach my $word (@words) {
202 last if ($word =~ m/^#/);
203 push (@conf_args, $word);
207 unshift(@ARGV, @conf_args) if @conf_args;
210 my @ignore_emails = ();
211 my $ignore_file = which_conf(".get_maintainer.ignore");
212 if (-f $ignore_file) {
213 open(my $ignore, '<', "$ignore_file")
214 or warn "$P: Can't find a readable .get_maintainer.ignore file $!\n";
218 $line =~ s/\s*\n?$//;
223 next if ($line =~ m/^\s*$/);
224 if (rfc822_valid($line)) {
225 push(@ignore_emails, $line);
233 if ($_ =~ /^-{1,2}self-test(?:=|$)/) {
234 die "$P: using --self-test does not allow any other option or argument\n";
241 'git!' => \$email_git,
242 'git-all-signature-types!' => \$email_git_all_signature_types,
243 'git-blame!' => \$email_git_blame,
244 'git-blame-signatures!' => \$email_git_blame_signatures,
245 'git-fallback!' => \$email_git_fallback,
246 'git-chief-penguins!' => \$email_git_penguin_chiefs,
247 'git-min-signatures=i' => \$email_git_min_signatures,
248 'git-max-maintainers=i' => \$email_git_max_maintainers,
249 'git-min-percent=i' => \$email_git_min_percent,
250 'git-since=s' => \$email_git_since,
251 'hg-since=s' => \$email_hg_since,
252 'i|interactive!' => \$interactive,
253 'remove-duplicates!' => \$email_remove_duplicates,
254 'mailmap!' => \$email_use_mailmap,
255 'm!' => \$email_maintainer,
256 'r!' => \$email_reviewer,
257 'n!' => \$email_usename,
258 'l!' => \$email_list,
259 'fixes!' => \$email_fixes,
260 'moderated!' => \$email_moderated_list,
261 's!' => \$email_subscriber_list,
262 'multiline!' => \$output_multiline,
263 'roles!' => \$output_roles,
264 'rolestats!' => \$output_rolestats,
265 'separator=s' => \$output_separator,
266 'subsystem!' => \$subsystem,
267 'status!' => \$status,
271 'letters=s' => \$letters,
272 'pattern-depth=i' => \$pattern_depth,
273 'k|keywords!' => \$keywords,
274 'sections!' => \$sections,
275 'fe|file-emails!' => \$email_file_emails,
276 'f|file' => \$from_filename,
277 'find-maintainer-files' => \$find_maintainer_files,
278 'mpath|maintainer-path=s' => \$maintainer_path,
279 'self-test:s' => \$self_test,
280 'v|version' => \$version,
281 'h|help|usage' => \$help,
283 die "$P: invalid argument - use --help if necessary\n";
292 print("${P} ${V}\n");
296 if (defined $self_test) {
297 read_all_maintainer_files();
302 if (-t STDIN && !@ARGV) {
303 # We're talking to a terminal, but have no command line arguments.
304 die "$P: missing patchfile or -f file - use --help if necessary\n";
307 $output_multiline = 0 if ($output_separator ne ", ");
308 $output_rolestats = 1 if ($interactive);
309 $output_roles = 1 if ($output_rolestats);
311 if ($sections || $letters ne "") {
322 my $selections = $email + $scm + $status + $subsystem + $web;
323 if ($selections == 0) {
324 die "$P: Missing required option: email, scm, status, subsystem or web\n";
329 ($email_maintainer + $email_reviewer +
330 $email_list + $email_subscriber_list +
331 $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
332 die "$P: Please select at least 1 email option\n";
335 if ($tree && !top_of_kernel_tree($lk_path)) {
336 die "$P: The current directory does not appear to be "
337 . "a linux kernel source tree.\n";
340 ## Read MAINTAINERS for type/value pairs
345 my @self_test_info = ();
347 sub read_maintainer_file {
350 open (my $maint, '<', "$file")
351 or die "$P: Can't open MAINTAINERS file '$file': $!\n";
357 if ($line =~ m/^([A-Z]):\s*(.*)/) {
361 ##Filename pattern matching
362 if ($type eq "F" || $type eq "X") {
363 $value =~ s@\.@\\\.@g; ##Convert . to \.
364 $value =~ s/\*/\.\*/g; ##Convert * to .*
365 $value =~ s/\?/\./g; ##Convert ? to .
366 ##if pattern is a directory and it lacks a trailing slash, add one
368 $value =~ s@([^/])$@$1/@;
370 } elsif ($type eq "K") {
371 $keyword_hash{@typevalue} = $value;
373 push(@typevalue, "$type:$value");
374 } elsif (!(/^\s*$/ || /^\s*\#/)) {
375 push(@typevalue, $line);
377 if (defined $self_test) {
378 push(@self_test_info, {file=>$file, linenr=>$i, line=>$line});
385 sub find_is_maintainer_file {
387 return if ($file !~ m@/MAINTAINERS$@);
388 $file = $File::Find::name;
389 return if (! -f $file);
390 push(@mfiles, $file);
393 sub find_ignore_git {
394 return grep { $_ !~ /^\.git$/; } @_;
397 read_all_maintainer_files();
399 sub read_all_maintainer_files {
400 my $path = "${lk_path}MAINTAINERS";
401 if (defined $maintainer_path) {
402 $path = $maintainer_path;
403 # Perl Cookbook tilde expansion if necessary
404 $path =~ s@^~([^/]*)@ $1 ? (getpwnam($1))[7] : ( $ENV{HOME} || $ENV{LOGDIR} || (getpwuid($<))[7])@ex;
408 $path .= '/' if ($path !~ m@/$@);
409 if ($find_maintainer_files) {
410 find( { wanted => \&find_is_maintainer_file,
411 preprocess => \&find_ignore_git,
415 opendir(DIR, "$path") or die $!;
416 my @files = readdir(DIR);
418 foreach my $file (@files) {
419 push(@mfiles, "$path$file") if ($file !~ /^\./);
422 } elsif (-f "$path") {
423 push(@mfiles, "$path");
425 die "$P: MAINTAINER file not found '$path'\n";
427 die "$P: No MAINTAINER files found in '$path'\n" if (scalar(@mfiles) == 0);
428 foreach my $file (@mfiles) {
429 read_maintainer_file("$file");
433 sub maintainers_in_file {
436 return if ($file =~ m@\bMAINTAINERS$@);
438 if (-f $file && ($email_file_emails || $file =~ /\.yaml$/)) {
439 open(my $f, '<', $file)
440 or die "$P: Can't open $file: $!\n";
441 my $text = do { local($/) ; <$f> };
444 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;
445 push(@file_emails, clean_file_emails(@poss_addr));
450 # Read mail address map
463 return if (!$email_use_mailmap || !(-f "${lk_path}.mailmap"));
465 open(my $mailmap_file, '<', "${lk_path}.mailmap")
466 or warn "$P: Can't open .mailmap: $!\n";
468 while (<$mailmap_file>) {
469 s/#.*$//; #strip comments
470 s/^\s+|\s+$//g; #trim
472 next if (/^\s*$/); #skip empty lines
473 #entries have one of the following formats:
476 # name1 <mail1> <mail2>
477 # name1 <mail1> name2 <mail2>
478 # (see man git-shortlog)
480 if (/^([^<]+)<([^>]+)>$/) {
484 $real_name =~ s/\s+$//;
485 ($real_name, $address) = parse_email("$real_name <$address>");
486 $mailmap->{names}->{$address} = $real_name;
488 } elsif (/^<([^>]+)>\s*<([^>]+)>$/) {
489 my $real_address = $1;
490 my $wrong_address = $2;
492 $mailmap->{addresses}->{$wrong_address} = $real_address;
494 } elsif (/^(.+)<([^>]+)>\s*<([^>]+)>$/) {
496 my $real_address = $2;
497 my $wrong_address = $3;
499 $real_name =~ s/\s+$//;
500 ($real_name, $real_address) =
501 parse_email("$real_name <$real_address>");
502 $mailmap->{names}->{$wrong_address} = $real_name;
503 $mailmap->{addresses}->{$wrong_address} = $real_address;
505 } elsif (/^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$/) {
507 my $real_address = $2;
509 my $wrong_address = $4;
511 $real_name =~ s/\s+$//;
512 ($real_name, $real_address) =
513 parse_email("$real_name <$real_address>");
515 $wrong_name =~ s/\s+$//;
516 ($wrong_name, $wrong_address) =
517 parse_email("$wrong_name <$wrong_address>");
519 my $wrong_email = format_email($wrong_name, $wrong_address, 1);
520 $mailmap->{names}->{$wrong_email} = $real_name;
521 $mailmap->{addresses}->{$wrong_email} = $real_address;
524 close($mailmap_file);
527 ## use the filenames on the command line or find the filenames in the patchfiles
530 push(@ARGV, "&STDIN");
533 foreach my $file (@ARGV) {
534 if ($file ne "&STDIN") {
535 ##if $file is a directory and it lacks a trailing slash, add one
537 $file =~ s@([^/])$@$1/@;
538 } elsif (!(-f $file)) {
539 die "$P: file '${file}' not found\n";
542 if ($from_filename || ($file ne "&STDIN" && vcs_file_exists($file))) {
543 $file =~ s/^\Q${cur_path}\E//; #strip any absolute path
544 $file =~ s/^\Q${lk_path}\E//; #or the path to the lk tree
546 if ($file ne "MAINTAINERS" && -f $file && $keywords) {
547 open(my $f, '<', $file)
548 or die "$P: Can't open $file: $!\n";
549 my $text = do { local($/) ; <$f> };
552 foreach my $line (keys %keyword_hash) {
553 if ($text =~ m/$keyword_hash{$line}/x) {
554 push(@keyword_tvi, $line);
560 my $file_cnt = @files;
563 open(my $patch, "< $file")
564 or die "$P: Can't open $file: $!\n";
566 # We can check arbitrary information before the patch
567 # like the commit message, mail headers, etc...
568 # This allows us to match arbitrary keywords against any part
569 # of a git format-patch generated file (subject tags, etc...)
571 my $patch_prefix = ""; #Parsing the intro
575 if (m/^ mode change [0-7]+ => [0-7]+ (\S+)\s*$/) {
577 push(@files, $filename);
578 } elsif (m/^rename (?:from|to) (\S+)\s*$/) {
580 push(@files, $filename);
581 } elsif (m/^diff --git a\/(\S+) b\/(\S+)\s*$/) {
584 push(@files, $filename1);
585 push(@files, $filename2);
586 } elsif (m/^Fixes:\s+([0-9a-fA-F]{6,40})/) {
587 push(@fixes, $1) if ($email_fixes);
588 } elsif (m/^\+\+\+\s+(\S+)/ or m/^---\s+(\S+)/) {
590 $filename =~ s@^[^/]*/@@;
592 $lastfile = $filename;
593 push(@files, $filename);
594 $patch_prefix = "^[+-].*"; #Now parsing the actual patch
595 } elsif (m/^\@\@ -(\d+),(\d+)/) {
596 if ($email_git_blame) {
597 push(@range, "$lastfile:$1:$2");
599 } elsif ($keywords) {
600 foreach my $line (keys %keyword_hash) {
601 if ($patch_line =~ m/${patch_prefix}$keyword_hash{$line}/x) {
602 push(@keyword_tvi, $line);
609 if ($file_cnt == @files) {
610 warn "$P: file '${file}' doesn't appear to be a patch. "
611 . "Add -f to options?\n";
613 @files = sort_and_uniq(@files);
617 @file_emails = uniq(@file_emails);
618 @fixes = uniq(@fixes);
621 my %email_hash_address;
629 my %deduplicate_name_hash = ();
630 my %deduplicate_address_hash = ();
632 my @maintainers = get_maintainers();
634 @maintainers = merge_email(@maintainers);
635 output(@maintainers);
644 @status = uniq(@status);
649 @subsystem = uniq(@subsystem);
664 my @section_headers = ();
667 @lsfiles = vcs_list_files($lk_path);
669 for my $x (@self_test_info) {
672 ## Section header duplication and missing section content
673 if (($self_test eq "" || $self_test =~ /\bsections\b/) &&
674 $x->{line} =~ /^\S[^:]/ &&
675 defined $self_test_info[$index] &&
676 $self_test_info[$index]->{line} =~ /^([A-Z]):\s*\S/) {
681 if (grep(m@^\Q$x->{line}\E@, @section_headers)) {
682 print("$x->{file}:$x->{linenr}: warning: duplicate section header\t$x->{line}\n");
684 push(@section_headers, $x->{line});
686 my $nextline = $index;
687 while (defined $self_test_info[$nextline] &&
688 $self_test_info[$nextline]->{line} =~ /^([A-Z]):\s*(\S.*)/) {
694 } elsif ($type eq "F" || $type eq "N") {
696 } elsif ($type eq "M" || $type eq "R" || $type eq "L") {
701 if (!$has_ML && $status !~ /orphan|obsolete/i) {
702 print("$x->{file}:$x->{linenr}: warning: section without email address\t$x->{line}\n");
705 print("$x->{file}:$x->{linenr}: warning: section without status \t$x->{line}\n");
708 print("$x->{file}:$x->{linenr}: warning: section without file pattern\t$x->{line}\n");
712 next if ($x->{line} !~ /^([A-Z]):\s*(.*)/);
717 ## Filename pattern matching
718 if (($type eq "F" || $type eq "X") &&
719 ($self_test eq "" || $self_test =~ /\bpatterns\b/)) {
720 $value =~ s@\.@\\\.@g; ##Convert . to \.
721 $value =~ s/\*/\.\*/g; ##Convert * to .*
722 $value =~ s/\?/\./g; ##Convert ? to .
723 ##if pattern is a directory and it lacks a trailing slash, add one
725 $value =~ s@([^/])$@$1/@;
727 if (!grep(m@^$value@, @lsfiles)) {
728 print("$x->{file}:$x->{linenr}: warning: no file matches\t$x->{line}\n");
732 } elsif (($type eq "W" || $type eq "Q" || $type eq "B") &&
733 $value =~ /^https?:/ &&
734 ($self_test eq "" || $self_test =~ /\blinks\b/)) {
735 next if (grep(m@^\Q$value\E$@, @good_links));
737 if (grep(m@^\Q$value\E$@, @bad_links)) {
740 my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $value`;
742 push(@good_links, $value);
744 push(@bad_links, $value);
749 print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
753 } elsif ($type eq "T" &&
754 ($self_test eq "" || $self_test =~ /\bscm\b/)) {
755 next if (grep(m@^\Q$value\E$@, @good_links));
757 if (grep(m@^\Q$value\E$@, @bad_links)) {
759 } elsif ($value !~ /^(?:git|quilt|hg)\s+\S/) {
760 print("$x->{file}:$x->{linenr}: warning: malformed entry\t$x->{line}\n");
761 } elsif ($value =~ /^git\s+(\S+)(\s+([^\(]+\S+))?/) {
765 my $output = `git ls-remote --exit-code -h "$url" $branch > /dev/null 2>&1`;
767 push(@good_links, $value);
769 push(@bad_links, $value);
772 } elsif ($value =~ /^(?:quilt|hg)\s+(https?:\S+)/) {
774 my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $url`;
776 push(@good_links, $value);
778 push(@bad_links, $value);
783 print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
789 sub ignore_email_address {
792 foreach my $ignore (@ignore_emails) {
793 return 1 if ($ignore eq $address);
799 sub range_is_maintained {
800 my ($start, $end) = @_;
802 for (my $i = $start; $i < $end; $i++) {
803 my $line = $typevalue[$i];
804 if ($line =~ m/^([A-Z]):\s*(.*)/) {
808 if ($value =~ /(maintain|support)/i) {
817 sub range_has_maintainer {
818 my ($start, $end) = @_;
820 for (my $i = $start; $i < $end; $i++) {
821 my $line = $typevalue[$i];
822 if ($line =~ m/^([A-Z]):\s*(.*)/) {
833 sub get_maintainers {
834 %email_hash_name = ();
835 %email_hash_address = ();
836 %commit_author_hash = ();
837 %commit_signer_hash = ();
845 %deduplicate_name_hash = ();
846 %deduplicate_address_hash = ();
847 if ($email_git_all_signature_types) {
848 $signature_pattern = "(.+?)[Bb][Yy]:";
850 $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
853 # Find responsible parties
855 my %exact_pattern_match_hash = ();
857 foreach my $file (@files) {
860 my $tvi = find_first_section();
861 while ($tvi < @typevalue) {
862 my $start = find_starting_index($tvi);
863 my $end = find_ending_index($tvi);
867 #Do not match excluded file patterns
869 for ($i = $start; $i < $end; $i++) {
870 my $line = $typevalue[$i];
871 if ($line =~ m/^([A-Z]):\s*(.*)/) {
875 if (file_match_pattern($file, $value)) {
884 for ($i = $start; $i < $end; $i++) {
885 my $line = $typevalue[$i];
886 if ($line =~ m/^([A-Z]):\s*(.*)/) {
890 if (file_match_pattern($file, $value)) {
891 my $value_pd = ($value =~ tr@/@@);
892 my $file_pd = ($file =~ tr@/@@);
893 $value_pd++ if (substr($value,-1,1) ne "/");
894 $value_pd = -1 if ($value =~ /^\.\*/);
895 if ($value_pd >= $file_pd &&
896 range_is_maintained($start, $end) &&
897 range_has_maintainer($start, $end)) {
898 $exact_pattern_match_hash{$file} = 1;
900 if ($pattern_depth == 0 ||
901 (($file_pd - $value_pd) < $pattern_depth)) {
902 $hash{$tvi} = $value_pd;
905 } elsif ($type eq 'N') {
906 if ($file =~ m/$value/x) {
916 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
917 add_categories($line);
920 my $start = find_starting_index($line);
921 my $end = find_ending_index($line);
922 for ($i = $start; $i < $end; $i++) {
923 my $line = $typevalue[$i];
924 if ($line =~ /^[FX]:/) { ##Restore file patterns
925 $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
926 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ?
927 $line =~ s/\\\./\./g; ##Convert \. to .
928 $line =~ s/\.\*/\*/g; ##Convert .* to *
930 my $count = $line =~ s/^([A-Z]):/$1:\t/g;
931 if ($letters eq "" || (!$count || $letters =~ /$1/i)) {
939 maintainers_in_file($file);
943 @keyword_tvi = sort_and_uniq(@keyword_tvi);
944 foreach my $line (@keyword_tvi) {
945 add_categories($line);
949 foreach my $email (@email_to, @list_to) {
950 $email->[0] = deduplicate_email($email->[0]);
953 foreach my $file (@files) {
955 ($email_git || ($email_git_fallback &&
956 !$exact_pattern_match_hash{$file}))) {
957 vcs_file_signoffs($file);
959 if ($email && $email_git_blame) {
960 vcs_file_blame($file);
965 foreach my $chief (@penguin_chief) {
966 if ($chief =~ m/^(.*):(.*)/) {
969 $email_address = format_email($1, $2, $email_usename);
970 if ($email_git_penguin_chiefs) {
971 push(@email_to, [$email_address, 'chief penguin']);
973 @email_to = grep($_->[0] !~ /${email_address}/, @email_to);
978 foreach my $email (@file_emails) {
979 my ($name, $address) = parse_email($email);
981 my $tmp_email = format_email($name, $address, $email_usename);
982 push_email_address($tmp_email, '');
983 add_role($tmp_email, 'in file');
987 foreach my $fix (@fixes) {
988 vcs_add_commit_signers($fix, "blamed_fixes");
992 if ($email || $email_list) {
994 @to = (@to, @email_to);
997 @to = (@to, @list_to);
1002 @to = interactive_get_maintainers(\@to);
1008 sub file_match_pattern {
1009 my ($file, $pattern) = @_;
1010 if (substr($pattern, -1) eq "/") {
1011 if ($file =~ m@^$pattern@) {
1015 if ($file =~ m@^$pattern@) {
1016 my $s1 = ($file =~ tr@/@@);
1017 my $s2 = ($pattern =~ tr@/@@);
1028 usage: $P [options] patchfile
1029 $P [options] -f file|directory
1032 MAINTAINER field selection options:
1033 --email => print email address(es) if any
1034 --git => include recent git \*-by: signers
1035 --git-all-signature-types => include signers regardless of signature type
1036 or use only ${signature_pattern} signers (default: $email_git_all_signature_types)
1037 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
1038 --git-chief-penguins => include ${penguin_chiefs}
1039 --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
1040 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
1041 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
1042 --git-blame => use git blame to find modified commits for patch or file
1043 --git-blame-signatures => when used with --git-blame, also include all commit signers
1044 --git-since => git history to use (default: $email_git_since)
1045 --hg-since => hg history to use (default: $email_hg_since)
1046 --interactive => display a menu (mostly useful if used with the --git option)
1047 --m => include maintainer(s) if any
1048 --r => include reviewer(s) if any
1049 --n => include name 'Full Name <addr\@domain.tld>'
1050 --l => include list(s) if any
1051 --moderated => include moderated lists(s) if any (default: true)
1052 --s => include subscriber only list(s) if any (default: false)
1053 --remove-duplicates => minimize duplicate email names/addresses
1054 --roles => show roles (status:subsystem, git-signer, list, etc...)
1055 --rolestats => show roles and statistics (commits/total_commits, %)
1056 --file-emails => add email addresses found in -f file (default: 0 (off))
1057 --fixes => for patches, add signatures of commits with 'Fixes: <commit>' (default: 1 (on))
1058 --scm => print SCM tree(s) if any
1059 --status => print status if any
1060 --subsystem => print subsystem name if any
1061 --web => print website(s) if any
1063 Output type options:
1064 --separator [, ] => separator for multiple entries on 1 line
1065 using --separator also sets --nomultiline if --separator is not [, ]
1066 --multiline => print 1 entry per line
1069 --pattern-depth => Number of pattern directory traversals (default: 0 (all))
1070 --keywords => scan patch for keywords (default: $keywords)
1071 --sections => print all of the subsystem sections with pattern matches
1072 --letters => print all matching 'letter' types from all matching sections
1073 --mailmap => use .mailmap file (default: $email_use_mailmap)
1074 --no-tree => run without a kernel tree
1075 --self-test => show potential issues with MAINTAINERS file content
1076 --version => show version
1077 --help => show this help information
1080 [--email --tree --nogit --git-fallback --m --r --n --l --multiline
1081 --pattern-depth=0 --remove-duplicates --rolestats]
1084 Using "-f directory" may give unexpected results:
1085 Used with "--git", git signators for _all_ files in and below
1086 directory are examined as git recurses directories.
1087 Any specified X: (exclude) pattern matches are _not_ ignored.
1088 Used with "--nogit", directory is used as a pattern match,
1089 no individual file within the directory or subdirectory
1091 Used with "--git-blame", does not iterate all files in directory
1092 Using "--git-blame" is slow and may add old committers and authors
1093 that are no longer active maintainers to the output.
1094 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
1095 other automated tools that expect only ["name"] <email address>
1096 may not work because of additional output after <email address>.
1097 Using "--rolestats" and "--git-blame" shows the #/total=% commits,
1098 not the percentage of the entire file authored. # of commits is
1099 not a good measure of amount of code authored. 1 major commit may
1100 contain a thousand lines, 5 trivial commits may modify a single line.
1101 If git is not installed, but mercurial (hg) is installed and an .hg
1102 repository exists, the following options apply to mercurial:
1104 --git-min-signatures, --git-max-maintainers, --git-min-percent, and
1106 Use --hg-since not --git-since to control date selection
1107 File ".get_maintainer.conf", if it exists in the linux kernel source root
1108 directory, can change whatever get_maintainer defaults are desired.
1109 Entries in this file can be any command line argument.
1110 This file is prepended to any additional command line arguments.
1111 Multiple lines and # comments are allowed.
1112 Most options have both positive and negative forms.
1113 The negative forms for --<foo> are --no<foo> and --no-<foo>.
1118 sub top_of_kernel_tree {
1121 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
1124 if ( (-f "${lk_path}COPYING")
1125 && (-f "${lk_path}CREDITS")
1126 && (-f "${lk_path}Kbuild")
1127 && (-e "${lk_path}MAINTAINERS")
1128 && (-f "${lk_path}Makefile")
1129 && (-f "${lk_path}README")
1130 && (-d "${lk_path}Documentation")
1131 && (-d "${lk_path}arch")
1132 && (-d "${lk_path}include")
1133 && (-d "${lk_path}drivers")
1134 && (-d "${lk_path}fs")
1135 && (-d "${lk_path}init")
1136 && (-d "${lk_path}ipc")
1137 && (-d "${lk_path}kernel")
1138 && (-d "${lk_path}lib")
1139 && (-d "${lk_path}scripts")) {
1146 my ($formatted_email) = @_;
1151 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
1154 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
1156 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
1160 $name =~ s/^\s+|\s+$//g;
1161 $name =~ s/^\"|\"$//g;
1162 $address =~ s/^\s+|\s+$//g;
1164 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
1165 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
1166 $name = "\"$name\"";
1169 return ($name, $address);
1173 my ($name, $address, $usename) = @_;
1175 my $formatted_email;
1177 $name =~ s/^\s+|\s+$//g;
1178 $name =~ s/^\"|\"$//g;
1179 $address =~ s/^\s+|\s+$//g;
1181 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
1182 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
1183 $name = "\"$name\"";
1187 if ("$name" eq "") {
1188 $formatted_email = "$address";
1190 $formatted_email = "$name <$address>";
1193 $formatted_email = $address;
1196 return $formatted_email;
1199 sub find_first_section {
1202 while ($index < @typevalue) {
1203 my $tv = $typevalue[$index];
1204 if (($tv =~ m/^([A-Z]):\s*(.*)/)) {
1213 sub find_starting_index {
1216 while ($index > 0) {
1217 my $tv = $typevalue[$index];
1218 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
1227 sub find_ending_index {
1230 while ($index < @typevalue) {
1231 my $tv = $typevalue[$index];
1232 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
1241 sub get_subsystem_name {
1244 my $start = find_starting_index($index);
1246 my $subsystem = $typevalue[$start];
1247 if ($output_section_maxlen && length($subsystem) > $output_section_maxlen) {
1248 $subsystem = substr($subsystem, 0, $output_section_maxlen - 3);
1249 $subsystem =~ s/\s*$//;
1250 $subsystem = $subsystem . "...";
1255 sub get_maintainer_role {
1259 my $start = find_starting_index($index);
1260 my $end = find_ending_index($index);
1262 my $role = "unknown";
1263 my $subsystem = get_subsystem_name($index);
1265 for ($i = $start + 1; $i < $end; $i++) {
1266 my $tv = $typevalue[$i];
1267 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1270 if ($ptype eq "S") {
1277 if ($role eq "supported") {
1278 $role = "supporter";
1279 } elsif ($role eq "maintained") {
1280 $role = "maintainer";
1281 } elsif ($role eq "odd fixes") {
1282 $role = "odd fixer";
1283 } elsif ($role eq "orphan") {
1284 $role = "orphan minder";
1285 } elsif ($role eq "obsolete") {
1286 $role = "obsolete minder";
1287 } elsif ($role eq "buried alive in reporters") {
1288 $role = "chief penguin";
1291 return $role . ":" . $subsystem;
1297 my $subsystem = get_subsystem_name($index);
1299 if ($subsystem eq "THE REST") {
1306 sub add_categories {
1310 my $start = find_starting_index($index);
1311 my $end = find_ending_index($index);
1313 push(@subsystem, $typevalue[$start]);
1315 for ($i = $start + 1; $i < $end; $i++) {
1316 my $tv = $typevalue[$i];
1317 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1320 if ($ptype eq "L") {
1321 my $list_address = $pvalue;
1322 my $list_additional = "";
1323 my $list_role = get_list_role($i);
1325 if ($list_role ne "") {
1326 $list_role = ":" . $list_role;
1328 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
1330 $list_additional = $2;
1332 if ($list_additional =~ m/subscribers-only/) {
1333 if ($email_subscriber_list) {
1334 if (!$hash_list_to{lc($list_address)}) {
1335 $hash_list_to{lc($list_address)} = 1;
1336 push(@list_to, [$list_address,
1337 "subscriber list${list_role}"]);
1342 if (!$hash_list_to{lc($list_address)}) {
1343 if ($list_additional =~ m/moderated/) {
1344 if ($email_moderated_list) {
1345 $hash_list_to{lc($list_address)} = 1;
1346 push(@list_to, [$list_address,
1347 "moderated list${list_role}"]);
1350 $hash_list_to{lc($list_address)} = 1;
1351 push(@list_to, [$list_address,
1352 "open list${list_role}"]);
1357 } elsif ($ptype eq "M") {
1358 if ($email_maintainer) {
1359 my $role = get_maintainer_role($i);
1360 push_email_addresses($pvalue, $role);
1362 } elsif ($ptype eq "R") {
1363 if ($email_reviewer) {
1364 my $subsystem = get_subsystem_name($i);
1365 push_email_addresses($pvalue, "reviewer:$subsystem");
1367 } elsif ($ptype eq "T") {
1368 push(@scm, $pvalue);
1369 } elsif ($ptype eq "W") {
1370 push(@web, $pvalue);
1371 } elsif ($ptype eq "S") {
1372 push(@status, $pvalue);
1379 my ($name, $address) = @_;
1381 return 1 if (($name eq "") && ($address eq ""));
1382 return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
1383 return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
1388 sub push_email_address {
1389 my ($line, $role) = @_;
1391 my ($name, $address) = parse_email($line);
1393 if ($address eq "") {
1397 if (!$email_remove_duplicates) {
1398 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1399 } elsif (!email_inuse($name, $address)) {
1400 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1401 $email_hash_name{lc($name)}++ if ($name ne "");
1402 $email_hash_address{lc($address)}++;
1408 sub push_email_addresses {
1409 my ($address, $role) = @_;
1411 my @address_list = ();
1413 if (rfc822_valid($address)) {
1414 push_email_address($address, $role);
1415 } elsif (@address_list = rfc822_validlist($address)) {
1416 my $array_count = shift(@address_list);
1417 while (my $entry = shift(@address_list)) {
1418 push_email_address($entry, $role);
1421 if (!push_email_address($address, $role)) {
1422 warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1428 my ($line, $role) = @_;
1430 my ($name, $address) = parse_email($line);
1431 my $email = format_email($name, $address, $email_usename);
1433 foreach my $entry (@email_to) {
1434 if ($email_remove_duplicates) {
1435 my ($entry_name, $entry_address) = parse_email($entry->[0]);
1436 if (($name eq $entry_name || $address eq $entry_address)
1437 && ($role eq "" || !($entry->[1] =~ m/$role/))
1439 if ($entry->[1] eq "") {
1440 $entry->[1] = "$role";
1442 $entry->[1] = "$entry->[1],$role";
1446 if ($email eq $entry->[0]
1447 && ($role eq "" || !($entry->[1] =~ m/$role/))
1449 if ($entry->[1] eq "") {
1450 $entry->[1] = "$role";
1452 $entry->[1] = "$entry->[1],$role";
1462 foreach my $path (split(/:/, $ENV{PATH})) {
1463 if (-e "$path/$bin") {
1464 return "$path/$bin";
1474 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1475 if (-e "$path/$conf") {
1476 return "$path/$conf";
1486 my ($name, $address) = parse_email($line);
1487 my $email = format_email($name, $address, 1);
1488 my $real_name = $name;
1489 my $real_address = $address;
1491 if (exists $mailmap->{names}->{$email} ||
1492 exists $mailmap->{addresses}->{$email}) {
1493 if (exists $mailmap->{names}->{$email}) {
1494 $real_name = $mailmap->{names}->{$email};
1496 if (exists $mailmap->{addresses}->{$email}) {
1497 $real_address = $mailmap->{addresses}->{$email};
1500 if (exists $mailmap->{names}->{$address}) {
1501 $real_name = $mailmap->{names}->{$address};
1503 if (exists $mailmap->{addresses}->{$address}) {
1504 $real_address = $mailmap->{addresses}->{$address};
1507 return format_email($real_name, $real_address, 1);
1511 my (@addresses) = @_;
1513 my @mapped_emails = ();
1514 foreach my $line (@addresses) {
1515 push(@mapped_emails, mailmap_email($line));
1517 merge_by_realname(@mapped_emails) if ($email_use_mailmap);
1518 return @mapped_emails;
1521 sub merge_by_realname {
1525 foreach my $email (@emails) {
1526 my ($name, $address) = parse_email($email);
1527 if (exists $address_map{$name}) {
1528 $address = $address_map{$name};
1529 $email = format_email($name, $address, 1);
1531 $address_map{$name} = $address;
1536 sub git_execute_cmd {
1540 my $output = `$cmd`;
1541 $output =~ s/^\s*//gm;
1542 @lines = split("\n", $output);
1547 sub hg_execute_cmd {
1551 my $output = `$cmd`;
1552 @lines = split("\n", $output);
1557 sub extract_formatted_signatures {
1558 my (@signature_lines) = @_;
1560 my @type = @signature_lines;
1562 s/\s*(.*):.*/$1/ for (@type);
1565 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1567 ## Reformat email addresses (with names) to avoid badly written signatures
1569 foreach my $signer (@signature_lines) {
1570 $signer = deduplicate_email($signer);
1573 return (\@type, \@signature_lines);
1576 sub vcs_find_signers {
1577 my ($cmd, $file) = @_;
1580 my @signatures = ();
1584 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1586 my $pattern = $VCS_cmds{"commit_pattern"};
1587 my $author_pattern = $VCS_cmds{"author_pattern"};
1588 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1590 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1592 $commits = grep(/$pattern/, @lines); # of commits
1594 @authors = grep(/$author_pattern/, @lines);
1595 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1596 @stats = grep(/$stat_pattern/, @lines);
1598 # print("stats: <@stats>\n");
1600 return (0, \@signatures, \@authors, \@stats) if !@signatures;
1602 save_commits_by_author(@lines) if ($interactive);
1603 save_commits_by_signer(@lines) if ($interactive);
1605 if (!$email_git_penguin_chiefs) {
1606 @signatures = grep(!/${penguin_chiefs}/i, @signatures);
1609 my ($author_ref, $authors_ref) = extract_formatted_signatures(@authors);
1610 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1612 return ($commits, $signers_ref, $authors_ref, \@stats);
1615 sub vcs_find_author {
1619 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1621 if (!$email_git_penguin_chiefs) {
1622 @lines = grep(!/${penguin_chiefs}/i, @lines);
1625 return @lines if !@lines;
1628 foreach my $line (@lines) {
1629 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1631 my ($name, $address) = parse_email($author);
1632 $author = format_email($name, $address, 1);
1633 push(@authors, $author);
1637 save_commits_by_author(@lines) if ($interactive);
1638 save_commits_by_signer(@lines) if ($interactive);
1643 sub vcs_save_commits {
1648 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1650 foreach my $line (@lines) {
1651 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1664 return @commits if (!(-f $file));
1666 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1667 my @all_commits = ();
1669 $cmd = $VCS_cmds{"blame_file_cmd"};
1670 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1671 @all_commits = vcs_save_commits($cmd);
1673 foreach my $file_range_diff (@range) {
1674 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1676 my $diff_start = $2;
1677 my $diff_length = $3;
1678 next if ("$file" ne "$diff_file");
1679 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1680 push(@commits, $all_commits[$i]);
1684 foreach my $file_range_diff (@range) {
1685 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1687 my $diff_start = $2;
1688 my $diff_length = $3;
1689 next if ("$file" ne "$diff_file");
1690 $cmd = $VCS_cmds{"blame_range_cmd"};
1691 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1692 push(@commits, vcs_save_commits($cmd));
1695 $cmd = $VCS_cmds{"blame_file_cmd"};
1696 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1697 @commits = vcs_save_commits($cmd);
1700 foreach my $commit (@commits) {
1701 $commit =~ s/^\^//g;
1707 my $printed_novcs = 0;
1709 %VCS_cmds = %VCS_cmds_git;
1710 return 1 if eval $VCS_cmds{"available"};
1711 %VCS_cmds = %VCS_cmds_hg;
1712 return 2 if eval $VCS_cmds{"available"};
1714 if (!$printed_novcs) {
1715 warn("$P: No supported VCS found. Add --nogit to options?\n");
1716 warn("Using a git repository produces better results.\n");
1717 warn("Try Linus Torvalds' latest git repository using:\n");
1718 warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n");
1726 return $vcs_used == 1;
1730 return $vcs_used == 2;
1733 sub vcs_add_commit_signers {
1734 return if (!vcs_exists());
1736 my ($commit, $desc) = @_;
1737 my $commit_count = 0;
1738 my $commit_authors_ref;
1739 my $commit_signers_ref;
1741 my @commit_authors = ();
1742 my @commit_signers = ();
1745 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1746 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1748 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, "");
1749 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
1750 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
1752 foreach my $signer (@commit_signers) {
1753 $signer = deduplicate_email($signer);
1756 vcs_assign($desc, 1, @commit_signers);
1759 sub interactive_get_maintainers {
1760 my ($list_ref) = @_;
1761 my @list = @$list_ref;
1770 foreach my $entry (@list) {
1771 $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
1772 $selected{$count} = 1;
1773 $authored{$count} = 0;
1774 $signed{$count} = 0;
1780 my $print_options = 0;
1785 printf STDERR "\n%1s %2s %-65s",
1786 "*", "#", "email/list and role:stats";
1788 ($email_git_fallback && !$maintained) ||
1790 print STDERR "auth sign";
1793 foreach my $entry (@list) {
1794 my $email = $entry->[0];
1795 my $role = $entry->[1];
1797 $sel = "*" if ($selected{$count});
1798 my $commit_author = $commit_author_hash{$email};
1799 my $commit_signer = $commit_signer_hash{$email};
1802 $authored++ for (@{$commit_author});
1803 $signed++ for (@{$commit_signer});
1804 printf STDERR "%1s %2d %-65s", $sel, $count + 1, $email;
1805 printf STDERR "%4d %4d", $authored, $signed
1806 if ($authored > 0 || $signed > 0);
1807 printf STDERR "\n %s\n", $role;
1808 if ($authored{$count}) {
1809 my $commit_author = $commit_author_hash{$email};
1810 foreach my $ref (@{$commit_author}) {
1811 print STDERR " Author: @{$ref}[1]\n";
1814 if ($signed{$count}) {
1815 my $commit_signer = $commit_signer_hash{$email};
1816 foreach my $ref (@{$commit_signer}) {
1817 print STDERR " @{$ref}[2]: @{$ref}[1]\n";
1824 my $date_ref = \$email_git_since;
1825 $date_ref = \$email_hg_since if (vcs_is_hg());
1826 if ($print_options) {
1831 Version Control options:
1832 g use git history [$email_git]
1833 gf use git-fallback [$email_git_fallback]
1834 b use git blame [$email_git_blame]
1835 bs use blame signatures [$email_git_blame_signatures]
1836 c# minimum commits [$email_git_min_signatures]
1837 %# min percent [$email_git_min_percent]
1838 d# history to use [$$date_ref]
1839 x# max maintainers [$email_git_max_maintainers]
1840 t all signature types [$email_git_all_signature_types]
1841 m use .mailmap [$email_use_mailmap]
1848 tm toggle maintainers
1849 tg toggle git entries
1850 tl toggle open list entries
1851 ts toggle subscriber list entries
1852 f emails in file [$email_file_emails]
1853 k keywords in file [$keywords]
1854 r remove duplicates [$email_remove_duplicates]
1855 p# pattern match depth [$pattern_depth]
1859 "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1861 my $input = <STDIN>;
1866 my @wish = split(/[, ]+/, $input);
1867 foreach my $nr (@wish) {
1869 my $sel = substr($nr, 0, 1);
1870 my $str = substr($nr, 1);
1872 $val = $1 if $str =~ /^(\d+)$/;
1877 $output_rolestats = 0;
1880 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1881 $selected{$nr - 1} = !$selected{$nr - 1};
1882 } elsif ($sel eq "*" || $sel eq '^') {
1884 $toggle = 1 if ($sel eq '*');
1885 for (my $i = 0; $i < $count; $i++) {
1886 $selected{$i} = $toggle;
1888 } elsif ($sel eq "0") {
1889 for (my $i = 0; $i < $count; $i++) {
1890 $selected{$i} = !$selected{$i};
1892 } elsif ($sel eq "t") {
1893 if (lc($str) eq "m") {
1894 for (my $i = 0; $i < $count; $i++) {
1895 $selected{$i} = !$selected{$i}
1896 if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
1898 } elsif (lc($str) eq "g") {
1899 for (my $i = 0; $i < $count; $i++) {
1900 $selected{$i} = !$selected{$i}
1901 if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
1903 } elsif (lc($str) eq "l") {
1904 for (my $i = 0; $i < $count; $i++) {
1905 $selected{$i} = !$selected{$i}
1906 if ($list[$i]->[1] =~ /^(open list)/i);
1908 } elsif (lc($str) eq "s") {
1909 for (my $i = 0; $i < $count; $i++) {
1910 $selected{$i} = !$selected{$i}
1911 if ($list[$i]->[1] =~ /^(subscriber list)/i);
1914 } elsif ($sel eq "a") {
1915 if ($val > 0 && $val <= $count) {
1916 $authored{$val - 1} = !$authored{$val - 1};
1917 } elsif ($str eq '*' || $str eq '^') {
1919 $toggle = 1 if ($str eq '*');
1920 for (my $i = 0; $i < $count; $i++) {
1921 $authored{$i} = $toggle;
1924 } elsif ($sel eq "s") {
1925 if ($val > 0 && $val <= $count) {
1926 $signed{$val - 1} = !$signed{$val - 1};
1927 } elsif ($str eq '*' || $str eq '^') {
1929 $toggle = 1 if ($str eq '*');
1930 for (my $i = 0; $i < $count; $i++) {
1931 $signed{$i} = $toggle;
1934 } elsif ($sel eq "o") {
1937 } elsif ($sel eq "g") {
1939 bool_invert(\$email_git_fallback);
1941 bool_invert(\$email_git);
1944 } elsif ($sel eq "b") {
1946 bool_invert(\$email_git_blame_signatures);
1948 bool_invert(\$email_git_blame);
1951 } elsif ($sel eq "c") {
1953 $email_git_min_signatures = $val;
1956 } elsif ($sel eq "x") {
1958 $email_git_max_maintainers = $val;
1961 } elsif ($sel eq "%") {
1962 if ($str ne "" && $val >= 0) {
1963 $email_git_min_percent = $val;
1966 } elsif ($sel eq "d") {
1968 $email_git_since = $str;
1969 } elsif (vcs_is_hg()) {
1970 $email_hg_since = $str;
1973 } elsif ($sel eq "t") {
1974 bool_invert(\$email_git_all_signature_types);
1976 } elsif ($sel eq "f") {
1977 bool_invert(\$email_file_emails);
1979 } elsif ($sel eq "r") {
1980 bool_invert(\$email_remove_duplicates);
1982 } elsif ($sel eq "m") {
1983 bool_invert(\$email_use_mailmap);
1986 } elsif ($sel eq "k") {
1987 bool_invert(\$keywords);
1989 } elsif ($sel eq "p") {
1990 if ($str ne "" && $val >= 0) {
1991 $pattern_depth = $val;
1994 } elsif ($sel eq "h" || $sel eq "?") {
1997 Interactive mode allows you to select the various maintainers, submitters,
1998 commit signers and mailing lists that could be CC'd on a patch.
2000 Any *'d entry is selected.
2002 If you have git or hg installed, you can choose to summarize the commit
2003 history of files in the patch. Also, each line of the current file can
2004 be matched to its commit author and that commits signers with blame.
2006 Various knobs exist to control the length of time for active commit
2007 tracking, the maximum number of commit authors and signers to add,
2010 Enter selections at the prompt until you are satisfied that the selected
2011 maintainers are appropriate. You may enter multiple selections separated
2012 by either commas or spaces.
2016 print STDERR "invalid option: '$nr'\n";
2021 print STDERR "git-blame can be very slow, please have patience..."
2022 if ($email_git_blame);
2023 goto &get_maintainers;
2027 #drop not selected entries
2029 my @new_emailto = ();
2030 foreach my $entry (@list) {
2031 if ($selected{$count}) {
2032 push(@new_emailto, $list[$count]);
2036 return @new_emailto;
2040 my ($bool_ref) = @_;
2049 sub deduplicate_email {
2053 my ($name, $address) = parse_email($email);
2054 $email = format_email($name, $address, 1);
2055 $email = mailmap_email($email);
2057 return $email if (!$email_remove_duplicates);
2059 ($name, $address) = parse_email($email);
2061 if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
2062 $name = $deduplicate_name_hash{lc($name)}->[0];
2063 $address = $deduplicate_name_hash{lc($name)}->[1];
2065 } elsif ($deduplicate_address_hash{lc($address)}) {
2066 $name = $deduplicate_address_hash{lc($address)}->[0];
2067 $address = $deduplicate_address_hash{lc($address)}->[1];
2071 $deduplicate_name_hash{lc($name)} = [ $name, $address ];
2072 $deduplicate_address_hash{lc($address)} = [ $name, $address ];
2074 $email = format_email($name, $address, 1);
2075 $email = mailmap_email($email);
2079 sub save_commits_by_author {
2086 foreach my $line (@lines) {
2087 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2089 $author = deduplicate_email($author);
2090 push(@authors, $author);
2092 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
2093 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
2096 for (my $i = 0; $i < @authors; $i++) {
2098 foreach my $ref(@{$commit_author_hash{$authors[$i]}}) {
2099 if (@{$ref}[0] eq $commits[$i] &&
2100 @{$ref}[1] eq $subjects[$i]) {
2106 push(@{$commit_author_hash{$authors[$i]}},
2107 [ ($commits[$i], $subjects[$i]) ]);
2112 sub save_commits_by_signer {
2118 foreach my $line (@lines) {
2119 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
2120 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
2121 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
2122 my @signatures = ($line);
2123 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
2124 my @types = @$types_ref;
2125 my @signers = @$signers_ref;
2127 my $type = $types[0];
2128 my $signer = $signers[0];
2130 $signer = deduplicate_email($signer);
2133 foreach my $ref(@{$commit_signer_hash{$signer}}) {
2134 if (@{$ref}[0] eq $commit &&
2135 @{$ref}[1] eq $subject &&
2136 @{$ref}[2] eq $type) {
2142 push(@{$commit_signer_hash{$signer}},
2143 [ ($commit, $subject, $type) ]);
2150 my ($role, $divisor, @lines) = @_;
2155 return if (@lines <= 0);
2157 if ($divisor <= 0) {
2158 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
2162 @lines = mailmap(@lines);
2164 return if (@lines <= 0);
2166 @lines = sort(@lines);
2169 $hash{$_}++ for @lines;
2172 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
2173 my $sign_offs = $hash{$line};
2174 my $percent = $sign_offs * 100 / $divisor;
2176 $percent = 100 if ($percent > 100);
2177 next if (ignore_email_address($line));
2179 last if ($sign_offs < $email_git_min_signatures ||
2180 $count > $email_git_max_maintainers ||
2181 $percent < $email_git_min_percent);
2182 push_email_address($line, '');
2183 if ($output_rolestats) {
2184 my $fmt_percent = sprintf("%.0f", $percent);
2185 add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%");
2187 add_role($line, $role);
2192 sub vcs_file_signoffs {
2203 $vcs_used = vcs_exists();
2204 return if (!$vcs_used);
2206 my $cmd = $VCS_cmds{"find_signers_cmd"};
2207 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2209 ($commits, $signers_ref, $authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2211 @signers = @{$signers_ref} if defined $signers_ref;
2212 @authors = @{$authors_ref} if defined $authors_ref;
2213 @stats = @{$stats_ref} if defined $stats_ref;
2215 # print("commits: <$commits>\nsigners:<@signers>\nauthors: <@authors>\nstats: <@stats>\n");
2217 foreach my $signer (@signers) {
2218 $signer = deduplicate_email($signer);
2221 vcs_assign("commit_signer", $commits, @signers);
2222 vcs_assign("authored", $commits, @authors);
2223 if ($#authors == $#stats) {
2224 my $stat_pattern = $VCS_cmds{"stat_pattern"};
2225 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
2229 for (my $i = 0; $i <= $#stats; $i++) {
2230 if ($stats[$i] =~ /$stat_pattern/) {
2235 my @tmp_authors = uniq(@authors);
2236 foreach my $author (@tmp_authors) {
2237 $author = deduplicate_email($author);
2239 @tmp_authors = uniq(@tmp_authors);
2240 my @list_added = ();
2241 my @list_deleted = ();
2242 foreach my $author (@tmp_authors) {
2244 my $auth_deleted = 0;
2245 for (my $i = 0; $i <= $#stats; $i++) {
2246 if ($author eq deduplicate_email($authors[$i]) &&
2247 $stats[$i] =~ /$stat_pattern/) {
2249 $auth_deleted += $2;
2252 for (my $i = 0; $i < $auth_added; $i++) {
2253 push(@list_added, $author);
2255 for (my $i = 0; $i < $auth_deleted; $i++) {
2256 push(@list_deleted, $author);
2259 vcs_assign("added_lines", $added, @list_added);
2260 vcs_assign("removed_lines", $deleted, @list_deleted);
2264 sub vcs_file_blame {
2268 my @all_commits = ();
2273 $vcs_used = vcs_exists();
2274 return if (!$vcs_used);
2276 @all_commits = vcs_blame($file);
2277 @commits = uniq(@all_commits);
2278 $total_commits = @commits;
2279 $total_lines = @all_commits;
2281 if ($email_git_blame_signatures) {
2284 my $commit_authors_ref;
2285 my $commit_signers_ref;
2287 my @commit_authors = ();
2288 my @commit_signers = ();
2289 my $commit = join(" -r ", @commits);
2292 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2293 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2295 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2296 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2297 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2299 push(@signers, @commit_signers);
2301 foreach my $commit (@commits) {
2303 my $commit_authors_ref;
2304 my $commit_signers_ref;
2306 my @commit_authors = ();
2307 my @commit_signers = ();
2310 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2311 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2313 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2314 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2315 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2317 push(@signers, @commit_signers);
2322 if ($from_filename) {
2323 if ($output_rolestats) {
2325 if (vcs_is_hg()) {{ # Double brace for last exit
2327 my @commit_signers = ();
2328 @commits = uniq(@commits);
2329 @commits = sort(@commits);
2330 my $commit = join(" -r ", @commits);
2333 $cmd = $VCS_cmds{"find_commit_author_cmd"};
2334 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2338 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
2340 if (!$email_git_penguin_chiefs) {
2341 @lines = grep(!/${penguin_chiefs}/i, @lines);
2347 foreach my $line (@lines) {
2348 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2350 $author = deduplicate_email($author);
2351 push(@authors, $author);
2355 save_commits_by_author(@lines) if ($interactive);
2356 save_commits_by_signer(@lines) if ($interactive);
2358 push(@signers, @authors);
2361 foreach my $commit (@commits) {
2363 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
2364 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
2365 my @author = vcs_find_author($cmd);
2368 my $formatted_author = deduplicate_email($author[0]);
2370 my $count = grep(/$commit/, @all_commits);
2371 for ($i = 0; $i < $count ; $i++) {
2372 push(@blame_signers, $formatted_author);
2376 if (@blame_signers) {
2377 vcs_assign("authored lines", $total_lines, @blame_signers);
2380 foreach my $signer (@signers) {
2381 $signer = deduplicate_email($signer);
2383 vcs_assign("commits", $total_commits, @signers);
2385 foreach my $signer (@signers) {
2386 $signer = deduplicate_email($signer);
2388 vcs_assign("modified commits", $total_commits, @signers);
2392 sub vcs_file_exists {
2397 my $vcs_used = vcs_exists();
2398 return 0 if (!$vcs_used);
2400 my $cmd = $VCS_cmds{"file_exists_cmd"};
2401 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2403 $exists = &{$VCS_cmds{"execute_cmd"}}($cmd);
2405 return 0 if ($? != 0);
2410 sub vcs_list_files {
2415 my $vcs_used = vcs_exists();
2416 return 0 if (!$vcs_used);
2418 my $cmd = $VCS_cmds{"list_files_cmd"};
2419 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2420 @lsfiles = &{$VCS_cmds{"execute_cmd"}}($cmd);
2422 return () if ($? != 0);
2431 @parms = grep(!$saw{$_}++, @parms);
2439 @parms = sort @parms;
2440 @parms = grep(!$saw{$_}++, @parms);
2444 sub clean_file_emails {
2445 my (@file_emails) = @_;
2446 my @fmt_emails = ();
2448 foreach my $email (@file_emails) {
2449 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
2450 my ($name, $address) = parse_email($email);
2451 if ($name eq '"[,\.]"') {
2455 my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name);
2457 my $first = $nw[@nw - 3];
2458 my $middle = $nw[@nw - 2];
2459 my $last = $nw[@nw - 1];
2461 if (((length($first) == 1 && $first =~ m/[A-Za-z]/) ||
2462 (length($first) == 2 && substr($first, -1) eq ".")) ||
2463 (length($middle) == 1 ||
2464 (length($middle) == 2 && substr($middle, -1) eq "."))) {
2465 $name = "$first $middle $last";
2467 $name = "$middle $last";
2471 if (substr($name, -1) =~ /[,\.]/) {
2472 $name = substr($name, 0, length($name) - 1);
2473 } elsif (substr($name, -2) =~ /[,\.]"/) {
2474 $name = substr($name, 0, length($name) - 2) . '"';
2477 if (substr($name, 0, 1) =~ /[,\.]/) {
2478 $name = substr($name, 1, length($name) - 1);
2479 } elsif (substr($name, 0, 2) =~ /"[,\.]/) {
2480 $name = '"' . substr($name, 2, length($name) - 2);
2483 my $fmt_email = format_email($name, $address, $email_usename);
2484 push(@fmt_emails, $fmt_email);
2494 my ($address, $role) = @$_;
2495 if (!$saw{$address}) {
2496 if ($output_roles) {
2497 push(@lines, "$address ($role)");
2499 push(@lines, $address);
2511 if ($output_multiline) {
2512 foreach my $line (@parms) {
2516 print(join($output_separator, @parms));
2524 # Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
2525 # comment. We must allow for rfc822_lwsp (or comments) after each of these.
2526 # This regexp will only work on addresses which have had comments stripped
2527 # and replaced with rfc822_lwsp.
2529 my $specials = '()<>@,;:\\\\".\\[\\]';
2530 my $controls = '\\000-\\037\\177';
2532 my $dtext = "[^\\[\\]\\r\\\\]";
2533 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
2535 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
2537 # Use zero-width assertion to spot the limit of an atom. A simple
2538 # $rfc822_lwsp* causes the regexp engine to hang occasionally.
2539 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
2540 my $word = "(?:$atom|$quoted_string)";
2541 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
2543 my $sub_domain = "(?:$atom|$domain_literal)";
2544 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
2546 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
2548 my $phrase = "$word*";
2549 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
2550 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
2551 my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
2553 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
2554 my $address = "(?:$mailbox|$group)";
2556 return "$rfc822_lwsp*$address";
2559 sub rfc822_strip_comments {
2561 # Recursively remove comments, and replace with a single space. The simpler
2562 # regexps in the Email Addressing FAQ are imperfect - they will miss escaped
2563 # chars in atoms, for example.
2565 while ($s =~ s/^((?:[^"\\]|\\.)*
2566 (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
2567 \((?:[^()\\]|\\.)*\)/$1 /osx) {}
2571 # valid: returns true if the parameter is an RFC822 valid address
2574 my $s = rfc822_strip_comments(shift);
2577 $rfc822re = make_rfc822re();
2580 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
2583 # validlist: In scalar context, returns true if the parameter is an RFC822
2584 # valid list of addresses.
2586 # In list context, returns an empty list on failure (an invalid
2587 # address was found); otherwise a list whose first element is the
2588 # number of addresses found and whose remaining elements are the
2589 # addresses. This is needed to disambiguate failure (invalid)
2590 # from success with no addresses found, because an empty string is
2593 sub rfc822_validlist {
2594 my $s = rfc822_strip_comments(shift);
2597 $rfc822re = make_rfc822re();
2599 # * null list items are valid according to the RFC
2600 # * the '1' business is to aid in distinguishing failure from no results
2603 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
2604 $s =~ m/^$rfc822_char*$/) {
2605 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
2608 return wantarray ? (scalar(@r), @r) : 1;
2610 return wantarray ? () : 0;