use strict;
my $P = $0;
-my $V = '0.26-beta3';
+my $V = '0.26-beta6';
use Getopt::Long qw(:config no_auto_abbrev);
my $email_hg_since = "-365";
my $interactive = 0;
my $email_remove_duplicates = 1;
+my $email_use_mailmap = 1;
my $output_multiline = 1;
my $output_separator = ", ";
my $output_roles = 0;
'hg-since=s' => \$email_hg_since,
'i|interactive!' => \$interactive,
'remove-duplicates!' => \$email_remove_duplicates,
+ 'mailmap!' => \$email_use_mailmap,
'm!' => \$email_maintainer,
'n!' => \$email_usename,
'l!' => \$email_list,
$subsystem = 0;
$web = 0;
$keywords = 0;
+ $interactive = 0;
} else {
my $selections = $email + $scm + $status + $subsystem + $web;
if ($selections == 0) {
}
close($maint);
-my %mailmap;
-if ($email_remove_duplicates) {
- open(my $mailmap, '<', "${lk_path}.mailmap")
- or warn "$P: Can't open .mailmap: $!\n";
- while (<$mailmap>) {
- my $line = $_;
+#
+# Read mail address map
+#
- next if ($line =~ m/^\s*#/);
- next if ($line =~ m/^\s*$/);
+my $mailmap;
- my ($name, $address) = parse_email($line);
- $line = format_email($name, $address, $email_usename);
+read_mailmap();
- next if ($line =~ m/^\s*$/);
+sub read_mailmap {
+ $mailmap = {
+ names => {},
+ addresses => {}
+ };
- if (exists($mailmap{$name})) {
- my $obj = $mailmap{$name};
- push(@$obj, $address);
- } else {
- my @arr = ($address);
- $mailmap{$name} = \@arr;
+ return if (!$email_use_mailmap || !(-f "${lk_path}.mailmap"));
+
+ open(my $mailmap_file, '<', "${lk_path}.mailmap")
+ or warn "$P: Can't open .mailmap: $!\n";
+
+ while (<$mailmap_file>) {
+ s/#.*$//; #strip comments
+ s/^\s+|\s+$//g; #trim
+
+ next if (/^\s*$/); #skip empty lines
+ #entries have one of the following formats:
+ # name1 <mail1>
+ # <mail1> <mail2>
+ # name1 <mail1> <mail2>
+ # name1 <mail1> name2 <mail2>
+ # (see man git-shortlog)
+ if (/^(.+)<(.+)>$/) {
+ my $real_name = $1;
+ my $address = $2;
+
+ $real_name =~ s/\s+$//;
+ ($real_name, $address) = parse_email("$real_name <$address>");
+ $mailmap->{names}->{$address} = $real_name;
+
+ } elsif (/^<([^\s]+)>\s*<([^\s]+)>$/) {
+ my $real_address = $1;
+ my $wrong_address = $2;
+
+ $mailmap->{addresses}->{$wrong_address} = $real_address;
+
+ } elsif (/^(.+)<([^\s]+)>\s*<([^\s]+)>$/) {
+ my $real_name = $1;
+ my $real_address = $2;
+ my $wrong_address = $3;
+
+ $real_name =~ s/\s+$//;
+ ($real_name, $real_address) =
+ parse_email("$real_name <$real_address>");
+ $mailmap->{names}->{$wrong_address} = $real_name;
+ $mailmap->{addresses}->{$wrong_address} = $real_address;
+
+ } elsif (/^(.+)<([^\s]+)>\s*([^\s].*)<([^\s]+)>$/) {
+ my $real_name = $1;
+ my $real_address = $2;
+ my $wrong_name = $3;
+ my $wrong_address = $4;
+
+ $real_name =~ s/\s+$//;
+ ($real_name, $real_address) =
+ parse_email("$real_name <$real_address>");
+
+ $wrong_name =~ s/\s+$//;
+ ($wrong_name, $wrong_address) =
+ parse_email("$wrong_name <$wrong_address>");
+
+ my $wrong_email = format_email($wrong_name, $wrong_address, 1);
+ $mailmap->{names}->{$wrong_email} = $real_name;
+ $mailmap->{addresses}->{$wrong_email} = $real_address;
}
}
- close($mailmap);
+ close($mailmap_file);
}
## use the filenames on the command line or find the filenames in the patchfiles
my @web = ();
my @subsystem = ();
my @status = ();
+my %deduplicate_name_hash = ();
+my %deduplicate_address_hash = ();
my $signature_pattern;
-my @to = get_maintainer();
+my @maintainers = get_maintainers();
-@to = merge_email(@to);
-
-output(@to) if (@to);
+if (@maintainers) {
+ @maintainers = merge_email(@maintainers);
+ output(@maintainers);
+}
if ($scm) {
@scm = uniq(@scm);
exit($exit);
-sub get_maintainer {
+sub get_maintainers {
%email_hash_name = ();
%email_hash_address = ();
%commit_author_hash = ();
@web = ();
@subsystem = ();
@status = ();
-
+ %deduplicate_name_hash = ();
+ %deduplicate_address_hash = ();
if ($email_git_all_signature_types) {
$signature_pattern = "(.+?)[Bb][Yy]:";
} else {
# Find responsible parties
+ my %exact_pattern_match_hash = ();
+
foreach my $file (@files) {
my %hash;
- my $exact_pattern_match = 0;
my $tvi = find_first_section();
while ($tvi < @typevalue) {
my $start = find_starting_index($tvi);
my $file_pd = ($file =~ tr@/@@);
$value_pd++ if (substr($value,-1,1) ne "/");
$value_pd = -1 if ($value =~ /^\.\*/);
- $exact_pattern_match = 1 if ($value_pd >= $file_pd);
+ if ($value_pd >= $file_pd) {
+ $exact_pattern_match_hash{$file} = 1;
+ }
if ($pattern_depth == 0 ||
(($file_pd - $value_pd) < $pattern_depth)) {
$hash{$tvi} = $value_pd;
print("\n");
}
}
-
- if ($email && ($email_git ||
- ($email_git_fallback && !$exact_pattern_match))) {
- vcs_file_signoffs($file);
- }
- if ($email && $email_git_blame) {
- vcs_file_blame($file);
- }
}
if ($keywords) {
}
}
+ foreach my $email (@email_to, @list_to) {
+ $email->[0] = deduplicate_email($email->[0]);
+ }
+
+ foreach my $file (@files) {
+ if ($email &&
+ ($email_git || ($email_git_fallback &&
+ !$exact_pattern_match_hash{$file}))) {
+ vcs_file_signoffs($file);
+ }
+ if ($email && $email_git_blame) {
+ vcs_file_blame($file);
+ }
+ }
+
if ($email) {
foreach my $chief (@penguin_chief) {
if ($chief =~ m/^(.*):(.*)/) {
}
}
- @to = interactive_get_maintainer(\@to) if ($interactive);
+ if ($interactive) {
+ @to = interactive_get_maintainers(\@to);
+ }
return @to;
}
Other options:
--pattern-depth => Number of pattern directory traversals (default: 0 (all))
- --keywords => scan patch for keywords (default: 1 (on))
- --sections => print the entire subsystem sections with pattern matches
+ --keywords => scan patch for keywords (default: $keywords)
+ --sections => print all of the subsystem sections with pattern matches
+ --mailmap => use .mailmap file (default: $email_use_mailmap)
--version => show version
--help => show this help information
}
sub top_of_kernel_tree {
- my ($lk_path) = @_;
+ my ($lk_path) = @_;
- if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
- $lk_path .= "/";
- }
- if ( (-f "${lk_path}COPYING")
- && (-f "${lk_path}CREDITS")
- && (-f "${lk_path}Kbuild")
- && (-f "${lk_path}MAINTAINERS")
- && (-f "${lk_path}Makefile")
- && (-f "${lk_path}README")
- && (-d "${lk_path}Documentation")
- && (-d "${lk_path}arch")
- && (-d "${lk_path}include")
- && (-d "${lk_path}drivers")
- && (-d "${lk_path}fs")
- && (-d "${lk_path}init")
- && (-d "${lk_path}ipc")
- && (-d "${lk_path}kernel")
- && (-d "${lk_path}lib")
- && (-d "${lk_path}scripts")) {
- return 1;
- }
- return 0;
+ if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
+ $lk_path .= "/";
+ }
+ if ( (-f "${lk_path}COPYING")
+ && (-f "${lk_path}CREDITS")
+ && (-f "${lk_path}Kbuild")
+ && (-f "${lk_path}MAINTAINERS")
+ && (-f "${lk_path}Makefile")
+ && (-f "${lk_path}README")
+ && (-d "${lk_path}Documentation")
+ && (-d "${lk_path}arch")
+ && (-d "${lk_path}include")
+ && (-d "${lk_path}drivers")
+ && (-d "${lk_path}fs")
+ && (-d "${lk_path}init")
+ && (-d "${lk_path}ipc")
+ && (-d "${lk_path}kernel")
+ && (-d "${lk_path}lib")
+ && (-d "${lk_path}scripts")) {
+ return 1;
+ }
+ return 0;
}
sub parse_email {
}
if ($list_additional =~ m/subscribers-only/) {
if ($email_subscriber_list) {
- if (!$hash_list_to{$list_address}) {
- $hash_list_to{$list_address} = 1;
+ if (!$hash_list_to{lc($list_address)}) {
+ $hash_list_to{lc($list_address)} = 1;
push(@list_to, [$list_address,
"subscriber list${list_role}"]);
}
}
} else {
if ($email_list) {
- if (!$hash_list_to{$list_address}) {
- $hash_list_to{$list_address} = 1;
+ if (!$hash_list_to{lc($list_address)}) {
+ $hash_list_to{lc($list_address)} = 1;
push(@list_to, [$list_address,
"open list${list_role}"]);
}
my ($name, $address) = @_;
return 1 if (($name eq "") && ($address eq ""));
- return 1 if (($name ne "") && exists($email_hash_name{$name}));
- return 1 if (($address ne "") && exists($email_hash_address{$address}));
+ return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
+ return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
return 0;
}
push(@email_to, [format_email($name, $address, $email_usename), $role]);
} elsif (!email_inuse($name, $address)) {
push(@email_to, [format_email($name, $address, $email_usename), $role]);
- $email_hash_name{$name}++;
- $email_hash_address{$address}++;
+ $email_hash_name{lc($name)}++ if ($name ne "");
+ $email_hash_address{lc($address)}++;
}
return 1;
return "";
}
-sub mailmap {
- my (@lines) = @_;
- my %hash;
+sub mailmap_email {
+ my ($line) = @_;
- foreach my $line (@lines) {
- my ($name, $address) = parse_email($line);
- if (!exists($hash{$name})) {
- $hash{$name} = $address;
- } elsif ($address ne $hash{$name}) {
- $address = $hash{$name};
- $line = format_email($name, $address, $email_usename);
+ my ($name, $address) = parse_email($line);
+ my $email = format_email($name, $address, 1);
+ my $real_name = $name;
+ my $real_address = $address;
+
+ if (exists $mailmap->{names}->{$email} ||
+ exists $mailmap->{addresses}->{$email}) {
+ if (exists $mailmap->{names}->{$email}) {
+ $real_name = $mailmap->{names}->{$email};
}
- if (exists($mailmap{$name})) {
- my $obj = $mailmap{$name};
- foreach my $map_address (@$obj) {
- if (($map_address eq $address) &&
- ($map_address ne $hash{$name})) {
- $line = format_email($name, $hash{$name}, $email_usename);
- }
- }
+ if (exists $mailmap->{addresses}->{$email}) {
+ $real_address = $mailmap->{addresses}->{$email};
+ }
+ } else {
+ if (exists $mailmap->{names}->{$address}) {
+ $real_name = $mailmap->{names}->{$address};
+ }
+ if (exists $mailmap->{addresses}->{$address}) {
+ $real_address = $mailmap->{addresses}->{$address};
}
}
+ return format_email($real_name, $real_address, 1);
+}
- return @lines;
+sub mailmap {
+ my (@addresses) = @_;
+
+ my @mapped_emails = ();
+ foreach my $line (@addresses) {
+ push(@mapped_emails, mailmap_email($line));
+ }
+ merge_by_realname(@mapped_emails) if ($email_use_mailmap);
+ return @mapped_emails;
+}
+
+sub merge_by_realname {
+ my %address_map;
+ my (@emails) = @_;
+
+ foreach my $email (@emails) {
+ my ($name, $address) = parse_email($email);
+ if (exists $address_map{$name}) {
+ $address = $address_map{$name};
+ $email = format_email($name, $address, 1);
+ } else {
+ $address_map{$name} = $address;
+ }
+ }
}
sub git_execute_cmd {
## Reformat email addresses (with names) to avoid badly written signatures
foreach my $signer (@signature_lines) {
- my ($name, $address) = parse_email($signer);
- $signer = format_email($name, $address, 1);
+ $signer = deduplicate_email($signer);
}
return (\@type, \@signature_lines);
}
sub vcs_is_git {
+ vcs_exists();
return $vcs_used == 1;
}
return $vcs_used == 2;
}
-sub interactive_get_maintainer {
+sub interactive_get_maintainers {
my ($list_ref) = @_;
my @list = @$list_ref;
my %authored;
my %signed;
my $count = 0;
-
- #select maintainers by default
- foreach my $entry (@list){
- my $role = $entry->[1];
- $selected{$count} = ($role =~ /^(maintainer|supporter|open list)/);
+ my $maintained = 0;
+ foreach my $entry (@list) {
+ $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
+ $selected{$count} = 1;
$authored{$count} = 0;
$signed{$count} = 0;
$count++;
while (!$done) {
$count = 0;
if ($redraw) {
- printf STDERR "\n%1s %2s %-65sauth sign\n",
- "*", "#", "email/list and role:stats";
+ printf STDERR "\n%1s %2s %-65s",
+ "*", "#", "email/list and role:stats";
+ if ($email_git ||
+ ($email_git_fallback && !$maintained) ||
+ $email_git_blame) {
+ print STDERR "auth sign";
+ }
+ print STDERR "\n";
foreach my $entry (@list) {
my $email = $entry->[0];
my $role = $entry->[1];
if ($print_options) {
$print_options = 0;
if (vcs_exists()) {
- print STDERR
-"\nVersion Control options:\n" .
-"g use git history [$email_git]\n" .
-"gf use git-fallback [$email_git_fallback]\n" .
-"b use git blame [$email_git_blame]\n" .
-"bs use blame signatures [$email_git_blame_signatures]\n" .
-"c# minimum commits [$email_git_min_signatures]\n" .
-"%# min percent [$email_git_min_percent]\n" .
-"d# history to use [$$date_ref]\n" .
-"x# max maintainers [$email_git_max_maintainers]\n" .
-"t all signature types [$email_git_all_signature_types]\n";
+ print STDERR <<EOT
+
+Version Control options:
+g use git history [$email_git]
+gf use git-fallback [$email_git_fallback]
+b use git blame [$email_git_blame]
+bs use blame signatures [$email_git_blame_signatures]
+c# minimum commits [$email_git_min_signatures]
+%# min percent [$email_git_min_percent]
+d# history to use [$$date_ref]
+x# max maintainers [$email_git_max_maintainers]
+t all signature types [$email_git_all_signature_types]
+m use .mailmap [$email_use_mailmap]
+EOT
}
- print STDERR "\nAdditional options:\n" .
-"0 toggle all\n" .
-"f emails in file [$file_emails]\n" .
-"k keywords in file [$keywords]\n" .
-"r remove duplicates [$email_remove_duplicates]\n" .
-"p# pattern match depth [$pattern_depth]\n";
+ print STDERR <<EOT
+
+Additional options:
+0 toggle all
+tm toggle maintainers
+tg toggle git entries
+tl toggle open list entries
+ts toggle subscriber list entries
+f emails in file [$file_emails]
+k keywords in file [$keywords]
+r remove duplicates [$email_remove_duplicates]
+p# pattern match depth [$pattern_depth]
+EOT
}
print STDERR
"\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
for (my $i = 0; $i < $count; $i++) {
$selected{$i} = !$selected{$i};
}
+ } elsif ($sel eq "t") {
+ if (lc($str) eq "m") {
+ for (my $i = 0; $i < $count; $i++) {
+ $selected{$i} = !$selected{$i}
+ if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
+ }
+ } elsif (lc($str) eq "g") {
+ for (my $i = 0; $i < $count; $i++) {
+ $selected{$i} = !$selected{$i}
+ if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
+ }
+ } elsif (lc($str) eq "l") {
+ for (my $i = 0; $i < $count; $i++) {
+ $selected{$i} = !$selected{$i}
+ if ($list[$i]->[1] =~ /^(open list)/i);
+ }
+ } elsif (lc($str) eq "s") {
+ for (my $i = 0; $i < $count; $i++) {
+ $selected{$i} = !$selected{$i}
+ if ($list[$i]->[1] =~ /^(subscriber list)/i);
+ }
+ }
} elsif ($sel eq "a") {
if ($val > 0 && $val <= $count) {
$authored{$val - 1} = !$authored{$val - 1};
} elsif ($sel eq "r") {
bool_invert(\$email_remove_duplicates);
$rerun = 1;
+ } elsif ($sel eq "m") {
+ bool_invert(\$email_use_mailmap);
+ read_mailmap();
+ $rerun = 1;
} elsif ($sel eq "k") {
bool_invert(\$keywords);
$rerun = 1;
$pattern_depth = $val;
$rerun = 1;
}
+ } elsif ($sel eq "h" || $sel eq "?") {
+ print STDERR <<EOT
+
+Interactive mode allows you to select the various maintainers, submitters,
+commit signers and mailing lists that could be CC'd on a patch.
+
+Any *'d entry is selected.
+
+If you have git or hg installed, you can choose to summarize the commit
+history of files in the patch. Also, each line of the current file can
+be matched to its commit author and that commits signers with blame.
+
+Various knobs exist to control the length of time for active commit
+tracking, the maximum number of commit authors and signers to add,
+and such.
+
+Enter selections at the prompt until you are satisfied that the selected
+maintainers are appropriate. You may enter multiple selections separated
+by either commas or spaces.
+
+EOT
} else {
print STDERR "invalid option: '$nr'\n";
$redraw = 0;
if ($rerun) {
print STDERR "git-blame can be very slow, please have patience..."
if ($email_git_blame);
- goto &get_maintainer;
+ goto &get_maintainers;
}
}
}
}
+sub deduplicate_email {
+ my ($email) = @_;
+
+ my $matched = 0;
+ my ($name, $address) = parse_email($email);
+ $email = format_email($name, $address, 1);
+ $email = mailmap_email($email);
+
+ return $email if (!$email_remove_duplicates);
+
+ ($name, $address) = parse_email($email);
+
+ if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
+ $name = $deduplicate_name_hash{lc($name)}->[0];
+ $address = $deduplicate_name_hash{lc($name)}->[1];
+ $matched = 1;
+ } elsif ($deduplicate_address_hash{lc($address)}) {
+ $name = $deduplicate_address_hash{lc($address)}->[0];
+ $address = $deduplicate_address_hash{lc($address)}->[1];
+ $matched = 1;
+ }
+ if (!$matched) {
+ $deduplicate_name_hash{lc($name)} = [ $name, $address ];
+ $deduplicate_address_hash{lc($address)} = [ $name, $address ];
+ }
+ $email = format_email($name, $address, 1);
+ $email = mailmap_email($email);
+ return $email;
+}
+
sub save_commits_by_author {
my (@lines) = @_;
foreach my $line (@lines) {
if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
my $author = $1;
- my ($name, $address) = parse_email($author);
- $author = format_email($name, $address, 1);
+ $author = deduplicate_email($author);
push(@authors, $author);
}
push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
my $type = $types[0];
my $signer = $signers[0];
+ $signer = deduplicate_email($signer);
+
my $exists = 0;
foreach my $ref(@{$commit_signer_hash{$signer}}) {
if (@{$ref}[0] eq $commit &&
$divisor = 1;
}
- if ($email_remove_duplicates) {
- @lines = mailmap(@lines);
- }
+ @lines = mailmap(@lines);
return if (@lines <= 0);
$cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
($commits, @signers) = vcs_find_signers($cmd);
+
+ foreach my $signer (@signers) {
+ $signer = deduplicate_email($signer);
+ }
+
vcs_assign("commit_signer", $commits, @signers);
}
foreach my $line (@lines) {
if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
my $author = $1;
- my ($name, $address) = parse_email($author);
- $author = format_email($name, $address, 1);
- push(@authors, $1);
+ $author = deduplicate_email($author);
+ push(@authors, $author);
}
}
$cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
my @author = vcs_find_author($cmd);
next if !@author;
+
+ my $formatted_author = deduplicate_email($author[0]);
+
my $count = grep(/$commit/, @all_commits);
for ($i = 0; $i < $count ; $i++) {
- push(@blame_signers, $author[0]);
+ push(@blame_signers, $formatted_author);
}
}
}
vcs_assign("authored lines", $total_lines, @blame_signers);
}
}
+ foreach my $signer (@signers) {
+ $signer = deduplicate_email($signer);
+ }
vcs_assign("commits", $total_commits, @signers);
} else {
+ foreach my $signer (@signers) {
+ $signer = deduplicate_email($signer);
+ }
vcs_assign("modified commits", $total_commits, @signers);
}
}