split dev_queue
[cor.git] / scripts / get_maintainer.pl
blob34085d146fa2cadb0a92353c0f6cc0d89303af29
1 #!/usr/bin/env perl
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>
13 use warnings;
14 use strict;
16 my $P = $0;
17 my $V = '0.26';
19 use Getopt::Long qw(:config no_auto_abbrev);
20 use Cwd;
21 use File::Find;
23 my $cur_path = fastgetcwd() . '/';
24 my $lk_path = "./";
25 my $email = 1;
26 my $email_usename = 1;
27 my $email_maintainer = 1;
28 my $email_reviewer = 1;
29 my $email_fixes = 1;
30 my $email_list = 1;
31 my $email_moderated_list = 1;
32 my $email_subscriber_list = 0;
33 my $email_git_penguin_chiefs = 0;
34 my $email_git = 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";
44 my $interactive = 0;
45 my $email_remove_duplicates = 1;
46 my $email_use_mailmap = 1;
47 my $output_multiline = 1;
48 my $output_separator = ", ";
49 my $output_roles = 0;
50 my $output_rolestats = 1;
51 my $output_section_maxlen = 50;
52 my $scm = 0;
53 my $tree = 1;
54 my $web = 0;
55 my $subsystem = 0;
56 my $status = 0;
57 my $letters = "";
58 my $keywords = 1;
59 my $sections = 0;
60 my $file_emails = 0;
61 my $from_filename = 0;
62 my $pattern_depth = 0;
63 my $self_test = undef;
64 my $version = 0;
65 my $help = 0;
66 my $find_maintainer_files = 0;
67 my $maintainer_path;
68 my $vcs_used = 0;
70 my $exit = 0;
72 my %commit_author_hash;
73 my %commit_signer_hash;
75 my @penguin_chief = ();
76 push(@penguin_chief, "Linus Torvalds:torvalds\@linux-foundation.org");
77 #Andrew wants in on most everything - 2009/01/14
78 #push(@penguin_chief, "Andrew Morton:akpm\@linux-foundation.org");
80 my @penguin_chief_names = ();
81 foreach my $chief (@penguin_chief) {
82 if ($chief =~ m/^(.*):(.*)/) {
83 my $chief_name = $1;
84 my $chief_addr = $2;
85 push(@penguin_chief_names, $chief_name);
88 my $penguin_chiefs = "\(" . join("|", @penguin_chief_names) . "\)";
90 # Signature types of people who are either
91 # a) responsible for the code in question, or
92 # b) familiar enough with it to give relevant feedback
93 my @signature_tags = ();
94 push(@signature_tags, "Signed-off-by:");
95 push(@signature_tags, "Reviewed-by:");
96 push(@signature_tags, "Acked-by:");
98 my $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
100 # rfc822 email address - preloaded methods go here.
101 my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
102 my $rfc822_char = '[\\000-\\377]';
104 # VCS command support: class-like functions and strings
106 my %VCS_cmds;
108 my %VCS_cmds_git = (
109 "execute_cmd" => \&git_execute_cmd,
110 "available" => '(which("git") ne "") && (-e ".git")',
111 "find_signers_cmd" =>
112 "git log --no-color --follow --since=\$email_git_since " .
113 '--numstat --no-merges ' .
114 '--format="GitCommit: %H%n' .
115 'GitAuthor: %an <%ae>%n' .
116 'GitDate: %aD%n' .
117 'GitSubject: %s%n' .
118 '%b%n"' .
119 " -- \$file",
120 "find_commit_signers_cmd" =>
121 "git log --no-color " .
122 '--numstat ' .
123 '--format="GitCommit: %H%n' .
124 'GitAuthor: %an <%ae>%n' .
125 'GitDate: %aD%n' .
126 'GitSubject: %s%n' .
127 '%b%n"' .
128 " -1 \$commit",
129 "find_commit_author_cmd" =>
130 "git log --no-color " .
131 '--numstat ' .
132 '--format="GitCommit: %H%n' .
133 'GitAuthor: %an <%ae>%n' .
134 'GitDate: %aD%n' .
135 'GitSubject: %s%n"' .
136 " -1 \$commit",
137 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
138 "blame_file_cmd" => "git blame -l \$file",
139 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})",
140 "blame_commit_pattern" => "^([0-9a-f]+) ",
141 "author_pattern" => "^GitAuthor: (.*)",
142 "subject_pattern" => "^GitSubject: (.*)",
143 "stat_pattern" => "^(\\d+)\\t(\\d+)\\t\$file\$",
144 "file_exists_cmd" => "git ls-files \$file",
145 "list_files_cmd" => "git ls-files \$file",
148 my %VCS_cmds_hg = (
149 "execute_cmd" => \&hg_execute_cmd,
150 "available" => '(which("hg") ne "") && (-d ".hg")',
151 "find_signers_cmd" =>
152 "hg log --date=\$email_hg_since " .
153 "--template='HgCommit: {node}\\n" .
154 "HgAuthor: {author}\\n" .
155 "HgSubject: {desc}\\n'" .
156 " -- \$file",
157 "find_commit_signers_cmd" =>
158 "hg log " .
159 "--template='HgSubject: {desc}\\n'" .
160 " -r \$commit",
161 "find_commit_author_cmd" =>
162 "hg log " .
163 "--template='HgCommit: {node}\\n" .
164 "HgAuthor: {author}\\n" .
165 "HgSubject: {desc|firstline}\\n'" .
166 " -r \$commit",
167 "blame_range_cmd" => "", # not supported
168 "blame_file_cmd" => "hg blame -n \$file",
169 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})",
170 "blame_commit_pattern" => "^([ 0-9a-f]+):",
171 "author_pattern" => "^HgAuthor: (.*)",
172 "subject_pattern" => "^HgSubject: (.*)",
173 "stat_pattern" => "^(\\d+)\t(\\d+)\t\$file\$",
174 "file_exists_cmd" => "hg files \$file",
175 "list_files_cmd" => "hg manifest -R \$file",
178 my $conf = which_conf(".get_maintainer.conf");
179 if (-f $conf) {
180 my @conf_args;
181 open(my $conffile, '<', "$conf")
182 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
184 while (<$conffile>) {
185 my $line = $_;
187 $line =~ s/\s*\n?$//g;
188 $line =~ s/^\s*//g;
189 $line =~ s/\s+/ /g;
191 next if ($line =~ m/^\s*#/);
192 next if ($line =~ m/^\s*$/);
194 my @words = split(" ", $line);
195 foreach my $word (@words) {
196 last if ($word =~ m/^#/);
197 push (@conf_args, $word);
200 close($conffile);
201 unshift(@ARGV, @conf_args) if @conf_args;
204 my @ignore_emails = ();
205 my $ignore_file = which_conf(".get_maintainer.ignore");
206 if (-f $ignore_file) {
207 open(my $ignore, '<', "$ignore_file")
208 or warn "$P: Can't find a readable .get_maintainer.ignore file $!\n";
209 while (<$ignore>) {
210 my $line = $_;
212 $line =~ s/\s*\n?$//;
213 $line =~ s/^\s*//;
214 $line =~ s/\s+$//;
215 $line =~ s/#.*$//;
217 next if ($line =~ m/^\s*$/);
218 if (rfc822_valid($line)) {
219 push(@ignore_emails, $line);
222 close($ignore);
225 if ($#ARGV > 0) {
226 foreach (@ARGV) {
227 if ($_ =~ /^-{1,2}self-test(?:=|$)/) {
228 die "$P: using --self-test does not allow any other option or argument\n";
233 if (!GetOptions(
234 'email!' => \$email,
235 'git!' => \$email_git,
236 'git-all-signature-types!' => \$email_git_all_signature_types,
237 'git-blame!' => \$email_git_blame,
238 'git-blame-signatures!' => \$email_git_blame_signatures,
239 'git-fallback!' => \$email_git_fallback,
240 'git-chief-penguins!' => \$email_git_penguin_chiefs,
241 'git-min-signatures=i' => \$email_git_min_signatures,
242 'git-max-maintainers=i' => \$email_git_max_maintainers,
243 'git-min-percent=i' => \$email_git_min_percent,
244 'git-since=s' => \$email_git_since,
245 'hg-since=s' => \$email_hg_since,
246 'i|interactive!' => \$interactive,
247 'remove-duplicates!' => \$email_remove_duplicates,
248 'mailmap!' => \$email_use_mailmap,
249 'm!' => \$email_maintainer,
250 'r!' => \$email_reviewer,
251 'n!' => \$email_usename,
252 'l!' => \$email_list,
253 'fixes!' => \$email_fixes,
254 'moderated!' => \$email_moderated_list,
255 's!' => \$email_subscriber_list,
256 'multiline!' => \$output_multiline,
257 'roles!' => \$output_roles,
258 'rolestats!' => \$output_rolestats,
259 'separator=s' => \$output_separator,
260 'subsystem!' => \$subsystem,
261 'status!' => \$status,
262 'scm!' => \$scm,
263 'tree!' => \$tree,
264 'web!' => \$web,
265 'letters=s' => \$letters,
266 'pattern-depth=i' => \$pattern_depth,
267 'k|keywords!' => \$keywords,
268 'sections!' => \$sections,
269 'fe|file-emails!' => \$file_emails,
270 'f|file' => \$from_filename,
271 'find-maintainer-files' => \$find_maintainer_files,
272 'mpath|maintainer-path=s' => \$maintainer_path,
273 'self-test:s' => \$self_test,
274 'v|version' => \$version,
275 'h|help|usage' => \$help,
276 )) {
277 die "$P: invalid argument - use --help if necessary\n";
280 if ($help != 0) {
281 usage();
282 exit 0;
285 if ($version != 0) {
286 print("${P} ${V}\n");
287 exit 0;
290 if (defined $self_test) {
291 read_all_maintainer_files();
292 self_test();
293 exit 0;
296 if (-t STDIN && !@ARGV) {
297 # We're talking to a terminal, but have no command line arguments.
298 die "$P: missing patchfile or -f file - use --help if necessary\n";
301 $output_multiline = 0 if ($output_separator ne ", ");
302 $output_rolestats = 1 if ($interactive);
303 $output_roles = 1 if ($output_rolestats);
305 if ($sections || $letters ne "") {
306 $sections = 1;
307 $email = 0;
308 $email_list = 0;
309 $scm = 0;
310 $status = 0;
311 $subsystem = 0;
312 $web = 0;
313 $keywords = 0;
314 $interactive = 0;
315 } else {
316 my $selections = $email + $scm + $status + $subsystem + $web;
317 if ($selections == 0) {
318 die "$P: Missing required option: email, scm, status, subsystem or web\n";
322 if ($email &&
323 ($email_maintainer + $email_reviewer +
324 $email_list + $email_subscriber_list +
325 $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
326 die "$P: Please select at least 1 email option\n";
329 if ($tree && !top_of_kernel_tree($lk_path)) {
330 die "$P: The current directory does not appear to be "
331 . "a linux kernel source tree.\n";
334 ## Read MAINTAINERS for type/value pairs
336 my @typevalue = ();
337 my %keyword_hash;
338 my @mfiles = ();
339 my @self_test_info = ();
341 sub read_maintainer_file {
342 my ($file) = @_;
344 open (my $maint, '<', "$file")
345 or die "$P: Can't open MAINTAINERS file '$file': $!\n";
346 my $i = 1;
347 while (<$maint>) {
348 my $line = $_;
349 chomp $line;
351 if ($line =~ m/^([A-Z]):\s*(.*)/) {
352 my $type = $1;
353 my $value = $2;
355 ##Filename pattern matching
356 if ($type eq "F" || $type eq "X") {
357 $value =~ s@\.@\\\.@g; ##Convert . to \.
358 $value =~ s/\*/\.\*/g; ##Convert * to .*
359 $value =~ s/\?/\./g; ##Convert ? to .
360 ##if pattern is a directory and it lacks a trailing slash, add one
361 if ((-d $value)) {
362 $value =~ s@([^/])$@$1/@;
364 } elsif ($type eq "K") {
365 $keyword_hash{@typevalue} = $value;
367 push(@typevalue, "$type:$value");
368 } elsif (!(/^\s*$/ || /^\s*\#/)) {
369 push(@typevalue, $line);
371 if (defined $self_test) {
372 push(@self_test_info, {file=>$file, linenr=>$i, line=>$line});
374 $i++;
376 close($maint);
379 sub find_is_maintainer_file {
380 my ($file) = $_;
381 return if ($file !~ m@/MAINTAINERS$@);
382 $file = $File::Find::name;
383 return if (! -f $file);
384 push(@mfiles, $file);
387 sub find_ignore_git {
388 return grep { $_ !~ /^\.git$/; } @_;
391 read_all_maintainer_files();
393 sub read_all_maintainer_files {
394 my $path = "${lk_path}MAINTAINERS";
395 if (defined $maintainer_path) {
396 $path = $maintainer_path;
397 # Perl Cookbook tilde expansion if necessary
398 $path =~ s@^~([^/]*)@ $1 ? (getpwnam($1))[7] : ( $ENV{HOME} || $ENV{LOGDIR} || (getpwuid($<))[7])@ex;
401 if (-d $path) {
402 $path .= '/' if ($path !~ m@/$@);
403 if ($find_maintainer_files) {
404 find( { wanted => \&find_is_maintainer_file,
405 preprocess => \&find_ignore_git,
406 no_chdir => 1,
407 }, "$path");
408 } else {
409 opendir(DIR, "$path") or die $!;
410 my @files = readdir(DIR);
411 closedir(DIR);
412 foreach my $file (@files) {
413 push(@mfiles, "$path$file") if ($file !~ /^\./);
416 } elsif (-f "$path") {
417 push(@mfiles, "$path");
418 } else {
419 die "$P: MAINTAINER file not found '$path'\n";
421 die "$P: No MAINTAINER files found in '$path'\n" if (scalar(@mfiles) == 0);
422 foreach my $file (@mfiles) {
423 read_maintainer_file("$file");
428 # Read mail address map
431 my $mailmap;
433 read_mailmap();
435 sub read_mailmap {
436 $mailmap = {
437 names => {},
438 addresses => {}
441 return if (!$email_use_mailmap || !(-f "${lk_path}.mailmap"));
443 open(my $mailmap_file, '<', "${lk_path}.mailmap")
444 or warn "$P: Can't open .mailmap: $!\n";
446 while (<$mailmap_file>) {
447 s/#.*$//; #strip comments
448 s/^\s+|\s+$//g; #trim
450 next if (/^\s*$/); #skip empty lines
451 #entries have one of the following formats:
452 # name1 <mail1>
453 # <mail1> <mail2>
454 # name1 <mail1> <mail2>
455 # name1 <mail1> name2 <mail2>
456 # (see man git-shortlog)
458 if (/^([^<]+)<([^>]+)>$/) {
459 my $real_name = $1;
460 my $address = $2;
462 $real_name =~ s/\s+$//;
463 ($real_name, $address) = parse_email("$real_name <$address>");
464 $mailmap->{names}->{$address} = $real_name;
466 } elsif (/^<([^>]+)>\s*<([^>]+)>$/) {
467 my $real_address = $1;
468 my $wrong_address = $2;
470 $mailmap->{addresses}->{$wrong_address} = $real_address;
472 } elsif (/^(.+)<([^>]+)>\s*<([^>]+)>$/) {
473 my $real_name = $1;
474 my $real_address = $2;
475 my $wrong_address = $3;
477 $real_name =~ s/\s+$//;
478 ($real_name, $real_address) =
479 parse_email("$real_name <$real_address>");
480 $mailmap->{names}->{$wrong_address} = $real_name;
481 $mailmap->{addresses}->{$wrong_address} = $real_address;
483 } elsif (/^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$/) {
484 my $real_name = $1;
485 my $real_address = $2;
486 my $wrong_name = $3;
487 my $wrong_address = $4;
489 $real_name =~ s/\s+$//;
490 ($real_name, $real_address) =
491 parse_email("$real_name <$real_address>");
493 $wrong_name =~ s/\s+$//;
494 ($wrong_name, $wrong_address) =
495 parse_email("$wrong_name <$wrong_address>");
497 my $wrong_email = format_email($wrong_name, $wrong_address, 1);
498 $mailmap->{names}->{$wrong_email} = $real_name;
499 $mailmap->{addresses}->{$wrong_email} = $real_address;
502 close($mailmap_file);
505 ## use the filenames on the command line or find the filenames in the patchfiles
507 my @files = ();
508 my @fixes = (); # If a patch description includes Fixes: lines
509 my @range = ();
510 my @keyword_tvi = ();
511 my @file_emails = ();
513 if (!@ARGV) {
514 push(@ARGV, "&STDIN");
517 foreach my $file (@ARGV) {
518 if ($file ne "&STDIN") {
519 ##if $file is a directory and it lacks a trailing slash, add one
520 if ((-d $file)) {
521 $file =~ s@([^/])$@$1/@;
522 } elsif (!(-f $file)) {
523 die "$P: file '${file}' not found\n";
526 if ($from_filename || ($file ne "&STDIN" && vcs_file_exists($file))) {
527 $file =~ s/^\Q${cur_path}\E//; #strip any absolute path
528 $file =~ s/^\Q${lk_path}\E//; #or the path to the lk tree
529 push(@files, $file);
530 if ($file ne "MAINTAINERS" && -f $file && ($keywords || $file_emails)) {
531 open(my $f, '<', $file)
532 or die "$P: Can't open $file: $!\n";
533 my $text = do { local($/) ; <$f> };
534 close($f);
535 if ($keywords) {
536 foreach my $line (keys %keyword_hash) {
537 if ($text =~ m/$keyword_hash{$line}/x) {
538 push(@keyword_tvi, $line);
542 if ($file_emails) {
543 my @poss_addr = $text =~ m$[A-Za--ÿ\"\' \,\.\+-]*\s*[\,]*\s*[\(\<\{]{0,1}[A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+\.[A-Za-z0-9]+[\)\>\}]{0,1}$g;
544 push(@file_emails, clean_file_emails(@poss_addr));
547 } else {
548 my $file_cnt = @files;
549 my $lastfile;
551 open(my $patch, "< $file")
552 or die "$P: Can't open $file: $!\n";
554 # We can check arbitrary information before the patch
555 # like the commit message, mail headers, etc...
556 # This allows us to match arbitrary keywords against any part
557 # of a git format-patch generated file (subject tags, etc...)
559 my $patch_prefix = ""; #Parsing the intro
561 while (<$patch>) {
562 my $patch_line = $_;
563 if (m/^ mode change [0-7]+ => [0-7]+ (\S+)\s*$/) {
564 my $filename = $1;
565 push(@files, $filename);
566 } elsif (m/^rename (?:from|to) (\S+)\s*$/) {
567 my $filename = $1;
568 push(@files, $filename);
569 } elsif (m/^diff --git a\/(\S+) b\/(\S+)\s*$/) {
570 my $filename1 = $1;
571 my $filename2 = $2;
572 push(@files, $filename1);
573 push(@files, $filename2);
574 } elsif (m/^Fixes:\s+([0-9a-fA-F]{6,40})/) {
575 push(@fixes, $1) if ($email_fixes);
576 } elsif (m/^\+\+\+\s+(\S+)/ or m/^---\s+(\S+)/) {
577 my $filename = $1;
578 $filename =~ s@^[^/]*/@@;
579 $filename =~ s@\n@@;
580 $lastfile = $filename;
581 push(@files, $filename);
582 $patch_prefix = "^[+-].*"; #Now parsing the actual patch
583 } elsif (m/^\@\@ -(\d+),(\d+)/) {
584 if ($email_git_blame) {
585 push(@range, "$lastfile:$1:$2");
587 } elsif ($keywords) {
588 foreach my $line (keys %keyword_hash) {
589 if ($patch_line =~ m/${patch_prefix}$keyword_hash{$line}/x) {
590 push(@keyword_tvi, $line);
595 close($patch);
597 if ($file_cnt == @files) {
598 warn "$P: file '${file}' doesn't appear to be a patch. "
599 . "Add -f to options?\n";
601 @files = sort_and_uniq(@files);
605 @file_emails = uniq(@file_emails);
606 @fixes = uniq(@fixes);
608 my %email_hash_name;
609 my %email_hash_address;
610 my @email_to = ();
611 my %hash_list_to;
612 my @list_to = ();
613 my @scm = ();
614 my @web = ();
615 my @subsystem = ();
616 my @status = ();
617 my %deduplicate_name_hash = ();
618 my %deduplicate_address_hash = ();
620 my @maintainers = get_maintainers();
621 if (@maintainers) {
622 @maintainers = merge_email(@maintainers);
623 output(@maintainers);
626 if ($scm) {
627 @scm = uniq(@scm);
628 output(@scm);
631 if ($status) {
632 @status = uniq(@status);
633 output(@status);
636 if ($subsystem) {
637 @subsystem = uniq(@subsystem);
638 output(@subsystem);
641 if ($web) {
642 @web = uniq(@web);
643 output(@web);
646 exit($exit);
648 sub self_test {
649 my @lsfiles = ();
650 my @good_links = ();
651 my @bad_links = ();
652 my @section_headers = ();
653 my $index = 0;
655 @lsfiles = vcs_list_files($lk_path);
657 for my $x (@self_test_info) {
658 $index++;
660 ## Section header duplication and missing section content
661 if (($self_test eq "" || $self_test =~ /\bsections\b/) &&
662 $x->{line} =~ /^\S[^:]/ &&
663 defined $self_test_info[$index] &&
664 $self_test_info[$index]->{line} =~ /^([A-Z]):\s*\S/) {
665 my $has_S = 0;
666 my $has_F = 0;
667 my $has_ML = 0;
668 my $status = "";
669 if (grep(m@^\Q$x->{line}\E@, @section_headers)) {
670 print("$x->{file}:$x->{linenr}: warning: duplicate section header\t$x->{line}\n");
671 } else {
672 push(@section_headers, $x->{line});
674 my $nextline = $index;
675 while (defined $self_test_info[$nextline] &&
676 $self_test_info[$nextline]->{line} =~ /^([A-Z]):\s*(\S.*)/) {
677 my $type = $1;
678 my $value = $2;
679 if ($type eq "S") {
680 $has_S = 1;
681 $status = $value;
682 } elsif ($type eq "F" || $type eq "N") {
683 $has_F = 1;
684 } elsif ($type eq "M" || $type eq "R" || $type eq "L") {
685 $has_ML = 1;
687 $nextline++;
689 if (!$has_ML && $status !~ /orphan|obsolete/i) {
690 print("$x->{file}:$x->{linenr}: warning: section without email address\t$x->{line}\n");
692 if (!$has_S) {
693 print("$x->{file}:$x->{linenr}: warning: section without status \t$x->{line}\n");
695 if (!$has_F) {
696 print("$x->{file}:$x->{linenr}: warning: section without file pattern\t$x->{line}\n");
700 next if ($x->{line} !~ /^([A-Z]):\s*(.*)/);
702 my $type = $1;
703 my $value = $2;
705 ## Filename pattern matching
706 if (($type eq "F" || $type eq "X") &&
707 ($self_test eq "" || $self_test =~ /\bpatterns\b/)) {
708 $value =~ s@\.@\\\.@g; ##Convert . to \.
709 $value =~ s/\*/\.\*/g; ##Convert * to .*
710 $value =~ s/\?/\./g; ##Convert ? to .
711 ##if pattern is a directory and it lacks a trailing slash, add one
712 if ((-d $value)) {
713 $value =~ s@([^/])$@$1/@;
715 if (!grep(m@^$value@, @lsfiles)) {
716 print("$x->{file}:$x->{linenr}: warning: no file matches\t$x->{line}\n");
719 ## Link reachability
720 } elsif (($type eq "W" || $type eq "Q" || $type eq "B") &&
721 $value =~ /^https?:/ &&
722 ($self_test eq "" || $self_test =~ /\blinks\b/)) {
723 next if (grep(m@^\Q$value\E$@, @good_links));
724 my $isbad = 0;
725 if (grep(m@^\Q$value\E$@, @bad_links)) {
726 $isbad = 1;
727 } else {
728 my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $value`;
729 if ($? == 0) {
730 push(@good_links, $value);
731 } else {
732 push(@bad_links, $value);
733 $isbad = 1;
736 if ($isbad) {
737 print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
740 ## SCM reachability
741 } elsif ($type eq "T" &&
742 ($self_test eq "" || $self_test =~ /\bscm\b/)) {
743 next if (grep(m@^\Q$value\E$@, @good_links));
744 my $isbad = 0;
745 if (grep(m@^\Q$value\E$@, @bad_links)) {
746 $isbad = 1;
747 } elsif ($value !~ /^(?:git|quilt|hg)\s+\S/) {
748 print("$x->{file}:$x->{linenr}: warning: malformed entry\t$x->{line}\n");
749 } elsif ($value =~ /^git\s+(\S+)(\s+([^\(]+\S+))?/) {
750 my $url = $1;
751 my $branch = "";
752 $branch = $3 if $3;
753 my $output = `git ls-remote --exit-code -h "$url" $branch > /dev/null 2>&1`;
754 if ($? == 0) {
755 push(@good_links, $value);
756 } else {
757 push(@bad_links, $value);
758 $isbad = 1;
760 } elsif ($value =~ /^(?:quilt|hg)\s+(https?:\S+)/) {
761 my $url = $1;
762 my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $url`;
763 if ($? == 0) {
764 push(@good_links, $value);
765 } else {
766 push(@bad_links, $value);
767 $isbad = 1;
770 if ($isbad) {
771 print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
777 sub ignore_email_address {
778 my ($address) = @_;
780 foreach my $ignore (@ignore_emails) {
781 return 1 if ($ignore eq $address);
784 return 0;
787 sub range_is_maintained {
788 my ($start, $end) = @_;
790 for (my $i = $start; $i < $end; $i++) {
791 my $line = $typevalue[$i];
792 if ($line =~ m/^([A-Z]):\s*(.*)/) {
793 my $type = $1;
794 my $value = $2;
795 if ($type eq 'S') {
796 if ($value =~ /(maintain|support)/i) {
797 return 1;
802 return 0;
805 sub range_has_maintainer {
806 my ($start, $end) = @_;
808 for (my $i = $start; $i < $end; $i++) {
809 my $line = $typevalue[$i];
810 if ($line =~ m/^([A-Z]):\s*(.*)/) {
811 my $type = $1;
812 my $value = $2;
813 if ($type eq 'M') {
814 return 1;
818 return 0;
821 sub get_maintainers {
822 %email_hash_name = ();
823 %email_hash_address = ();
824 %commit_author_hash = ();
825 %commit_signer_hash = ();
826 @email_to = ();
827 %hash_list_to = ();
828 @list_to = ();
829 @scm = ();
830 @web = ();
831 @subsystem = ();
832 @status = ();
833 %deduplicate_name_hash = ();
834 %deduplicate_address_hash = ();
835 if ($email_git_all_signature_types) {
836 $signature_pattern = "(.+?)[Bb][Yy]:";
837 } else {
838 $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
841 # Find responsible parties
843 my %exact_pattern_match_hash = ();
845 foreach my $file (@files) {
847 my %hash;
848 my $tvi = find_first_section();
849 while ($tvi < @typevalue) {
850 my $start = find_starting_index($tvi);
851 my $end = find_ending_index($tvi);
852 my $exclude = 0;
853 my $i;
855 #Do not match excluded file patterns
857 for ($i = $start; $i < $end; $i++) {
858 my $line = $typevalue[$i];
859 if ($line =~ m/^([A-Z]):\s*(.*)/) {
860 my $type = $1;
861 my $value = $2;
862 if ($type eq 'X') {
863 if (file_match_pattern($file, $value)) {
864 $exclude = 1;
865 last;
871 if (!$exclude) {
872 for ($i = $start; $i < $end; $i++) {
873 my $line = $typevalue[$i];
874 if ($line =~ m/^([A-Z]):\s*(.*)/) {
875 my $type = $1;
876 my $value = $2;
877 if ($type eq 'F') {
878 if (file_match_pattern($file, $value)) {
879 my $value_pd = ($value =~ tr@/@@);
880 my $file_pd = ($file =~ tr@/@@);
881 $value_pd++ if (substr($value,-1,1) ne "/");
882 $value_pd = -1 if ($value =~ /^\.\*/);
883 if ($value_pd >= $file_pd &&
884 range_is_maintained($start, $end) &&
885 range_has_maintainer($start, $end)) {
886 $exact_pattern_match_hash{$file} = 1;
888 if ($pattern_depth == 0 ||
889 (($file_pd - $value_pd) < $pattern_depth)) {
890 $hash{$tvi} = $value_pd;
893 } elsif ($type eq 'N') {
894 if ($file =~ m/$value/x) {
895 $hash{$tvi} = 0;
901 $tvi = $end + 1;
904 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
905 add_categories($line);
906 if ($sections) {
907 my $i;
908 my $start = find_starting_index($line);
909 my $end = find_ending_index($line);
910 for ($i = $start; $i < $end; $i++) {
911 my $line = $typevalue[$i];
912 if ($line =~ /^[FX]:/) { ##Restore file patterns
913 $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
914 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ?
915 $line =~ s/\\\./\./g; ##Convert \. to .
916 $line =~ s/\.\*/\*/g; ##Convert .* to *
918 my $count = $line =~ s/^([A-Z]):/$1:\t/g;
919 if ($letters eq "" || (!$count || $letters =~ /$1/i)) {
920 print("$line\n");
923 print("\n");
928 if ($keywords) {
929 @keyword_tvi = sort_and_uniq(@keyword_tvi);
930 foreach my $line (@keyword_tvi) {
931 add_categories($line);
935 foreach my $fix (@fixes) {
936 vcs_add_commit_signers($fix, "blamed_fixes");
939 foreach my $email (@email_to, @list_to) {
940 $email->[0] = deduplicate_email($email->[0]);
943 foreach my $file (@files) {
944 if ($email &&
945 ($email_git || ($email_git_fallback &&
946 !$exact_pattern_match_hash{$file}))) {
947 vcs_file_signoffs($file);
949 if ($email && $email_git_blame) {
950 vcs_file_blame($file);
954 if ($email) {
955 foreach my $chief (@penguin_chief) {
956 if ($chief =~ m/^(.*):(.*)/) {
957 my $email_address;
959 $email_address = format_email($1, $2, $email_usename);
960 if ($email_git_penguin_chiefs) {
961 push(@email_to, [$email_address, 'chief penguin']);
962 } else {
963 @email_to = grep($_->[0] !~ /${email_address}/, @email_to);
968 foreach my $email (@file_emails) {
969 my ($name, $address) = parse_email($email);
971 my $tmp_email = format_email($name, $address, $email_usename);
972 push_email_address($tmp_email, '');
973 add_role($tmp_email, 'in file');
977 my @to = ();
978 if ($email || $email_list) {
979 if ($email) {
980 @to = (@to, @email_to);
982 if ($email_list) {
983 @to = (@to, @list_to);
987 if ($interactive) {
988 @to = interactive_get_maintainers(\@to);
991 return @to;
994 sub file_match_pattern {
995 my ($file, $pattern) = @_;
996 if (substr($pattern, -1) eq "/") {
997 if ($file =~ m@^$pattern@) {
998 return 1;
1000 } else {
1001 if ($file =~ m@^$pattern@) {
1002 my $s1 = ($file =~ tr@/@@);
1003 my $s2 = ($pattern =~ tr@/@@);
1004 if ($s1 == $s2) {
1005 return 1;
1009 return 0;
1012 sub usage {
1013 print <<EOT;
1014 usage: $P [options] patchfile
1015 $P [options] -f file|directory
1016 version: $V
1018 MAINTAINER field selection options:
1019 --email => print email address(es) if any
1020 --git => include recent git \*-by: signers
1021 --git-all-signature-types => include signers regardless of signature type
1022 or use only ${signature_pattern} signers (default: $email_git_all_signature_types)
1023 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
1024 --git-chief-penguins => include ${penguin_chiefs}
1025 --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
1026 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
1027 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
1028 --git-blame => use git blame to find modified commits for patch or file
1029 --git-blame-signatures => when used with --git-blame, also include all commit signers
1030 --git-since => git history to use (default: $email_git_since)
1031 --hg-since => hg history to use (default: $email_hg_since)
1032 --interactive => display a menu (mostly useful if used with the --git option)
1033 --m => include maintainer(s) if any
1034 --r => include reviewer(s) if any
1035 --n => include name 'Full Name <addr\@domain.tld>'
1036 --l => include list(s) if any
1037 --moderated => include moderated lists(s) if any (default: true)
1038 --s => include subscriber only list(s) if any (default: false)
1039 --remove-duplicates => minimize duplicate email names/addresses
1040 --roles => show roles (status:subsystem, git-signer, list, etc...)
1041 --rolestats => show roles and statistics (commits/total_commits, %)
1042 --file-emails => add email addresses found in -f file (default: 0 (off))
1043 --fixes => for patches, add signatures of commits with 'Fixes: <commit>' (default: 1 (on))
1044 --scm => print SCM tree(s) if any
1045 --status => print status if any
1046 --subsystem => print subsystem name if any
1047 --web => print website(s) if any
1049 Output type options:
1050 --separator [, ] => separator for multiple entries on 1 line
1051 using --separator also sets --nomultiline if --separator is not [, ]
1052 --multiline => print 1 entry per line
1054 Other options:
1055 --pattern-depth => Number of pattern directory traversals (default: 0 (all))
1056 --keywords => scan patch for keywords (default: $keywords)
1057 --sections => print all of the subsystem sections with pattern matches
1058 --letters => print all matching 'letter' types from all matching sections
1059 --mailmap => use .mailmap file (default: $email_use_mailmap)
1060 --no-tree => run without a kernel tree
1061 --self-test => show potential issues with MAINTAINERS file content
1062 --version => show version
1063 --help => show this help information
1065 Default options:
1066 [--email --tree --nogit --git-fallback --m --r --n --l --multiline
1067 --pattern-depth=0 --remove-duplicates --rolestats]
1069 Notes:
1070 Using "-f directory" may give unexpected results:
1071 Used with "--git", git signators for _all_ files in and below
1072 directory are examined as git recurses directories.
1073 Any specified X: (exclude) pattern matches are _not_ ignored.
1074 Used with "--nogit", directory is used as a pattern match,
1075 no individual file within the directory or subdirectory
1076 is matched.
1077 Used with "--git-blame", does not iterate all files in directory
1078 Using "--git-blame" is slow and may add old committers and authors
1079 that are no longer active maintainers to the output.
1080 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
1081 other automated tools that expect only ["name"] <email address>
1082 may not work because of additional output after <email address>.
1083 Using "--rolestats" and "--git-blame" shows the #/total=% commits,
1084 not the percentage of the entire file authored. # of commits is
1085 not a good measure of amount of code authored. 1 major commit may
1086 contain a thousand lines, 5 trivial commits may modify a single line.
1087 If git is not installed, but mercurial (hg) is installed and an .hg
1088 repository exists, the following options apply to mercurial:
1089 --git,
1090 --git-min-signatures, --git-max-maintainers, --git-min-percent, and
1091 --git-blame
1092 Use --hg-since not --git-since to control date selection
1093 File ".get_maintainer.conf", if it exists in the linux kernel source root
1094 directory, can change whatever get_maintainer defaults are desired.
1095 Entries in this file can be any command line argument.
1096 This file is prepended to any additional command line arguments.
1097 Multiple lines and # comments are allowed.
1098 Most options have both positive and negative forms.
1099 The negative forms for --<foo> are --no<foo> and --no-<foo>.
1104 sub top_of_kernel_tree {
1105 my ($lk_path) = @_;
1107 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
1108 $lk_path .= "/";
1110 if ( (-f "${lk_path}COPYING")
1111 && (-f "${lk_path}CREDITS")
1112 && (-f "${lk_path}Kbuild")
1113 && (-e "${lk_path}MAINTAINERS")
1114 && (-f "${lk_path}Makefile")
1115 && (-f "${lk_path}README")
1116 && (-d "${lk_path}Documentation")
1117 && (-d "${lk_path}arch")
1118 && (-d "${lk_path}include")
1119 && (-d "${lk_path}drivers")
1120 && (-d "${lk_path}fs")
1121 && (-d "${lk_path}init")
1122 && (-d "${lk_path}ipc")
1123 && (-d "${lk_path}kernel")
1124 && (-d "${lk_path}lib")
1125 && (-d "${lk_path}scripts")) {
1126 return 1;
1128 return 0;
1131 sub parse_email {
1132 my ($formatted_email) = @_;
1134 my $name = "";
1135 my $address = "";
1137 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
1138 $name = $1;
1139 $address = $2;
1140 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
1141 $address = $1;
1142 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
1143 $address = $1;
1146 $name =~ s/^\s+|\s+$//g;
1147 $name =~ s/^\"|\"$//g;
1148 $address =~ s/^\s+|\s+$//g;
1150 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
1151 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
1152 $name = "\"$name\"";
1155 return ($name, $address);
1158 sub format_email {
1159 my ($name, $address, $usename) = @_;
1161 my $formatted_email;
1163 $name =~ s/^\s+|\s+$//g;
1164 $name =~ s/^\"|\"$//g;
1165 $address =~ s/^\s+|\s+$//g;
1167 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
1168 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
1169 $name = "\"$name\"";
1172 if ($usename) {
1173 if ("$name" eq "") {
1174 $formatted_email = "$address";
1175 } else {
1176 $formatted_email = "$name <$address>";
1178 } else {
1179 $formatted_email = $address;
1182 return $formatted_email;
1185 sub find_first_section {
1186 my $index = 0;
1188 while ($index < @typevalue) {
1189 my $tv = $typevalue[$index];
1190 if (($tv =~ m/^([A-Z]):\s*(.*)/)) {
1191 last;
1193 $index++;
1196 return $index;
1199 sub find_starting_index {
1200 my ($index) = @_;
1202 while ($index > 0) {
1203 my $tv = $typevalue[$index];
1204 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
1205 last;
1207 $index--;
1210 return $index;
1213 sub find_ending_index {
1214 my ($index) = @_;
1216 while ($index < @typevalue) {
1217 my $tv = $typevalue[$index];
1218 if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
1219 last;
1221 $index++;
1224 return $index;
1227 sub get_subsystem_name {
1228 my ($index) = @_;
1230 my $start = find_starting_index($index);
1232 my $subsystem = $typevalue[$start];
1233 if ($output_section_maxlen && length($subsystem) > $output_section_maxlen) {
1234 $subsystem = substr($subsystem, 0, $output_section_maxlen - 3);
1235 $subsystem =~ s/\s*$//;
1236 $subsystem = $subsystem . "...";
1238 return $subsystem;
1241 sub get_maintainer_role {
1242 my ($index) = @_;
1244 my $i;
1245 my $start = find_starting_index($index);
1246 my $end = find_ending_index($index);
1248 my $role = "unknown";
1249 my $subsystem = get_subsystem_name($index);
1251 for ($i = $start + 1; $i < $end; $i++) {
1252 my $tv = $typevalue[$i];
1253 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1254 my $ptype = $1;
1255 my $pvalue = $2;
1256 if ($ptype eq "S") {
1257 $role = $pvalue;
1262 $role = lc($role);
1263 if ($role eq "supported") {
1264 $role = "supporter";
1265 } elsif ($role eq "maintained") {
1266 $role = "maintainer";
1267 } elsif ($role eq "odd fixes") {
1268 $role = "odd fixer";
1269 } elsif ($role eq "orphan") {
1270 $role = "orphan minder";
1271 } elsif ($role eq "obsolete") {
1272 $role = "obsolete minder";
1273 } elsif ($role eq "buried alive in reporters") {
1274 $role = "chief penguin";
1277 return $role . ":" . $subsystem;
1280 sub get_list_role {
1281 my ($index) = @_;
1283 my $subsystem = get_subsystem_name($index);
1285 if ($subsystem eq "THE REST") {
1286 $subsystem = "";
1289 return $subsystem;
1292 sub add_categories {
1293 my ($index) = @_;
1295 my $i;
1296 my $start = find_starting_index($index);
1297 my $end = find_ending_index($index);
1299 push(@subsystem, $typevalue[$start]);
1301 for ($i = $start + 1; $i < $end; $i++) {
1302 my $tv = $typevalue[$i];
1303 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1304 my $ptype = $1;
1305 my $pvalue = $2;
1306 if ($ptype eq "L") {
1307 my $list_address = $pvalue;
1308 my $list_additional = "";
1309 my $list_role = get_list_role($i);
1311 if ($list_role ne "") {
1312 $list_role = ":" . $list_role;
1314 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
1315 $list_address = $1;
1316 $list_additional = $2;
1318 if ($list_additional =~ m/subscribers-only/) {
1319 if ($email_subscriber_list) {
1320 if (!$hash_list_to{lc($list_address)}) {
1321 $hash_list_to{lc($list_address)} = 1;
1322 push(@list_to, [$list_address,
1323 "subscriber list${list_role}"]);
1326 } else {
1327 if ($email_list) {
1328 if (!$hash_list_to{lc($list_address)}) {
1329 if ($list_additional =~ m/moderated/) {
1330 if ($email_moderated_list) {
1331 $hash_list_to{lc($list_address)} = 1;
1332 push(@list_to, [$list_address,
1333 "moderated list${list_role}"]);
1335 } else {
1336 $hash_list_to{lc($list_address)} = 1;
1337 push(@list_to, [$list_address,
1338 "open list${list_role}"]);
1343 } elsif ($ptype eq "M") {
1344 my ($name, $address) = parse_email($pvalue);
1345 if ($name eq "") {
1346 if ($i > 0) {
1347 my $tv = $typevalue[$i - 1];
1348 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1349 if ($1 eq "P") {
1350 $name = $2;
1351 $pvalue = format_email($name, $address, $email_usename);
1356 if ($email_maintainer) {
1357 my $role = get_maintainer_role($i);
1358 push_email_addresses($pvalue, $role);
1360 } elsif ($ptype eq "R") {
1361 my ($name, $address) = parse_email($pvalue);
1362 if ($name eq "") {
1363 if ($i > 0) {
1364 my $tv = $typevalue[$i - 1];
1365 if ($tv =~ m/^([A-Z]):\s*(.*)/) {
1366 if ($1 eq "P") {
1367 $name = $2;
1368 $pvalue = format_email($name, $address, $email_usename);
1373 if ($email_reviewer) {
1374 my $subsystem = get_subsystem_name($i);
1375 push_email_addresses($pvalue, "reviewer:$subsystem");
1377 } elsif ($ptype eq "T") {
1378 push(@scm, $pvalue);
1379 } elsif ($ptype eq "W") {
1380 push(@web, $pvalue);
1381 } elsif ($ptype eq "S") {
1382 push(@status, $pvalue);
1388 sub email_inuse {
1389 my ($name, $address) = @_;
1391 return 1 if (($name eq "") && ($address eq ""));
1392 return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
1393 return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
1395 return 0;
1398 sub push_email_address {
1399 my ($line, $role) = @_;
1401 my ($name, $address) = parse_email($line);
1403 if ($address eq "") {
1404 return 0;
1407 if (!$email_remove_duplicates) {
1408 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1409 } elsif (!email_inuse($name, $address)) {
1410 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1411 $email_hash_name{lc($name)}++ if ($name ne "");
1412 $email_hash_address{lc($address)}++;
1415 return 1;
1418 sub push_email_addresses {
1419 my ($address, $role) = @_;
1421 my @address_list = ();
1423 if (rfc822_valid($address)) {
1424 push_email_address($address, $role);
1425 } elsif (@address_list = rfc822_validlist($address)) {
1426 my $array_count = shift(@address_list);
1427 while (my $entry = shift(@address_list)) {
1428 push_email_address($entry, $role);
1430 } else {
1431 if (!push_email_address($address, $role)) {
1432 warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1437 sub add_role {
1438 my ($line, $role) = @_;
1440 my ($name, $address) = parse_email($line);
1441 my $email = format_email($name, $address, $email_usename);
1443 foreach my $entry (@email_to) {
1444 if ($email_remove_duplicates) {
1445 my ($entry_name, $entry_address) = parse_email($entry->[0]);
1446 if (($name eq $entry_name || $address eq $entry_address)
1447 && ($role eq "" || !($entry->[1] =~ m/$role/))
1449 if ($entry->[1] eq "") {
1450 $entry->[1] = "$role";
1451 } else {
1452 $entry->[1] = "$entry->[1],$role";
1455 } else {
1456 if ($email eq $entry->[0]
1457 && ($role eq "" || !($entry->[1] =~ m/$role/))
1459 if ($entry->[1] eq "") {
1460 $entry->[1] = "$role";
1461 } else {
1462 $entry->[1] = "$entry->[1],$role";
1469 sub which {
1470 my ($bin) = @_;
1472 foreach my $path (split(/:/, $ENV{PATH})) {
1473 if (-e "$path/$bin") {
1474 return "$path/$bin";
1478 return "";
1481 sub which_conf {
1482 my ($conf) = @_;
1484 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1485 if (-e "$path/$conf") {
1486 return "$path/$conf";
1490 return "";
1493 sub mailmap_email {
1494 my ($line) = @_;
1496 my ($name, $address) = parse_email($line);
1497 my $email = format_email($name, $address, 1);
1498 my $real_name = $name;
1499 my $real_address = $address;
1501 if (exists $mailmap->{names}->{$email} ||
1502 exists $mailmap->{addresses}->{$email}) {
1503 if (exists $mailmap->{names}->{$email}) {
1504 $real_name = $mailmap->{names}->{$email};
1506 if (exists $mailmap->{addresses}->{$email}) {
1507 $real_address = $mailmap->{addresses}->{$email};
1509 } else {
1510 if (exists $mailmap->{names}->{$address}) {
1511 $real_name = $mailmap->{names}->{$address};
1513 if (exists $mailmap->{addresses}->{$address}) {
1514 $real_address = $mailmap->{addresses}->{$address};
1517 return format_email($real_name, $real_address, 1);
1520 sub mailmap {
1521 my (@addresses) = @_;
1523 my @mapped_emails = ();
1524 foreach my $line (@addresses) {
1525 push(@mapped_emails, mailmap_email($line));
1527 merge_by_realname(@mapped_emails) if ($email_use_mailmap);
1528 return @mapped_emails;
1531 sub merge_by_realname {
1532 my %address_map;
1533 my (@emails) = @_;
1535 foreach my $email (@emails) {
1536 my ($name, $address) = parse_email($email);
1537 if (exists $address_map{$name}) {
1538 $address = $address_map{$name};
1539 $email = format_email($name, $address, 1);
1540 } else {
1541 $address_map{$name} = $address;
1546 sub git_execute_cmd {
1547 my ($cmd) = @_;
1548 my @lines = ();
1550 my $output = `$cmd`;
1551 $output =~ s/^\s*//gm;
1552 @lines = split("\n", $output);
1554 return @lines;
1557 sub hg_execute_cmd {
1558 my ($cmd) = @_;
1559 my @lines = ();
1561 my $output = `$cmd`;
1562 @lines = split("\n", $output);
1564 return @lines;
1567 sub extract_formatted_signatures {
1568 my (@signature_lines) = @_;
1570 my @type = @signature_lines;
1572 s/\s*(.*):.*/$1/ for (@type);
1574 # cut -f2- -d":"
1575 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1577 ## Reformat email addresses (with names) to avoid badly written signatures
1579 foreach my $signer (@signature_lines) {
1580 $signer = deduplicate_email($signer);
1583 return (\@type, \@signature_lines);
1586 sub vcs_find_signers {
1587 my ($cmd, $file) = @_;
1588 my $commits;
1589 my @lines = ();
1590 my @signatures = ();
1591 my @authors = ();
1592 my @stats = ();
1594 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1596 my $pattern = $VCS_cmds{"commit_pattern"};
1597 my $author_pattern = $VCS_cmds{"author_pattern"};
1598 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1600 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1602 $commits = grep(/$pattern/, @lines); # of commits
1604 @authors = grep(/$author_pattern/, @lines);
1605 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1606 @stats = grep(/$stat_pattern/, @lines);
1608 # print("stats: <@stats>\n");
1610 return (0, \@signatures, \@authors, \@stats) if !@signatures;
1612 save_commits_by_author(@lines) if ($interactive);
1613 save_commits_by_signer(@lines) if ($interactive);
1615 if (!$email_git_penguin_chiefs) {
1616 @signatures = grep(!/${penguin_chiefs}/i, @signatures);
1619 my ($author_ref, $authors_ref) = extract_formatted_signatures(@authors);
1620 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1622 return ($commits, $signers_ref, $authors_ref, \@stats);
1625 sub vcs_find_author {
1626 my ($cmd) = @_;
1627 my @lines = ();
1629 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1631 if (!$email_git_penguin_chiefs) {
1632 @lines = grep(!/${penguin_chiefs}/i, @lines);
1635 return @lines if !@lines;
1637 my @authors = ();
1638 foreach my $line (@lines) {
1639 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1640 my $author = $1;
1641 my ($name, $address) = parse_email($author);
1642 $author = format_email($name, $address, 1);
1643 push(@authors, $author);
1647 save_commits_by_author(@lines) if ($interactive);
1648 save_commits_by_signer(@lines) if ($interactive);
1650 return @authors;
1653 sub vcs_save_commits {
1654 my ($cmd) = @_;
1655 my @lines = ();
1656 my @commits = ();
1658 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1660 foreach my $line (@lines) {
1661 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1662 push(@commits, $1);
1666 return @commits;
1669 sub vcs_blame {
1670 my ($file) = @_;
1671 my $cmd;
1672 my @commits = ();
1674 return @commits if (!(-f $file));
1676 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1677 my @all_commits = ();
1679 $cmd = $VCS_cmds{"blame_file_cmd"};
1680 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1681 @all_commits = vcs_save_commits($cmd);
1683 foreach my $file_range_diff (@range) {
1684 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1685 my $diff_file = $1;
1686 my $diff_start = $2;
1687 my $diff_length = $3;
1688 next if ("$file" ne "$diff_file");
1689 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1690 push(@commits, $all_commits[$i]);
1693 } elsif (@range) {
1694 foreach my $file_range_diff (@range) {
1695 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1696 my $diff_file = $1;
1697 my $diff_start = $2;
1698 my $diff_length = $3;
1699 next if ("$file" ne "$diff_file");
1700 $cmd = $VCS_cmds{"blame_range_cmd"};
1701 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1702 push(@commits, vcs_save_commits($cmd));
1704 } else {
1705 $cmd = $VCS_cmds{"blame_file_cmd"};
1706 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1707 @commits = vcs_save_commits($cmd);
1710 foreach my $commit (@commits) {
1711 $commit =~ s/^\^//g;
1714 return @commits;
1717 my $printed_novcs = 0;
1718 sub vcs_exists {
1719 %VCS_cmds = %VCS_cmds_git;
1720 return 1 if eval $VCS_cmds{"available"};
1721 %VCS_cmds = %VCS_cmds_hg;
1722 return 2 if eval $VCS_cmds{"available"};
1723 %VCS_cmds = ();
1724 if (!$printed_novcs) {
1725 warn("$P: No supported VCS found. Add --nogit to options?\n");
1726 warn("Using a git repository produces better results.\n");
1727 warn("Try Linus Torvalds' latest git repository using:\n");
1728 warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n");
1729 $printed_novcs = 1;
1731 return 0;
1734 sub vcs_is_git {
1735 vcs_exists();
1736 return $vcs_used == 1;
1739 sub vcs_is_hg {
1740 return $vcs_used == 2;
1743 sub vcs_add_commit_signers {
1744 return if (!vcs_exists());
1746 my ($commit, $desc) = @_;
1747 my $commit_count = 0;
1748 my $commit_authors_ref;
1749 my $commit_signers_ref;
1750 my $stats_ref;
1751 my @commit_authors = ();
1752 my @commit_signers = ();
1753 my $cmd;
1755 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1756 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1758 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, "");
1759 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
1760 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
1762 foreach my $signer (@commit_signers) {
1763 $signer = deduplicate_email($signer);
1766 vcs_assign($desc, 1, @commit_signers);
1769 sub interactive_get_maintainers {
1770 my ($list_ref) = @_;
1771 my @list = @$list_ref;
1773 vcs_exists();
1775 my %selected;
1776 my %authored;
1777 my %signed;
1778 my $count = 0;
1779 my $maintained = 0;
1780 foreach my $entry (@list) {
1781 $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
1782 $selected{$count} = 1;
1783 $authored{$count} = 0;
1784 $signed{$count} = 0;
1785 $count++;
1788 #menu loop
1789 my $done = 0;
1790 my $print_options = 0;
1791 my $redraw = 1;
1792 while (!$done) {
1793 $count = 0;
1794 if ($redraw) {
1795 printf STDERR "\n%1s %2s %-65s",
1796 "*", "#", "email/list and role:stats";
1797 if ($email_git ||
1798 ($email_git_fallback && !$maintained) ||
1799 $email_git_blame) {
1800 print STDERR "auth sign";
1802 print STDERR "\n";
1803 foreach my $entry (@list) {
1804 my $email = $entry->[0];
1805 my $role = $entry->[1];
1806 my $sel = "";
1807 $sel = "*" if ($selected{$count});
1808 my $commit_author = $commit_author_hash{$email};
1809 my $commit_signer = $commit_signer_hash{$email};
1810 my $authored = 0;
1811 my $signed = 0;
1812 $authored++ for (@{$commit_author});
1813 $signed++ for (@{$commit_signer});
1814 printf STDERR "%1s %2d %-65s", $sel, $count + 1, $email;
1815 printf STDERR "%4d %4d", $authored, $signed
1816 if ($authored > 0 || $signed > 0);
1817 printf STDERR "\n %s\n", $role;
1818 if ($authored{$count}) {
1819 my $commit_author = $commit_author_hash{$email};
1820 foreach my $ref (@{$commit_author}) {
1821 print STDERR " Author: @{$ref}[1]\n";
1824 if ($signed{$count}) {
1825 my $commit_signer = $commit_signer_hash{$email};
1826 foreach my $ref (@{$commit_signer}) {
1827 print STDERR " @{$ref}[2]: @{$ref}[1]\n";
1831 $count++;
1834 my $date_ref = \$email_git_since;
1835 $date_ref = \$email_hg_since if (vcs_is_hg());
1836 if ($print_options) {
1837 $print_options = 0;
1838 if (vcs_exists()) {
1839 print STDERR <<EOT
1841 Version Control options:
1842 g use git history [$email_git]
1843 gf use git-fallback [$email_git_fallback]
1844 b use git blame [$email_git_blame]
1845 bs use blame signatures [$email_git_blame_signatures]
1846 c# minimum commits [$email_git_min_signatures]
1847 %# min percent [$email_git_min_percent]
1848 d# history to use [$$date_ref]
1849 x# max maintainers [$email_git_max_maintainers]
1850 t all signature types [$email_git_all_signature_types]
1851 m use .mailmap [$email_use_mailmap]
1854 print STDERR <<EOT
1856 Additional options:
1857 0 toggle all
1858 tm toggle maintainers
1859 tg toggle git entries
1860 tl toggle open list entries
1861 ts toggle subscriber list entries
1862 f emails in file [$file_emails]
1863 k keywords in file [$keywords]
1864 r remove duplicates [$email_remove_duplicates]
1865 p# pattern match depth [$pattern_depth]
1868 print STDERR
1869 "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1871 my $input = <STDIN>;
1872 chomp($input);
1874 $redraw = 1;
1875 my $rerun = 0;
1876 my @wish = split(/[, ]+/, $input);
1877 foreach my $nr (@wish) {
1878 $nr = lc($nr);
1879 my $sel = substr($nr, 0, 1);
1880 my $str = substr($nr, 1);
1881 my $val = 0;
1882 $val = $1 if $str =~ /^(\d+)$/;
1884 if ($sel eq "y") {
1885 $interactive = 0;
1886 $done = 1;
1887 $output_rolestats = 0;
1888 $output_roles = 0;
1889 last;
1890 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1891 $selected{$nr - 1} = !$selected{$nr - 1};
1892 } elsif ($sel eq "*" || $sel eq '^') {
1893 my $toggle = 0;
1894 $toggle = 1 if ($sel eq '*');
1895 for (my $i = 0; $i < $count; $i++) {
1896 $selected{$i} = $toggle;
1898 } elsif ($sel eq "0") {
1899 for (my $i = 0; $i < $count; $i++) {
1900 $selected{$i} = !$selected{$i};
1902 } elsif ($sel eq "t") {
1903 if (lc($str) eq "m") {
1904 for (my $i = 0; $i < $count; $i++) {
1905 $selected{$i} = !$selected{$i}
1906 if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
1908 } elsif (lc($str) eq "g") {
1909 for (my $i = 0; $i < $count; $i++) {
1910 $selected{$i} = !$selected{$i}
1911 if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
1913 } elsif (lc($str) eq "l") {
1914 for (my $i = 0; $i < $count; $i++) {
1915 $selected{$i} = !$selected{$i}
1916 if ($list[$i]->[1] =~ /^(open list)/i);
1918 } elsif (lc($str) eq "s") {
1919 for (my $i = 0; $i < $count; $i++) {
1920 $selected{$i} = !$selected{$i}
1921 if ($list[$i]->[1] =~ /^(subscriber list)/i);
1924 } elsif ($sel eq "a") {
1925 if ($val > 0 && $val <= $count) {
1926 $authored{$val - 1} = !$authored{$val - 1};
1927 } elsif ($str eq '*' || $str eq '^') {
1928 my $toggle = 0;
1929 $toggle = 1 if ($str eq '*');
1930 for (my $i = 0; $i < $count; $i++) {
1931 $authored{$i} = $toggle;
1934 } elsif ($sel eq "s") {
1935 if ($val > 0 && $val <= $count) {
1936 $signed{$val - 1} = !$signed{$val - 1};
1937 } elsif ($str eq '*' || $str eq '^') {
1938 my $toggle = 0;
1939 $toggle = 1 if ($str eq '*');
1940 for (my $i = 0; $i < $count; $i++) {
1941 $signed{$i} = $toggle;
1944 } elsif ($sel eq "o") {
1945 $print_options = 1;
1946 $redraw = 1;
1947 } elsif ($sel eq "g") {
1948 if ($str eq "f") {
1949 bool_invert(\$email_git_fallback);
1950 } else {
1951 bool_invert(\$email_git);
1953 $rerun = 1;
1954 } elsif ($sel eq "b") {
1955 if ($str eq "s") {
1956 bool_invert(\$email_git_blame_signatures);
1957 } else {
1958 bool_invert(\$email_git_blame);
1960 $rerun = 1;
1961 } elsif ($sel eq "c") {
1962 if ($val > 0) {
1963 $email_git_min_signatures = $val;
1964 $rerun = 1;
1966 } elsif ($sel eq "x") {
1967 if ($val > 0) {
1968 $email_git_max_maintainers = $val;
1969 $rerun = 1;
1971 } elsif ($sel eq "%") {
1972 if ($str ne "" && $val >= 0) {
1973 $email_git_min_percent = $val;
1974 $rerun = 1;
1976 } elsif ($sel eq "d") {
1977 if (vcs_is_git()) {
1978 $email_git_since = $str;
1979 } elsif (vcs_is_hg()) {
1980 $email_hg_since = $str;
1982 $rerun = 1;
1983 } elsif ($sel eq "t") {
1984 bool_invert(\$email_git_all_signature_types);
1985 $rerun = 1;
1986 } elsif ($sel eq "f") {
1987 bool_invert(\$file_emails);
1988 $rerun = 1;
1989 } elsif ($sel eq "r") {
1990 bool_invert(\$email_remove_duplicates);
1991 $rerun = 1;
1992 } elsif ($sel eq "m") {
1993 bool_invert(\$email_use_mailmap);
1994 read_mailmap();
1995 $rerun = 1;
1996 } elsif ($sel eq "k") {
1997 bool_invert(\$keywords);
1998 $rerun = 1;
1999 } elsif ($sel eq "p") {
2000 if ($str ne "" && $val >= 0) {
2001 $pattern_depth = $val;
2002 $rerun = 1;
2004 } elsif ($sel eq "h" || $sel eq "?") {
2005 print STDERR <<EOT
2007 Interactive mode allows you to select the various maintainers, submitters,
2008 commit signers and mailing lists that could be CC'd on a patch.
2010 Any *'d entry is selected.
2012 If you have git or hg installed, you can choose to summarize the commit
2013 history of files in the patch. Also, each line of the current file can
2014 be matched to its commit author and that commits signers with blame.
2016 Various knobs exist to control the length of time for active commit
2017 tracking, the maximum number of commit authors and signers to add,
2018 and such.
2020 Enter selections at the prompt until you are satisfied that the selected
2021 maintainers are appropriate. You may enter multiple selections separated
2022 by either commas or spaces.
2025 } else {
2026 print STDERR "invalid option: '$nr'\n";
2027 $redraw = 0;
2030 if ($rerun) {
2031 print STDERR "git-blame can be very slow, please have patience..."
2032 if ($email_git_blame);
2033 goto &get_maintainers;
2037 #drop not selected entries
2038 $count = 0;
2039 my @new_emailto = ();
2040 foreach my $entry (@list) {
2041 if ($selected{$count}) {
2042 push(@new_emailto, $list[$count]);
2044 $count++;
2046 return @new_emailto;
2049 sub bool_invert {
2050 my ($bool_ref) = @_;
2052 if ($$bool_ref) {
2053 $$bool_ref = 0;
2054 } else {
2055 $$bool_ref = 1;
2059 sub deduplicate_email {
2060 my ($email) = @_;
2062 my $matched = 0;
2063 my ($name, $address) = parse_email($email);
2064 $email = format_email($name, $address, 1);
2065 $email = mailmap_email($email);
2067 return $email if (!$email_remove_duplicates);
2069 ($name, $address) = parse_email($email);
2071 if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
2072 $name = $deduplicate_name_hash{lc($name)}->[0];
2073 $address = $deduplicate_name_hash{lc($name)}->[1];
2074 $matched = 1;
2075 } elsif ($deduplicate_address_hash{lc($address)}) {
2076 $name = $deduplicate_address_hash{lc($address)}->[0];
2077 $address = $deduplicate_address_hash{lc($address)}->[1];
2078 $matched = 1;
2080 if (!$matched) {
2081 $deduplicate_name_hash{lc($name)} = [ $name, $address ];
2082 $deduplicate_address_hash{lc($address)} = [ $name, $address ];
2084 $email = format_email($name, $address, 1);
2085 $email = mailmap_email($email);
2086 return $email;
2089 sub save_commits_by_author {
2090 my (@lines) = @_;
2092 my @authors = ();
2093 my @commits = ();
2094 my @subjects = ();
2096 foreach my $line (@lines) {
2097 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2098 my $author = $1;
2099 $author = deduplicate_email($author);
2100 push(@authors, $author);
2102 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
2103 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
2106 for (my $i = 0; $i < @authors; $i++) {
2107 my $exists = 0;
2108 foreach my $ref(@{$commit_author_hash{$authors[$i]}}) {
2109 if (@{$ref}[0] eq $commits[$i] &&
2110 @{$ref}[1] eq $subjects[$i]) {
2111 $exists = 1;
2112 last;
2115 if (!$exists) {
2116 push(@{$commit_author_hash{$authors[$i]}},
2117 [ ($commits[$i], $subjects[$i]) ]);
2122 sub save_commits_by_signer {
2123 my (@lines) = @_;
2125 my $commit = "";
2126 my $subject = "";
2128 foreach my $line (@lines) {
2129 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
2130 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
2131 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
2132 my @signatures = ($line);
2133 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
2134 my @types = @$types_ref;
2135 my @signers = @$signers_ref;
2137 my $type = $types[0];
2138 my $signer = $signers[0];
2140 $signer = deduplicate_email($signer);
2142 my $exists = 0;
2143 foreach my $ref(@{$commit_signer_hash{$signer}}) {
2144 if (@{$ref}[0] eq $commit &&
2145 @{$ref}[1] eq $subject &&
2146 @{$ref}[2] eq $type) {
2147 $exists = 1;
2148 last;
2151 if (!$exists) {
2152 push(@{$commit_signer_hash{$signer}},
2153 [ ($commit, $subject, $type) ]);
2159 sub vcs_assign {
2160 my ($role, $divisor, @lines) = @_;
2162 my %hash;
2163 my $count = 0;
2165 return if (@lines <= 0);
2167 if ($divisor <= 0) {
2168 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
2169 $divisor = 1;
2172 @lines = mailmap(@lines);
2174 return if (@lines <= 0);
2176 @lines = sort(@lines);
2178 # uniq -c
2179 $hash{$_}++ for @lines;
2181 # sort -rn
2182 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
2183 my $sign_offs = $hash{$line};
2184 my $percent = $sign_offs * 100 / $divisor;
2186 $percent = 100 if ($percent > 100);
2187 next if (ignore_email_address($line));
2188 $count++;
2189 last if ($sign_offs < $email_git_min_signatures ||
2190 $count > $email_git_max_maintainers ||
2191 $percent < $email_git_min_percent);
2192 push_email_address($line, '');
2193 if ($output_rolestats) {
2194 my $fmt_percent = sprintf("%.0f", $percent);
2195 add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%");
2196 } else {
2197 add_role($line, $role);
2202 sub vcs_file_signoffs {
2203 my ($file) = @_;
2205 my $authors_ref;
2206 my $signers_ref;
2207 my $stats_ref;
2208 my @authors = ();
2209 my @signers = ();
2210 my @stats = ();
2211 my $commits;
2213 $vcs_used = vcs_exists();
2214 return if (!$vcs_used);
2216 my $cmd = $VCS_cmds{"find_signers_cmd"};
2217 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2219 ($commits, $signers_ref, $authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2221 @signers = @{$signers_ref} if defined $signers_ref;
2222 @authors = @{$authors_ref} if defined $authors_ref;
2223 @stats = @{$stats_ref} if defined $stats_ref;
2225 # print("commits: <$commits>\nsigners:<@signers>\nauthors: <@authors>\nstats: <@stats>\n");
2227 foreach my $signer (@signers) {
2228 $signer = deduplicate_email($signer);
2231 vcs_assign("commit_signer", $commits, @signers);
2232 vcs_assign("authored", $commits, @authors);
2233 if ($#authors == $#stats) {
2234 my $stat_pattern = $VCS_cmds{"stat_pattern"};
2235 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
2237 my $added = 0;
2238 my $deleted = 0;
2239 for (my $i = 0; $i <= $#stats; $i++) {
2240 if ($stats[$i] =~ /$stat_pattern/) {
2241 $added += $1;
2242 $deleted += $2;
2245 my @tmp_authors = uniq(@authors);
2246 foreach my $author (@tmp_authors) {
2247 $author = deduplicate_email($author);
2249 @tmp_authors = uniq(@tmp_authors);
2250 my @list_added = ();
2251 my @list_deleted = ();
2252 foreach my $author (@tmp_authors) {
2253 my $auth_added = 0;
2254 my $auth_deleted = 0;
2255 for (my $i = 0; $i <= $#stats; $i++) {
2256 if ($author eq deduplicate_email($authors[$i]) &&
2257 $stats[$i] =~ /$stat_pattern/) {
2258 $auth_added += $1;
2259 $auth_deleted += $2;
2262 for (my $i = 0; $i < $auth_added; $i++) {
2263 push(@list_added, $author);
2265 for (my $i = 0; $i < $auth_deleted; $i++) {
2266 push(@list_deleted, $author);
2269 vcs_assign("added_lines", $added, @list_added);
2270 vcs_assign("removed_lines", $deleted, @list_deleted);
2274 sub vcs_file_blame {
2275 my ($file) = @_;
2277 my @signers = ();
2278 my @all_commits = ();
2279 my @commits = ();
2280 my $total_commits;
2281 my $total_lines;
2283 $vcs_used = vcs_exists();
2284 return if (!$vcs_used);
2286 @all_commits = vcs_blame($file);
2287 @commits = uniq(@all_commits);
2288 $total_commits = @commits;
2289 $total_lines = @all_commits;
2291 if ($email_git_blame_signatures) {
2292 if (vcs_is_hg()) {
2293 my $commit_count;
2294 my $commit_authors_ref;
2295 my $commit_signers_ref;
2296 my $stats_ref;
2297 my @commit_authors = ();
2298 my @commit_signers = ();
2299 my $commit = join(" -r ", @commits);
2300 my $cmd;
2302 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2303 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2305 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2306 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2307 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2309 push(@signers, @commit_signers);
2310 } else {
2311 foreach my $commit (@commits) {
2312 my $commit_count;
2313 my $commit_authors_ref;
2314 my $commit_signers_ref;
2315 my $stats_ref;
2316 my @commit_authors = ();
2317 my @commit_signers = ();
2318 my $cmd;
2320 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2321 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2323 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2324 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2325 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2327 push(@signers, @commit_signers);
2332 if ($from_filename) {
2333 if ($output_rolestats) {
2334 my @blame_signers;
2335 if (vcs_is_hg()) {{ # Double brace for last exit
2336 my $commit_count;
2337 my @commit_signers = ();
2338 @commits = uniq(@commits);
2339 @commits = sort(@commits);
2340 my $commit = join(" -r ", @commits);
2341 my $cmd;
2343 $cmd = $VCS_cmds{"find_commit_author_cmd"};
2344 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2346 my @lines = ();
2348 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
2350 if (!$email_git_penguin_chiefs) {
2351 @lines = grep(!/${penguin_chiefs}/i, @lines);
2354 last if !@lines;
2356 my @authors = ();
2357 foreach my $line (@lines) {
2358 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2359 my $author = $1;
2360 $author = deduplicate_email($author);
2361 push(@authors, $author);
2365 save_commits_by_author(@lines) if ($interactive);
2366 save_commits_by_signer(@lines) if ($interactive);
2368 push(@signers, @authors);
2370 else {
2371 foreach my $commit (@commits) {
2372 my $i;
2373 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
2374 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
2375 my @author = vcs_find_author($cmd);
2376 next if !@author;
2378 my $formatted_author = deduplicate_email($author[0]);
2380 my $count = grep(/$commit/, @all_commits);
2381 for ($i = 0; $i < $count ; $i++) {
2382 push(@blame_signers, $formatted_author);
2386 if (@blame_signers) {
2387 vcs_assign("authored lines", $total_lines, @blame_signers);
2390 foreach my $signer (@signers) {
2391 $signer = deduplicate_email($signer);
2393 vcs_assign("commits", $total_commits, @signers);
2394 } else {
2395 foreach my $signer (@signers) {
2396 $signer = deduplicate_email($signer);
2398 vcs_assign("modified commits", $total_commits, @signers);
2402 sub vcs_file_exists {
2403 my ($file) = @_;
2405 my $exists;
2407 my $vcs_used = vcs_exists();
2408 return 0 if (!$vcs_used);
2410 my $cmd = $VCS_cmds{"file_exists_cmd"};
2411 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2412 $cmd .= " 2>&1";
2413 $exists = &{$VCS_cmds{"execute_cmd"}}($cmd);
2415 return 0 if ($? != 0);
2417 return $exists;
2420 sub vcs_list_files {
2421 my ($file) = @_;
2423 my @lsfiles = ();
2425 my $vcs_used = vcs_exists();
2426 return 0 if (!$vcs_used);
2428 my $cmd = $VCS_cmds{"list_files_cmd"};
2429 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
2430 @lsfiles = &{$VCS_cmds{"execute_cmd"}}($cmd);
2432 return () if ($? != 0);
2434 return @lsfiles;
2437 sub uniq {
2438 my (@parms) = @_;
2440 my %saw;
2441 @parms = grep(!$saw{$_}++, @parms);
2442 return @parms;
2445 sub sort_and_uniq {
2446 my (@parms) = @_;
2448 my %saw;
2449 @parms = sort @parms;
2450 @parms = grep(!$saw{$_}++, @parms);
2451 return @parms;
2454 sub clean_file_emails {
2455 my (@file_emails) = @_;
2456 my @fmt_emails = ();
2458 foreach my $email (@file_emails) {
2459 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
2460 my ($name, $address) = parse_email($email);
2461 if ($name eq '"[,\.]"') {
2462 $name = "";
2465 my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name);
2466 if (@nw > 2) {
2467 my $first = $nw[@nw - 3];
2468 my $middle = $nw[@nw - 2];
2469 my $last = $nw[@nw - 1];
2471 if (((length($first) == 1 && $first =~ m/[A-Za-z]/) ||
2472 (length($first) == 2 && substr($first, -1) eq ".")) ||
2473 (length($middle) == 1 ||
2474 (length($middle) == 2 && substr($middle, -1) eq "."))) {
2475 $name = "$first $middle $last";
2476 } else {
2477 $name = "$middle $last";
2481 if (substr($name, -1) =~ /[,\.]/) {
2482 $name = substr($name, 0, length($name) - 1);
2483 } elsif (substr($name, -2) =~ /[,\.]"/) {
2484 $name = substr($name, 0, length($name) - 2) . '"';
2487 if (substr($name, 0, 1) =~ /[,\.]/) {
2488 $name = substr($name, 1, length($name) - 1);
2489 } elsif (substr($name, 0, 2) =~ /"[,\.]/) {
2490 $name = '"' . substr($name, 2, length($name) - 2);
2493 my $fmt_email = format_email($name, $address, $email_usename);
2494 push(@fmt_emails, $fmt_email);
2496 return @fmt_emails;
2499 sub merge_email {
2500 my @lines;
2501 my %saw;
2503 for (@_) {
2504 my ($address, $role) = @$_;
2505 if (!$saw{$address}) {
2506 if ($output_roles) {
2507 push(@lines, "$address ($role)");
2508 } else {
2509 push(@lines, $address);
2511 $saw{$address} = 1;
2515 return @lines;
2518 sub output {
2519 my (@parms) = @_;
2521 if ($output_multiline) {
2522 foreach my $line (@parms) {
2523 print("${line}\n");
2525 } else {
2526 print(join($output_separator, @parms));
2527 print("\n");
2531 my $rfc822re;
2533 sub make_rfc822re {
2534 # Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
2535 # comment. We must allow for rfc822_lwsp (or comments) after each of these.
2536 # This regexp will only work on addresses which have had comments stripped
2537 # and replaced with rfc822_lwsp.
2539 my $specials = '()<>@,;:\\\\".\\[\\]';
2540 my $controls = '\\000-\\037\\177';
2542 my $dtext = "[^\\[\\]\\r\\\\]";
2543 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
2545 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
2547 # Use zero-width assertion to spot the limit of an atom. A simple
2548 # $rfc822_lwsp* causes the regexp engine to hang occasionally.
2549 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
2550 my $word = "(?:$atom|$quoted_string)";
2551 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
2553 my $sub_domain = "(?:$atom|$domain_literal)";
2554 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
2556 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
2558 my $phrase = "$word*";
2559 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
2560 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
2561 my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
2563 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
2564 my $address = "(?:$mailbox|$group)";
2566 return "$rfc822_lwsp*$address";
2569 sub rfc822_strip_comments {
2570 my $s = shift;
2571 # Recursively remove comments, and replace with a single space. The simpler
2572 # regexps in the Email Addressing FAQ are imperfect - they will miss escaped
2573 # chars in atoms, for example.
2575 while ($s =~ s/^((?:[^"\\]|\\.)*
2576 (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
2577 \((?:[^()\\]|\\.)*\)/$1 /osx) {}
2578 return $s;
2581 # valid: returns true if the parameter is an RFC822 valid address
2583 sub rfc822_valid {
2584 my $s = rfc822_strip_comments(shift);
2586 if (!$rfc822re) {
2587 $rfc822re = make_rfc822re();
2590 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
2593 # validlist: In scalar context, returns true if the parameter is an RFC822
2594 # valid list of addresses.
2596 # In list context, returns an empty list on failure (an invalid
2597 # address was found); otherwise a list whose first element is the
2598 # number of addresses found and whose remaining elements are the
2599 # addresses. This is needed to disambiguate failure (invalid)
2600 # from success with no addresses found, because an empty string is
2601 # a valid list.
2603 sub rfc822_validlist {
2604 my $s = rfc822_strip_comments(shift);
2606 if (!$rfc822re) {
2607 $rfc822re = make_rfc822re();
2609 # * null list items are valid according to the RFC
2610 # * the '1' business is to aid in distinguishing failure from no results
2612 my @r;
2613 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
2614 $s =~ m/^$rfc822_char*$/) {
2615 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
2616 push(@r, $1);
2618 return wantarray ? (scalar(@r), @r) : 1;
2620 return wantarray ? () : 0;