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
18 use Getopt::Long qw(:config no_auto_abbrev);
22 my $email_usename = 1;
23 my $email_maintainer = 1;
24 my $email_reviewer = 1;
26 my $email_subscriber_list = 0;
27 my $email_git_penguin_chiefs = 0;
29 my $email_git_all_signature_types = 0;
30 my $email_git_blame = 0;
31 my $email_git_blame_signatures = 1;
32 my $email_git_fallback = 1;
33 my $email_git_min_signatures = 1;
34 my $email_git_max_maintainers = 5;
35 my $email_git_min_percent = 5;
36 my $email_git_since = "1-year-ago";
37 my $email_hg_since = "-365";
39 my $email_remove_duplicates = 1;
40 my $email_use_mailmap = 1;
41 my $output_multiline = 1;
42 my $output_separator = ", ";
44 my $output_rolestats = 1;
52 my $from_filename = 0;
53 my $pattern_depth = 0;
61 my %commit_author_hash;
62 my %commit_signer_hash;
64 my @penguin_chief = ();
65 push(@penguin_chief, "Linus Torvalds:torvalds\@linux-foundation.org");
66 #Andrew wants in on most everything - 2009/01/14
67 #push(@penguin_chief, "Andrew Morton:akpm\@linux-foundation.org");
69 my @penguin_chief_names = ();
70 foreach my $chief (@penguin_chief) {
71 if ($chief =~ m/^(.*):(.*)/) {
74 push(@penguin_chief_names, $chief_name);
77 my $penguin_chiefs = "\(" . join("|", @penguin_chief_names) . "\)";
79 # Signature types of people who are either
80 # a) responsible for the code in question, or
81 # b) familiar enough with it to give relevant feedback
82 my @signature_tags = ();
83 push(@signature_tags, "Signed-off-by:");
84 push(@signature_tags, "Reviewed-by:");
85 push(@signature_tags, "Acked-by:");
87 my $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
89 # rfc822 email address - preloaded methods go here.
90 my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
91 my $rfc822_char = '[\\000-\\377]';
93 # VCS command support: class-like functions and strings
98 "execute_cmd" => \&git_execute_cmd,
99 "available" => '(which("git") ne "") && (-e ".git")',
100 "find_signers_cmd" =>
101 "git log --no-color --follow --since=\$email_git_since " .
102 '--numstat --no-merges ' .
103 '--format="GitCommit: %H%n' .
104 'GitAuthor: %an <%ae>%n' .
109 "find_commit_signers_cmd" =>
110 "git log --no-color " .
112 '--format="GitCommit: %H%n' .
113 'GitAuthor: %an <%ae>%n' .
118 "find_commit_author_cmd" =>
119 "git log --no-color " .
121 '--format="GitCommit: %H%n' .
122 'GitAuthor: %an <%ae>%n' .
124 'GitSubject: %s%n"' .
126 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
127 "blame_file_cmd" => "git blame -l \$file",
128 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})",
129 "blame_commit_pattern" => "^([0-9a-f]+) ",
130 "author_pattern" => "^GitAuthor: (.*)",
131 "subject_pattern" => "^GitSubject: (.*)",
132 "stat_pattern" => "^(\\d+)\\t(\\d+)\\t\$file\$",
136 "execute_cmd" => \&hg_execute_cmd,
137 "available" => '(which("hg") ne "") && (-d ".hg")',
138 "find_signers_cmd" =>
139 "hg log --date=\$email_hg_since " .
140 "--template='HgCommit: {node}\\n" .
141 "HgAuthor: {author}\\n" .
142 "HgSubject: {desc}\\n'" .
144 "find_commit_signers_cmd" =>
146 "--template='HgSubject: {desc}\\n'" .
148 "find_commit_author_cmd" =>
150 "--template='HgCommit: {node}\\n" .
151 "HgAuthor: {author}\\n" .
152 "HgSubject: {desc|firstline}\\n'" .
154 "blame_range_cmd" => "", # not supported
155 "blame_file_cmd" => "hg blame -n \$file",
156 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})",
157 "blame_commit_pattern" => "^([ 0-9a-f]+):",
158 "author_pattern" => "^HgAuthor: (.*)",
159 "subject_pattern" => "^HgSubject: (.*)",
160 "stat_pattern" => "^(\\d+)\t(\\d+)\t\$file\$",
163 my $conf = which_conf(".get_maintainer.conf");
166 open(my $conffile, '<', "$conf")
167 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
169 while (<$conffile>) {
172 $line =~ s/\s*\n?$//g;
176 next if ($line =~ m/^\s*#/);
177 next if ($line =~ m/^\s*$/);
179 my @words = split(" ", $line);
180 foreach my $word (@words) {
181 last if ($word =~ m/^#/);
182 push (@conf_args, $word);
186 unshift(@ARGV, @conf_args) if @conf_args;
191 'git!' => \$email_git,
192 'git-all-signature-types!' => \$email_git_all_signature_types,
193 'git-blame!' => \$email_git_blame,
194 'git-blame-signatures!' => \$email_git_blame_signatures,
195 'git-fallback!' => \$email_git_fallback,
196 'git-chief-penguins!' => \$email_git_penguin_chiefs,
197 'git-min-signatures=i' => \$email_git_min_signatures,
198 'git-max-maintainers=i' => \$email_git_max_maintainers,
199 'git-min-percent=i' => \$email_git_min_percent,
200 'git-since=s' => \$email_git_since,
201 'hg-since=s' => \$email_hg_since,
202 'i|interactive!' => \$interactive,
203 'remove-duplicates!' => \$email_remove_duplicates,
204 'mailmap!' => \$email_use_mailmap,
205 'm!' => \$email_maintainer,
206 'r!' => \$email_reviewer,
207 'n!' => \$email_usename,
208 'l!' => \$email_list,
209 's!' => \$email_subscriber_list,
210 'multiline!' => \$output_multiline,
211 'roles!' => \$output_roles,
212 'rolestats!' => \$output_rolestats,
213 'separator=s' => \$output_separator,
214 'subsystem!' => \$subsystem,
215 'status!' => \$status,
218 'pattern-depth=i' => \$pattern_depth,
219 'k|keywords!' => \$keywords,
220 'sections!' => \$sections,
221 'fe|file-emails!' => \$file_emails,
222 'f|file' => \$from_filename,
223 'v|version' => \$version,
224 'h|help|usage' => \$help,
226 die "$P: invalid argument - use --help if necessary\n";
235 print("${P} ${V}\n");
239 if (-t STDIN && !@ARGV) {
240 # We're talking to a terminal, but have no command line arguments.
241 die "$P: missing patchfile or -f file - use --help if necessary\n";
244 $output_multiline = 0 if ($output_separator ne ", ");
245 $output_rolestats = 1 if ($interactive);
246 $output_roles = 1 if ($output_rolestats);
258 my $selections = $email + $scm + $status + $subsystem + $web;
259 if ($selections == 0) {
260 die "$P: Missing required option: email, scm, status, subsystem or web\n";
265 ($email_maintainer + $email_reviewer +
266 $email_list + $email_subscriber_list +
267 $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
268 die "$P: Please select at least 1 email option\n";
271 if (!top_of_kernel_tree($lk_path)) {
272 die "$P: The current directory does not appear to be "
273 . "a linux kernel source tree.\n";
276 ## Read MAINTAINERS for type/value pairs
281 open (my $maint, '<', "${lk_path}MAINTAINERS")
282 or die "$P: Can't open MAINTAINERS: $!\n";
286 if ($line =~ m/^(\C):\s*(.*)/) {
290 ##Filename pattern matching
291 if ($type eq "F" || $type eq "X") {
292 $value =~ s@\.@\\\.@g; ##Convert . to \.
293 $value =~ s/\*/\.\*/g; ##Convert * to .*
294 $value =~ s/\?/\./g; ##Convert ? to .
295 ##if pattern is a directory and it lacks a trailing slash, add one
297 $value =~ s@([^/])$@$1/@;
299 } elsif ($type eq "K") {
300 $keyword_hash{@typevalue} = $value;
302 push(@typevalue, "$type:$value");
303 } elsif (!/^(\s)*$/) {
305 push(@typevalue, $line);
312 # Read mail address map
325 return if (!$email_use_mailmap || !(-f "${lk_path}.mailmap"));
327 open(my $mailmap_file, '<', "${lk_path}.mailmap")
328 or warn "$P: Can't open .mailmap: $!\n";
330 while (<$mailmap_file>) {
331 s/#.*$//; #strip comments
332 s/^\s+|\s+$//g; #trim
334 next if (/^\s*$/); #skip empty lines
335 #entries have one of the following formats:
338 # name1 <mail1> <mail2>
339 # name1 <mail1> name2 <mail2>
340 # (see man git-shortlog)
342 if (/^([^<]+)<([^>]+)>$/) {
346 $real_name =~ s/\s+$//;
347 ($real_name, $address) = parse_email("$real_name <$address>");
348 $mailmap->{names}->{$address} = $real_name;
350 } elsif (/^<([^>]+)>\s*<([^>]+)>$/) {
351 my $real_address = $1;
352 my $wrong_address = $2;
354 $mailmap->{addresses}->{$wrong_address} = $real_address;
356 } elsif (/^(.+)<([^>]+)>\s*<([^>]+)>$/) {
358 my $real_address = $2;
359 my $wrong_address = $3;
361 $real_name =~ s/\s+$//;
362 ($real_name, $real_address) =
363 parse_email("$real_name <$real_address>");
364 $mailmap->{names}->{$wrong_address} = $real_name;
365 $mailmap->{addresses}->{$wrong_address} = $real_address;
367 } elsif (/^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$/) {
369 my $real_address = $2;
371 my $wrong_address = $4;
373 $real_name =~ s/\s+$//;
374 ($real_name, $real_address) =
375 parse_email("$real_name <$real_address>");
377 $wrong_name =~ s/\s+$//;
378 ($wrong_name, $wrong_address) =
379 parse_email("$wrong_name <$wrong_address>");
381 my $wrong_email = format_email($wrong_name, $wrong_address, 1);
382 $mailmap->{names}->{$wrong_email} = $real_name;
383 $mailmap->{addresses}->{$wrong_email} = $real_address;
386 close($mailmap_file);
389 ## use the filenames on the command line or find the filenames in the patchfiles
393 my @keyword_tvi = ();
394 my @file_emails = ();
397 push(@ARGV, "&STDIN");
400 foreach my $file (@ARGV) {
401 if ($file ne "&STDIN") {
402 ##if $file is a directory and it lacks a trailing slash, add one
404 $file =~ s@([^/])$@$1/@;
405 } elsif (!(-f $file)) {
406 die "$P: file '${file}' not found\n";
409 if ($from_filename) {
411 if ($file ne "MAINTAINERS" && -f $file && ($keywords || $file_emails)) {
412 open(my $f, '<', $file)
413 or die "$P: Can't open $file: $!\n";
414 my $text = do { local($/) ; <$f> };
417 foreach my $line (keys %keyword_hash) {
418 if ($text =~ m/$keyword_hash{$line}/x) {
419 push(@keyword_tvi, $line);
424 my @poss_addr = $text =~ m$[A-Za-zÀ-ÿ\"\' \,\.\+-]*\s*[\,]*\s*[\(\<\{]{0,1}[A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+\.[A-Za-z0-9]+[\)\>\}]{0,1}$g;
425 push(@file_emails, clean_file_emails(@poss_addr));
429 my $file_cnt = @files;
432 open(my $patch, "< $file")
433 or die "$P: Can't open $file: $!\n";
435 # We can check arbitrary information before the patch
436 # like the commit message, mail headers, etc...
437 # This allows us to match arbitrary keywords against any part
438 # of a git format-patch generated file (subject tags, etc...)
440 my $patch_prefix = ""; #Parsing the intro
444 if (m/^\+\+\+\s+(\S+)/ or m/^---\s+(\S+)/) {
446 $filename =~ s@^[^/]*/@@;
448 $lastfile = $filename;
449 push(@files, $filename);
450 $patch_prefix = "^[+-].*"; #Now parsing the actual patch
451 } elsif (m/^\@\@ -(\d+),(\d+)/) {
452 if ($email_git_blame) {
453 push(@range, "$lastfile:$1:$2");
455 } elsif ($keywords) {
456 foreach my $line (keys %keyword_hash) {
457 if ($patch_line =~ m/${patch_prefix}$keyword_hash{$line}/x) {
458 push(@keyword_tvi, $line);
465 if ($file_cnt == @files) {
466 warn "$P: file '${file}' doesn't appear to be a patch. "
467 . "Add -f to options?\n";
469 @files = sort_and_uniq(@files);
473 @file_emails = uniq(@file_emails);
476 my %email_hash_address;
484 my %deduplicate_name_hash = ();
485 my %deduplicate_address_hash = ();
487 my @maintainers = get_maintainers();
490 @maintainers = merge_email(@maintainers);
491 output(@maintainers);
500 @status = uniq(@status);
505 @subsystem = uniq(@subsystem);
516 sub range_is_maintained {
517 my ($start, $end) = @_;
519 for (my $i = $start; $i < $end; $i++) {
520 my $line = $typevalue[$i];
521 if ($line =~ m/^(\C):\s*(.*)/) {
525 if ($value =~ /(maintain|support)/i) {
534 sub range_has_maintainer {
535 my ($start, $end) = @_;
537 for (my $i = $start; $i < $end; $i++) {
538 my $line = $typevalue[$i];
539 if ($line =~ m/^(\C):\s*(.*)/) {
550 sub get_maintainers {
551 %email_hash_name = ();
552 %email_hash_address = ();
553 %commit_author_hash = ();
554 %commit_signer_hash = ();
562 %deduplicate_name_hash = ();
563 %deduplicate_address_hash = ();
564 if ($email_git_all_signature_types) {
565 $signature_pattern = "(.+?)[Bb][Yy]:";
567 $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
570 # Find responsible parties
572 my %exact_pattern_match_hash = ();
574 foreach my $file (@files) {
577 my $tvi = find_first_section();
578 while ($tvi < @typevalue) {
579 my $start = find_starting_index($tvi);
580 my $end = find_ending_index($tvi);
584 #Do not match excluded file patterns
586 for ($i = $start; $i < $end; $i++) {
587 my $line = $typevalue[$i];
588 if ($line =~ m/^(\C):\s*(.*)/) {
592 if (file_match_pattern($file, $value)) {
601 for ($i = $start; $i < $end; $i++) {
602 my $line = $typevalue[$i];
603 if ($line =~ m/^(\C):\s*(.*)/) {
607 if (file_match_pattern($file, $value)) {
608 my $value_pd = ($value =~ tr@/@@);
609 my $file_pd = ($file =~ tr@/@@);
610 $value_pd++ if (substr($value,-1,1) ne "/");
611 $value_pd = -1 if ($value =~ /^\.\*/);
612 if ($value_pd >= $file_pd &&
613 range_is_maintained($start, $end) &&
614 range_has_maintainer($start, $end)) {
615 $exact_pattern_match_hash{$file} = 1;
617 if ($pattern_depth == 0 ||
618 (($file_pd - $value_pd) < $pattern_depth)) {
619 $hash{$tvi} = $value_pd;
622 } elsif ($type eq 'N') {
623 if ($file =~ m/$value/x) {
633 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
634 add_categories($line);
637 my $start = find_starting_index($line);
638 my $end = find_ending_index($line);
639 for ($i = $start; $i < $end; $i++) {
640 my $line = $typevalue[$i];
641 if ($line =~ /^[FX]:/) { ##Restore file patterns
642 $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
643 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ?
644 $line =~ s/\\\./\./g; ##Convert \. to .
645 $line =~ s/\.\*/\*/g; ##Convert .* to *
647 $line =~ s/^([A-Z]):/$1:\t/g;
656 @keyword_tvi = sort_and_uniq(@keyword_tvi);
657 foreach my $line (@keyword_tvi) {
658 add_categories($line);
662 foreach my $email (@email_to, @list_to) {
663 $email->[0] = deduplicate_email($email->[0]);
666 foreach my $file (@files) {
668 ($email_git || ($email_git_fallback &&
669 !$exact_pattern_match_hash{$file}))) {
670 vcs_file_signoffs($file);
672 if ($email && $email_git_blame) {
673 vcs_file_blame($file);
678 foreach my $chief (@penguin_chief) {
679 if ($chief =~ m/^(.*):(.*)/) {
682 $email_address = format_email($1, $2, $email_usename);
683 if ($email_git_penguin_chiefs) {
684 push(@email_to, [$email_address, 'chief penguin']);
686 @email_to = grep($_->[0] !~ /${email_address}/, @email_to);
691 foreach my $email (@file_emails) {
692 my ($name, $address) = parse_email($email);
694 my $tmp_email = format_email($name, $address, $email_usename);
695 push_email_address($tmp_email, '');
696 add_role($tmp_email, 'in file');
701 if ($email || $email_list) {
703 @to = (@to, @email_to);
706 @to = (@to, @list_to);
711 @to = interactive_get_maintainers(\@to);
717 sub file_match_pattern {
718 my ($file, $pattern) = @_;
719 if (substr($pattern, -1) eq "/") {
720 if ($file =~ m@^$pattern@) {
724 if ($file =~ m@^$pattern@) {
725 my $s1 = ($file =~ tr@/@@);
726 my $s2 = ($pattern =~ tr@/@@);
737 usage: $P [options] patchfile
738 $P [options] -f file|directory
741 MAINTAINER field selection options:
742 --email => print email address(es) if any
743 --git => include recent git \*-by: signers
744 --git-all-signature-types => include signers regardless of signature type
745 or use only ${signature_pattern} signers (default: $email_git_all_signature_types)
746 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
747 --git-chief-penguins => include ${penguin_chiefs}
748 --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
749 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
750 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
751 --git-blame => use git blame to find modified commits for patch or file
752 --git-since => git history to use (default: $email_git_since)
753 --hg-since => hg history to use (default: $email_hg_since)
754 --interactive => display a menu (mostly useful if used with the --git option)
755 --m => include maintainer(s) if any
756 --r => include reviewer(s) if any
757 --n => include name 'Full Name <addr\@domain.tld>'
758 --l => include list(s) if any
759 --s => include subscriber only list(s) if any
760 --remove-duplicates => minimize duplicate email names/addresses
761 --roles => show roles (status:subsystem, git-signer, list, etc...)
762 --rolestats => show roles and statistics (commits/total_commits, %)
763 --file-emails => add email addresses found in -f file (default: 0 (off))
764 --scm => print SCM tree(s) if any
765 --status => print status if any
766 --subsystem => print subsystem name if any
767 --web => print website(s) if any
770 --separator [, ] => separator for multiple entries on 1 line
771 using --separator also sets --nomultiline if --separator is not [, ]
772 --multiline => print 1 entry per line
775 --pattern-depth => Number of pattern directory traversals (default: 0 (all))
776 --keywords => scan patch for keywords (default: $keywords)
777 --sections => print all of the subsystem sections with pattern matches
778 --mailmap => use .mailmap file (default: $email_use_mailmap)
779 --version => show version
780 --help => show this help information
783 [--email --nogit --git-fallback --m --n --l --multiline -pattern-depth=0
784 --remove-duplicates --rolestats]
787 Using "-f directory" may give unexpected results:
788 Used with "--git", git signators for _all_ files in and below
789 directory are examined as git recurses directories.
790 Any specified X: (exclude) pattern matches are _not_ ignored.
791 Used with "--nogit", directory is used as a pattern match,
792 no individual file within the directory or subdirectory
794 Used with "--git-blame", does not iterate all files in directory
795 Using "--git-blame" is slow and may add old committers and authors
796 that are no longer active maintainers to the output.
797 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
798 other automated tools that expect only ["name"] <email address>
799 may not work because of additional output after <email address>.
800 Using "--rolestats" and "--git-blame" shows the #/total=% commits,
801 not the percentage of the entire file authored. # of commits is
802 not a good measure of amount of code authored. 1 major commit may
803 contain a thousand lines, 5 trivial commits may modify a single line.
804 If git is not installed, but mercurial (hg) is installed and an .hg
805 repository exists, the following options apply to mercurial:
807 --git-min-signatures, --git-max-maintainers, --git-min-percent, and
809 Use --hg-since not --git-since to control date selection
810 File ".get_maintainer.conf", if it exists in the linux kernel source root
811 directory, can change whatever get_maintainer defaults are desired.
812 Entries in this file can be any command line argument.
813 This file is prepended to any additional command line arguments.
814 Multiple lines and # comments are allowed.
818 sub top_of_kernel_tree {
821 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
824 if ( (-f "${lk_path}COPYING")
825 && (-f "${lk_path}CREDITS")
826 && (-f "${lk_path}Kbuild")
827 && (-f "${lk_path}MAINTAINERS")
828 && (-f "${lk_path}Makefile")
829 && (-f "${lk_path}README")
830 && (-d "${lk_path}Documentation")
831 && (-d "${lk_path}arch")
832 && (-d "${lk_path}include")
833 && (-d "${lk_path}drivers")
834 && (-d "${lk_path}fs")
835 && (-d "${lk_path}init")
836 && (-d "${lk_path}ipc")
837 && (-d "${lk_path}kernel")
838 && (-d "${lk_path}lib")
839 && (-d "${lk_path}scripts")) {
846 my ($formatted_email) = @_;
851 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
854 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
856 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
860 $name =~ s/^\s+|\s+$//g;
861 $name =~ s/^\"|\"$//g;
862 $address =~ s/^\s+|\s+$//g;
864 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
865 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
869 return ($name, $address);
873 my ($name, $address, $usename) = @_;
877 $name =~ s/^\s+|\s+$//g;
878 $name =~ s/^\"|\"$//g;
879 $address =~ s/^\s+|\s+$//g;
881 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
882 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
888 $formatted_email = "$address";
890 $formatted_email = "$name <$address>";
893 $formatted_email = $address;
896 return $formatted_email;
899 sub find_first_section {
902 while ($index < @typevalue) {
903 my $tv = $typevalue[$index];
904 if (($tv =~ m/^(\C):\s*(.*)/)) {
913 sub find_starting_index {
917 my $tv = $typevalue[$index];
918 if (!($tv =~ m/^(\C):\s*(.*)/)) {
927 sub find_ending_index {
930 while ($index < @typevalue) {
931 my $tv = $typevalue[$index];
932 if (!($tv =~ m/^(\C):\s*(.*)/)) {
941 sub get_maintainer_role {
945 my $start = find_starting_index($index);
946 my $end = find_ending_index($index);
948 my $role = "unknown";
949 my $subsystem = $typevalue[$start];
950 if (length($subsystem) > 20) {
951 $subsystem = substr($subsystem, 0, 17);
952 $subsystem =~ s/\s*$//;
953 $subsystem = $subsystem . "...";
956 for ($i = $start + 1; $i < $end; $i++) {
957 my $tv = $typevalue[$i];
958 if ($tv =~ m/^(\C):\s*(.*)/) {
968 if ($role eq "supported") {
970 } elsif ($role eq "maintained") {
971 $role = "maintainer";
972 } elsif ($role eq "odd fixes") {
974 } elsif ($role eq "orphan") {
975 $role = "orphan minder";
976 } elsif ($role eq "obsolete") {
977 $role = "obsolete minder";
978 } elsif ($role eq "buried alive in reporters") {
979 $role = "chief penguin";
982 return $role . ":" . $subsystem;
989 my $start = find_starting_index($index);
990 my $end = find_ending_index($index);
992 my $subsystem = $typevalue[$start];
993 if (length($subsystem) > 20) {
994 $subsystem = substr($subsystem, 0, 17);
995 $subsystem =~ s/\s*$//;
996 $subsystem = $subsystem . "...";
999 if ($subsystem eq "THE REST") {
1006 sub add_categories {
1010 my $start = find_starting_index($index);
1011 my $end = find_ending_index($index);
1013 push(@subsystem, $typevalue[$start]);
1015 for ($i = $start + 1; $i < $end; $i++) {
1016 my $tv = $typevalue[$i];
1017 if ($tv =~ m/^(\C):\s*(.*)/) {
1020 if ($ptype eq "L") {
1021 my $list_address = $pvalue;
1022 my $list_additional = "";
1023 my $list_role = get_list_role($i);
1025 if ($list_role ne "") {
1026 $list_role = ":" . $list_role;
1028 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
1030 $list_additional = $2;
1032 if ($list_additional =~ m/subscribers-only/) {
1033 if ($email_subscriber_list) {
1034 if (!$hash_list_to{lc($list_address)}) {
1035 $hash_list_to{lc($list_address)} = 1;
1036 push(@list_to, [$list_address,
1037 "subscriber list${list_role}"]);
1042 if (!$hash_list_to{lc($list_address)}) {
1043 $hash_list_to{lc($list_address)} = 1;
1044 if ($list_additional =~ m/moderated/) {
1045 push(@list_to, [$list_address,
1046 "moderated list${list_role}"]);
1048 push(@list_to, [$list_address,
1049 "open list${list_role}"]);
1054 } elsif ($ptype eq "M") {
1055 my ($name, $address) = parse_email($pvalue);
1058 my $tv = $typevalue[$i - 1];
1059 if ($tv =~ m/^(\C):\s*(.*)/) {
1062 $pvalue = format_email($name, $address, $email_usename);
1067 if ($email_maintainer) {
1068 my $role = get_maintainer_role($i);
1069 push_email_addresses($pvalue, $role);
1071 } elsif ($ptype eq "R") {
1072 my ($name, $address) = parse_email($pvalue);
1075 my $tv = $typevalue[$i - 1];
1076 if ($tv =~ m/^(\C):\s*(.*)/) {
1079 $pvalue = format_email($name, $address, $email_usename);
1084 if ($email_reviewer) {
1085 push_email_addresses($pvalue, 'reviewer');
1087 } elsif ($ptype eq "T") {
1088 push(@scm, $pvalue);
1089 } elsif ($ptype eq "W") {
1090 push(@web, $pvalue);
1091 } elsif ($ptype eq "S") {
1092 push(@status, $pvalue);
1099 my ($name, $address) = @_;
1101 return 1 if (($name eq "") && ($address eq ""));
1102 return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
1103 return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
1108 sub push_email_address {
1109 my ($line, $role) = @_;
1111 my ($name, $address) = parse_email($line);
1113 if ($address eq "") {
1117 if (!$email_remove_duplicates) {
1118 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1119 } elsif (!email_inuse($name, $address)) {
1120 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1121 $email_hash_name{lc($name)}++ if ($name ne "");
1122 $email_hash_address{lc($address)}++;
1128 sub push_email_addresses {
1129 my ($address, $role) = @_;
1131 my @address_list = ();
1133 if (rfc822_valid($address)) {
1134 push_email_address($address, $role);
1135 } elsif (@address_list = rfc822_validlist($address)) {
1136 my $array_count = shift(@address_list);
1137 while (my $entry = shift(@address_list)) {
1138 push_email_address($entry, $role);
1141 if (!push_email_address($address, $role)) {
1142 warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1148 my ($line, $role) = @_;
1150 my ($name, $address) = parse_email($line);
1151 my $email = format_email($name, $address, $email_usename);
1153 foreach my $entry (@email_to) {
1154 if ($email_remove_duplicates) {
1155 my ($entry_name, $entry_address) = parse_email($entry->[0]);
1156 if (($name eq $entry_name || $address eq $entry_address)
1157 && ($role eq "" || !($entry->[1] =~ m/$role/))
1159 if ($entry->[1] eq "") {
1160 $entry->[1] = "$role";
1162 $entry->[1] = "$entry->[1],$role";
1166 if ($email eq $entry->[0]
1167 && ($role eq "" || !($entry->[1] =~ m/$role/))
1169 if ($entry->[1] eq "") {
1170 $entry->[1] = "$role";
1172 $entry->[1] = "$entry->[1],$role";
1182 foreach my $path (split(/:/, $ENV{PATH})) {
1183 if (-e "$path/$bin") {
1184 return "$path/$bin";
1194 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1195 if (-e "$path/$conf") {
1196 return "$path/$conf";
1206 my ($name, $address) = parse_email($line);
1207 my $email = format_email($name, $address, 1);
1208 my $real_name = $name;
1209 my $real_address = $address;
1211 if (exists $mailmap->{names}->{$email} ||
1212 exists $mailmap->{addresses}->{$email}) {
1213 if (exists $mailmap->{names}->{$email}) {
1214 $real_name = $mailmap->{names}->{$email};
1216 if (exists $mailmap->{addresses}->{$email}) {
1217 $real_address = $mailmap->{addresses}->{$email};
1220 if (exists $mailmap->{names}->{$address}) {
1221 $real_name = $mailmap->{names}->{$address};
1223 if (exists $mailmap->{addresses}->{$address}) {
1224 $real_address = $mailmap->{addresses}->{$address};
1227 return format_email($real_name, $real_address, 1);
1231 my (@addresses) = @_;
1233 my @mapped_emails = ();
1234 foreach my $line (@addresses) {
1235 push(@mapped_emails, mailmap_email($line));
1237 merge_by_realname(@mapped_emails) if ($email_use_mailmap);
1238 return @mapped_emails;
1241 sub merge_by_realname {
1245 foreach my $email (@emails) {
1246 my ($name, $address) = parse_email($email);
1247 if (exists $address_map{$name}) {
1248 $address = $address_map{$name};
1249 $email = format_email($name, $address, 1);
1251 $address_map{$name} = $address;
1256 sub git_execute_cmd {
1260 my $output = `$cmd`;
1261 $output =~ s/^\s*//gm;
1262 @lines = split("\n", $output);
1267 sub hg_execute_cmd {
1271 my $output = `$cmd`;
1272 @lines = split("\n", $output);
1277 sub extract_formatted_signatures {
1278 my (@signature_lines) = @_;
1280 my @type = @signature_lines;
1282 s/\s*(.*):.*/$1/ for (@type);
1285 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1287 ## Reformat email addresses (with names) to avoid badly written signatures
1289 foreach my $signer (@signature_lines) {
1290 $signer = deduplicate_email($signer);
1293 return (\@type, \@signature_lines);
1296 sub vcs_find_signers {
1297 my ($cmd, $file) = @_;
1300 my @signatures = ();
1304 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1306 my $pattern = $VCS_cmds{"commit_pattern"};
1307 my $author_pattern = $VCS_cmds{"author_pattern"};
1308 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1310 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1312 $commits = grep(/$pattern/, @lines); # of commits
1314 @authors = grep(/$author_pattern/, @lines);
1315 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1316 @stats = grep(/$stat_pattern/, @lines);
1318 # print("stats: <@stats>\n");
1320 return (0, \@signatures, \@authors, \@stats) if !@signatures;
1322 save_commits_by_author(@lines) if ($interactive);
1323 save_commits_by_signer(@lines) if ($interactive);
1325 if (!$email_git_penguin_chiefs) {
1326 @signatures = grep(!/${penguin_chiefs}/i, @signatures);
1329 my ($author_ref, $authors_ref) = extract_formatted_signatures(@authors);
1330 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1332 return ($commits, $signers_ref, $authors_ref, \@stats);
1335 sub vcs_find_author {
1339 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1341 if (!$email_git_penguin_chiefs) {
1342 @lines = grep(!/${penguin_chiefs}/i, @lines);
1345 return @lines if !@lines;
1348 foreach my $line (@lines) {
1349 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1351 my ($name, $address) = parse_email($author);
1352 $author = format_email($name, $address, 1);
1353 push(@authors, $author);
1357 save_commits_by_author(@lines) if ($interactive);
1358 save_commits_by_signer(@lines) if ($interactive);
1363 sub vcs_save_commits {
1368 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1370 foreach my $line (@lines) {
1371 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1384 return @commits if (!(-f $file));
1386 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1387 my @all_commits = ();
1389 $cmd = $VCS_cmds{"blame_file_cmd"};
1390 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1391 @all_commits = vcs_save_commits($cmd);
1393 foreach my $file_range_diff (@range) {
1394 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1396 my $diff_start = $2;
1397 my $diff_length = $3;
1398 next if ("$file" ne "$diff_file");
1399 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1400 push(@commits, $all_commits[$i]);
1404 foreach my $file_range_diff (@range) {
1405 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1407 my $diff_start = $2;
1408 my $diff_length = $3;
1409 next if ("$file" ne "$diff_file");
1410 $cmd = $VCS_cmds{"blame_range_cmd"};
1411 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1412 push(@commits, vcs_save_commits($cmd));
1415 $cmd = $VCS_cmds{"blame_file_cmd"};
1416 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1417 @commits = vcs_save_commits($cmd);
1420 foreach my $commit (@commits) {
1421 $commit =~ s/^\^//g;
1427 my $printed_novcs = 0;
1429 %VCS_cmds = %VCS_cmds_git;
1430 return 1 if eval $VCS_cmds{"available"};
1431 %VCS_cmds = %VCS_cmds_hg;
1432 return 2 if eval $VCS_cmds{"available"};
1434 if (!$printed_novcs) {
1435 warn("$P: No supported VCS found. Add --nogit to options?\n");
1436 warn("Using a git repository produces better results.\n");
1437 warn("Try Linus Torvalds' latest git repository using:\n");
1438 warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n");
1446 return $vcs_used == 1;
1450 return $vcs_used == 2;
1453 sub interactive_get_maintainers {
1454 my ($list_ref) = @_;
1455 my @list = @$list_ref;
1464 foreach my $entry (@list) {
1465 $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
1466 $selected{$count} = 1;
1467 $authored{$count} = 0;
1468 $signed{$count} = 0;
1474 my $print_options = 0;
1479 printf STDERR "\n%1s %2s %-65s",
1480 "*", "#", "email/list and role:stats";
1482 ($email_git_fallback && !$maintained) ||
1484 print STDERR "auth sign";
1487 foreach my $entry (@list) {
1488 my $email = $entry->[0];
1489 my $role = $entry->[1];
1491 $sel = "*" if ($selected{$count});
1492 my $commit_author = $commit_author_hash{$email};
1493 my $commit_signer = $commit_signer_hash{$email};
1496 $authored++ for (@{$commit_author});
1497 $signed++ for (@{$commit_signer});
1498 printf STDERR "%1s %2d %-65s", $sel, $count + 1, $email;
1499 printf STDERR "%4d %4d", $authored, $signed
1500 if ($authored > 0 || $signed > 0);
1501 printf STDERR "\n %s\n", $role;
1502 if ($authored{$count}) {
1503 my $commit_author = $commit_author_hash{$email};
1504 foreach my $ref (@{$commit_author}) {
1505 print STDERR " Author: @{$ref}[1]\n";
1508 if ($signed{$count}) {
1509 my $commit_signer = $commit_signer_hash{$email};
1510 foreach my $ref (@{$commit_signer}) {
1511 print STDERR " @{$ref}[2]: @{$ref}[1]\n";
1518 my $date_ref = \$email_git_since;
1519 $date_ref = \$email_hg_since if (vcs_is_hg());
1520 if ($print_options) {
1525 Version Control options:
1526 g use git history [$email_git]
1527 gf use git-fallback [$email_git_fallback]
1528 b use git blame [$email_git_blame]
1529 bs use blame signatures [$email_git_blame_signatures]
1530 c# minimum commits [$email_git_min_signatures]
1531 %# min percent [$email_git_min_percent]
1532 d# history to use [$$date_ref]
1533 x# max maintainers [$email_git_max_maintainers]
1534 t all signature types [$email_git_all_signature_types]
1535 m use .mailmap [$email_use_mailmap]
1542 tm toggle maintainers
1543 tg toggle git entries
1544 tl toggle open list entries
1545 ts toggle subscriber list entries
1546 f emails in file [$file_emails]
1547 k keywords in file [$keywords]
1548 r remove duplicates [$email_remove_duplicates]
1549 p# pattern match depth [$pattern_depth]
1553 "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1555 my $input = <STDIN>;
1560 my @wish = split(/[, ]+/, $input);
1561 foreach my $nr (@wish) {
1563 my $sel = substr($nr, 0, 1);
1564 my $str = substr($nr, 1);
1566 $val = $1 if $str =~ /^(\d+)$/;
1571 $output_rolestats = 0;
1574 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1575 $selected{$nr - 1} = !$selected{$nr - 1};
1576 } elsif ($sel eq "*" || $sel eq '^') {
1578 $toggle = 1 if ($sel eq '*');
1579 for (my $i = 0; $i < $count; $i++) {
1580 $selected{$i} = $toggle;
1582 } elsif ($sel eq "0") {
1583 for (my $i = 0; $i < $count; $i++) {
1584 $selected{$i} = !$selected{$i};
1586 } elsif ($sel eq "t") {
1587 if (lc($str) eq "m") {
1588 for (my $i = 0; $i < $count; $i++) {
1589 $selected{$i} = !$selected{$i}
1590 if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
1592 } elsif (lc($str) eq "g") {
1593 for (my $i = 0; $i < $count; $i++) {
1594 $selected{$i} = !$selected{$i}
1595 if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
1597 } elsif (lc($str) eq "l") {
1598 for (my $i = 0; $i < $count; $i++) {
1599 $selected{$i} = !$selected{$i}
1600 if ($list[$i]->[1] =~ /^(open list)/i);
1602 } elsif (lc($str) eq "s") {
1603 for (my $i = 0; $i < $count; $i++) {
1604 $selected{$i} = !$selected{$i}
1605 if ($list[$i]->[1] =~ /^(subscriber list)/i);
1608 } elsif ($sel eq "a") {
1609 if ($val > 0 && $val <= $count) {
1610 $authored{$val - 1} = !$authored{$val - 1};
1611 } elsif ($str eq '*' || $str eq '^') {
1613 $toggle = 1 if ($str eq '*');
1614 for (my $i = 0; $i < $count; $i++) {
1615 $authored{$i} = $toggle;
1618 } elsif ($sel eq "s") {
1619 if ($val > 0 && $val <= $count) {
1620 $signed{$val - 1} = !$signed{$val - 1};
1621 } elsif ($str eq '*' || $str eq '^') {
1623 $toggle = 1 if ($str eq '*');
1624 for (my $i = 0; $i < $count; $i++) {
1625 $signed{$i} = $toggle;
1628 } elsif ($sel eq "o") {
1631 } elsif ($sel eq "g") {
1633 bool_invert(\$email_git_fallback);
1635 bool_invert(\$email_git);
1638 } elsif ($sel eq "b") {
1640 bool_invert(\$email_git_blame_signatures);
1642 bool_invert(\$email_git_blame);
1645 } elsif ($sel eq "c") {
1647 $email_git_min_signatures = $val;
1650 } elsif ($sel eq "x") {
1652 $email_git_max_maintainers = $val;
1655 } elsif ($sel eq "%") {
1656 if ($str ne "" && $val >= 0) {
1657 $email_git_min_percent = $val;
1660 } elsif ($sel eq "d") {
1662 $email_git_since = $str;
1663 } elsif (vcs_is_hg()) {
1664 $email_hg_since = $str;
1667 } elsif ($sel eq "t") {
1668 bool_invert(\$email_git_all_signature_types);
1670 } elsif ($sel eq "f") {
1671 bool_invert(\$file_emails);
1673 } elsif ($sel eq "r") {
1674 bool_invert(\$email_remove_duplicates);
1676 } elsif ($sel eq "m") {
1677 bool_invert(\$email_use_mailmap);
1680 } elsif ($sel eq "k") {
1681 bool_invert(\$keywords);
1683 } elsif ($sel eq "p") {
1684 if ($str ne "" && $val >= 0) {
1685 $pattern_depth = $val;
1688 } elsif ($sel eq "h" || $sel eq "?") {
1691 Interactive mode allows you to select the various maintainers, submitters,
1692 commit signers and mailing lists that could be CC'd on a patch.
1694 Any *'d entry is selected.
1696 If you have git or hg installed, you can choose to summarize the commit
1697 history of files in the patch. Also, each line of the current file can
1698 be matched to its commit author and that commits signers with blame.
1700 Various knobs exist to control the length of time for active commit
1701 tracking, the maximum number of commit authors and signers to add,
1704 Enter selections at the prompt until you are satisfied that the selected
1705 maintainers are appropriate. You may enter multiple selections separated
1706 by either commas or spaces.
1710 print STDERR "invalid option: '$nr'\n";
1715 print STDERR "git-blame can be very slow, please have patience..."
1716 if ($email_git_blame);
1717 goto &get_maintainers;
1721 #drop not selected entries
1723 my @new_emailto = ();
1724 foreach my $entry (@list) {
1725 if ($selected{$count}) {
1726 push(@new_emailto, $list[$count]);
1730 return @new_emailto;
1734 my ($bool_ref) = @_;
1743 sub deduplicate_email {
1747 my ($name, $address) = parse_email($email);
1748 $email = format_email($name, $address, 1);
1749 $email = mailmap_email($email);
1751 return $email if (!$email_remove_duplicates);
1753 ($name, $address) = parse_email($email);
1755 if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
1756 $name = $deduplicate_name_hash{lc($name)}->[0];
1757 $address = $deduplicate_name_hash{lc($name)}->[1];
1759 } elsif ($deduplicate_address_hash{lc($address)}) {
1760 $name = $deduplicate_address_hash{lc($address)}->[0];
1761 $address = $deduplicate_address_hash{lc($address)}->[1];
1765 $deduplicate_name_hash{lc($name)} = [ $name, $address ];
1766 $deduplicate_address_hash{lc($address)} = [ $name, $address ];
1768 $email = format_email($name, $address, 1);
1769 $email = mailmap_email($email);
1773 sub save_commits_by_author {
1780 foreach my $line (@lines) {
1781 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1783 $author = deduplicate_email($author);
1784 push(@authors, $author);
1786 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1787 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1790 for (my $i = 0; $i < @authors; $i++) {
1792 foreach my $ref(@{$commit_author_hash{$authors[$i]}}) {
1793 if (@{$ref}[0] eq $commits[$i] &&
1794 @{$ref}[1] eq $subjects[$i]) {
1800 push(@{$commit_author_hash{$authors[$i]}},
1801 [ ($commits[$i], $subjects[$i]) ]);
1806 sub save_commits_by_signer {
1812 foreach my $line (@lines) {
1813 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1814 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1815 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
1816 my @signatures = ($line);
1817 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1818 my @types = @$types_ref;
1819 my @signers = @$signers_ref;
1821 my $type = $types[0];
1822 my $signer = $signers[0];
1824 $signer = deduplicate_email($signer);
1827 foreach my $ref(@{$commit_signer_hash{$signer}}) {
1828 if (@{$ref}[0] eq $commit &&
1829 @{$ref}[1] eq $subject &&
1830 @{$ref}[2] eq $type) {
1836 push(@{$commit_signer_hash{$signer}},
1837 [ ($commit, $subject, $type) ]);
1844 my ($role, $divisor, @lines) = @_;
1849 return if (@lines <= 0);
1851 if ($divisor <= 0) {
1852 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
1856 @lines = mailmap(@lines);
1858 return if (@lines <= 0);
1860 @lines = sort(@lines);
1863 $hash{$_}++ for @lines;
1866 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
1867 my $sign_offs = $hash{$line};
1868 my $percent = $sign_offs * 100 / $divisor;
1870 $percent = 100 if ($percent > 100);
1872 last if ($sign_offs < $email_git_min_signatures ||
1873 $count > $email_git_max_maintainers ||
1874 $percent < $email_git_min_percent);
1875 push_email_address($line, '');
1876 if ($output_rolestats) {
1877 my $fmt_percent = sprintf("%.0f", $percent);
1878 add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%");
1880 add_role($line, $role);
1885 sub vcs_file_signoffs {
1896 $vcs_used = vcs_exists();
1897 return if (!$vcs_used);
1899 my $cmd = $VCS_cmds{"find_signers_cmd"};
1900 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
1902 ($commits, $signers_ref, $authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
1904 @signers = @{$signers_ref} if defined $signers_ref;
1905 @authors = @{$authors_ref} if defined $authors_ref;
1906 @stats = @{$stats_ref} if defined $stats_ref;
1908 # print("commits: <$commits>\nsigners:<@signers>\nauthors: <@authors>\nstats: <@stats>\n");
1910 foreach my $signer (@signers) {
1911 $signer = deduplicate_email($signer);
1914 vcs_assign("commit_signer", $commits, @signers);
1915 vcs_assign("authored", $commits, @authors);
1916 if ($#authors == $#stats) {
1917 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1918 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1922 for (my $i = 0; $i <= $#stats; $i++) {
1923 if ($stats[$i] =~ /$stat_pattern/) {
1928 my @tmp_authors = uniq(@authors);
1929 foreach my $author (@tmp_authors) {
1930 $author = deduplicate_email($author);
1932 @tmp_authors = uniq(@tmp_authors);
1933 my @list_added = ();
1934 my @list_deleted = ();
1935 foreach my $author (@tmp_authors) {
1937 my $auth_deleted = 0;
1938 for (my $i = 0; $i <= $#stats; $i++) {
1939 if ($author eq deduplicate_email($authors[$i]) &&
1940 $stats[$i] =~ /$stat_pattern/) {
1942 $auth_deleted += $2;
1945 for (my $i = 0; $i < $auth_added; $i++) {
1946 push(@list_added, $author);
1948 for (my $i = 0; $i < $auth_deleted; $i++) {
1949 push(@list_deleted, $author);
1952 vcs_assign("added_lines", $added, @list_added);
1953 vcs_assign("removed_lines", $deleted, @list_deleted);
1957 sub vcs_file_blame {
1961 my @all_commits = ();
1966 $vcs_used = vcs_exists();
1967 return if (!$vcs_used);
1969 @all_commits = vcs_blame($file);
1970 @commits = uniq(@all_commits);
1971 $total_commits = @commits;
1972 $total_lines = @all_commits;
1974 if ($email_git_blame_signatures) {
1977 my $commit_authors_ref;
1978 my $commit_signers_ref;
1980 my @commit_authors = ();
1981 my @commit_signers = ();
1982 my $commit = join(" -r ", @commits);
1985 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1986 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1988 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
1989 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
1990 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
1992 push(@signers, @commit_signers);
1994 foreach my $commit (@commits) {
1996 my $commit_authors_ref;
1997 my $commit_signers_ref;
1999 my @commit_authors = ();
2000 my @commit_signers = ();
2003 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2004 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2006 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2007 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2008 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2010 push(@signers, @commit_signers);
2015 if ($from_filename) {
2016 if ($output_rolestats) {
2018 if (vcs_is_hg()) {{ # Double brace for last exit
2020 my @commit_signers = ();
2021 @commits = uniq(@commits);
2022 @commits = sort(@commits);
2023 my $commit = join(" -r ", @commits);
2026 $cmd = $VCS_cmds{"find_commit_author_cmd"};
2027 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2031 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
2033 if (!$email_git_penguin_chiefs) {
2034 @lines = grep(!/${penguin_chiefs}/i, @lines);
2040 foreach my $line (@lines) {
2041 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2043 $author = deduplicate_email($author);
2044 push(@authors, $author);
2048 save_commits_by_author(@lines) if ($interactive);
2049 save_commits_by_signer(@lines) if ($interactive);
2051 push(@signers, @authors);
2054 foreach my $commit (@commits) {
2056 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
2057 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
2058 my @author = vcs_find_author($cmd);
2061 my $formatted_author = deduplicate_email($author[0]);
2063 my $count = grep(/$commit/, @all_commits);
2064 for ($i = 0; $i < $count ; $i++) {
2065 push(@blame_signers, $formatted_author);
2069 if (@blame_signers) {
2070 vcs_assign("authored lines", $total_lines, @blame_signers);
2073 foreach my $signer (@signers) {
2074 $signer = deduplicate_email($signer);
2076 vcs_assign("commits", $total_commits, @signers);
2078 foreach my $signer (@signers) {
2079 $signer = deduplicate_email($signer);
2081 vcs_assign("modified commits", $total_commits, @signers);
2089 @parms = grep(!$saw{$_}++, @parms);
2097 @parms = sort @parms;
2098 @parms = grep(!$saw{$_}++, @parms);
2102 sub clean_file_emails {
2103 my (@file_emails) = @_;
2104 my @fmt_emails = ();
2106 foreach my $email (@file_emails) {
2107 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
2108 my ($name, $address) = parse_email($email);
2109 if ($name eq '"[,\.]"') {
2113 my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name);
2115 my $first = $nw[@nw - 3];
2116 my $middle = $nw[@nw - 2];
2117 my $last = $nw[@nw - 1];
2119 if (((length($first) == 1 && $first =~ m/[A-Za-z]/) ||
2120 (length($first) == 2 && substr($first, -1) eq ".")) ||
2121 (length($middle) == 1 ||
2122 (length($middle) == 2 && substr($middle, -1) eq "."))) {
2123 $name = "$first $middle $last";
2125 $name = "$middle $last";
2129 if (substr($name, -1) =~ /[,\.]/) {
2130 $name = substr($name, 0, length($name) - 1);
2131 } elsif (substr($name, -2) =~ /[,\.]"/) {
2132 $name = substr($name, 0, length($name) - 2) . '"';
2135 if (substr($name, 0, 1) =~ /[,\.]/) {
2136 $name = substr($name, 1, length($name) - 1);
2137 } elsif (substr($name, 0, 2) =~ /"[,\.]/) {
2138 $name = '"' . substr($name, 2, length($name) - 2);
2141 my $fmt_email = format_email($name, $address, $email_usename);
2142 push(@fmt_emails, $fmt_email);
2152 my ($address, $role) = @$_;
2153 if (!$saw{$address}) {
2154 if ($output_roles) {
2155 push(@lines, "$address ($role)");
2157 push(@lines, $address);
2169 if ($output_multiline) {
2170 foreach my $line (@parms) {
2174 print(join($output_separator, @parms));
2182 # Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
2183 # comment. We must allow for rfc822_lwsp (or comments) after each of these.
2184 # This regexp will only work on addresses which have had comments stripped
2185 # and replaced with rfc822_lwsp.
2187 my $specials = '()<>@,;:\\\\".\\[\\]';
2188 my $controls = '\\000-\\037\\177';
2190 my $dtext = "[^\\[\\]\\r\\\\]";
2191 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
2193 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
2195 # Use zero-width assertion to spot the limit of an atom. A simple
2196 # $rfc822_lwsp* causes the regexp engine to hang occasionally.
2197 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
2198 my $word = "(?:$atom|$quoted_string)";
2199 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
2201 my $sub_domain = "(?:$atom|$domain_literal)";
2202 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
2204 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
2206 my $phrase = "$word*";
2207 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
2208 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
2209 my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
2211 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
2212 my $address = "(?:$mailbox|$group)";
2214 return "$rfc822_lwsp*$address";
2217 sub rfc822_strip_comments {
2219 # Recursively remove comments, and replace with a single space. The simpler
2220 # regexps in the Email Addressing FAQ are imperfect - they will miss escaped
2221 # chars in atoms, for example.
2223 while ($s =~ s/^((?:[^"\\]|\\.)*
2224 (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
2225 \((?:[^()\\]|\\.)*\)/$1 /osx) {}
2229 # valid: returns true if the parameter is an RFC822 valid address
2232 my $s = rfc822_strip_comments(shift);
2235 $rfc822re = make_rfc822re();
2238 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
2241 # validlist: In scalar context, returns true if the parameter is an RFC822
2242 # valid list of addresses.
2244 # In list context, returns an empty list on failure (an invalid
2245 # address was found); otherwise a list whose first element is the
2246 # number of addresses found and whose remaining elements are the
2247 # addresses. This is needed to disambiguate failure (invalid)
2248 # from success with no addresses found, because an empty string is
2251 sub rfc822_validlist {
2252 my $s = rfc822_strip_comments(shift);
2255 $rfc822re = make_rfc822re();
2257 # * null list items are valid according to the RFC
2258 # * the '1' business is to aid in distinguishing failure from no results
2261 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
2262 $s =~ m/^$rfc822_char*$/) {
2263 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
2266 return wantarray ? (scalar(@r), @r) : 1;
2268 return wantarray ? () : 0;