scripts/get_maintainer.pl: use case insensitive name de-duplication
[linux-2.6/linux-acpi-2.6/ibm-acpi-2.6.git] / scripts / get_maintainer.pl
blob61d3bb51bddf571d0c7a1dd3edb4bfe36b25860b
1 #!/usr/bin/perl -w
2 # (c) 2007, Joe Perches <joe@perches.com>
3 # created from checkpatch.pl
5 # Print selected MAINTAINERS information for
6 # the files modified in a patch or for a file
8 # usage: perl scripts/get_maintainer.pl [OPTIONS] <patch>
9 # perl scripts/get_maintainer.pl [OPTIONS] -f <file>
11 # Licensed under the terms of the GNU GPL License version 2
13 use strict;
15 my $P = $0;
16 my $V = '0.26-beta4';
18 use Getopt::Long qw(:config no_auto_abbrev);
20 my $lk_path = "./";
21 my $email = 1;
22 my $email_usename = 1;
23 my $email_maintainer = 1;
24 my $email_list = 1;
25 my $email_subscriber_list = 0;
26 my $email_git_penguin_chiefs = 0;
27 my $email_git = 0;
28 my $email_git_all_signature_types = 0;
29 my $email_git_blame = 0;
30 my $email_git_blame_signatures = 1;
31 my $email_git_fallback = 1;
32 my $email_git_min_signatures = 1;
33 my $email_git_max_maintainers = 5;
34 my $email_git_min_percent = 5;
35 my $email_git_since = "1-year-ago";
36 my $email_hg_since = "-365";
37 my $interactive = 0;
38 my $email_remove_duplicates = 1;
39 my $output_multiline = 1;
40 my $output_separator = ", ";
41 my $output_roles = 0;
42 my $output_rolestats = 0;
43 my $scm = 0;
44 my $web = 0;
45 my $subsystem = 0;
46 my $status = 0;
47 my $keywords = 1;
48 my $sections = 0;
49 my $file_emails = 0;
50 my $from_filename = 0;
51 my $pattern_depth = 0;
52 my $version = 0;
53 my $help = 0;
55 my $vcs_used = 0;
57 my $exit = 0;
59 my %commit_author_hash;
60 my %commit_signer_hash;
62 my @penguin_chief = ();
63 push(@penguin_chief, "Linus Torvalds:torvalds\@linux-foundation.org");
64 #Andrew wants in on most everything - 2009/01/14
65 #push(@penguin_chief, "Andrew Morton:akpm\@linux-foundation.org");
67 my @penguin_chief_names = ();
68 foreach my $chief (@penguin_chief) {
69 if ($chief =~ m/^(.*):(.*)/) {
70 my $chief_name = $1;
71 my $chief_addr = $2;
72 push(@penguin_chief_names, $chief_name);
75 my $penguin_chiefs = "\(" . join("|", @penguin_chief_names) . "\)";
77 # Signature types of people who are either
78 # a) responsible for the code in question, or
79 # b) familiar enough with it to give relevant feedback
80 my @signature_tags = ();
81 push(@signature_tags, "Signed-off-by:");
82 push(@signature_tags, "Reviewed-by:");
83 push(@signature_tags, "Acked-by:");
85 # rfc822 email address - preloaded methods go here.
86 my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
87 my $rfc822_char = '[\\000-\\377]';
89 # VCS command support: class-like functions and strings
91 my %VCS_cmds;
93 my %VCS_cmds_git = (
94 "execute_cmd" => \&git_execute_cmd,
95 "available" => '(which("git") ne "") && (-d ".git")',
96 "find_signers_cmd" =>
97 "git log --no-color --since=\$email_git_since " .
98 '--format="GitCommit: %H%n' .
99 'GitAuthor: %an <%ae>%n' .
100 'GitDate: %aD%n' .
101 'GitSubject: %s%n' .
102 '%b%n"' .
103 " -- \$file",
104 "find_commit_signers_cmd" =>
105 "git log --no-color " .
106 '--format="GitCommit: %H%n' .
107 'GitAuthor: %an <%ae>%n' .
108 'GitDate: %aD%n' .
109 'GitSubject: %s%n' .
110 '%b%n"' .
111 " -1 \$commit",
112 "find_commit_author_cmd" =>
113 "git log --no-color " .
114 '--format="GitCommit: %H%n' .
115 'GitAuthor: %an <%ae>%n' .
116 'GitDate: %aD%n' .
117 'GitSubject: %s%n"' .
118 " -1 \$commit",
119 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
120 "blame_file_cmd" => "git blame -l \$file",
121 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})",
122 "blame_commit_pattern" => "^([0-9a-f]+) ",
123 "author_pattern" => "^GitAuthor: (.*)",
124 "subject_pattern" => "^GitSubject: (.*)",
127 my %VCS_cmds_hg = (
128 "execute_cmd" => \&hg_execute_cmd,
129 "available" => '(which("hg") ne "") && (-d ".hg")',
130 "find_signers_cmd" =>
131 "hg log --date=\$email_hg_since " .
132 "--template='HgCommit: {node}\\n" .
133 "HgAuthor: {author}\\n" .
134 "HgSubject: {desc}\\n'" .
135 " -- \$file",
136 "find_commit_signers_cmd" =>
137 "hg log " .
138 "--template='HgSubject: {desc}\\n'" .
139 " -r \$commit",
140 "find_commit_author_cmd" =>
141 "hg log " .
142 "--template='HgCommit: {node}\\n" .
143 "HgAuthor: {author}\\n" .
144 "HgSubject: {desc|firstline}\\n'" .
145 " -r \$commit",
146 "blame_range_cmd" => "", # not supported
147 "blame_file_cmd" => "hg blame -n \$file",
148 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})",
149 "blame_commit_pattern" => "^([ 0-9a-f]+):",
150 "author_pattern" => "^HgAuthor: (.*)",
151 "subject_pattern" => "^HgSubject: (.*)",
154 my $conf = which_conf(".get_maintainer.conf");
155 if (-f $conf) {
156 my @conf_args;
157 open(my $conffile, '<', "$conf")
158 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
160 while (<$conffile>) {
161 my $line = $_;
163 $line =~ s/\s*\n?$//g;
164 $line =~ s/^\s*//g;
165 $line =~ s/\s+/ /g;
167 next if ($line =~ m/^\s*#/);
168 next if ($line =~ m/^\s*$/);
170 my @words = split(" ", $line);
171 foreach my $word (@words) {
172 last if ($word =~ m/^#/);
173 push (@conf_args, $word);
176 close($conffile);
177 unshift(@ARGV, @conf_args) if @conf_args;
180 if (!GetOptions(
181 'email!' => \$email,
182 'git!' => \$email_git,
183 'git-all-signature-types!' => \$email_git_all_signature_types,
184 'git-blame!' => \$email_git_blame,
185 'git-blame-signatures!' => \$email_git_blame_signatures,
186 'git-fallback!' => \$email_git_fallback,
187 'git-chief-penguins!' => \$email_git_penguin_chiefs,
188 'git-min-signatures=i' => \$email_git_min_signatures,
189 'git-max-maintainers=i' => \$email_git_max_maintainers,
190 'git-min-percent=i' => \$email_git_min_percent,
191 'git-since=s' => \$email_git_since,
192 'hg-since=s' => \$email_hg_since,
193 'i|interactive!' => \$interactive,
194 'remove-duplicates!' => \$email_remove_duplicates,
195 'm!' => \$email_maintainer,
196 'n!' => \$email_usename,
197 'l!' => \$email_list,
198 's!' => \$email_subscriber_list,
199 'multiline!' => \$output_multiline,
200 'roles!' => \$output_roles,
201 'rolestats!' => \$output_rolestats,
202 'separator=s' => \$output_separator,
203 'subsystem!' => \$subsystem,
204 'status!' => \$status,
205 'scm!' => \$scm,
206 'web!' => \$web,
207 'pattern-depth=i' => \$pattern_depth,
208 'k|keywords!' => \$keywords,
209 'sections!' => \$sections,
210 'fe|file-emails!' => \$file_emails,
211 'f|file' => \$from_filename,
212 'v|version' => \$version,
213 'h|help|usage' => \$help,
214 )) {
215 die "$P: invalid argument - use --help if necessary\n";
218 if ($help != 0) {
219 usage();
220 exit 0;
223 if ($version != 0) {
224 print("${P} ${V}\n");
225 exit 0;
228 if (-t STDIN && !@ARGV) {
229 # We're talking to a terminal, but have no command line arguments.
230 die "$P: missing patchfile or -f file - use --help if necessary\n";
233 $output_multiline = 0 if ($output_separator ne ", ");
234 $output_rolestats = 1 if ($interactive);
235 $output_roles = 1 if ($output_rolestats);
237 if ($sections) {
238 $email = 0;
239 $email_list = 0;
240 $scm = 0;
241 $status = 0;
242 $subsystem = 0;
243 $web = 0;
244 $keywords = 0;
245 $interactive = 0;
246 } else {
247 my $selections = $email + $scm + $status + $subsystem + $web;
248 if ($selections == 0) {
249 die "$P: Missing required option: email, scm, status, subsystem or web\n";
253 if ($email &&
254 ($email_maintainer + $email_list + $email_subscriber_list +
255 $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
256 die "$P: Please select at least 1 email option\n";
259 if (!top_of_kernel_tree($lk_path)) {
260 die "$P: The current directory does not appear to be "
261 . "a linux kernel source tree.\n";
264 ## Read MAINTAINERS for type/value pairs
266 my @typevalue = ();
267 my %keyword_hash;
269 open (my $maint, '<', "${lk_path}MAINTAINERS")
270 or die "$P: Can't open MAINTAINERS: $!\n";
271 while (<$maint>) {
272 my $line = $_;
274 if ($line =~ m/^(\C):\s*(.*)/) {
275 my $type = $1;
276 my $value = $2;
278 ##Filename pattern matching
279 if ($type eq "F" || $type eq "X") {
280 $value =~ s@\.@\\\.@g; ##Convert . to \.
281 $value =~ s/\*/\.\*/g; ##Convert * to .*
282 $value =~ s/\?/\./g; ##Convert ? to .
283 ##if pattern is a directory and it lacks a trailing slash, add one
284 if ((-d $value)) {
285 $value =~ s@([^/])$@$1/@;
287 } elsif ($type eq "K") {
288 $keyword_hash{@typevalue} = $value;
290 push(@typevalue, "$type:$value");
291 } elsif (!/^(\s)*$/) {
292 $line =~ s/\n$//g;
293 push(@typevalue, $line);
296 close($maint);
298 my %mailmap;
300 if ($email_remove_duplicates) {
301 open(my $mailmap, '<', "${lk_path}.mailmap")
302 or warn "$P: Can't open .mailmap: $!\n";
303 while (<$mailmap>) {
304 my $line = $_;
306 next if ($line =~ m/^\s*#/);
307 next if ($line =~ m/^\s*$/);
309 my ($name, $address) = parse_email($line);
310 $line = format_email($name, $address, $email_usename);
312 next if ($line =~ m/^\s*$/);
314 if (exists($mailmap{$name})) {
315 my $obj = $mailmap{$name};
316 push(@$obj, $address);
317 } else {
318 my @arr = ($address);
319 $mailmap{$name} = \@arr;
322 close($mailmap);
325 ## use the filenames on the command line or find the filenames in the patchfiles
327 my @files = ();
328 my @range = ();
329 my @keyword_tvi = ();
330 my @file_emails = ();
332 if (!@ARGV) {
333 push(@ARGV, "&STDIN");
336 foreach my $file (@ARGV) {
337 if ($file ne "&STDIN") {
338 ##if $file is a directory and it lacks a trailing slash, add one
339 if ((-d $file)) {
340 $file =~ s@([^/])$@$1/@;
341 } elsif (!(-f $file)) {
342 die "$P: file '${file}' not found\n";
345 if ($from_filename) {
346 push(@files, $file);
347 if ($file ne "MAINTAINERS" && -f $file && ($keywords || $file_emails)) {
348 open(my $f, '<', $file)
349 or die "$P: Can't open $file: $!\n";
350 my $text = do { local($/) ; <$f> };
351 close($f);
352 if ($keywords) {
353 foreach my $line (keys %keyword_hash) {
354 if ($text =~ m/$keyword_hash{$line}/x) {
355 push(@keyword_tvi, $line);
359 if ($file_emails) {
360 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;
361 push(@file_emails, clean_file_emails(@poss_addr));
364 } else {
365 my $file_cnt = @files;
366 my $lastfile;
368 open(my $patch, "< $file")
369 or die "$P: Can't open $file: $!\n";
370 while (<$patch>) {
371 my $patch_line = $_;
372 if (m/^\+\+\+\s+(\S+)/) {
373 my $filename = $1;
374 $filename =~ s@^[^/]*/@@;
375 $filename =~ s@\n@@;
376 $lastfile = $filename;
377 push(@files, $filename);
378 } elsif (m/^\@\@ -(\d+),(\d+)/) {
379 if ($email_git_blame) {
380 push(@range, "$lastfile:$1:$2");
382 } elsif ($keywords) {
383 foreach my $line (keys %keyword_hash) {
384 if ($patch_line =~ m/^[+-].*$keyword_hash{$line}/x) {
385 push(@keyword_tvi, $line);
390 close($patch);
392 if ($file_cnt == @files) {
393 warn "$P: file '${file}' doesn't appear to be a patch. "
394 . "Add -f to options?\n";
396 @files = sort_and_uniq(@files);
400 @file_emails = uniq(@file_emails);
402 my %email_hash_name;
403 my %email_hash_address;
404 my @email_to = ();
405 my %hash_list_to;
406 my @list_to = ();
407 my @scm = ();
408 my @web = ();
409 my @subsystem = ();
410 my @status = ();
411 my @interactive_to = ();
412 my $signature_pattern;
414 my @maintainers = get_maintainers();
416 if (@maintainers) {
417 @maintainers = merge_email(@maintainers);
418 output(@maintainers);
421 if ($scm) {
422 @scm = uniq(@scm);
423 output(@scm);
426 if ($status) {
427 @status = uniq(@status);
428 output(@status);
431 if ($subsystem) {
432 @subsystem = uniq(@subsystem);
433 output(@subsystem);
436 if ($web) {
437 @web = uniq(@web);
438 output(@web);
441 exit($exit);
443 sub get_maintainers {
444 %email_hash_name = ();
445 %email_hash_address = ();
446 %commit_author_hash = ();
447 %commit_signer_hash = ();
448 @email_to = ();
449 %hash_list_to = ();
450 @list_to = ();
451 @scm = ();
452 @web = ();
453 @subsystem = ();
454 @status = ();
455 @interactive_to = ();
456 if ($email_git_all_signature_types) {
457 $signature_pattern = "(.+?)[Bb][Yy]:";
458 } else {
459 $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
462 # Find responsible parties
464 my %exact_pattern_match_hash;
466 foreach my $file (@files) {
468 my %hash;
469 my $tvi = find_first_section();
470 while ($tvi < @typevalue) {
471 my $start = find_starting_index($tvi);
472 my $end = find_ending_index($tvi);
473 my $exclude = 0;
474 my $i;
476 #Do not match excluded file patterns
478 for ($i = $start; $i < $end; $i++) {
479 my $line = $typevalue[$i];
480 if ($line =~ m/^(\C):\s*(.*)/) {
481 my $type = $1;
482 my $value = $2;
483 if ($type eq 'X') {
484 if (file_match_pattern($file, $value)) {
485 $exclude = 1;
486 last;
492 if (!$exclude) {
493 for ($i = $start; $i < $end; $i++) {
494 my $line = $typevalue[$i];
495 if ($line =~ m/^(\C):\s*(.*)/) {
496 my $type = $1;
497 my $value = $2;
498 if ($type eq 'F') {
499 if (file_match_pattern($file, $value)) {
500 my $value_pd = ($value =~ tr@/@@);
501 my $file_pd = ($file =~ tr@/@@);
502 $value_pd++ if (substr($value,-1,1) ne "/");
503 $value_pd = -1 if ($value =~ /^\.\*/);
504 if ($value_pd >= $file_pd) {
505 $exact_pattern_match_hash{$file} = 1;
507 if ($pattern_depth == 0 ||
508 (($file_pd - $value_pd) < $pattern_depth)) {
509 $hash{$tvi} = $value_pd;
516 $tvi = $end + 1;
519 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
520 add_categories($line);
521 if ($sections) {
522 my $i;
523 my $start = find_starting_index($line);
524 my $end = find_ending_index($line);
525 for ($i = $start; $i < $end; $i++) {
526 my $line = $typevalue[$i];
527 if ($line =~ /^[FX]:/) { ##Restore file patterns
528 $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
529 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ?
530 $line =~ s/\\\./\./g; ##Convert \. to .
531 $line =~ s/\.\*/\*/g; ##Convert .* to *
533 $line =~ s/^([A-Z]):/$1:\t/g;
534 print("$line\n");
536 print("\n");
541 if ($keywords) {
542 @keyword_tvi = sort_and_uniq(@keyword_tvi);
543 foreach my $line (@keyword_tvi) {
544 add_categories($line);
548 @interactive_to = (@email_to, @list_to);
550 foreach my $file (@files) {
551 if ($email &&
552 ($email_git || ($email_git_fallback &&
553 !$exact_pattern_match_hash{$file}))) {
554 vcs_file_signoffs($file);
556 if ($email && $email_git_blame) {
557 vcs_file_blame($file);
561 if ($email) {
562 foreach my $chief (@penguin_chief) {
563 if ($chief =~ m/^(.*):(.*)/) {
564 my $email_address;
566 $email_address = format_email($1, $2, $email_usename);
567 if ($email_git_penguin_chiefs) {
568 push(@email_to, [$email_address, 'chief penguin']);
569 } else {
570 @email_to = grep($_->[0] !~ /${email_address}/, @email_to);
575 foreach my $email (@file_emails) {
576 my ($name, $address) = parse_email($email);
578 my $tmp_email = format_email($name, $address, $email_usename);
579 push_email_address($tmp_email, '');
580 add_role($tmp_email, 'in file');
584 my @to = ();
585 if ($email || $email_list) {
586 if ($email) {
587 @to = (@to, @email_to);
589 if ($email_list) {
590 @to = (@to, @list_to);
594 if ($interactive) {
595 @interactive_to = @to;
596 @to = interactive_get_maintainers(\@interactive_to);
599 return @to;
602 sub file_match_pattern {
603 my ($file, $pattern) = @_;
604 if (substr($pattern, -1) eq "/") {
605 if ($file =~ m@^$pattern@) {
606 return 1;
608 } else {
609 if ($file =~ m@^$pattern@) {
610 my $s1 = ($file =~ tr@/@@);
611 my $s2 = ($pattern =~ tr@/@@);
612 if ($s1 == $s2) {
613 return 1;
617 return 0;
620 sub usage {
621 print <<EOT;
622 usage: $P [options] patchfile
623 $P [options] -f file|directory
624 version: $V
626 MAINTAINER field selection options:
627 --email => print email address(es) if any
628 --git => include recent git \*-by: signers
629 --git-all-signature-types => include signers regardless of signature type
630 or use only ${signature_pattern} signers (default: $email_git_all_signature_types)
631 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
632 --git-chief-penguins => include ${penguin_chiefs}
633 --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
634 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
635 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
636 --git-blame => use git blame to find modified commits for patch or file
637 --git-since => git history to use (default: $email_git_since)
638 --hg-since => hg history to use (default: $email_hg_since)
639 --interactive => display a menu (mostly useful if used with the --git option)
640 --m => include maintainer(s) if any
641 --n => include name 'Full Name <addr\@domain.tld>'
642 --l => include list(s) if any
643 --s => include subscriber only list(s) if any
644 --remove-duplicates => minimize duplicate email names/addresses
645 --roles => show roles (status:subsystem, git-signer, list, etc...)
646 --rolestats => show roles and statistics (commits/total_commits, %)
647 --file-emails => add email addresses found in -f file (default: 0 (off))
648 --scm => print SCM tree(s) if any
649 --status => print status if any
650 --subsystem => print subsystem name if any
651 --web => print website(s) if any
653 Output type options:
654 --separator [, ] => separator for multiple entries on 1 line
655 using --separator also sets --nomultiline if --separator is not [, ]
656 --multiline => print 1 entry per line
658 Other options:
659 --pattern-depth => Number of pattern directory traversals (default: 0 (all))
660 --keywords => scan patch for keywords (default: 1 (on))
661 --sections => print the entire subsystem sections with pattern matches
662 --version => show version
663 --help => show this help information
665 Default options:
666 [--email --git --m --n --l --multiline --pattern-depth=0 --remove-duplicates]
668 Notes:
669 Using "-f directory" may give unexpected results:
670 Used with "--git", git signators for _all_ files in and below
671 directory are examined as git recurses directories.
672 Any specified X: (exclude) pattern matches are _not_ ignored.
673 Used with "--nogit", directory is used as a pattern match,
674 no individual file within the directory or subdirectory
675 is matched.
676 Used with "--git-blame", does not iterate all files in directory
677 Using "--git-blame" is slow and may add old committers and authors
678 that are no longer active maintainers to the output.
679 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
680 other automated tools that expect only ["name"] <email address>
681 may not work because of additional output after <email address>.
682 Using "--rolestats" and "--git-blame" shows the #/total=% commits,
683 not the percentage of the entire file authored. # of commits is
684 not a good measure of amount of code authored. 1 major commit may
685 contain a thousand lines, 5 trivial commits may modify a single line.
686 If git is not installed, but mercurial (hg) is installed and an .hg
687 repository exists, the following options apply to mercurial:
688 --git,
689 --git-min-signatures, --git-max-maintainers, --git-min-percent, and
690 --git-blame
691 Use --hg-since not --git-since to control date selection
692 File ".get_maintainer.conf", if it exists in the linux kernel source root
693 directory, can change whatever get_maintainer defaults are desired.
694 Entries in this file can be any command line argument.
695 This file is prepended to any additional command line arguments.
696 Multiple lines and # comments are allowed.
700 sub top_of_kernel_tree {
701 my ($lk_path) = @_;
703 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
704 $lk_path .= "/";
706 if ( (-f "${lk_path}COPYING")
707 && (-f "${lk_path}CREDITS")
708 && (-f "${lk_path}Kbuild")
709 && (-f "${lk_path}MAINTAINERS")
710 && (-f "${lk_path}Makefile")
711 && (-f "${lk_path}README")
712 && (-d "${lk_path}Documentation")
713 && (-d "${lk_path}arch")
714 && (-d "${lk_path}include")
715 && (-d "${lk_path}drivers")
716 && (-d "${lk_path}fs")
717 && (-d "${lk_path}init")
718 && (-d "${lk_path}ipc")
719 && (-d "${lk_path}kernel")
720 && (-d "${lk_path}lib")
721 && (-d "${lk_path}scripts")) {
722 return 1;
724 return 0;
727 sub parse_email {
728 my ($formatted_email) = @_;
730 my $name = "";
731 my $address = "";
733 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
734 $name = $1;
735 $address = $2;
736 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
737 $address = $1;
738 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
739 $address = $1;
742 $name =~ s/^\s+|\s+$//g;
743 $name =~ s/^\"|\"$//g;
744 $address =~ s/^\s+|\s+$//g;
746 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
747 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
748 $name = "\"$name\"";
751 return ($name, $address);
754 sub format_email {
755 my ($name, $address, $usename) = @_;
757 my $formatted_email;
759 $name =~ s/^\s+|\s+$//g;
760 $name =~ s/^\"|\"$//g;
761 $address =~ s/^\s+|\s+$//g;
763 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
764 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
765 $name = "\"$name\"";
768 if ($usename) {
769 if ("$name" eq "") {
770 $formatted_email = "$address";
771 } else {
772 $formatted_email = "$name <$address>";
774 } else {
775 $formatted_email = $address;
778 return $formatted_email;
781 sub find_first_section {
782 my $index = 0;
784 while ($index < @typevalue) {
785 my $tv = $typevalue[$index];
786 if (($tv =~ m/^(\C):\s*(.*)/)) {
787 last;
789 $index++;
792 return $index;
795 sub find_starting_index {
796 my ($index) = @_;
798 while ($index > 0) {
799 my $tv = $typevalue[$index];
800 if (!($tv =~ m/^(\C):\s*(.*)/)) {
801 last;
803 $index--;
806 return $index;
809 sub find_ending_index {
810 my ($index) = @_;
812 while ($index < @typevalue) {
813 my $tv = $typevalue[$index];
814 if (!($tv =~ m/^(\C):\s*(.*)/)) {
815 last;
817 $index++;
820 return $index;
823 sub get_maintainer_role {
824 my ($index) = @_;
826 my $i;
827 my $start = find_starting_index($index);
828 my $end = find_ending_index($index);
830 my $role;
831 my $subsystem = $typevalue[$start];
832 if (length($subsystem) > 20) {
833 $subsystem = substr($subsystem, 0, 17);
834 $subsystem =~ s/\s*$//;
835 $subsystem = $subsystem . "...";
838 for ($i = $start + 1; $i < $end; $i++) {
839 my $tv = $typevalue[$i];
840 if ($tv =~ m/^(\C):\s*(.*)/) {
841 my $ptype = $1;
842 my $pvalue = $2;
843 if ($ptype eq "S") {
844 $role = $pvalue;
849 $role = lc($role);
850 if ($role eq "supported") {
851 $role = "supporter";
852 } elsif ($role eq "maintained") {
853 $role = "maintainer";
854 } elsif ($role eq "odd fixes") {
855 $role = "odd fixer";
856 } elsif ($role eq "orphan") {
857 $role = "orphan minder";
858 } elsif ($role eq "obsolete") {
859 $role = "obsolete minder";
860 } elsif ($role eq "buried alive in reporters") {
861 $role = "chief penguin";
864 return $role . ":" . $subsystem;
867 sub get_list_role {
868 my ($index) = @_;
870 my $i;
871 my $start = find_starting_index($index);
872 my $end = find_ending_index($index);
874 my $subsystem = $typevalue[$start];
875 if (length($subsystem) > 20) {
876 $subsystem = substr($subsystem, 0, 17);
877 $subsystem =~ s/\s*$//;
878 $subsystem = $subsystem . "...";
881 if ($subsystem eq "THE REST") {
882 $subsystem = "";
885 return $subsystem;
888 sub add_categories {
889 my ($index) = @_;
891 my $i;
892 my $start = find_starting_index($index);
893 my $end = find_ending_index($index);
895 push(@subsystem, $typevalue[$start]);
897 for ($i = $start + 1; $i < $end; $i++) {
898 my $tv = $typevalue[$i];
899 if ($tv =~ m/^(\C):\s*(.*)/) {
900 my $ptype = $1;
901 my $pvalue = $2;
902 if ($ptype eq "L") {
903 my $list_address = $pvalue;
904 my $list_additional = "";
905 my $list_role = get_list_role($i);
907 if ($list_role ne "") {
908 $list_role = ":" . $list_role;
910 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
911 $list_address = $1;
912 $list_additional = $2;
914 if ($list_additional =~ m/subscribers-only/) {
915 if ($email_subscriber_list) {
916 if (!$hash_list_to{lc($list_address)}) {
917 $hash_list_to{lc($list_address)} = 1;
918 push(@list_to, [$list_address,
919 "subscriber list${list_role}"]);
922 } else {
923 if ($email_list) {
924 if (!$hash_list_to{lc($list_address)}) {
925 $hash_list_to{lc($list_address)} = 1;
926 push(@list_to, [$list_address,
927 "open list${list_role}"]);
931 } elsif ($ptype eq "M") {
932 my ($name, $address) = parse_email($pvalue);
933 if ($name eq "") {
934 if ($i > 0) {
935 my $tv = $typevalue[$i - 1];
936 if ($tv =~ m/^(\C):\s*(.*)/) {
937 if ($1 eq "P") {
938 $name = $2;
939 $pvalue = format_email($name, $address, $email_usename);
944 if ($email_maintainer) {
945 my $role = get_maintainer_role($i);
946 push_email_addresses($pvalue, $role);
948 } elsif ($ptype eq "T") {
949 push(@scm, $pvalue);
950 } elsif ($ptype eq "W") {
951 push(@web, $pvalue);
952 } elsif ($ptype eq "S") {
953 push(@status, $pvalue);
959 sub email_inuse {
960 my ($name, $address) = @_;
962 return 1 if (($name eq "") && ($address eq ""));
963 return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
964 return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
966 return 0;
969 sub push_email_address {
970 my ($line, $role) = @_;
972 my ($name, $address) = parse_email($line);
974 if ($address eq "") {
975 return 0;
978 if (!$email_remove_duplicates) {
979 push(@email_to, [format_email($name, $address, $email_usename), $role]);
980 } elsif (!email_inuse($name, $address)) {
981 push(@email_to, [format_email($name, $address, $email_usename), $role]);
982 $email_hash_name{lc($name)}++;
983 $email_hash_address{lc($address)}++;
986 return 1;
989 sub push_email_addresses {
990 my ($address, $role) = @_;
992 my @address_list = ();
994 if (rfc822_valid($address)) {
995 push_email_address($address, $role);
996 } elsif (@address_list = rfc822_validlist($address)) {
997 my $array_count = shift(@address_list);
998 while (my $entry = shift(@address_list)) {
999 push_email_address($entry, $role);
1001 } else {
1002 if (!push_email_address($address, $role)) {
1003 warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1008 sub add_role {
1009 my ($line, $role) = @_;
1011 my ($name, $address) = parse_email($line);
1012 my $email = format_email($name, $address, $email_usename);
1014 foreach my $entry (@email_to) {
1015 if ($email_remove_duplicates) {
1016 my ($entry_name, $entry_address) = parse_email($entry->[0]);
1017 if (($name eq $entry_name || $address eq $entry_address)
1018 && ($role eq "" || !($entry->[1] =~ m/$role/))
1020 if ($entry->[1] eq "") {
1021 $entry->[1] = "$role";
1022 } else {
1023 $entry->[1] = "$entry->[1],$role";
1026 } else {
1027 if ($email eq $entry->[0]
1028 && ($role eq "" || !($entry->[1] =~ m/$role/))
1030 if ($entry->[1] eq "") {
1031 $entry->[1] = "$role";
1032 } else {
1033 $entry->[1] = "$entry->[1],$role";
1040 sub which {
1041 my ($bin) = @_;
1043 foreach my $path (split(/:/, $ENV{PATH})) {
1044 if (-e "$path/$bin") {
1045 return "$path/$bin";
1049 return "";
1052 sub which_conf {
1053 my ($conf) = @_;
1055 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1056 if (-e "$path/$conf") {
1057 return "$path/$conf";
1061 return "";
1064 sub mailmap {
1065 my (@lines) = @_;
1066 my %hash;
1068 foreach my $line (@lines) {
1069 my ($name, $address) = parse_email($line);
1070 if (!exists($hash{$name})) {
1071 $hash{$name} = $address;
1072 } elsif ($address ne $hash{$name}) {
1073 $address = $hash{$name};
1074 $line = format_email($name, $address, $email_usename);
1076 if (exists($mailmap{$name})) {
1077 my $obj = $mailmap{$name};
1078 foreach my $map_address (@$obj) {
1079 if (($map_address eq $address) &&
1080 ($map_address ne $hash{$name})) {
1081 $line = format_email($name, $hash{$name}, $email_usename);
1087 return @lines;
1090 sub git_execute_cmd {
1091 my ($cmd) = @_;
1092 my @lines = ();
1094 my $output = `$cmd`;
1095 $output =~ s/^\s*//gm;
1096 @lines = split("\n", $output);
1098 return @lines;
1101 sub hg_execute_cmd {
1102 my ($cmd) = @_;
1103 my @lines = ();
1105 my $output = `$cmd`;
1106 @lines = split("\n", $output);
1108 return @lines;
1111 sub extract_formatted_signatures {
1112 my (@signature_lines) = @_;
1114 my @type = @signature_lines;
1116 s/\s*(.*):.*/$1/ for (@type);
1118 # cut -f2- -d":"
1119 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1121 ## Reformat email addresses (with names) to avoid badly written signatures
1123 foreach my $signer (@signature_lines) {
1124 my ($name, $address) = parse_email($signer);
1125 $signer = format_email($name, $address, 1);
1128 return (\@type, \@signature_lines);
1131 sub vcs_find_signers {
1132 my ($cmd) = @_;
1133 my $commits;
1134 my @lines = ();
1135 my @signatures = ();
1137 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1139 my $pattern = $VCS_cmds{"commit_pattern"};
1141 $commits = grep(/$pattern/, @lines); # of commits
1143 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1145 return (0, @signatures) if !@signatures;
1147 save_commits_by_author(@lines) if ($interactive);
1148 save_commits_by_signer(@lines) if ($interactive);
1150 if (!$email_git_penguin_chiefs) {
1151 @signatures = grep(!/${penguin_chiefs}/i, @signatures);
1154 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1156 return ($commits, @$signers_ref);
1159 sub vcs_find_author {
1160 my ($cmd) = @_;
1161 my @lines = ();
1163 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1165 if (!$email_git_penguin_chiefs) {
1166 @lines = grep(!/${penguin_chiefs}/i, @lines);
1169 return @lines if !@lines;
1171 my @authors = ();
1172 foreach my $line (@lines) {
1173 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1174 my $author = $1;
1175 my ($name, $address) = parse_email($author);
1176 $author = format_email($name, $address, 1);
1177 push(@authors, $author);
1181 save_commits_by_author(@lines) if ($interactive);
1182 save_commits_by_signer(@lines) if ($interactive);
1184 return @authors;
1187 sub vcs_save_commits {
1188 my ($cmd) = @_;
1189 my @lines = ();
1190 my @commits = ();
1192 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1194 foreach my $line (@lines) {
1195 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1196 push(@commits, $1);
1200 return @commits;
1203 sub vcs_blame {
1204 my ($file) = @_;
1205 my $cmd;
1206 my @commits = ();
1208 return @commits if (!(-f $file));
1210 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1211 my @all_commits = ();
1213 $cmd = $VCS_cmds{"blame_file_cmd"};
1214 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1215 @all_commits = vcs_save_commits($cmd);
1217 foreach my $file_range_diff (@range) {
1218 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1219 my $diff_file = $1;
1220 my $diff_start = $2;
1221 my $diff_length = $3;
1222 next if ("$file" ne "$diff_file");
1223 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1224 push(@commits, $all_commits[$i]);
1227 } elsif (@range) {
1228 foreach my $file_range_diff (@range) {
1229 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1230 my $diff_file = $1;
1231 my $diff_start = $2;
1232 my $diff_length = $3;
1233 next if ("$file" ne "$diff_file");
1234 $cmd = $VCS_cmds{"blame_range_cmd"};
1235 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1236 push(@commits, vcs_save_commits($cmd));
1238 } else {
1239 $cmd = $VCS_cmds{"blame_file_cmd"};
1240 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1241 @commits = vcs_save_commits($cmd);
1244 foreach my $commit (@commits) {
1245 $commit =~ s/^\^//g;
1248 return @commits;
1251 my $printed_novcs = 0;
1252 sub vcs_exists {
1253 %VCS_cmds = %VCS_cmds_git;
1254 return 1 if eval $VCS_cmds{"available"};
1255 %VCS_cmds = %VCS_cmds_hg;
1256 return 2 if eval $VCS_cmds{"available"};
1257 %VCS_cmds = ();
1258 if (!$printed_novcs) {
1259 warn("$P: No supported VCS found. Add --nogit to options?\n");
1260 warn("Using a git repository produces better results.\n");
1261 warn("Try Linus Torvalds' latest git repository using:\n");
1262 warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git\n");
1263 $printed_novcs = 1;
1265 return 0;
1268 sub vcs_is_git {
1269 return $vcs_used == 1;
1272 sub vcs_is_hg {
1273 return $vcs_used == 2;
1276 sub interactive_get_maintainers {
1277 my ($list_ref) = @_;
1278 my @list = @$list_ref;
1280 vcs_exists();
1282 my %selected;
1283 my %authored;
1284 my %signed;
1285 my $count = 0;
1286 my $maintained = 0;
1287 #select maintainers by default
1288 foreach my $entry (@list) {
1289 my $role = $entry->[1];
1290 $selected{$count} = ($role =~ /^(maintainer|supporter|open list)/i);
1291 $maintained = 1 if ($role =~ /^(maintainer|supporter)/i);
1292 $authored{$count} = 0;
1293 $signed{$count} = 0;
1294 $count++;
1297 #menu loop
1298 my $done = 0;
1299 my $print_options = 0;
1300 my $redraw = 1;
1301 while (!$done) {
1302 $count = 0;
1303 if ($redraw) {
1304 printf STDERR "\n%1s %2s %-65s",
1305 "*", "#", "email/list and role:stats";
1306 if ($email_git ||
1307 ($email_git_fallback && !$maintained) ||
1308 $email_git_blame) {
1309 print STDERR "auth sign";
1311 print STDERR "\n";
1312 foreach my $entry (@list) {
1313 my $email = $entry->[0];
1314 my $role = $entry->[1];
1315 my $sel = "";
1316 $sel = "*" if ($selected{$count});
1317 my $commit_author = $commit_author_hash{$email};
1318 my $commit_signer = $commit_signer_hash{$email};
1319 my $authored = 0;
1320 my $signed = 0;
1321 $authored++ for (@{$commit_author});
1322 $signed++ for (@{$commit_signer});
1323 printf STDERR "%1s %2d %-65s", $sel, $count + 1, $email;
1324 printf STDERR "%4d %4d", $authored, $signed
1325 if ($authored > 0 || $signed > 0);
1326 printf STDERR "\n %s\n", $role;
1327 if ($authored{$count}) {
1328 my $commit_author = $commit_author_hash{$email};
1329 foreach my $ref (@{$commit_author}) {
1330 print STDERR " Author: @{$ref}[1]\n";
1333 if ($signed{$count}) {
1334 my $commit_signer = $commit_signer_hash{$email};
1335 foreach my $ref (@{$commit_signer}) {
1336 print STDERR " @{$ref}[2]: @{$ref}[1]\n";
1340 $count++;
1343 my $date_ref = \$email_git_since;
1344 $date_ref = \$email_hg_since if (vcs_is_hg());
1345 if ($print_options) {
1346 $print_options = 0;
1347 if (vcs_exists()) {
1348 print STDERR
1349 "\nVersion Control options:\n" .
1350 "g use git history [$email_git]\n" .
1351 "gf use git-fallback [$email_git_fallback]\n" .
1352 "b use git blame [$email_git_blame]\n" .
1353 "bs use blame signatures [$email_git_blame_signatures]\n" .
1354 "c# minimum commits [$email_git_min_signatures]\n" .
1355 "%# min percent [$email_git_min_percent]\n" .
1356 "d# history to use [$$date_ref]\n" .
1357 "x# max maintainers [$email_git_max_maintainers]\n" .
1358 "t all signature types [$email_git_all_signature_types]\n";
1360 print STDERR "\nAdditional options:\n" .
1361 "0 toggle all\n" .
1362 "f emails in file [$file_emails]\n" .
1363 "k keywords in file [$keywords]\n" .
1364 "r remove duplicates [$email_remove_duplicates]\n" .
1365 "p# pattern match depth [$pattern_depth]\n";
1367 print STDERR
1368 "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1370 my $input = <STDIN>;
1371 chomp($input);
1373 $redraw = 1;
1374 my $rerun = 0;
1375 my @wish = split(/[, ]+/, $input);
1376 foreach my $nr (@wish) {
1377 $nr = lc($nr);
1378 my $sel = substr($nr, 0, 1);
1379 my $str = substr($nr, 1);
1380 my $val = 0;
1381 $val = $1 if $str =~ /^(\d+)$/;
1383 if ($sel eq "y") {
1384 $interactive = 0;
1385 $done = 1;
1386 $output_rolestats = 0;
1387 $output_roles = 0;
1388 last;
1389 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1390 $selected{$nr - 1} = !$selected{$nr - 1};
1391 } elsif ($sel eq "*" || $sel eq '^') {
1392 my $toggle = 0;
1393 $toggle = 1 if ($sel eq '*');
1394 for (my $i = 0; $i < $count; $i++) {
1395 $selected{$i} = $toggle;
1397 } elsif ($sel eq "0") {
1398 for (my $i = 0; $i < $count; $i++) {
1399 $selected{$i} = !$selected{$i};
1401 } elsif ($sel eq "a") {
1402 if ($val > 0 && $val <= $count) {
1403 $authored{$val - 1} = !$authored{$val - 1};
1404 } elsif ($str eq '*' || $str eq '^') {
1405 my $toggle = 0;
1406 $toggle = 1 if ($str eq '*');
1407 for (my $i = 0; $i < $count; $i++) {
1408 $authored{$i} = $toggle;
1411 } elsif ($sel eq "s") {
1412 if ($val > 0 && $val <= $count) {
1413 $signed{$val - 1} = !$signed{$val - 1};
1414 } elsif ($str eq '*' || $str eq '^') {
1415 my $toggle = 0;
1416 $toggle = 1 if ($str eq '*');
1417 for (my $i = 0; $i < $count; $i++) {
1418 $signed{$i} = $toggle;
1421 } elsif ($sel eq "o") {
1422 $print_options = 1;
1423 $redraw = 1;
1424 } elsif ($sel eq "g") {
1425 if ($str eq "f") {
1426 bool_invert(\$email_git_fallback);
1427 } else {
1428 bool_invert(\$email_git);
1430 $rerun = 1;
1431 } elsif ($sel eq "b") {
1432 if ($str eq "s") {
1433 bool_invert(\$email_git_blame_signatures);
1434 } else {
1435 bool_invert(\$email_git_blame);
1437 $rerun = 1;
1438 } elsif ($sel eq "c") {
1439 if ($val > 0) {
1440 $email_git_min_signatures = $val;
1441 $rerun = 1;
1443 } elsif ($sel eq "x") {
1444 if ($val > 0) {
1445 $email_git_max_maintainers = $val;
1446 $rerun = 1;
1448 } elsif ($sel eq "%") {
1449 if ($str ne "" && $val >= 0) {
1450 $email_git_min_percent = $val;
1451 $rerun = 1;
1453 } elsif ($sel eq "d") {
1454 if (vcs_is_git()) {
1455 $email_git_since = $str;
1456 } elsif (vcs_is_hg()) {
1457 $email_hg_since = $str;
1459 $rerun = 1;
1460 } elsif ($sel eq "t") {
1461 bool_invert(\$email_git_all_signature_types);
1462 $rerun = 1;
1463 } elsif ($sel eq "f") {
1464 bool_invert(\$file_emails);
1465 $rerun = 1;
1466 } elsif ($sel eq "r") {
1467 bool_invert(\$email_remove_duplicates);
1468 $rerun = 1;
1469 } elsif ($sel eq "k") {
1470 bool_invert(\$keywords);
1471 $rerun = 1;
1472 } elsif ($sel eq "p") {
1473 if ($str ne "" && $val >= 0) {
1474 $pattern_depth = $val;
1475 $rerun = 1;
1477 } elsif ($sel eq "h" || $sel eq "?") {
1478 print STDERR <<EOT
1480 Interactive mode allows you to select the various maintainers, submitters,
1481 commit signers and mailing lists that could be CC'd on a patch.
1483 Any *'d entry is selected.
1485 If you have git or hg installed, You can choose to summarize the commit
1486 history of files in the patch. Also, each line of the current file can
1487 be matched to its commit author and that commits signers with blame.
1489 Various knobs exist to control the length of time for active commit
1490 tracking, the maximum number of commit authors and signers to add,
1491 and such.
1493 Enter selections at the prompt until you are satisfied that the selected
1494 maintainers are appropriate. You may enter multiple selections separated
1495 by either commas or spaces.
1498 } else {
1499 print STDERR "invalid option: '$nr'\n";
1500 $redraw = 0;
1503 if ($rerun) {
1504 print STDERR "git-blame can be very slow, please have patience..."
1505 if ($email_git_blame);
1506 goto &get_maintainers;
1510 #drop not selected entries
1511 $count = 0;
1512 my @new_emailto = ();
1513 foreach my $entry (@list) {
1514 if ($selected{$count}) {
1515 push(@new_emailto, $list[$count]);
1517 $count++;
1519 return @new_emailto;
1522 sub bool_invert {
1523 my ($bool_ref) = @_;
1525 if ($$bool_ref) {
1526 $$bool_ref = 0;
1527 } else {
1528 $$bool_ref = 1;
1532 sub save_commits_by_author {
1533 my (@lines) = @_;
1535 my @authors = ();
1536 my @commits = ();
1537 my @subjects = ();
1539 foreach my $line (@lines) {
1540 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1541 my $matched = 0;
1542 my $author = $1;
1543 my ($name, $address) = parse_email($author);
1544 foreach my $to (@interactive_to) {
1545 my ($to_name, $to_address) = parse_email($to->[0]);
1546 if ($email_remove_duplicates &&
1547 ((lc($name) eq lc($to_name)) ||
1548 (lc($address) eq lc($to_address)))) {
1549 $author = $to->[0];
1550 $matched = 1;
1551 last;
1554 $author = format_email($name, $address, 1) if (!$matched);
1555 push(@authors, $author);
1557 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1558 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1561 for (my $i = 0; $i < @authors; $i++) {
1562 my $exists = 0;
1563 foreach my $ref(@{$commit_author_hash{$authors[$i]}}) {
1564 if (@{$ref}[0] eq $commits[$i] &&
1565 @{$ref}[1] eq $subjects[$i]) {
1566 $exists = 1;
1567 last;
1570 if (!$exists) {
1571 push(@{$commit_author_hash{$authors[$i]}},
1572 [ ($commits[$i], $subjects[$i]) ]);
1577 sub save_commits_by_signer {
1578 my (@lines) = @_;
1580 my $commit = "";
1581 my $subject = "";
1583 foreach my $line (@lines) {
1584 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1585 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1586 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
1587 my @signatures = ($line);
1588 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1589 my @types = @$types_ref;
1590 my @signers = @$signers_ref;
1592 my $type = $types[0];
1593 my $signer = $signers[0];
1595 my $matched = 0;
1596 my ($name, $address) = parse_email($signer);
1597 foreach my $to (@interactive_to) {
1598 my ($to_name, $to_address) = parse_email($to->[0]);
1599 if ($email_remove_duplicates &&
1600 ((lc($name) eq lc($to_name)) ||
1601 (lc($address) eq lc($to_address)))) {
1602 $signer = $to->[0];
1603 $matched = 1;
1604 last;
1606 $signer = format_email($name, $address, 1) if (!$matched);
1609 my $exists = 0;
1610 foreach my $ref(@{$commit_signer_hash{$signer}}) {
1611 if (@{$ref}[0] eq $commit &&
1612 @{$ref}[1] eq $subject &&
1613 @{$ref}[2] eq $type) {
1614 $exists = 1;
1615 last;
1618 if (!$exists) {
1619 push(@{$commit_signer_hash{$signer}},
1620 [ ($commit, $subject, $type) ]);
1626 sub vcs_assign {
1627 my ($role, $divisor, @lines) = @_;
1629 my %hash;
1630 my $count = 0;
1632 return if (@lines <= 0);
1634 if ($divisor <= 0) {
1635 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
1636 $divisor = 1;
1639 if ($email_remove_duplicates) {
1640 @lines = mailmap(@lines);
1643 return if (@lines <= 0);
1645 @lines = sort(@lines);
1647 # uniq -c
1648 $hash{$_}++ for @lines;
1650 # sort -rn
1651 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
1652 my $sign_offs = $hash{$line};
1653 my $percent = $sign_offs * 100 / $divisor;
1655 $percent = 100 if ($percent > 100);
1656 $count++;
1657 last if ($sign_offs < $email_git_min_signatures ||
1658 $count > $email_git_max_maintainers ||
1659 $percent < $email_git_min_percent);
1660 push_email_address($line, '');
1661 if ($output_rolestats) {
1662 my $fmt_percent = sprintf("%.0f", $percent);
1663 add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%");
1664 } else {
1665 add_role($line, $role);
1670 sub vcs_file_signoffs {
1671 my ($file) = @_;
1673 my @signers = ();
1674 my $commits;
1676 $vcs_used = vcs_exists();
1677 return if (!$vcs_used);
1679 my $cmd = $VCS_cmds{"find_signers_cmd"};
1680 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
1682 ($commits, @signers) = vcs_find_signers($cmd);
1683 vcs_assign("commit_signer", $commits, @signers);
1686 sub vcs_file_blame {
1687 my ($file) = @_;
1689 my @signers = ();
1690 my @all_commits = ();
1691 my @commits = ();
1692 my $total_commits;
1693 my $total_lines;
1695 $vcs_used = vcs_exists();
1696 return if (!$vcs_used);
1698 @all_commits = vcs_blame($file);
1699 @commits = uniq(@all_commits);
1700 $total_commits = @commits;
1701 $total_lines = @all_commits;
1703 if ($email_git_blame_signatures) {
1704 if (vcs_is_hg()) {
1705 my $commit_count;
1706 my @commit_signers = ();
1707 my $commit = join(" -r ", @commits);
1708 my $cmd;
1710 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1711 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1713 ($commit_count, @commit_signers) = vcs_find_signers($cmd);
1715 push(@signers, @commit_signers);
1716 } else {
1717 foreach my $commit (@commits) {
1718 my $commit_count;
1719 my @commit_signers = ();
1720 my $cmd;
1722 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1723 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1725 ($commit_count, @commit_signers) = vcs_find_signers($cmd);
1727 push(@signers, @commit_signers);
1732 if ($from_filename) {
1733 if ($output_rolestats) {
1734 my @blame_signers;
1735 if (vcs_is_hg()) {{ # Double brace for last exit
1736 my $commit_count;
1737 my @commit_signers = ();
1738 @commits = uniq(@commits);
1739 @commits = sort(@commits);
1740 my $commit = join(" -r ", @commits);
1741 my $cmd;
1743 $cmd = $VCS_cmds{"find_commit_author_cmd"};
1744 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1746 my @lines = ();
1748 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1750 if (!$email_git_penguin_chiefs) {
1751 @lines = grep(!/${penguin_chiefs}/i, @lines);
1754 last if !@lines;
1756 my @authors = ();
1757 foreach my $line (@lines) {
1758 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1759 my $author = $1;
1760 my ($name, $address) = parse_email($author);
1761 $author = format_email($name, $address, 1);
1762 push(@authors, $1);
1766 save_commits_by_author(@lines) if ($interactive);
1767 save_commits_by_signer(@lines) if ($interactive);
1769 push(@signers, @authors);
1771 else {
1772 foreach my $commit (@commits) {
1773 my $i;
1774 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
1775 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1776 my @author = vcs_find_author($cmd);
1777 next if !@author;
1778 my $count = grep(/$commit/, @all_commits);
1779 for ($i = 0; $i < $count ; $i++) {
1780 push(@blame_signers, $author[0]);
1784 if (@blame_signers) {
1785 vcs_assign("authored lines", $total_lines, @blame_signers);
1788 vcs_assign("commits", $total_commits, @signers);
1789 } else {
1790 vcs_assign("modified commits", $total_commits, @signers);
1794 sub uniq {
1795 my (@parms) = @_;
1797 my %saw;
1798 @parms = grep(!$saw{$_}++, @parms);
1799 return @parms;
1802 sub sort_and_uniq {
1803 my (@parms) = @_;
1805 my %saw;
1806 @parms = sort @parms;
1807 @parms = grep(!$saw{$_}++, @parms);
1808 return @parms;
1811 sub clean_file_emails {
1812 my (@file_emails) = @_;
1813 my @fmt_emails = ();
1815 foreach my $email (@file_emails) {
1816 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
1817 my ($name, $address) = parse_email($email);
1818 if ($name eq '"[,\.]"') {
1819 $name = "";
1822 my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name);
1823 if (@nw > 2) {
1824 my $first = $nw[@nw - 3];
1825 my $middle = $nw[@nw - 2];
1826 my $last = $nw[@nw - 1];
1828 if (((length($first) == 1 && $first =~ m/[A-Za-z]/) ||
1829 (length($first) == 2 && substr($first, -1) eq ".")) ||
1830 (length($middle) == 1 ||
1831 (length($middle) == 2 && substr($middle, -1) eq "."))) {
1832 $name = "$first $middle $last";
1833 } else {
1834 $name = "$middle $last";
1838 if (substr($name, -1) =~ /[,\.]/) {
1839 $name = substr($name, 0, length($name) - 1);
1840 } elsif (substr($name, -2) =~ /[,\.]"/) {
1841 $name = substr($name, 0, length($name) - 2) . '"';
1844 if (substr($name, 0, 1) =~ /[,\.]/) {
1845 $name = substr($name, 1, length($name) - 1);
1846 } elsif (substr($name, 0, 2) =~ /"[,\.]/) {
1847 $name = '"' . substr($name, 2, length($name) - 2);
1850 my $fmt_email = format_email($name, $address, $email_usename);
1851 push(@fmt_emails, $fmt_email);
1853 return @fmt_emails;
1856 sub merge_email {
1857 my @lines;
1858 my %saw;
1860 for (@_) {
1861 my ($address, $role) = @$_;
1862 if (!$saw{$address}) {
1863 if ($output_roles) {
1864 push(@lines, "$address ($role)");
1865 } else {
1866 push(@lines, $address);
1868 $saw{$address} = 1;
1872 return @lines;
1875 sub output {
1876 my (@parms) = @_;
1878 if ($output_multiline) {
1879 foreach my $line (@parms) {
1880 print("${line}\n");
1882 } else {
1883 print(join($output_separator, @parms));
1884 print("\n");
1888 my $rfc822re;
1890 sub make_rfc822re {
1891 # Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
1892 # comment. We must allow for rfc822_lwsp (or comments) after each of these.
1893 # This regexp will only work on addresses which have had comments stripped
1894 # and replaced with rfc822_lwsp.
1896 my $specials = '()<>@,;:\\\\".\\[\\]';
1897 my $controls = '\\000-\\037\\177';
1899 my $dtext = "[^\\[\\]\\r\\\\]";
1900 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
1902 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
1904 # Use zero-width assertion to spot the limit of an atom. A simple
1905 # $rfc822_lwsp* causes the regexp engine to hang occasionally.
1906 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
1907 my $word = "(?:$atom|$quoted_string)";
1908 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
1910 my $sub_domain = "(?:$atom|$domain_literal)";
1911 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
1913 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
1915 my $phrase = "$word*";
1916 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
1917 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
1918 my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
1920 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
1921 my $address = "(?:$mailbox|$group)";
1923 return "$rfc822_lwsp*$address";
1926 sub rfc822_strip_comments {
1927 my $s = shift;
1928 # Recursively remove comments, and replace with a single space. The simpler
1929 # regexps in the Email Addressing FAQ are imperfect - they will miss escaped
1930 # chars in atoms, for example.
1932 while ($s =~ s/^((?:[^"\\]|\\.)*
1933 (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
1934 \((?:[^()\\]|\\.)*\)/$1 /osx) {}
1935 return $s;
1938 # valid: returns true if the parameter is an RFC822 valid address
1940 sub rfc822_valid {
1941 my $s = rfc822_strip_comments(shift);
1943 if (!$rfc822re) {
1944 $rfc822re = make_rfc822re();
1947 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
1950 # validlist: In scalar context, returns true if the parameter is an RFC822
1951 # valid list of addresses.
1953 # In list context, returns an empty list on failure (an invalid
1954 # address was found); otherwise a list whose first element is the
1955 # number of addresses found and whose remaining elements are the
1956 # addresses. This is needed to disambiguate failure (invalid)
1957 # from success with no addresses found, because an empty string is
1958 # a valid list.
1960 sub rfc822_validlist {
1961 my $s = rfc822_strip_comments(shift);
1963 if (!$rfc822re) {
1964 $rfc822re = make_rfc822re();
1966 # * null list items are valid according to the RFC
1967 # * the '1' business is to aid in distinguishing failure from no results
1969 my @r;
1970 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
1971 $s =~ m/^$rfc822_char*$/) {
1972 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
1973 push(@r, $1);
1975 return wantarray ? (scalar(@r), @r) : 1;
1977 return wantarray ? () : 0;