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);
23 my $email_usename = 1;
24 my $email_maintainer = 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, "Tom Rini:trini\@ti.com");
67 my @penguin_chief_names = ();
68 foreach my $chief (@penguin_chief) {
69 if ($chief =~ m/^(.*):(.*)/) {
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 my $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
87 # rfc822 email address - preloaded methods go here.
88 my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
89 my $rfc822_char = '[\\000-\\377]';
91 # VCS command support: class-like functions and strings
96 "execute_cmd" => \&git_execute_cmd,
97 "available" => '(which("git") ne "") && (-e ".git")',
99 "git log --no-color --follow --since=\$email_git_since " .
100 '--numstat --no-merges ' .
101 '--format="GitCommit: %H%n' .
102 'GitAuthor: %an <%ae>%n' .
107 "find_commit_signers_cmd" =>
108 "git log --no-color " .
110 '--format="GitCommit: %H%n' .
111 'GitAuthor: %an <%ae>%n' .
116 "find_commit_author_cmd" =>
117 "git log --no-color " .
119 '--format="GitCommit: %H%n' .
120 'GitAuthor: %an <%ae>%n' .
122 'GitSubject: %s%n"' .
124 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
125 "blame_file_cmd" => "git blame -l \$file",
126 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})",
127 "blame_commit_pattern" => "^([0-9a-f]+) ",
128 "author_pattern" => "^GitAuthor: (.*)",
129 "subject_pattern" => "^GitSubject: (.*)",
130 "stat_pattern" => "^(\\d+)\\t(\\d+)\\t\$file\$",
134 "execute_cmd" => \&hg_execute_cmd,
135 "available" => '(which("hg") ne "") && (-d ".hg")',
136 "find_signers_cmd" =>
137 "hg log --date=\$email_hg_since " .
138 "--template='HgCommit: {node}\\n" .
139 "HgAuthor: {author}\\n" .
140 "HgSubject: {desc}\\n'" .
142 "find_commit_signers_cmd" =>
144 "--template='HgSubject: {desc}\\n'" .
146 "find_commit_author_cmd" =>
148 "--template='HgCommit: {node}\\n" .
149 "HgAuthor: {author}\\n" .
150 "HgSubject: {desc|firstline}\\n'" .
152 "blame_range_cmd" => "", # not supported
153 "blame_file_cmd" => "hg blame -n \$file",
154 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})",
155 "blame_commit_pattern" => "^([ 0-9a-f]+):",
156 "author_pattern" => "^HgAuthor: (.*)",
157 "subject_pattern" => "^HgSubject: (.*)",
158 "stat_pattern" => "^(\\d+)\t(\\d+)\t\$file\$",
161 my $conf = which_conf(".get_maintainer.conf");
164 open(my $conffile, '<', "$conf")
165 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
167 while (<$conffile>) {
170 $line =~ s/\s*\n?$//g;
174 next if ($line =~ m/^\s*#/);
175 next if ($line =~ m/^\s*$/);
177 my @words = split(" ", $line);
178 foreach my $word (@words) {
179 last if ($word =~ m/^#/);
180 push (@conf_args, $word);
184 unshift(@ARGV, @conf_args) if @conf_args;
189 'git!' => \$email_git,
190 'git-all-signature-types!' => \$email_git_all_signature_types,
191 'git-blame!' => \$email_git_blame,
192 'git-blame-signatures!' => \$email_git_blame_signatures,
193 'git-fallback!' => \$email_git_fallback,
194 'git-chief-penguins!' => \$email_git_penguin_chiefs,
195 'git-min-signatures=i' => \$email_git_min_signatures,
196 'git-max-maintainers=i' => \$email_git_max_maintainers,
197 'git-min-percent=i' => \$email_git_min_percent,
198 'git-since=s' => \$email_git_since,
199 'hg-since=s' => \$email_hg_since,
200 'i|interactive!' => \$interactive,
201 'remove-duplicates!' => \$email_remove_duplicates,
202 'mailmap!' => \$email_use_mailmap,
203 'm!' => \$email_maintainer,
204 'n!' => \$email_usename,
205 'l!' => \$email_list,
206 's!' => \$email_subscriber_list,
207 'multiline!' => \$output_multiline,
208 'roles!' => \$output_roles,
209 'rolestats!' => \$output_rolestats,
210 'separator=s' => \$output_separator,
211 'subsystem!' => \$subsystem,
212 'status!' => \$status,
215 'pattern-depth=i' => \$pattern_depth,
216 'k|keywords!' => \$keywords,
217 'sections!' => \$sections,
218 'fe|file-emails!' => \$file_emails,
219 'f|file' => \$from_filename,
220 'v|version' => \$version,
221 'h|help|usage' => \$help,
223 die "$P: invalid argument - use --help if necessary\n";
232 print("${P} ${V}\n");
236 if (-t STDIN && !@ARGV) {
237 # We're talking to a terminal, but have no command line arguments.
238 die "$P: missing patchfile or -f file - use --help if necessary\n";
241 $output_multiline = 0 if ($output_separator ne ", ");
242 $output_rolestats = 1 if ($interactive);
243 $output_roles = 1 if ($output_rolestats);
255 my $selections = $email + $scm + $status + $subsystem + $web;
256 if ($selections == 0) {
257 die "$P: Missing required option: email, scm, status, subsystem or web\n";
262 ($email_maintainer + $email_list + $email_subscriber_list +
263 $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
264 die "$P: Please select at least 1 email option\n";
267 if (!top_of_kernel_tree($lk_path)) {
268 die "$P: The current directory does not appear to be "
269 . "a linux kernel source tree.\n";
272 ## Read MAINTAINERS for type/value pairs
277 my @maint_files = ();
278 push(@maint_files, "${lk_path}MAINTAINERS");
281 return unless $_ =~ /^MAINTAINERS/;
282 push(@maint_files, "$File::Find::name");
285 File::Find::find(\&maint_wanted, "${lk_path}board");
287 foreach my $maint_file (@maint_files) {
289 open ($maint, '<', "$maint_file")
290 or die "$P: Can't open $maint_file: $!\n";
291 read_maintainers($maint);
295 sub read_maintainers {
301 if ($line =~ m/^(\C):\s*(.*)/) {
305 ##Filename pattern matching
306 if ($type eq "F" || $type eq "X") {
307 $value =~ s@\.@\\\.@g; ##Convert . to \.
308 $value =~ s/\*/\.\*/g; ##Convert * to .*
309 $value =~ s/\?/\./g; ##Convert ? to .
310 ##if pattern is a directory and it lacks a trailing slash, add one
312 $value =~ s@([^/])$@$1/@;
314 } elsif ($type eq "K") {
315 $keyword_hash{@typevalue} = $value;
317 push(@typevalue, "$type:$value");
318 } elsif (!/^(\s)*$/) {
320 push(@typevalue, $line);
327 # Read mail address map
340 return if (!$email_use_mailmap || !(-f "${lk_path}.mailmap"));
342 open(my $mailmap_file, '<', "${lk_path}.mailmap")
343 or warn "$P: Can't open .mailmap: $!\n";
345 while (<$mailmap_file>) {
346 s/#.*$//; #strip comments
347 s/^\s+|\s+$//g; #trim
349 next if (/^\s*$/); #skip empty lines
350 #entries have one of the following formats:
353 # name1 <mail1> <mail2>
354 # name1 <mail1> name2 <mail2>
355 # (see man git-shortlog)
357 if (/^([^<]+)<([^>]+)>$/) {
361 $real_name =~ s/\s+$//;
362 ($real_name, $address) = parse_email("$real_name <$address>");
363 $mailmap->{names}->{$address} = $real_name;
365 } elsif (/^<([^>]+)>\s*<([^>]+)>$/) {
366 my $real_address = $1;
367 my $wrong_address = $2;
369 $mailmap->{addresses}->{$wrong_address} = $real_address;
371 } elsif (/^(.+)<([^>]+)>\s*<([^>]+)>$/) {
373 my $real_address = $2;
374 my $wrong_address = $3;
376 $real_name =~ s/\s+$//;
377 ($real_name, $real_address) =
378 parse_email("$real_name <$real_address>");
379 $mailmap->{names}->{$wrong_address} = $real_name;
380 $mailmap->{addresses}->{$wrong_address} = $real_address;
382 } elsif (/^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$/) {
384 my $real_address = $2;
386 my $wrong_address = $4;
388 $real_name =~ s/\s+$//;
389 ($real_name, $real_address) =
390 parse_email("$real_name <$real_address>");
392 $wrong_name =~ s/\s+$//;
393 ($wrong_name, $wrong_address) =
394 parse_email("$wrong_name <$wrong_address>");
396 my $wrong_email = format_email($wrong_name, $wrong_address, 1);
397 $mailmap->{names}->{$wrong_email} = $real_name;
398 $mailmap->{addresses}->{$wrong_email} = $real_address;
401 close($mailmap_file);
404 ## use the filenames on the command line or find the filenames in the patchfiles
408 my @keyword_tvi = ();
409 my @file_emails = ();
412 push(@ARGV, "&STDIN");
415 foreach my $file (@ARGV) {
416 if ($file ne "&STDIN") {
417 ##if $file is a directory and it lacks a trailing slash, add one
419 $file =~ s@([^/])$@$1/@;
420 } elsif (!(-f $file)) {
421 die "$P: file '${file}' not found\n";
424 if ($from_filename) {
426 if ($file ne "MAINTAINERS" && -f $file && ($keywords || $file_emails)) {
427 open(my $f, '<', $file)
428 or die "$P: Can't open $file: $!\n";
429 my $text = do { local($/) ; <$f> };
432 foreach my $line (keys %keyword_hash) {
433 if ($text =~ m/$keyword_hash{$line}/x) {
434 push(@keyword_tvi, $line);
439 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;
440 push(@file_emails, clean_file_emails(@poss_addr));
444 my $file_cnt = @files;
447 open(my $patch, "< $file")
448 or die "$P: Can't open $file: $!\n";
450 # We can check arbitrary information before the patch
451 # like the commit message, mail headers, etc...
452 # This allows us to match arbitrary keywords against any part
453 # of a git format-patch generated file (subject tags, etc...)
455 my $patch_prefix = ""; #Parsing the intro
459 if (m/^\+\+\+\s+(\S+)/ or m/^---\s+(\S+)/) {
461 $filename =~ s@^[^/]*/@@;
463 $lastfile = $filename;
464 push(@files, $filename);
465 $patch_prefix = "^[+-].*"; #Now parsing the actual patch
466 } elsif (m/^\@\@ -(\d+),(\d+)/) {
467 if ($email_git_blame) {
468 push(@range, "$lastfile:$1:$2");
470 } elsif ($keywords) {
471 foreach my $line (keys %keyword_hash) {
472 if ($patch_line =~ m/${patch_prefix}$keyword_hash{$line}/x) {
473 push(@keyword_tvi, $line);
480 if ($file_cnt == @files) {
481 warn "$P: file '${file}' doesn't appear to be a patch. "
482 . "Add -f to options?\n";
484 @files = sort_and_uniq(@files);
488 @file_emails = uniq(@file_emails);
491 my %email_hash_address;
499 my %deduplicate_name_hash = ();
500 my %deduplicate_address_hash = ();
502 my @maintainers = get_maintainers();
505 @maintainers = merge_email(@maintainers);
506 output(@maintainers);
515 @status = uniq(@status);
520 @subsystem = uniq(@subsystem);
531 sub range_is_maintained {
532 my ($start, $end) = @_;
534 for (my $i = $start; $i < $end; $i++) {
535 my $line = $typevalue[$i];
536 if ($line =~ m/^(\C):\s*(.*)/) {
540 if ($value =~ /(maintain|support)/i) {
549 sub range_has_maintainer {
550 my ($start, $end) = @_;
552 for (my $i = $start; $i < $end; $i++) {
553 my $line = $typevalue[$i];
554 if ($line =~ m/^(\C):\s*(.*)/) {
565 sub get_maintainers {
566 %email_hash_name = ();
567 %email_hash_address = ();
568 %commit_author_hash = ();
569 %commit_signer_hash = ();
577 %deduplicate_name_hash = ();
578 %deduplicate_address_hash = ();
579 if ($email_git_all_signature_types) {
580 $signature_pattern = "(.+?)[Bb][Yy]:";
582 $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
585 # Find responsible parties
587 my %exact_pattern_match_hash = ();
589 foreach my $file (@files) {
592 my $tvi = find_first_section();
593 while ($tvi < @typevalue) {
594 my $start = find_starting_index($tvi);
595 my $end = find_ending_index($tvi);
599 #Do not match excluded file patterns
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)) {
616 for ($i = $start; $i < $end; $i++) {
617 my $line = $typevalue[$i];
618 if ($line =~ m/^(\C):\s*(.*)/) {
622 if (file_match_pattern($file, $value)) {
623 my $value_pd = ($value =~ tr@/@@);
624 my $file_pd = ($file =~ tr@/@@);
625 $value_pd++ if (substr($value,-1,1) ne "/");
626 $value_pd = -1 if ($value =~ /^\.\*/);
627 if ($value_pd >= $file_pd &&
628 range_is_maintained($start, $end) &&
629 range_has_maintainer($start, $end)) {
630 $exact_pattern_match_hash{$file} = 1;
632 if ($pattern_depth == 0 ||
633 (($file_pd - $value_pd) < $pattern_depth)) {
634 $hash{$tvi} = $value_pd;
637 } elsif ($type eq 'N') {
638 if ($file =~ m/$value/x) {
648 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
649 add_categories($line);
652 my $start = find_starting_index($line);
653 my $end = find_ending_index($line);
654 for ($i = $start; $i < $end; $i++) {
655 my $line = $typevalue[$i];
656 if ($line =~ /^[FX]:/) { ##Restore file patterns
657 $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
658 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ?
659 $line =~ s/\\\./\./g; ##Convert \. to .
660 $line =~ s/\.\*/\*/g; ##Convert .* to *
662 $line =~ s/^([A-Z]):/$1:\t/g;
671 @keyword_tvi = sort_and_uniq(@keyword_tvi);
672 foreach my $line (@keyword_tvi) {
673 add_categories($line);
677 foreach my $email (@email_to, @list_to) {
678 $email->[0] = deduplicate_email($email->[0]);
681 foreach my $file (@files) {
683 ($email_git || ($email_git_fallback &&
684 !$exact_pattern_match_hash{$file}))) {
685 vcs_file_signoffs($file);
687 if ($email && $email_git_blame) {
688 vcs_file_blame($file);
693 foreach my $chief (@penguin_chief) {
694 if ($chief =~ m/^(.*):(.*)/) {
697 $email_address = format_email($1, $2, $email_usename);
698 if ($email_git_penguin_chiefs) {
699 push(@email_to, [$email_address, 'chief penguin']);
701 @email_to = grep($_->[0] !~ /${email_address}/, @email_to);
706 foreach my $email (@file_emails) {
707 my ($name, $address) = parse_email($email);
709 my $tmp_email = format_email($name, $address, $email_usename);
710 push_email_address($tmp_email, '');
711 add_role($tmp_email, 'in file');
716 if ($email || $email_list) {
718 @to = (@to, @email_to);
721 @to = (@to, @list_to);
726 @to = interactive_get_maintainers(\@to);
732 sub file_match_pattern {
733 my ($file, $pattern) = @_;
734 if (substr($pattern, -1) eq "/") {
735 if ($file =~ m@^$pattern@) {
739 if ($file =~ m@^$pattern@) {
740 my $s1 = ($file =~ tr@/@@);
741 my $s2 = ($pattern =~ tr@/@@);
752 usage: $P [options] patchfile
753 $P [options] -f file|directory
756 MAINTAINER field selection options:
757 --email => print email address(es) if any
758 --git => include recent git \*-by: signers
759 --git-all-signature-types => include signers regardless of signature type
760 or use only ${signature_pattern} signers (default: $email_git_all_signature_types)
761 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
762 --git-chief-penguins => include ${penguin_chiefs}
763 --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
764 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
765 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
766 --git-blame => use git blame to find modified commits for patch or file
767 --git-since => git history to use (default: $email_git_since)
768 --hg-since => hg history to use (default: $email_hg_since)
769 --interactive => display a menu (mostly useful if used with the --git option)
770 --m => include maintainer(s) if any
771 --n => include name 'Full Name <addr\@domain.tld>'
772 --l => include list(s) if any
773 --s => include subscriber only list(s) if any
774 --remove-duplicates => minimize duplicate email names/addresses
775 --roles => show roles (status:subsystem, git-signer, list, etc...)
776 --rolestats => show roles and statistics (commits/total_commits, %)
777 --file-emails => add email addresses found in -f file (default: 0 (off))
778 --scm => print SCM tree(s) if any
779 --status => print status if any
780 --subsystem => print subsystem name if any
781 --web => print website(s) if any
784 --separator [, ] => separator for multiple entries on 1 line
785 using --separator also sets --nomultiline if --separator is not [, ]
786 --multiline => print 1 entry per line
789 --pattern-depth => Number of pattern directory traversals (default: 0 (all))
790 --keywords => scan patch for keywords (default: $keywords)
791 --sections => print all of the subsystem sections with pattern matches
792 --mailmap => use .mailmap file (default: $email_use_mailmap)
793 --version => show version
794 --help => show this help information
797 [--email --nogit --git-fallback --m --n --l --multiline -pattern-depth=0
798 --remove-duplicates --rolestats]
801 Using "-f directory" may give unexpected results:
802 Used with "--git", git signators for _all_ files in and below
803 directory are examined as git recurses directories.
804 Any specified X: (exclude) pattern matches are _not_ ignored.
805 Used with "--nogit", directory is used as a pattern match,
806 no individual file within the directory or subdirectory
808 Used with "--git-blame", does not iterate all files in directory
809 Using "--git-blame" is slow and may add old committers and authors
810 that are no longer active maintainers to the output.
811 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
812 other automated tools that expect only ["name"] <email address>
813 may not work because of additional output after <email address>.
814 Using "--rolestats" and "--git-blame" shows the #/total=% commits,
815 not the percentage of the entire file authored. # of commits is
816 not a good measure of amount of code authored. 1 major commit may
817 contain a thousand lines, 5 trivial commits may modify a single line.
818 If git is not installed, but mercurial (hg) is installed and an .hg
819 repository exists, the following options apply to mercurial:
821 --git-min-signatures, --git-max-maintainers, --git-min-percent, and
823 Use --hg-since not --git-since to control date selection
824 File ".get_maintainer.conf", if it exists in the linux kernel source root
825 directory, can change whatever get_maintainer defaults are desired.
826 Entries in this file can be any command line argument.
827 This file is prepended to any additional command line arguments.
828 Multiple lines and # comments are allowed.
832 sub top_of_kernel_tree {
835 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
838 if ( (-f "${lk_path}CREDITS")
839 && (-f "${lk_path}Kbuild")
840 && (-f "${lk_path}MAINTAINERS")
841 && (-f "${lk_path}Makefile")
842 && (-f "${lk_path}README")
843 && (-d "${lk_path}arch")
844 && (-d "${lk_path}board")
845 && (-d "${lk_path}common")
846 && (-d "${lk_path}doc")
847 && (-d "${lk_path}drivers")
848 && (-d "${lk_path}dts")
849 && (-d "${lk_path}fs")
850 && (-d "${lk_path}lib")
851 && (-d "${lk_path}include")
852 && (-d "${lk_path}net")
853 && (-d "${lk_path}post")
854 && (-d "${lk_path}scripts")
855 && (-d "${lk_path}test")
856 && (-d "${lk_path}tools")) {
863 my ($formatted_email) = @_;
868 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
871 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
873 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
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
886 return ($name, $address);
890 my ($name, $address, $usename) = @_;
894 $name =~ s/^\s+|\s+$//g;
895 $name =~ s/^\"|\"$//g;
896 $address =~ s/^\s+|\s+$//g;
898 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
899 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
905 $formatted_email = "$address";
907 $formatted_email = "$name <$address>";
910 $formatted_email = $address;
913 return $formatted_email;
916 sub find_first_section {
919 while ($index < @typevalue) {
920 my $tv = $typevalue[$index];
921 if (($tv =~ m/^(\C):\s*(.*)/)) {
930 sub find_starting_index {
934 my $tv = $typevalue[$index];
935 if (!($tv =~ m/^(\C):\s*(.*)/)) {
944 sub find_ending_index {
947 while ($index < @typevalue) {
948 my $tv = $typevalue[$index];
949 if (!($tv =~ m/^(\C):\s*(.*)/)) {
958 sub get_maintainer_role {
962 my $start = find_starting_index($index);
963 my $end = find_ending_index($index);
965 my $role = "unknown";
966 my $subsystem = $typevalue[$start];
967 if (length($subsystem) > 20) {
968 $subsystem = substr($subsystem, 0, 17);
969 $subsystem =~ s/\s*$//;
970 $subsystem = $subsystem . "...";
973 for ($i = $start + 1; $i < $end; $i++) {
974 my $tv = $typevalue[$i];
975 if ($tv =~ m/^(\C):\s*(.*)/) {
985 if ($role eq "supported") {
987 } elsif ($role eq "maintained") {
988 $role = "maintainer";
989 } elsif ($role eq "odd fixes") {
991 } elsif ($role eq "orphan") {
992 $role = "orphan minder";
993 } elsif ($role eq "obsolete") {
994 $role = "obsolete minder";
995 } elsif ($role eq "buried alive in reporters") {
996 $role = "chief penguin";
999 return $role . ":" . $subsystem;
1006 my $start = find_starting_index($index);
1007 my $end = find_ending_index($index);
1009 my $subsystem = $typevalue[$start];
1010 if (length($subsystem) > 20) {
1011 $subsystem = substr($subsystem, 0, 17);
1012 $subsystem =~ s/\s*$//;
1013 $subsystem = $subsystem . "...";
1016 if ($subsystem eq "THE REST") {
1023 sub add_categories {
1027 my $start = find_starting_index($index);
1028 my $end = find_ending_index($index);
1030 push(@subsystem, $typevalue[$start]);
1032 for ($i = $start + 1; $i < $end; $i++) {
1033 my $tv = $typevalue[$i];
1034 if ($tv =~ m/^(\C):\s*(.*)/) {
1037 if ($ptype eq "L") {
1038 my $list_address = $pvalue;
1039 my $list_additional = "";
1040 my $list_role = get_list_role($i);
1042 if ($list_role ne "") {
1043 $list_role = ":" . $list_role;
1045 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
1047 $list_additional = $2;
1049 if ($list_additional =~ m/subscribers-only/) {
1050 if ($email_subscriber_list) {
1051 if (!$hash_list_to{lc($list_address)}) {
1052 $hash_list_to{lc($list_address)} = 1;
1053 push(@list_to, [$list_address,
1054 "subscriber list${list_role}"]);
1059 if (!$hash_list_to{lc($list_address)}) {
1060 $hash_list_to{lc($list_address)} = 1;
1061 if ($list_additional =~ m/moderated/) {
1062 push(@list_to, [$list_address,
1063 "moderated list${list_role}"]);
1065 push(@list_to, [$list_address,
1066 "open list${list_role}"]);
1071 } elsif ($ptype eq "M") {
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_maintainer) {
1085 my $role = get_maintainer_role($i);
1086 push_email_addresses($pvalue, $role);
1088 } elsif ($ptype eq "T") {
1089 push(@scm, $pvalue);
1090 } elsif ($ptype eq "W") {
1091 push(@web, $pvalue);
1092 } elsif ($ptype eq "S") {
1093 push(@status, $pvalue);
1100 my ($name, $address) = @_;
1102 return 1 if (($name eq "") && ($address eq ""));
1103 return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
1104 return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
1109 sub push_email_address {
1110 my ($line, $role) = @_;
1112 my ($name, $address) = parse_email($line);
1114 if ($address eq "") {
1118 if (!$email_remove_duplicates) {
1119 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1120 } elsif (!email_inuse($name, $address)) {
1121 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1122 $email_hash_name{lc($name)}++ if ($name ne "");
1123 $email_hash_address{lc($address)}++;
1129 sub push_email_addresses {
1130 my ($address, $role) = @_;
1132 my @address_list = ();
1134 if (rfc822_valid($address)) {
1135 push_email_address($address, $role);
1136 } elsif (@address_list = rfc822_validlist($address)) {
1137 my $array_count = shift(@address_list);
1138 while (my $entry = shift(@address_list)) {
1139 push_email_address($entry, $role);
1142 if (!push_email_address($address, $role)) {
1143 warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1149 my ($line, $role) = @_;
1151 my ($name, $address) = parse_email($line);
1152 my $email = format_email($name, $address, $email_usename);
1154 foreach my $entry (@email_to) {
1155 if ($email_remove_duplicates) {
1156 my ($entry_name, $entry_address) = parse_email($entry->[0]);
1157 if (($name eq $entry_name || $address eq $entry_address)
1158 && ($role eq "" || !($entry->[1] =~ m/$role/))
1160 if ($entry->[1] eq "") {
1161 $entry->[1] = "$role";
1163 $entry->[1] = "$entry->[1],$role";
1167 if ($email eq $entry->[0]
1168 && ($role eq "" || !($entry->[1] =~ m/$role/))
1170 if ($entry->[1] eq "") {
1171 $entry->[1] = "$role";
1173 $entry->[1] = "$entry->[1],$role";
1183 foreach my $path (split(/:/, $ENV{PATH})) {
1184 if (-e "$path/$bin") {
1185 return "$path/$bin";
1195 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1196 if (-e "$path/$conf") {
1197 return "$path/$conf";
1207 my ($name, $address) = parse_email($line);
1208 my $email = format_email($name, $address, 1);
1209 my $real_name = $name;
1210 my $real_address = $address;
1212 if (exists $mailmap->{names}->{$email} ||
1213 exists $mailmap->{addresses}->{$email}) {
1214 if (exists $mailmap->{names}->{$email}) {
1215 $real_name = $mailmap->{names}->{$email};
1217 if (exists $mailmap->{addresses}->{$email}) {
1218 $real_address = $mailmap->{addresses}->{$email};
1221 if (exists $mailmap->{names}->{$address}) {
1222 $real_name = $mailmap->{names}->{$address};
1224 if (exists $mailmap->{addresses}->{$address}) {
1225 $real_address = $mailmap->{addresses}->{$address};
1228 return format_email($real_name, $real_address, 1);
1232 my (@addresses) = @_;
1234 my @mapped_emails = ();
1235 foreach my $line (@addresses) {
1236 push(@mapped_emails, mailmap_email($line));
1238 merge_by_realname(@mapped_emails) if ($email_use_mailmap);
1239 return @mapped_emails;
1242 sub merge_by_realname {
1246 foreach my $email (@emails) {
1247 my ($name, $address) = parse_email($email);
1248 if (exists $address_map{$name}) {
1249 $address = $address_map{$name};
1250 $email = format_email($name, $address, 1);
1252 $address_map{$name} = $address;
1257 sub git_execute_cmd {
1261 my $output = `$cmd`;
1262 $output =~ s/^\s*//gm;
1263 @lines = split("\n", $output);
1268 sub hg_execute_cmd {
1272 my $output = `$cmd`;
1273 @lines = split("\n", $output);
1278 sub extract_formatted_signatures {
1279 my (@signature_lines) = @_;
1281 my @type = @signature_lines;
1283 s/\s*(.*):.*/$1/ for (@type);
1286 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1288 ## Reformat email addresses (with names) to avoid badly written signatures
1290 foreach my $signer (@signature_lines) {
1291 $signer = deduplicate_email($signer);
1294 return (\@type, \@signature_lines);
1297 sub vcs_find_signers {
1298 my ($cmd, $file) = @_;
1301 my @signatures = ();
1305 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1307 my $pattern = $VCS_cmds{"commit_pattern"};
1308 my $author_pattern = $VCS_cmds{"author_pattern"};
1309 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1311 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1313 $commits = grep(/$pattern/, @lines); # of commits
1315 @authors = grep(/$author_pattern/, @lines);
1316 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1317 @stats = grep(/$stat_pattern/, @lines);
1319 # print("stats: <@stats>\n");
1321 return (0, \@signatures, \@authors, \@stats) if !@signatures;
1323 save_commits_by_author(@lines) if ($interactive);
1324 save_commits_by_signer(@lines) if ($interactive);
1326 if (!$email_git_penguin_chiefs) {
1327 @signatures = grep(!/${penguin_chiefs}/i, @signatures);
1330 my ($author_ref, $authors_ref) = extract_formatted_signatures(@authors);
1331 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1333 return ($commits, $signers_ref, $authors_ref, \@stats);
1336 sub vcs_find_author {
1340 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1342 if (!$email_git_penguin_chiefs) {
1343 @lines = grep(!/${penguin_chiefs}/i, @lines);
1346 return @lines if !@lines;
1349 foreach my $line (@lines) {
1350 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1352 my ($name, $address) = parse_email($author);
1353 $author = format_email($name, $address, 1);
1354 push(@authors, $author);
1358 save_commits_by_author(@lines) if ($interactive);
1359 save_commits_by_signer(@lines) if ($interactive);
1364 sub vcs_save_commits {
1369 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1371 foreach my $line (@lines) {
1372 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1385 return @commits if (!(-f $file));
1387 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1388 my @all_commits = ();
1390 $cmd = $VCS_cmds{"blame_file_cmd"};
1391 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1392 @all_commits = vcs_save_commits($cmd);
1394 foreach my $file_range_diff (@range) {
1395 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1397 my $diff_start = $2;
1398 my $diff_length = $3;
1399 next if ("$file" ne "$diff_file");
1400 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1401 push(@commits, $all_commits[$i]);
1405 foreach my $file_range_diff (@range) {
1406 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1408 my $diff_start = $2;
1409 my $diff_length = $3;
1410 next if ("$file" ne "$diff_file");
1411 $cmd = $VCS_cmds{"blame_range_cmd"};
1412 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1413 push(@commits, vcs_save_commits($cmd));
1416 $cmd = $VCS_cmds{"blame_file_cmd"};
1417 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1418 @commits = vcs_save_commits($cmd);
1421 foreach my $commit (@commits) {
1422 $commit =~ s/^\^//g;
1428 my $printed_novcs = 0;
1430 %VCS_cmds = %VCS_cmds_git;
1431 return 1 if eval $VCS_cmds{"available"};
1432 %VCS_cmds = %VCS_cmds_hg;
1433 return 2 if eval $VCS_cmds{"available"};
1435 if (!$printed_novcs) {
1436 warn("$P: No supported VCS found. Add --nogit to options?\n");
1437 warn("Using a git repository produces better results.\n");
1438 warn("Try Linus Torvalds' latest git repository using:\n");
1439 warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n");
1447 return $vcs_used == 1;
1451 return $vcs_used == 2;
1454 sub interactive_get_maintainers {
1455 my ($list_ref) = @_;
1456 my @list = @$list_ref;
1465 foreach my $entry (@list) {
1466 $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
1467 $selected{$count} = 1;
1468 $authored{$count} = 0;
1469 $signed{$count} = 0;
1475 my $print_options = 0;
1480 printf STDERR "\n%1s %2s %-65s",
1481 "*", "#", "email/list and role:stats";
1483 ($email_git_fallback && !$maintained) ||
1485 print STDERR "auth sign";
1488 foreach my $entry (@list) {
1489 my $email = $entry->[0];
1490 my $role = $entry->[1];
1492 $sel = "*" if ($selected{$count});
1493 my $commit_author = $commit_author_hash{$email};
1494 my $commit_signer = $commit_signer_hash{$email};
1497 $authored++ for (@{$commit_author});
1498 $signed++ for (@{$commit_signer});
1499 printf STDERR "%1s %2d %-65s", $sel, $count + 1, $email;
1500 printf STDERR "%4d %4d", $authored, $signed
1501 if ($authored > 0 || $signed > 0);
1502 printf STDERR "\n %s\n", $role;
1503 if ($authored{$count}) {
1504 my $commit_author = $commit_author_hash{$email};
1505 foreach my $ref (@{$commit_author}) {
1506 print STDERR " Author: @{$ref}[1]\n";
1509 if ($signed{$count}) {
1510 my $commit_signer = $commit_signer_hash{$email};
1511 foreach my $ref (@{$commit_signer}) {
1512 print STDERR " @{$ref}[2]: @{$ref}[1]\n";
1519 my $date_ref = \$email_git_since;
1520 $date_ref = \$email_hg_since if (vcs_is_hg());
1521 if ($print_options) {
1526 Version Control options:
1527 g use git history [$email_git]
1528 gf use git-fallback [$email_git_fallback]
1529 b use git blame [$email_git_blame]
1530 bs use blame signatures [$email_git_blame_signatures]
1531 c# minimum commits [$email_git_min_signatures]
1532 %# min percent [$email_git_min_percent]
1533 d# history to use [$$date_ref]
1534 x# max maintainers [$email_git_max_maintainers]
1535 t all signature types [$email_git_all_signature_types]
1536 m use .mailmap [$email_use_mailmap]
1543 tm toggle maintainers
1544 tg toggle git entries
1545 tl toggle open list entries
1546 ts toggle subscriber list entries
1547 f emails in file [$file_emails]
1548 k keywords in file [$keywords]
1549 r remove duplicates [$email_remove_duplicates]
1550 p# pattern match depth [$pattern_depth]
1554 "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1556 my $input = <STDIN>;
1561 my @wish = split(/[, ]+/, $input);
1562 foreach my $nr (@wish) {
1564 my $sel = substr($nr, 0, 1);
1565 my $str = substr($nr, 1);
1567 $val = $1 if $str =~ /^(\d+)$/;
1572 $output_rolestats = 0;
1575 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1576 $selected{$nr - 1} = !$selected{$nr - 1};
1577 } elsif ($sel eq "*" || $sel eq '^') {
1579 $toggle = 1 if ($sel eq '*');
1580 for (my $i = 0; $i < $count; $i++) {
1581 $selected{$i} = $toggle;
1583 } elsif ($sel eq "0") {
1584 for (my $i = 0; $i < $count; $i++) {
1585 $selected{$i} = !$selected{$i};
1587 } elsif ($sel eq "t") {
1588 if (lc($str) eq "m") {
1589 for (my $i = 0; $i < $count; $i++) {
1590 $selected{$i} = !$selected{$i}
1591 if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
1593 } elsif (lc($str) eq "g") {
1594 for (my $i = 0; $i < $count; $i++) {
1595 $selected{$i} = !$selected{$i}
1596 if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
1598 } elsif (lc($str) eq "l") {
1599 for (my $i = 0; $i < $count; $i++) {
1600 $selected{$i} = !$selected{$i}
1601 if ($list[$i]->[1] =~ /^(open list)/i);
1603 } elsif (lc($str) eq "s") {
1604 for (my $i = 0; $i < $count; $i++) {
1605 $selected{$i} = !$selected{$i}
1606 if ($list[$i]->[1] =~ /^(subscriber list)/i);
1609 } elsif ($sel eq "a") {
1610 if ($val > 0 && $val <= $count) {
1611 $authored{$val - 1} = !$authored{$val - 1};
1612 } elsif ($str eq '*' || $str eq '^') {
1614 $toggle = 1 if ($str eq '*');
1615 for (my $i = 0; $i < $count; $i++) {
1616 $authored{$i} = $toggle;
1619 } elsif ($sel eq "s") {
1620 if ($val > 0 && $val <= $count) {
1621 $signed{$val - 1} = !$signed{$val - 1};
1622 } elsif ($str eq '*' || $str eq '^') {
1624 $toggle = 1 if ($str eq '*');
1625 for (my $i = 0; $i < $count; $i++) {
1626 $signed{$i} = $toggle;
1629 } elsif ($sel eq "o") {
1632 } elsif ($sel eq "g") {
1634 bool_invert(\$email_git_fallback);
1636 bool_invert(\$email_git);
1639 } elsif ($sel eq "b") {
1641 bool_invert(\$email_git_blame_signatures);
1643 bool_invert(\$email_git_blame);
1646 } elsif ($sel eq "c") {
1648 $email_git_min_signatures = $val;
1651 } elsif ($sel eq "x") {
1653 $email_git_max_maintainers = $val;
1656 } elsif ($sel eq "%") {
1657 if ($str ne "" && $val >= 0) {
1658 $email_git_min_percent = $val;
1661 } elsif ($sel eq "d") {
1663 $email_git_since = $str;
1664 } elsif (vcs_is_hg()) {
1665 $email_hg_since = $str;
1668 } elsif ($sel eq "t") {
1669 bool_invert(\$email_git_all_signature_types);
1671 } elsif ($sel eq "f") {
1672 bool_invert(\$file_emails);
1674 } elsif ($sel eq "r") {
1675 bool_invert(\$email_remove_duplicates);
1677 } elsif ($sel eq "m") {
1678 bool_invert(\$email_use_mailmap);
1681 } elsif ($sel eq "k") {
1682 bool_invert(\$keywords);
1684 } elsif ($sel eq "p") {
1685 if ($str ne "" && $val >= 0) {
1686 $pattern_depth = $val;
1689 } elsif ($sel eq "h" || $sel eq "?") {
1692 Interactive mode allows you to select the various maintainers, submitters,
1693 commit signers and mailing lists that could be CC'd on a patch.
1695 Any *'d entry is selected.
1697 If you have git or hg installed, you can choose to summarize the commit
1698 history of files in the patch. Also, each line of the current file can
1699 be matched to its commit author and that commits signers with blame.
1701 Various knobs exist to control the length of time for active commit
1702 tracking, the maximum number of commit authors and signers to add,
1705 Enter selections at the prompt until you are satisfied that the selected
1706 maintainers are appropriate. You may enter multiple selections separated
1707 by either commas or spaces.
1711 print STDERR "invalid option: '$nr'\n";
1716 print STDERR "git-blame can be very slow, please have patience..."
1717 if ($email_git_blame);
1718 goto &get_maintainers;
1722 #drop not selected entries
1724 my @new_emailto = ();
1725 foreach my $entry (@list) {
1726 if ($selected{$count}) {
1727 push(@new_emailto, $list[$count]);
1731 return @new_emailto;
1735 my ($bool_ref) = @_;
1744 sub deduplicate_email {
1748 my ($name, $address) = parse_email($email);
1749 $email = format_email($name, $address, 1);
1750 $email = mailmap_email($email);
1752 return $email if (!$email_remove_duplicates);
1754 ($name, $address) = parse_email($email);
1756 if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
1757 $name = $deduplicate_name_hash{lc($name)}->[0];
1758 $address = $deduplicate_name_hash{lc($name)}->[1];
1760 } elsif ($deduplicate_address_hash{lc($address)}) {
1761 $name = $deduplicate_address_hash{lc($address)}->[0];
1762 $address = $deduplicate_address_hash{lc($address)}->[1];
1766 $deduplicate_name_hash{lc($name)} = [ $name, $address ];
1767 $deduplicate_address_hash{lc($address)} = [ $name, $address ];
1769 $email = format_email($name, $address, 1);
1770 $email = mailmap_email($email);
1774 sub save_commits_by_author {
1781 foreach my $line (@lines) {
1782 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1784 $author = deduplicate_email($author);
1785 push(@authors, $author);
1787 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1788 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1791 for (my $i = 0; $i < @authors; $i++) {
1793 foreach my $ref(@{$commit_author_hash{$authors[$i]}}) {
1794 if (@{$ref}[0] eq $commits[$i] &&
1795 @{$ref}[1] eq $subjects[$i]) {
1801 push(@{$commit_author_hash{$authors[$i]}},
1802 [ ($commits[$i], $subjects[$i]) ]);
1807 sub save_commits_by_signer {
1813 foreach my $line (@lines) {
1814 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1815 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1816 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
1817 my @signatures = ($line);
1818 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1819 my @types = @$types_ref;
1820 my @signers = @$signers_ref;
1822 my $type = $types[0];
1823 my $signer = $signers[0];
1825 $signer = deduplicate_email($signer);
1828 foreach my $ref(@{$commit_signer_hash{$signer}}) {
1829 if (@{$ref}[0] eq $commit &&
1830 @{$ref}[1] eq $subject &&
1831 @{$ref}[2] eq $type) {
1837 push(@{$commit_signer_hash{$signer}},
1838 [ ($commit, $subject, $type) ]);
1845 my ($role, $divisor, @lines) = @_;
1850 return if (@lines <= 0);
1852 if ($divisor <= 0) {
1853 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
1857 @lines = mailmap(@lines);
1859 return if (@lines <= 0);
1861 @lines = sort(@lines);
1864 $hash{$_}++ for @lines;
1867 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
1868 my $sign_offs = $hash{$line};
1869 my $percent = $sign_offs * 100 / $divisor;
1871 $percent = 100 if ($percent > 100);
1873 last if ($sign_offs < $email_git_min_signatures ||
1874 $count > $email_git_max_maintainers ||
1875 $percent < $email_git_min_percent);
1876 push_email_address($line, '');
1877 if ($output_rolestats) {
1878 my $fmt_percent = sprintf("%.0f", $percent);
1879 add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%");
1881 add_role($line, $role);
1886 sub vcs_file_signoffs {
1897 $vcs_used = vcs_exists();
1898 return if (!$vcs_used);
1900 my $cmd = $VCS_cmds{"find_signers_cmd"};
1901 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
1903 ($commits, $signers_ref, $authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
1905 @signers = @{$signers_ref} if defined $signers_ref;
1906 @authors = @{$authors_ref} if defined $authors_ref;
1907 @stats = @{$stats_ref} if defined $stats_ref;
1909 # print("commits: <$commits>\nsigners:<@signers>\nauthors: <@authors>\nstats: <@stats>\n");
1911 foreach my $signer (@signers) {
1912 $signer = deduplicate_email($signer);
1915 vcs_assign("commit_signer", $commits, @signers);
1916 vcs_assign("authored", $commits, @authors);
1917 if ($#authors == $#stats) {
1918 my $stat_pattern = $VCS_cmds{"stat_pattern"};
1919 $stat_pattern =~ s/(\$\w+)/$1/eeg; #interpolate $stat_pattern
1923 for (my $i = 0; $i <= $#stats; $i++) {
1924 if ($stats[$i] =~ /$stat_pattern/) {
1929 my @tmp_authors = uniq(@authors);
1930 foreach my $author (@tmp_authors) {
1931 $author = deduplicate_email($author);
1933 @tmp_authors = uniq(@tmp_authors);
1934 my @list_added = ();
1935 my @list_deleted = ();
1936 foreach my $author (@tmp_authors) {
1938 my $auth_deleted = 0;
1939 for (my $i = 0; $i <= $#stats; $i++) {
1940 if ($author eq deduplicate_email($authors[$i]) &&
1941 $stats[$i] =~ /$stat_pattern/) {
1943 $auth_deleted += $2;
1946 for (my $i = 0; $i < $auth_added; $i++) {
1947 push(@list_added, $author);
1949 for (my $i = 0; $i < $auth_deleted; $i++) {
1950 push(@list_deleted, $author);
1953 vcs_assign("added_lines", $added, @list_added);
1954 vcs_assign("removed_lines", $deleted, @list_deleted);
1958 sub vcs_file_blame {
1962 my @all_commits = ();
1967 $vcs_used = vcs_exists();
1968 return if (!$vcs_used);
1970 @all_commits = vcs_blame($file);
1971 @commits = uniq(@all_commits);
1972 $total_commits = @commits;
1973 $total_lines = @all_commits;
1975 if ($email_git_blame_signatures) {
1978 my $commit_authors_ref;
1979 my $commit_signers_ref;
1981 my @commit_authors = ();
1982 my @commit_signers = ();
1983 my $commit = join(" -r ", @commits);
1986 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1987 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1989 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
1990 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
1991 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
1993 push(@signers, @commit_signers);
1995 foreach my $commit (@commits) {
1997 my $commit_authors_ref;
1998 my $commit_signers_ref;
2000 my @commit_authors = ();
2001 my @commit_signers = ();
2004 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
2005 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2007 ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, $file);
2008 @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
2009 @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
2011 push(@signers, @commit_signers);
2016 if ($from_filename) {
2017 if ($output_rolestats) {
2019 if (vcs_is_hg()) {{ # Double brace for last exit
2021 my @commit_signers = ();
2022 @commits = uniq(@commits);
2023 @commits = sort(@commits);
2024 my $commit = join(" -r ", @commits);
2027 $cmd = $VCS_cmds{"find_commit_author_cmd"};
2028 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
2032 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
2034 if (!$email_git_penguin_chiefs) {
2035 @lines = grep(!/${penguin_chiefs}/i, @lines);
2041 foreach my $line (@lines) {
2042 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
2044 $author = deduplicate_email($author);
2045 push(@authors, $author);
2049 save_commits_by_author(@lines) if ($interactive);
2050 save_commits_by_signer(@lines) if ($interactive);
2052 push(@signers, @authors);
2055 foreach my $commit (@commits) {
2057 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
2058 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
2059 my @author = vcs_find_author($cmd);
2062 my $formatted_author = deduplicate_email($author[0]);
2064 my $count = grep(/$commit/, @all_commits);
2065 for ($i = 0; $i < $count ; $i++) {
2066 push(@blame_signers, $formatted_author);
2070 if (@blame_signers) {
2071 vcs_assign("authored lines", $total_lines, @blame_signers);
2074 foreach my $signer (@signers) {
2075 $signer = deduplicate_email($signer);
2077 vcs_assign("commits", $total_commits, @signers);
2079 foreach my $signer (@signers) {
2080 $signer = deduplicate_email($signer);
2082 vcs_assign("modified commits", $total_commits, @signers);
2090 @parms = grep(!$saw{$_}++, @parms);
2098 @parms = sort @parms;
2099 @parms = grep(!$saw{$_}++, @parms);
2103 sub clean_file_emails {
2104 my (@file_emails) = @_;
2105 my @fmt_emails = ();
2107 foreach my $email (@file_emails) {
2108 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
2109 my ($name, $address) = parse_email($email);
2110 if ($name eq '"[,\.]"') {
2114 my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name);
2116 my $first = $nw[@nw - 3];
2117 my $middle = $nw[@nw - 2];
2118 my $last = $nw[@nw - 1];
2120 if (((length($first) == 1 && $first =~ m/[A-Za-z]/) ||
2121 (length($first) == 2 && substr($first, -1) eq ".")) ||
2122 (length($middle) == 1 ||
2123 (length($middle) == 2 && substr($middle, -1) eq "."))) {
2124 $name = "$first $middle $last";
2126 $name = "$middle $last";
2130 if (substr($name, -1) =~ /[,\.]/) {
2131 $name = substr($name, 0, length($name) - 1);
2132 } elsif (substr($name, -2) =~ /[,\.]"/) {
2133 $name = substr($name, 0, length($name) - 2) . '"';
2136 if (substr($name, 0, 1) =~ /[,\.]/) {
2137 $name = substr($name, 1, length($name) - 1);
2138 } elsif (substr($name, 0, 2) =~ /"[,\.]/) {
2139 $name = '"' . substr($name, 2, length($name) - 2);
2142 my $fmt_email = format_email($name, $address, $email_usename);
2143 push(@fmt_emails, $fmt_email);
2153 my ($address, $role) = @$_;
2154 if (!$saw{$address}) {
2155 if ($output_roles) {
2156 push(@lines, "$address ($role)");
2158 push(@lines, $address);
2170 if ($output_multiline) {
2171 foreach my $line (@parms) {
2175 print(join($output_separator, @parms));
2183 # Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
2184 # comment. We must allow for rfc822_lwsp (or comments) after each of these.
2185 # This regexp will only work on addresses which have had comments stripped
2186 # and replaced with rfc822_lwsp.
2188 my $specials = '()<>@,;:\\\\".\\[\\]';
2189 my $controls = '\\000-\\037\\177';
2191 my $dtext = "[^\\[\\]\\r\\\\]";
2192 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
2194 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
2196 # Use zero-width assertion to spot the limit of an atom. A simple
2197 # $rfc822_lwsp* causes the regexp engine to hang occasionally.
2198 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
2199 my $word = "(?:$atom|$quoted_string)";
2200 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
2202 my $sub_domain = "(?:$atom|$domain_literal)";
2203 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
2205 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
2207 my $phrase = "$word*";
2208 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
2209 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
2210 my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
2212 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
2213 my $address = "(?:$mailbox|$group)";
2215 return "$rfc822_lwsp*$address";
2218 sub rfc822_strip_comments {
2220 # Recursively remove comments, and replace with a single space. The simpler
2221 # regexps in the Email Addressing FAQ are imperfect - they will miss escaped
2222 # chars in atoms, for example.
2224 while ($s =~ s/^((?:[^"\\]|\\.)*
2225 (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
2226 \((?:[^()\\]|\\.)*\)/$1 /osx) {}
2230 # valid: returns true if the parameter is an RFC822 valid address
2233 my $s = rfc822_strip_comments(shift);
2236 $rfc822re = make_rfc822re();
2239 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
2242 # validlist: In scalar context, returns true if the parameter is an RFC822
2243 # valid list of addresses.
2245 # In list context, returns an empty list on failure (an invalid
2246 # address was found); otherwise a list whose first element is the
2247 # number of addresses found and whose remaining elements are the
2248 # addresses. This is needed to disambiguate failure (invalid)
2249 # from success with no addresses found, because an empty string is
2252 sub rfc822_validlist {
2253 my $s = rfc822_strip_comments(shift);
2256 $rfc822re = make_rfc822re();
2258 # * null list items are valid according to the RFC
2259 # * the '1' business is to aid in distinguishing failure from no results
2262 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
2263 $s =~ m/^$rfc822_char*$/) {
2264 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
2267 return wantarray ? (scalar(@r), @r) : 1;
2269 return wantarray ? () : 0;