]> git.proxmox.com Git - mirror_qemu.git/blame - scripts/get_maintainer.pl
Merge remote-tracking branch 'remotes/mst/tags/for_upstream' into staging
[mirror_qemu.git] / scripts / get_maintainer.pl
CommitLineData
b7d5a9c2 1#!/usr/bin/env perl
c6a99b60
MT
2# (c) 2007, Joe Perches <joe@perches.com>
3# created from checkpatch.pl
4#
5# Print selected MAINTAINERS information for
6# the files modified in a patch or for a file
7#
8# usage: perl scripts/get_maintainer.pl [OPTIONS] <patch>
9# perl scripts/get_maintainer.pl [OPTIONS] -f <file>
10#
11# Licensed under the terms of the GNU GPL License version 2
12
13use strict;
b7d5a9c2 14use warnings;
c6a99b60
MT
15
16my $P = $0;
17my $V = '0.26';
18
19use Getopt::Long qw(:config no_auto_abbrev);
20
21my $lk_path = "./";
22my $email = 1;
23my $email_usename = 1;
24my $email_maintainer = 1;
6668a2af 25my $email_reviewer = 1;
c6a99b60
MT
26my $email_list = 1;
27my $email_subscriber_list = 0;
c6a99b60
MT
28my $email_git = 0;
29my $email_git_all_signature_types = 0;
30my $email_git_blame = 0;
31my $email_git_blame_signatures = 1;
32my $email_git_fallback = 1;
33my $email_git_min_signatures = 1;
34my $email_git_max_maintainers = 5;
35my $email_git_min_percent = 5;
36my $email_git_since = "1-year-ago";
37my $email_hg_since = "-365";
38my $interactive = 0;
39my $email_remove_duplicates = 1;
40my $email_use_mailmap = 1;
41my $output_multiline = 1;
42my $output_separator = ", ";
43my $output_roles = 0;
44my $output_rolestats = 1;
45my $scm = 0;
46my $web = 0;
47my $subsystem = 0;
48my $status = 0;
49my $keywords = 1;
50my $sections = 0;
51my $file_emails = 0;
52my $from_filename = 0;
53my $pattern_depth = 0;
54my $version = 0;
55my $help = 0;
56
57my $vcs_used = 0;
58
59my $exit = 0;
60
61my %commit_author_hash;
62my %commit_signer_hash;
63
c6a99b60
MT
64# Signature types of people who are either
65# a) responsible for the code in question, or
66# b) familiar enough with it to give relevant feedback
67my @signature_tags = ();
68push(@signature_tags, "Signed-off-by:");
69push(@signature_tags, "Reviewed-by:");
70push(@signature_tags, "Acked-by:");
71
c84a2b1a
MT
72my $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
73
c6a99b60
MT
74# rfc822 email address - preloaded methods go here.
75my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
76my $rfc822_char = '[\\000-\\377]';
77
78# VCS command support: class-like functions and strings
79
80my %VCS_cmds;
81
82my %VCS_cmds_git = (
83 "execute_cmd" => \&git_execute_cmd,
84 "available" => '(which("git") ne "") && (-d ".git")',
85 "find_signers_cmd" =>
c84a2b1a 86 "git log --no-color --follow --since=\$email_git_since " .
c6a99b60
MT
87 '--format="GitCommit: %H%n' .
88 'GitAuthor: %an <%ae>%n' .
89 'GitDate: %aD%n' .
90 'GitSubject: %s%n' .
91 '%b%n"' .
92 " -- \$file",
93 "find_commit_signers_cmd" =>
94 "git log --no-color " .
95 '--format="GitCommit: %H%n' .
96 'GitAuthor: %an <%ae>%n' .
97 'GitDate: %aD%n' .
98 'GitSubject: %s%n' .
99 '%b%n"' .
100 " -1 \$commit",
101 "find_commit_author_cmd" =>
102 "git log --no-color " .
103 '--format="GitCommit: %H%n' .
104 'GitAuthor: %an <%ae>%n' .
105 'GitDate: %aD%n' .
106 'GitSubject: %s%n"' .
107 " -1 \$commit",
108 "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
109 "blame_file_cmd" => "git blame -l \$file",
110 "commit_pattern" => "^GitCommit: ([0-9a-f]{40,40})",
111 "blame_commit_pattern" => "^([0-9a-f]+) ",
112 "author_pattern" => "^GitAuthor: (.*)",
113 "subject_pattern" => "^GitSubject: (.*)",
114);
115
116my %VCS_cmds_hg = (
117 "execute_cmd" => \&hg_execute_cmd,
118 "available" => '(which("hg") ne "") && (-d ".hg")',
119 "find_signers_cmd" =>
120 "hg log --date=\$email_hg_since " .
121 "--template='HgCommit: {node}\\n" .
122 "HgAuthor: {author}\\n" .
123 "HgSubject: {desc}\\n'" .
124 " -- \$file",
125 "find_commit_signers_cmd" =>
126 "hg log " .
127 "--template='HgSubject: {desc}\\n'" .
128 " -r \$commit",
129 "find_commit_author_cmd" =>
130 "hg log " .
131 "--template='HgCommit: {node}\\n" .
132 "HgAuthor: {author}\\n" .
133 "HgSubject: {desc|firstline}\\n'" .
134 " -r \$commit",
135 "blame_range_cmd" => "", # not supported
136 "blame_file_cmd" => "hg blame -n \$file",
137 "commit_pattern" => "^HgCommit: ([0-9a-f]{40,40})",
138 "blame_commit_pattern" => "^([ 0-9a-f]+):",
139 "author_pattern" => "^HgAuthor: (.*)",
140 "subject_pattern" => "^HgSubject: (.*)",
141);
142
143my $conf = which_conf(".get_maintainer.conf");
144if (-f $conf) {
145 my @conf_args;
146 open(my $conffile, '<', "$conf")
147 or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
148
149 while (<$conffile>) {
150 my $line = $_;
151
152 $line =~ s/\s*\n?$//g;
153 $line =~ s/^\s*//g;
154 $line =~ s/\s+/ /g;
155
156 next if ($line =~ m/^\s*#/);
157 next if ($line =~ m/^\s*$/);
158
159 my @words = split(" ", $line);
160 foreach my $word (@words) {
161 last if ($word =~ m/^#/);
162 push (@conf_args, $word);
163 }
164 }
165 close($conffile);
166 unshift(@ARGV, @conf_args) if @conf_args;
167}
168
169if (!GetOptions(
170 'email!' => \$email,
171 'git!' => \$email_git,
172 'git-all-signature-types!' => \$email_git_all_signature_types,
173 'git-blame!' => \$email_git_blame,
174 'git-blame-signatures!' => \$email_git_blame_signatures,
175 'git-fallback!' => \$email_git_fallback,
c6a99b60
MT
176 'git-min-signatures=i' => \$email_git_min_signatures,
177 'git-max-maintainers=i' => \$email_git_max_maintainers,
178 'git-min-percent=i' => \$email_git_min_percent,
179 'git-since=s' => \$email_git_since,
180 'hg-since=s' => \$email_hg_since,
181 'i|interactive!' => \$interactive,
182 'remove-duplicates!' => \$email_remove_duplicates,
183 'mailmap!' => \$email_use_mailmap,
184 'm!' => \$email_maintainer,
6668a2af 185 'r!' => \$email_reviewer,
c6a99b60
MT
186 'n!' => \$email_usename,
187 'l!' => \$email_list,
188 's!' => \$email_subscriber_list,
189 'multiline!' => \$output_multiline,
190 'roles!' => \$output_roles,
191 'rolestats!' => \$output_rolestats,
192 'separator=s' => \$output_separator,
193 'subsystem!' => \$subsystem,
194 'status!' => \$status,
195 'scm!' => \$scm,
196 'web!' => \$web,
197 'pattern-depth=i' => \$pattern_depth,
198 'k|keywords!' => \$keywords,
199 'sections!' => \$sections,
200 'fe|file-emails!' => \$file_emails,
201 'f|file' => \$from_filename,
202 'v|version' => \$version,
203 'h|help|usage' => \$help,
204 )) {
205 die "$P: invalid argument - use --help if necessary\n";
206}
207
208if ($help != 0) {
209 usage();
210 exit 0;
211}
212
213if ($version != 0) {
214 print("${P} ${V}\n");
215 exit 0;
216}
217
218if (-t STDIN && !@ARGV) {
219 # We're talking to a terminal, but have no command line arguments.
220 die "$P: missing patchfile or -f file - use --help if necessary\n";
221}
222
223$output_multiline = 0 if ($output_separator ne ", ");
224$output_rolestats = 1 if ($interactive);
225$output_roles = 1 if ($output_rolestats);
226
227if ($sections) {
228 $email = 0;
229 $email_list = 0;
230 $scm = 0;
231 $status = 0;
232 $subsystem = 0;
233 $web = 0;
234 $keywords = 0;
235 $interactive = 0;
236} else {
237 my $selections = $email + $scm + $status + $subsystem + $web;
238 if ($selections == 0) {
239 die "$P: Missing required option: email, scm, status, subsystem or web\n";
240 }
241}
242
243if ($email &&
6668a2af
JP
244 ($email_maintainer + $email_reviewer +
245 $email_list + $email_subscriber_list +
80b57dda 246 $email_git + $email_git_blame) == 0) {
c6a99b60
MT
247 die "$P: Please select at least 1 email option\n";
248}
249
990def58 250if (!top_of_tree($lk_path)) {
c6a99b60 251 die "$P: The current directory does not appear to be "
990def58 252 . "a QEMU source tree.\n";
c6a99b60
MT
253}
254
255## Read MAINTAINERS for type/value pairs
256
257my @typevalue = ();
258my %keyword_hash;
259
260open (my $maint, '<', "${lk_path}MAINTAINERS")
261 or die "$P: Can't open MAINTAINERS: $!\n";
262while (<$maint>) {
263 my $line = $_;
264
ba10f729 265 if ($line =~ m/^(.):\s*(.*)/) {
c6a99b60
MT
266 my $type = $1;
267 my $value = $2;
268
269 ##Filename pattern matching
270 if ($type eq "F" || $type eq "X") {
271 $value =~ s@\.@\\\.@g; ##Convert . to \.
272 $value =~ s/\*/\.\*/g; ##Convert * to .*
273 $value =~ s/\?/\./g; ##Convert ? to .
274 ##if pattern is a directory and it lacks a trailing slash, add one
275 if ((-d $value)) {
276 $value =~ s@([^/])$@$1/@;
277 }
278 } elsif ($type eq "K") {
279 $keyword_hash{@typevalue} = $value;
280 }
281 push(@typevalue, "$type:$value");
282 } elsif (!/^(\s)*$/) {
283 $line =~ s/\n$//g;
284 push(@typevalue, $line);
285 }
286}
287close($maint);
288
289
290#
291# Read mail address map
292#
293
294my $mailmap;
295
296read_mailmap();
297
298sub read_mailmap {
299 $mailmap = {
300 names => {},
301 addresses => {}
302 };
303
304 return if (!$email_use_mailmap || !(-f "${lk_path}.mailmap"));
305
306 open(my $mailmap_file, '<', "${lk_path}.mailmap")
307 or warn "$P: Can't open .mailmap: $!\n";
308
309 while (<$mailmap_file>) {
310 s/#.*$//; #strip comments
311 s/^\s+|\s+$//g; #trim
312
313 next if (/^\s*$/); #skip empty lines
314 #entries have one of the following formats:
315 # name1 <mail1>
316 # <mail1> <mail2>
317 # name1 <mail1> <mail2>
318 # name1 <mail1> name2 <mail2>
319 # (see man git-shortlog)
c84a2b1a
MT
320
321 if (/^([^<]+)<([^>]+)>$/) {
c6a99b60
MT
322 my $real_name = $1;
323 my $address = $2;
324
325 $real_name =~ s/\s+$//;
326 ($real_name, $address) = parse_email("$real_name <$address>");
327 $mailmap->{names}->{$address} = $real_name;
328
c84a2b1a 329 } elsif (/^<([^>]+)>\s*<([^>]+)>$/) {
c6a99b60
MT
330 my $real_address = $1;
331 my $wrong_address = $2;
332
333 $mailmap->{addresses}->{$wrong_address} = $real_address;
334
c84a2b1a 335 } elsif (/^(.+)<([^>]+)>\s*<([^>]+)>$/) {
c6a99b60
MT
336 my $real_name = $1;
337 my $real_address = $2;
338 my $wrong_address = $3;
339
340 $real_name =~ s/\s+$//;
341 ($real_name, $real_address) =
342 parse_email("$real_name <$real_address>");
343 $mailmap->{names}->{$wrong_address} = $real_name;
344 $mailmap->{addresses}->{$wrong_address} = $real_address;
345
c84a2b1a 346 } elsif (/^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$/) {
c6a99b60
MT
347 my $real_name = $1;
348 my $real_address = $2;
349 my $wrong_name = $3;
350 my $wrong_address = $4;
351
352 $real_name =~ s/\s+$//;
353 ($real_name, $real_address) =
354 parse_email("$real_name <$real_address>");
355
356 $wrong_name =~ s/\s+$//;
357 ($wrong_name, $wrong_address) =
358 parse_email("$wrong_name <$wrong_address>");
359
360 my $wrong_email = format_email($wrong_name, $wrong_address, 1);
361 $mailmap->{names}->{$wrong_email} = $real_name;
362 $mailmap->{addresses}->{$wrong_email} = $real_address;
363 }
364 }
365 close($mailmap_file);
366}
367
368## use the filenames on the command line or find the filenames in the patchfiles
369
370my @files = ();
371my @range = ();
372my @keyword_tvi = ();
373my @file_emails = ();
374
375if (!@ARGV) {
376 push(@ARGV, "&STDIN");
377}
378
379foreach my $file (@ARGV) {
380 if ($file ne "&STDIN") {
381 ##if $file is a directory and it lacks a trailing slash, add one
382 if ((-d $file)) {
383 $file =~ s@([^/])$@$1/@;
36b4cf19
IJ
384 } elsif (!(stat $file)) {
385 die "$P: file '${file}' not found: $!\n";
c6a99b60
MT
386 }
387 }
388 if ($from_filename) {
389 push(@files, $file);
390 if ($file ne "MAINTAINERS" && -f $file && ($keywords || $file_emails)) {
391 open(my $f, '<', $file)
392 or die "$P: Can't open $file: $!\n";
393 my $text = do { local($/) ; <$f> };
394 close($f);
395 if ($keywords) {
396 foreach my $line (keys %keyword_hash) {
397 if ($text =~ m/$keyword_hash{$line}/x) {
398 push(@keyword_tvi, $line);
399 }
400 }
401 }
402 if ($file_emails) {
403 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;
404 push(@file_emails, clean_file_emails(@poss_addr));
405 }
406 }
407 } else {
408 my $file_cnt = @files;
409 my $lastfile;
410
411 open(my $patch, "< $file")
412 or die "$P: Can't open $file: $!\n";
413
414 # We can check arbitrary information before the patch
415 # like the commit message, mail headers, etc...
416 # This allows us to match arbitrary keywords against any part
417 # of a git format-patch generated file (subject tags, etc...)
418
419 my $patch_prefix = ""; #Parsing the intro
420
421 while (<$patch>) {
422 my $patch_line = $_;
423 if (m/^\+\+\+\s+(\S+)/) {
424 my $filename = $1;
425 $filename =~ s@^[^/]*/@@;
426 $filename =~ s@\n@@;
427 $lastfile = $filename;
428 push(@files, $filename);
429 $patch_prefix = "^[+-].*"; #Now parsing the actual patch
430 } elsif (m/^\@\@ -(\d+),(\d+)/) {
431 if ($email_git_blame) {
432 push(@range, "$lastfile:$1:$2");
433 }
434 } elsif ($keywords) {
435 foreach my $line (keys %keyword_hash) {
436 if ($patch_line =~ m/${patch_prefix}$keyword_hash{$line}/x) {
437 push(@keyword_tvi, $line);
438 }
439 }
440 }
441 }
442 close($patch);
443
444 if ($file_cnt == @files) {
445 warn "$P: file '${file}' doesn't appear to be a patch. "
446 . "Add -f to options?\n";
447 }
448 @files = sort_and_uniq(@files);
449 }
450}
451
452@file_emails = uniq(@file_emails);
453
454my %email_hash_name;
455my %email_hash_address;
456my @email_to = ();
457my %hash_list_to;
458my @list_to = ();
459my @scm = ();
460my @web = ();
461my @subsystem = ();
462my @status = ();
463my %deduplicate_name_hash = ();
464my %deduplicate_address_hash = ();
c6a99b60
MT
465
466my @maintainers = get_maintainers();
467
468if (@maintainers) {
469 @maintainers = merge_email(@maintainers);
470 output(@maintainers);
471}
472
473if ($scm) {
474 @scm = uniq(@scm);
475 output(@scm);
476}
477
478if ($status) {
479 @status = uniq(@status);
480 output(@status);
481}
482
483if ($subsystem) {
484 @subsystem = uniq(@subsystem);
485 output(@subsystem);
486}
487
488if ($web) {
489 @web = uniq(@web);
490 output(@web);
491}
492
493exit($exit);
494
495sub range_is_maintained {
496 my ($start, $end) = @_;
497
498 for (my $i = $start; $i < $end; $i++) {
499 my $line = $typevalue[$i];
ba10f729 500 if ($line =~ m/^(.):\s*(.*)/) {
c6a99b60
MT
501 my $type = $1;
502 my $value = $2;
503 if ($type eq 'S') {
504 if ($value =~ /(maintain|support)/i) {
505 return 1;
506 }
507 }
508 }
509 }
510 return 0;
511}
512
513sub range_has_maintainer {
514 my ($start, $end) = @_;
515
516 for (my $i = $start; $i < $end; $i++) {
517 my $line = $typevalue[$i];
ba10f729 518 if ($line =~ m/^(.):\s*(.*)/) {
c6a99b60
MT
519 my $type = $1;
520 my $value = $2;
521 if ($type eq 'M') {
522 return 1;
523 }
524 }
525 }
526 return 0;
527}
528
529sub get_maintainers {
530 %email_hash_name = ();
531 %email_hash_address = ();
532 %commit_author_hash = ();
533 %commit_signer_hash = ();
534 @email_to = ();
535 %hash_list_to = ();
536 @list_to = ();
537 @scm = ();
538 @web = ();
539 @subsystem = ();
540 @status = ();
541 %deduplicate_name_hash = ();
542 %deduplicate_address_hash = ();
543 if ($email_git_all_signature_types) {
544 $signature_pattern = "(.+?)[Bb][Yy]:";
545 } else {
546 $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
547 }
548
549 # Find responsible parties
550
551 my %exact_pattern_match_hash = ();
552
553 foreach my $file (@files) {
554
555 my %hash;
556 my $tvi = find_first_section();
557 while ($tvi < @typevalue) {
558 my $start = find_starting_index($tvi);
559 my $end = find_ending_index($tvi);
560 my $exclude = 0;
561 my $i;
562
563 #Do not match excluded file patterns
564
565 for ($i = $start; $i < $end; $i++) {
566 my $line = $typevalue[$i];
ba10f729 567 if ($line =~ m/^(.):\s*(.*)/) {
c6a99b60
MT
568 my $type = $1;
569 my $value = $2;
570 if ($type eq 'X') {
571 if (file_match_pattern($file, $value)) {
572 $exclude = 1;
573 last;
574 }
575 }
576 }
577 }
578
579 if (!$exclude) {
580 for ($i = $start; $i < $end; $i++) {
581 my $line = $typevalue[$i];
ba10f729 582 if ($line =~ m/^(.):\s*(.*)/) {
c6a99b60
MT
583 my $type = $1;
584 my $value = $2;
585 if ($type eq 'F') {
586 if (file_match_pattern($file, $value)) {
587 my $value_pd = ($value =~ tr@/@@);
588 my $file_pd = ($file =~ tr@/@@);
589 $value_pd++ if (substr($value,-1,1) ne "/");
590 $value_pd = -1 if ($value =~ /^\.\*/);
591 if ($value_pd >= $file_pd &&
592 range_is_maintained($start, $end) &&
593 range_has_maintainer($start, $end)) {
594 $exact_pattern_match_hash{$file} = 1;
595 }
596 if ($pattern_depth == 0 ||
597 (($file_pd - $value_pd) < $pattern_depth)) {
598 $hash{$tvi} = $value_pd;
599 }
600 }
601 }
602 }
603 }
604 }
605 $tvi = $end + 1;
606 }
607
608 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
609 add_categories($line);
610 if ($sections) {
611 my $i;
612 my $start = find_starting_index($line);
613 my $end = find_ending_index($line);
614 for ($i = $start; $i < $end; $i++) {
615 my $line = $typevalue[$i];
616 if ($line =~ /^[FX]:/) { ##Restore file patterns
617 $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
618 $line =~ s/([^\\])\.$/$1\?/g; ##Convert . back to ?
619 $line =~ s/\\\./\./g; ##Convert \. to .
620 $line =~ s/\.\*/\*/g; ##Convert .* to *
621 }
622 $line =~ s/^([A-Z]):/$1:\t/g;
623 print("$line\n");
624 }
625 print("\n");
626 }
627 }
628 }
629
630 if ($keywords) {
631 @keyword_tvi = sort_and_uniq(@keyword_tvi);
632 foreach my $line (@keyword_tvi) {
633 add_categories($line);
634 }
635 }
636
637 foreach my $email (@email_to, @list_to) {
638 $email->[0] = deduplicate_email($email->[0]);
639 }
640
8ad2c0f0 641 if ($email) {
c6561586 642 if (! $interactive) {
4db84796 643 $email_git_fallback = 0 if @email_to > 0 || $email_git || $email_git_blame;
c6561586
PB
644 if ($email_git_fallback) {
645 print STDERR "get_maintainer.pl: No maintainers found, printing recent contributors.\n";
646 print STDERR "get_maintainer.pl: Do not blindly cc: them on patches! Use common sense.\n";
647 print STDERR "\n";
648 }
649 }
650
8ad2c0f0
PB
651 foreach my $file (@files) {
652 if ($email_git || ($email_git_fallback &&
653 !$exact_pattern_match_hash{$file})) {
654 vcs_file_signoffs($file);
655 }
656 if ($email_git_blame) {
657 vcs_file_blame($file);
658 }
c6a99b60 659 }
c6a99b60 660
c6a99b60
MT
661 foreach my $email (@file_emails) {
662 my ($name, $address) = parse_email($email);
663
664 my $tmp_email = format_email($name, $address, $email_usename);
665 push_email_address($tmp_email, '');
666 add_role($tmp_email, 'in file');
667 }
668 }
669
670 my @to = ();
671 if ($email || $email_list) {
672 if ($email) {
673 @to = (@to, @email_to);
674 }
675 if ($email_list) {
676 @to = (@to, @list_to);
677 }
678 }
679
680 if ($interactive) {
681 @to = interactive_get_maintainers(\@to);
682 }
683
684 return @to;
685}
686
687sub file_match_pattern {
688 my ($file, $pattern) = @_;
689 if (substr($pattern, -1) eq "/") {
690 if ($file =~ m@^$pattern@) {
691 return 1;
692 }
693 } else {
694 if ($file =~ m@^$pattern@) {
695 my $s1 = ($file =~ tr@/@@);
696 my $s2 = ($pattern =~ tr@/@@);
697 if ($s1 == $s2) {
698 return 1;
699 }
700 }
701 }
702 return 0;
703}
704
705sub usage {
706 print <<EOT;
707usage: $P [options] patchfile
708 $P [options] -f file|directory
709version: $V
710
711MAINTAINER field selection options:
712 --email => print email address(es) if any
713 --git => include recent git \*-by: signers
714 --git-all-signature-types => include signers regardless of signature type
715 or use only ${signature_pattern} signers (default: $email_git_all_signature_types)
716 --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
c6a99b60
MT
717 --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
718 --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
719 --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
720 --git-blame => use git blame to find modified commits for patch or file
721 --git-since => git history to use (default: $email_git_since)
722 --hg-since => hg history to use (default: $email_hg_since)
723 --interactive => display a menu (mostly useful if used with the --git option)
724 --m => include maintainer(s) if any
6668a2af 725 --r => include reviewer(s) if any
c6a99b60
MT
726 --n => include name 'Full Name <addr\@domain.tld>'
727 --l => include list(s) if any
728 --s => include subscriber only list(s) if any
729 --remove-duplicates => minimize duplicate email names/addresses
730 --roles => show roles (status:subsystem, git-signer, list, etc...)
731 --rolestats => show roles and statistics (commits/total_commits, %)
732 --file-emails => add email addresses found in -f file (default: 0 (off))
733 --scm => print SCM tree(s) if any
734 --status => print status if any
735 --subsystem => print subsystem name if any
736 --web => print website(s) if any
737
738Output type options:
739 --separator [, ] => separator for multiple entries on 1 line
740 using --separator also sets --nomultiline if --separator is not [, ]
741 --multiline => print 1 entry per line
742
743Other options:
744 --pattern-depth => Number of pattern directory traversals (default: 0 (all))
745 --keywords => scan patch for keywords (default: $keywords)
746 --sections => print all of the subsystem sections with pattern matches
747 --mailmap => use .mailmap file (default: $email_use_mailmap)
748 --version => show version
749 --help => show this help information
750
751Default options:
9ff3a5e6 752 [--email --nogit --git-fallback --m --r --n --l --multiline --pattern-depth=0
c6a99b60
MT
753 --remove-duplicates --rolestats]
754
755Notes:
756 Using "-f directory" may give unexpected results:
757 Used with "--git", git signators for _all_ files in and below
758 directory are examined as git recurses directories.
759 Any specified X: (exclude) pattern matches are _not_ ignored.
760 Used with "--nogit", directory is used as a pattern match,
761 no individual file within the directory or subdirectory
762 is matched.
763 Used with "--git-blame", does not iterate all files in directory
764 Using "--git-blame" is slow and may add old committers and authors
765 that are no longer active maintainers to the output.
766 Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
767 other automated tools that expect only ["name"] <email address>
768 may not work because of additional output after <email address>.
769 Using "--rolestats" and "--git-blame" shows the #/total=% commits,
770 not the percentage of the entire file authored. # of commits is
771 not a good measure of amount of code authored. 1 major commit may
772 contain a thousand lines, 5 trivial commits may modify a single line.
773 If git is not installed, but mercurial (hg) is installed and an .hg
774 repository exists, the following options apply to mercurial:
775 --git,
776 --git-min-signatures, --git-max-maintainers, --git-min-percent, and
777 --git-blame
778 Use --hg-since not --git-since to control date selection
990def58 779 File ".get_maintainer.conf", if it exists in the QEMU source root
c6a99b60
MT
780 directory, can change whatever get_maintainer defaults are desired.
781 Entries in this file can be any command line argument.
782 This file is prepended to any additional command line arguments.
783 Multiple lines and # comments are allowed.
784EOT
785}
786
990def58 787sub top_of_tree {
c6a99b60
MT
788 my ($lk_path) = @_;
789
790 if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
791 $lk_path .= "/";
792 }
990def58
MT
793 if ( (-f "${lk_path}COPYING")
794 && (-f "${lk_path}MAINTAINERS")
795 && (-f "${lk_path}Makefile")
796 && (-d "${lk_path}docs")
797 && (-f "${lk_path}VERSION")
798 && (-f "${lk_path}vl.c")) {
c6a99b60
MT
799 return 1;
800 }
801 return 0;
802}
803
804sub parse_email {
805 my ($formatted_email) = @_;
806
807 my $name = "";
808 my $address = "";
809
810 if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
811 $name = $1;
812 $address = $2;
813 } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
814 $address = $1;
815 } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
816 $address = $1;
817 }
818
819 $name =~ s/^\s+|\s+$//g;
820 $name =~ s/^\"|\"$//g;
821 $address =~ s/^\s+|\s+$//g;
822
823 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
824 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
825 $name = "\"$name\"";
826 }
827
828 return ($name, $address);
829}
830
831sub format_email {
832 my ($name, $address, $usename) = @_;
833
834 my $formatted_email;
835
836 $name =~ s/^\s+|\s+$//g;
837 $name =~ s/^\"|\"$//g;
838 $address =~ s/^\s+|\s+$//g;
839
840 if ($name =~ /[^\w \-]/i) { ##has "must quote" chars
841 $name =~ s/(?<!\\)"/\\"/g; ##escape quotes
842 $name = "\"$name\"";
843 }
844
845 if ($usename) {
846 if ("$name" eq "") {
847 $formatted_email = "$address";
848 } else {
849 $formatted_email = "$name <$address>";
850 }
851 } else {
852 $formatted_email = $address;
853 }
854
855 return $formatted_email;
856}
857
858sub find_first_section {
859 my $index = 0;
860
861 while ($index < @typevalue) {
862 my $tv = $typevalue[$index];
ba10f729 863 if (($tv =~ m/^(.):\s*(.*)/)) {
c6a99b60
MT
864 last;
865 }
866 $index++;
867 }
868
869 return $index;
870}
871
872sub find_starting_index {
873 my ($index) = @_;
874
875 while ($index > 0) {
876 my $tv = $typevalue[$index];
ba10f729 877 if (!($tv =~ m/^(.):\s*(.*)/)) {
c6a99b60
MT
878 last;
879 }
880 $index--;
881 }
882
883 return $index;
884}
885
886sub find_ending_index {
887 my ($index) = @_;
888
889 while ($index < @typevalue) {
890 my $tv = $typevalue[$index];
ba10f729 891 if (!($tv =~ m/^(.):\s*(.*)/)) {
c6a99b60
MT
892 last;
893 }
894 $index++;
895 }
896
897 return $index;
898}
899
622e42a7 900sub get_subsystem_name {
c6a99b60
MT
901 my ($index) = @_;
902
c6a99b60 903 my $start = find_starting_index($index);
c6a99b60 904
c6a99b60
MT
905 my $subsystem = $typevalue[$start];
906 if (length($subsystem) > 20) {
907 $subsystem = substr($subsystem, 0, 17);
908 $subsystem =~ s/\s*$//;
909 $subsystem = $subsystem . "...";
910 }
622e42a7
JP
911 return $subsystem;
912}
913
914sub get_maintainer_role {
915 my ($index) = @_;
916
917 my $i;
918 my $start = find_starting_index($index);
919 my $end = find_ending_index($index);
920
921 my $role = "unknown";
922 my $subsystem = get_subsystem_name($index);
c6a99b60
MT
923
924 for ($i = $start + 1; $i < $end; $i++) {
925 my $tv = $typevalue[$i];
ba10f729 926 if ($tv =~ m/^(.):\s*(.*)/) {
c6a99b60
MT
927 my $ptype = $1;
928 my $pvalue = $2;
929 if ($ptype eq "S") {
930 $role = $pvalue;
931 }
932 }
933 }
934
935 $role = lc($role);
936 if ($role eq "supported") {
937 $role = "supporter";
938 } elsif ($role eq "maintained") {
939 $role = "maintainer";
940 } elsif ($role eq "odd fixes") {
941 $role = "odd fixer";
942 } elsif ($role eq "orphan") {
943 $role = "orphan minder";
944 } elsif ($role eq "obsolete") {
945 $role = "obsolete minder";
946 } elsif ($role eq "buried alive in reporters") {
947 $role = "chief penguin";
948 }
949
950 return $role . ":" . $subsystem;
951}
952
953sub get_list_role {
954 my ($index) = @_;
955
622e42a7 956 my $subsystem = get_subsystem_name($index);
c6a99b60
MT
957
958 if ($subsystem eq "THE REST") {
959 $subsystem = "";
960 }
961
962 return $subsystem;
963}
964
965sub add_categories {
966 my ($index) = @_;
967
968 my $i;
969 my $start = find_starting_index($index);
970 my $end = find_ending_index($index);
971
972 push(@subsystem, $typevalue[$start]);
973
974 for ($i = $start + 1; $i < $end; $i++) {
975 my $tv = $typevalue[$i];
ba10f729 976 if ($tv =~ m/^(.):\s*(.*)/) {
c6a99b60
MT
977 my $ptype = $1;
978 my $pvalue = $2;
979 if ($ptype eq "L") {
980 my $list_address = $pvalue;
981 my $list_additional = "";
982 my $list_role = get_list_role($i);
983
984 if ($list_role ne "") {
985 $list_role = ":" . $list_role;
986 }
987 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
988 $list_address = $1;
989 $list_additional = $2;
990 }
991 if ($list_additional =~ m/subscribers-only/) {
992 if ($email_subscriber_list) {
993 if (!$hash_list_to{lc($list_address)}) {
994 $hash_list_to{lc($list_address)} = 1;
995 push(@list_to, [$list_address,
996 "subscriber list${list_role}"]);
997 }
998 }
999 } else {
1000 if ($email_list) {
1001 if (!$hash_list_to{lc($list_address)}) {
1002 $hash_list_to{lc($list_address)} = 1;
c84a2b1a
MT
1003 if ($list_additional =~ m/moderated/) {
1004 push(@list_to, [$list_address,
1005 "moderated list${list_role}"]);
1006 } else {
1007 push(@list_to, [$list_address,
1008 "open list${list_role}"]);
1009 }
c6a99b60
MT
1010 }
1011 }
1012 }
1013 } elsif ($ptype eq "M") {
1014 my ($name, $address) = parse_email($pvalue);
1015 if ($name eq "") {
1016 if ($i > 0) {
1017 my $tv = $typevalue[$i - 1];
ba10f729 1018 if ($tv =~ m/^(.):\s*(.*)/) {
c6a99b60
MT
1019 if ($1 eq "P") {
1020 $name = $2;
1021 $pvalue = format_email($name, $address, $email_usename);
1022 }
1023 }
1024 }
1025 }
1026 if ($email_maintainer) {
1027 my $role = get_maintainer_role($i);
1028 push_email_addresses($pvalue, $role);
1029 }
6668a2af
JP
1030 } elsif ($ptype eq "R") {
1031 my ($name, $address) = parse_email($pvalue);
1032 if ($name eq "") {
1033 if ($i > 0) {
1034 my $tv = $typevalue[$i - 1];
1035 if ($tv =~ m/^(.):\s*(.*)/) {
1036 if ($1 eq "P") {
1037 $name = $2;
1038 $pvalue = format_email($name, $address, $email_usename);
1039 }
1040 }
1041 }
1042 }
1043 if ($email_reviewer) {
622e42a7
JP
1044 my $subsystem = get_subsystem_name($i);
1045 push_email_addresses($pvalue, "reviewer:$subsystem");
6668a2af 1046 }
c6a99b60
MT
1047 } elsif ($ptype eq "T") {
1048 push(@scm, $pvalue);
1049 } elsif ($ptype eq "W") {
1050 push(@web, $pvalue);
1051 } elsif ($ptype eq "S") {
1052 push(@status, $pvalue);
1053 }
1054 }
1055 }
1056}
1057
1058sub email_inuse {
1059 my ($name, $address) = @_;
1060
1061 return 1 if (($name eq "") && ($address eq ""));
1062 return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
1063 return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
1064
1065 return 0;
1066}
1067
1068sub push_email_address {
1069 my ($line, $role) = @_;
1070
1071 my ($name, $address) = parse_email($line);
1072
1073 if ($address eq "") {
1074 return 0;
1075 }
1076
1077 if (!$email_remove_duplicates) {
1078 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1079 } elsif (!email_inuse($name, $address)) {
1080 push(@email_to, [format_email($name, $address, $email_usename), $role]);
1081 $email_hash_name{lc($name)}++ if ($name ne "");
1082 $email_hash_address{lc($address)}++;
1083 }
1084
1085 return 1;
1086}
1087
1088sub push_email_addresses {
1089 my ($address, $role) = @_;
1090
1091 my @address_list = ();
1092
1093 if (rfc822_valid($address)) {
1094 push_email_address($address, $role);
1095 } elsif (@address_list = rfc822_validlist($address)) {
1096 my $array_count = shift(@address_list);
1097 while (my $entry = shift(@address_list)) {
1098 push_email_address($entry, $role);
1099 }
1100 } else {
1101 if (!push_email_address($address, $role)) {
1102 warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1103 }
1104 }
1105}
1106
1107sub add_role {
1108 my ($line, $role) = @_;
1109
1110 my ($name, $address) = parse_email($line);
1111 my $email = format_email($name, $address, $email_usename);
1112
1113 foreach my $entry (@email_to) {
1114 if ($email_remove_duplicates) {
1115 my ($entry_name, $entry_address) = parse_email($entry->[0]);
1116 if (($name eq $entry_name || $address eq $entry_address)
1117 && ($role eq "" || !($entry->[1] =~ m/$role/))
1118 ) {
1119 if ($entry->[1] eq "") {
1120 $entry->[1] = "$role";
1121 } else {
1122 $entry->[1] = "$entry->[1],$role";
1123 }
1124 }
1125 } else {
1126 if ($email eq $entry->[0]
1127 && ($role eq "" || !($entry->[1] =~ m/$role/))
1128 ) {
1129 if ($entry->[1] eq "") {
1130 $entry->[1] = "$role";
1131 } else {
1132 $entry->[1] = "$entry->[1],$role";
1133 }
1134 }
1135 }
1136 }
1137}
1138
1139sub which {
1140 my ($bin) = @_;
1141
1142 foreach my $path (split(/:/, $ENV{PATH})) {
1143 if (-e "$path/$bin") {
1144 return "$path/$bin";
1145 }
1146 }
1147
1148 return "";
1149}
1150
1151sub which_conf {
1152 my ($conf) = @_;
1153
1154 foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1155 if (-e "$path/$conf") {
1156 return "$path/$conf";
1157 }
1158 }
1159
1160 return "";
1161}
1162
1163sub mailmap_email {
1164 my ($line) = @_;
1165
1166 my ($name, $address) = parse_email($line);
1167 my $email = format_email($name, $address, 1);
1168 my $real_name = $name;
1169 my $real_address = $address;
1170
1171 if (exists $mailmap->{names}->{$email} ||
1172 exists $mailmap->{addresses}->{$email}) {
1173 if (exists $mailmap->{names}->{$email}) {
1174 $real_name = $mailmap->{names}->{$email};
1175 }
1176 if (exists $mailmap->{addresses}->{$email}) {
1177 $real_address = $mailmap->{addresses}->{$email};
1178 }
1179 } else {
1180 if (exists $mailmap->{names}->{$address}) {
1181 $real_name = $mailmap->{names}->{$address};
1182 }
1183 if (exists $mailmap->{addresses}->{$address}) {
1184 $real_address = $mailmap->{addresses}->{$address};
1185 }
1186 }
1187 return format_email($real_name, $real_address, 1);
1188}
1189
1190sub mailmap {
1191 my (@addresses) = @_;
1192
1193 my @mapped_emails = ();
1194 foreach my $line (@addresses) {
1195 push(@mapped_emails, mailmap_email($line));
1196 }
1197 merge_by_realname(@mapped_emails) if ($email_use_mailmap);
1198 return @mapped_emails;
1199}
1200
1201sub merge_by_realname {
1202 my %address_map;
1203 my (@emails) = @_;
1204
1205 foreach my $email (@emails) {
1206 my ($name, $address) = parse_email($email);
1207 if (exists $address_map{$name}) {
1208 $address = $address_map{$name};
1209 $email = format_email($name, $address, 1);
1210 } else {
1211 $address_map{$name} = $address;
1212 }
1213 }
1214}
1215
1216sub git_execute_cmd {
1217 my ($cmd) = @_;
1218 my @lines = ();
1219
1220 my $output = `$cmd`;
1221 $output =~ s/^\s*//gm;
1222 @lines = split("\n", $output);
1223
1224 return @lines;
1225}
1226
1227sub hg_execute_cmd {
1228 my ($cmd) = @_;
1229 my @lines = ();
1230
1231 my $output = `$cmd`;
1232 @lines = split("\n", $output);
1233
1234 return @lines;
1235}
1236
1237sub extract_formatted_signatures {
1238 my (@signature_lines) = @_;
1239
1240 my @type = @signature_lines;
1241
1242 s/\s*(.*):.*/$1/ for (@type);
1243
1244 # cut -f2- -d":"
1245 s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1246
1247## Reformat email addresses (with names) to avoid badly written signatures
1248
1249 foreach my $signer (@signature_lines) {
1250 $signer = deduplicate_email($signer);
1251 }
1252
1253 return (\@type, \@signature_lines);
1254}
1255
1256sub vcs_find_signers {
1257 my ($cmd) = @_;
1258 my $commits;
1259 my @lines = ();
1260 my @signatures = ();
1261
1262 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1263
1264 my $pattern = $VCS_cmds{"commit_pattern"};
1265
1266 $commits = grep(/$pattern/, @lines); # of commits
1267
1268 @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1269
1270 return (0, @signatures) if !@signatures;
1271
1272 save_commits_by_author(@lines) if ($interactive);
1273 save_commits_by_signer(@lines) if ($interactive);
1274
c6a99b60
MT
1275 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1276
1277 return ($commits, @$signers_ref);
1278}
1279
1280sub vcs_find_author {
1281 my ($cmd) = @_;
1282 my @lines = ();
1283
1284 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1285
c6a99b60
MT
1286 return @lines if !@lines;
1287
1288 my @authors = ();
1289 foreach my $line (@lines) {
1290 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1291 my $author = $1;
1292 my ($name, $address) = parse_email($author);
1293 $author = format_email($name, $address, 1);
1294 push(@authors, $author);
1295 }
1296 }
1297
1298 save_commits_by_author(@lines) if ($interactive);
1299 save_commits_by_signer(@lines) if ($interactive);
1300
1301 return @authors;
1302}
1303
1304sub vcs_save_commits {
1305 my ($cmd) = @_;
1306 my @lines = ();
1307 my @commits = ();
1308
1309 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1310
1311 foreach my $line (@lines) {
1312 if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1313 push(@commits, $1);
1314 }
1315 }
1316
1317 return @commits;
1318}
1319
1320sub vcs_blame {
1321 my ($file) = @_;
1322 my $cmd;
1323 my @commits = ();
1324
1325 return @commits if (!(-f $file));
1326
1327 if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1328 my @all_commits = ();
1329
1330 $cmd = $VCS_cmds{"blame_file_cmd"};
1331 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1332 @all_commits = vcs_save_commits($cmd);
1333
1334 foreach my $file_range_diff (@range) {
1335 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1336 my $diff_file = $1;
1337 my $diff_start = $2;
1338 my $diff_length = $3;
1339 next if ("$file" ne "$diff_file");
1340 for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1341 push(@commits, $all_commits[$i]);
1342 }
1343 }
1344 } elsif (@range) {
1345 foreach my $file_range_diff (@range) {
1346 next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1347 my $diff_file = $1;
1348 my $diff_start = $2;
1349 my $diff_length = $3;
1350 next if ("$file" ne "$diff_file");
1351 $cmd = $VCS_cmds{"blame_range_cmd"};
1352 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1353 push(@commits, vcs_save_commits($cmd));
1354 }
1355 } else {
1356 $cmd = $VCS_cmds{"blame_file_cmd"};
1357 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1358 @commits = vcs_save_commits($cmd);
1359 }
1360
1361 foreach my $commit (@commits) {
1362 $commit =~ s/^\^//g;
1363 }
1364
1365 return @commits;
1366}
1367
1368my $printed_novcs = 0;
1369sub vcs_exists {
1370 %VCS_cmds = %VCS_cmds_git;
1371 return 1 if eval $VCS_cmds{"available"};
1372 %VCS_cmds = %VCS_cmds_hg;
1373 return 2 if eval $VCS_cmds{"available"};
1374 %VCS_cmds = ();
1375 if (!$printed_novcs) {
1376 warn("$P: No supported VCS found. Add --nogit to options?\n");
1377 warn("Using a git repository produces better results.\n");
990def58 1378 warn("Try latest git repository using:\n");
86e6010a 1379 warn("git clone https://git.qemu.org/git/qemu.git\n");
c6a99b60
MT
1380 $printed_novcs = 1;
1381 }
1382 return 0;
1383}
1384
1385sub vcs_is_git {
1386 vcs_exists();
1387 return $vcs_used == 1;
1388}
1389
1390sub vcs_is_hg {
1391 return $vcs_used == 2;
1392}
1393
1394sub interactive_get_maintainers {
1395 my ($list_ref) = @_;
1396 my @list = @$list_ref;
1397
1398 vcs_exists();
1399
1400 my %selected;
1401 my %authored;
1402 my %signed;
1403 my $count = 0;
1404 my $maintained = 0;
1405 foreach my $entry (@list) {
1406 $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
1407 $selected{$count} = 1;
1408 $authored{$count} = 0;
1409 $signed{$count} = 0;
1410 $count++;
1411 }
1412
1413 #menu loop
1414 my $done = 0;
1415 my $print_options = 0;
1416 my $redraw = 1;
1417 while (!$done) {
1418 $count = 0;
1419 if ($redraw) {
1420 printf STDERR "\n%1s %2s %-65s",
1421 "*", "#", "email/list and role:stats";
1422 if ($email_git ||
1423 ($email_git_fallback && !$maintained) ||
1424 $email_git_blame) {
1425 print STDERR "auth sign";
1426 }
1427 print STDERR "\n";
1428 foreach my $entry (@list) {
1429 my $email = $entry->[0];
1430 my $role = $entry->[1];
1431 my $sel = "";
1432 $sel = "*" if ($selected{$count});
1433 my $commit_author = $commit_author_hash{$email};
1434 my $commit_signer = $commit_signer_hash{$email};
1435 my $authored = 0;
1436 my $signed = 0;
1437 $authored++ for (@{$commit_author});
1438 $signed++ for (@{$commit_signer});
1439 printf STDERR "%1s %2d %-65s", $sel, $count + 1, $email;
1440 printf STDERR "%4d %4d", $authored, $signed
1441 if ($authored > 0 || $signed > 0);
1442 printf STDERR "\n %s\n", $role;
1443 if ($authored{$count}) {
1444 my $commit_author = $commit_author_hash{$email};
1445 foreach my $ref (@{$commit_author}) {
1446 print STDERR " Author: @{$ref}[1]\n";
1447 }
1448 }
1449 if ($signed{$count}) {
1450 my $commit_signer = $commit_signer_hash{$email};
1451 foreach my $ref (@{$commit_signer}) {
1452 print STDERR " @{$ref}[2]: @{$ref}[1]\n";
1453 }
1454 }
1455
1456 $count++;
1457 }
1458 }
1459 my $date_ref = \$email_git_since;
1460 $date_ref = \$email_hg_since if (vcs_is_hg());
1461 if ($print_options) {
1462 $print_options = 0;
1463 if (vcs_exists()) {
1464 print STDERR <<EOT
1465
1466Version Control options:
1467g use git history [$email_git]
1468gf use git-fallback [$email_git_fallback]
1469b use git blame [$email_git_blame]
1470bs use blame signatures [$email_git_blame_signatures]
1471c# minimum commits [$email_git_min_signatures]
1472%# min percent [$email_git_min_percent]
1473d# history to use [$$date_ref]
1474x# max maintainers [$email_git_max_maintainers]
1475t all signature types [$email_git_all_signature_types]
1476m use .mailmap [$email_use_mailmap]
1477EOT
1478 }
1479 print STDERR <<EOT
1480
1481Additional options:
14820 toggle all
1483tm toggle maintainers
1484tg toggle git entries
1485tl toggle open list entries
1486ts toggle subscriber list entries
1487f emails in file [$file_emails]
1488k keywords in file [$keywords]
1489r remove duplicates [$email_remove_duplicates]
1490p# pattern match depth [$pattern_depth]
1491EOT
1492 }
1493 print STDERR
1494"\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1495
1496 my $input = <STDIN>;
1497 chomp($input);
1498
1499 $redraw = 1;
1500 my $rerun = 0;
1501 my @wish = split(/[, ]+/, $input);
1502 foreach my $nr (@wish) {
1503 $nr = lc($nr);
1504 my $sel = substr($nr, 0, 1);
1505 my $str = substr($nr, 1);
1506 my $val = 0;
1507 $val = $1 if $str =~ /^(\d+)$/;
1508
1509 if ($sel eq "y") {
1510 $interactive = 0;
1511 $done = 1;
1512 $output_rolestats = 0;
1513 $output_roles = 0;
1514 last;
1515 } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1516 $selected{$nr - 1} = !$selected{$nr - 1};
1517 } elsif ($sel eq "*" || $sel eq '^') {
1518 my $toggle = 0;
1519 $toggle = 1 if ($sel eq '*');
1520 for (my $i = 0; $i < $count; $i++) {
1521 $selected{$i} = $toggle;
1522 }
1523 } elsif ($sel eq "0") {
1524 for (my $i = 0; $i < $count; $i++) {
1525 $selected{$i} = !$selected{$i};
1526 }
1527 } elsif ($sel eq "t") {
1528 if (lc($str) eq "m") {
1529 for (my $i = 0; $i < $count; $i++) {
1530 $selected{$i} = !$selected{$i}
1531 if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
1532 }
1533 } elsif (lc($str) eq "g") {
1534 for (my $i = 0; $i < $count; $i++) {
1535 $selected{$i} = !$selected{$i}
1536 if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
1537 }
1538 } elsif (lc($str) eq "l") {
1539 for (my $i = 0; $i < $count; $i++) {
1540 $selected{$i} = !$selected{$i}
1541 if ($list[$i]->[1] =~ /^(open list)/i);
1542 }
1543 } elsif (lc($str) eq "s") {
1544 for (my $i = 0; $i < $count; $i++) {
1545 $selected{$i} = !$selected{$i}
1546 if ($list[$i]->[1] =~ /^(subscriber list)/i);
1547 }
1548 }
1549 } elsif ($sel eq "a") {
1550 if ($val > 0 && $val <= $count) {
1551 $authored{$val - 1} = !$authored{$val - 1};
1552 } elsif ($str eq '*' || $str eq '^') {
1553 my $toggle = 0;
1554 $toggle = 1 if ($str eq '*');
1555 for (my $i = 0; $i < $count; $i++) {
1556 $authored{$i} = $toggle;
1557 }
1558 }
1559 } elsif ($sel eq "s") {
1560 if ($val > 0 && $val <= $count) {
1561 $signed{$val - 1} = !$signed{$val - 1};
1562 } elsif ($str eq '*' || $str eq '^') {
1563 my $toggle = 0;
1564 $toggle = 1 if ($str eq '*');
1565 for (my $i = 0; $i < $count; $i++) {
1566 $signed{$i} = $toggle;
1567 }
1568 }
1569 } elsif ($sel eq "o") {
1570 $print_options = 1;
1571 $redraw = 1;
1572 } elsif ($sel eq "g") {
1573 if ($str eq "f") {
1574 bool_invert(\$email_git_fallback);
1575 } else {
1576 bool_invert(\$email_git);
1577 }
1578 $rerun = 1;
1579 } elsif ($sel eq "b") {
1580 if ($str eq "s") {
1581 bool_invert(\$email_git_blame_signatures);
1582 } else {
1583 bool_invert(\$email_git_blame);
1584 }
1585 $rerun = 1;
1586 } elsif ($sel eq "c") {
1587 if ($val > 0) {
1588 $email_git_min_signatures = $val;
1589 $rerun = 1;
1590 }
1591 } elsif ($sel eq "x") {
1592 if ($val > 0) {
1593 $email_git_max_maintainers = $val;
1594 $rerun = 1;
1595 }
1596 } elsif ($sel eq "%") {
1597 if ($str ne "" && $val >= 0) {
1598 $email_git_min_percent = $val;
1599 $rerun = 1;
1600 }
1601 } elsif ($sel eq "d") {
1602 if (vcs_is_git()) {
1603 $email_git_since = $str;
1604 } elsif (vcs_is_hg()) {
1605 $email_hg_since = $str;
1606 }
1607 $rerun = 1;
1608 } elsif ($sel eq "t") {
1609 bool_invert(\$email_git_all_signature_types);
1610 $rerun = 1;
1611 } elsif ($sel eq "f") {
1612 bool_invert(\$file_emails);
1613 $rerun = 1;
1614 } elsif ($sel eq "r") {
1615 bool_invert(\$email_remove_duplicates);
1616 $rerun = 1;
1617 } elsif ($sel eq "m") {
1618 bool_invert(\$email_use_mailmap);
1619 read_mailmap();
1620 $rerun = 1;
1621 } elsif ($sel eq "k") {
1622 bool_invert(\$keywords);
1623 $rerun = 1;
1624 } elsif ($sel eq "p") {
1625 if ($str ne "" && $val >= 0) {
1626 $pattern_depth = $val;
1627 $rerun = 1;
1628 }
1629 } elsif ($sel eq "h" || $sel eq "?") {
1630 print STDERR <<EOT
1631
1632Interactive mode allows you to select the various maintainers, submitters,
1633commit signers and mailing lists that could be CC'd on a patch.
1634
1635Any *'d entry is selected.
1636
1637If you have git or hg installed, you can choose to summarize the commit
1638history of files in the patch. Also, each line of the current file can
1639be matched to its commit author and that commits signers with blame.
1640
1641Various knobs exist to control the length of time for active commit
1642tracking, the maximum number of commit authors and signers to add,
1643and such.
1644
1645Enter selections at the prompt until you are satisfied that the selected
1646maintainers are appropriate. You may enter multiple selections separated
1647by either commas or spaces.
1648
1649EOT
1650 } else {
1651 print STDERR "invalid option: '$nr'\n";
1652 $redraw = 0;
1653 }
1654 }
1655 if ($rerun) {
1656 print STDERR "git-blame can be very slow, please have patience..."
1657 if ($email_git_blame);
1658 goto &get_maintainers;
1659 }
1660 }
1661
1662 #drop not selected entries
1663 $count = 0;
1664 my @new_emailto = ();
1665 foreach my $entry (@list) {
1666 if ($selected{$count}) {
1667 push(@new_emailto, $list[$count]);
1668 }
1669 $count++;
1670 }
1671 return @new_emailto;
1672}
1673
1674sub bool_invert {
1675 my ($bool_ref) = @_;
1676
1677 if ($$bool_ref) {
1678 $$bool_ref = 0;
1679 } else {
1680 $$bool_ref = 1;
1681 }
1682}
1683
1684sub deduplicate_email {
1685 my ($email) = @_;
1686
1687 my $matched = 0;
1688 my ($name, $address) = parse_email($email);
1689 $email = format_email($name, $address, 1);
1690 $email = mailmap_email($email);
1691
1692 return $email if (!$email_remove_duplicates);
1693
1694 ($name, $address) = parse_email($email);
1695
1696 if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
1697 $name = $deduplicate_name_hash{lc($name)}->[0];
1698 $address = $deduplicate_name_hash{lc($name)}->[1];
1699 $matched = 1;
1700 } elsif ($deduplicate_address_hash{lc($address)}) {
1701 $name = $deduplicate_address_hash{lc($address)}->[0];
1702 $address = $deduplicate_address_hash{lc($address)}->[1];
1703 $matched = 1;
1704 }
1705 if (!$matched) {
1706 $deduplicate_name_hash{lc($name)} = [ $name, $address ];
1707 $deduplicate_address_hash{lc($address)} = [ $name, $address ];
1708 }
1709 $email = format_email($name, $address, 1);
1710 $email = mailmap_email($email);
1711 return $email;
1712}
1713
1714sub save_commits_by_author {
1715 my (@lines) = @_;
1716
1717 my @authors = ();
1718 my @commits = ();
1719 my @subjects = ();
1720
1721 foreach my $line (@lines) {
1722 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1723 my $author = $1;
1724 $author = deduplicate_email($author);
1725 push(@authors, $author);
1726 }
1727 push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1728 push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1729 }
1730
1731 for (my $i = 0; $i < @authors; $i++) {
1732 my $exists = 0;
1733 foreach my $ref(@{$commit_author_hash{$authors[$i]}}) {
1734 if (@{$ref}[0] eq $commits[$i] &&
1735 @{$ref}[1] eq $subjects[$i]) {
1736 $exists = 1;
1737 last;
1738 }
1739 }
1740 if (!$exists) {
1741 push(@{$commit_author_hash{$authors[$i]}},
1742 [ ($commits[$i], $subjects[$i]) ]);
1743 }
1744 }
1745}
1746
1747sub save_commits_by_signer {
1748 my (@lines) = @_;
1749
1750 my $commit = "";
1751 my $subject = "";
1752
1753 foreach my $line (@lines) {
1754 $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1755 $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1756 if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
1757 my @signatures = ($line);
1758 my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1759 my @types = @$types_ref;
1760 my @signers = @$signers_ref;
1761
1762 my $type = $types[0];
1763 my $signer = $signers[0];
1764
1765 $signer = deduplicate_email($signer);
1766
1767 my $exists = 0;
1768 foreach my $ref(@{$commit_signer_hash{$signer}}) {
1769 if (@{$ref}[0] eq $commit &&
1770 @{$ref}[1] eq $subject &&
1771 @{$ref}[2] eq $type) {
1772 $exists = 1;
1773 last;
1774 }
1775 }
1776 if (!$exists) {
1777 push(@{$commit_signer_hash{$signer}},
1778 [ ($commit, $subject, $type) ]);
1779 }
1780 }
1781 }
1782}
1783
1784sub vcs_assign {
1785 my ($role, $divisor, @lines) = @_;
1786
1787 my %hash;
1788 my $count = 0;
1789
1790 return if (@lines <= 0);
1791
1792 if ($divisor <= 0) {
1793 warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
1794 $divisor = 1;
1795 }
1796
1797 @lines = mailmap(@lines);
1798
1799 return if (@lines <= 0);
1800
1801 @lines = sort(@lines);
1802
1803 # uniq -c
1804 $hash{$_}++ for @lines;
1805
1806 # sort -rn
1807 foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
1808 my $sign_offs = $hash{$line};
1809 my $percent = $sign_offs * 100 / $divisor;
1810
1811 $percent = 100 if ($percent > 100);
1812 $count++;
1813 last if ($sign_offs < $email_git_min_signatures ||
1814 $count > $email_git_max_maintainers ||
1815 $percent < $email_git_min_percent);
1816 push_email_address($line, '');
1817 if ($output_rolestats) {
1818 my $fmt_percent = sprintf("%.0f", $percent);
1819 add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%");
1820 } else {
1821 add_role($line, $role);
1822 }
1823 }
1824}
1825
1826sub vcs_file_signoffs {
1827 my ($file) = @_;
1828
1829 my @signers = ();
1830 my $commits;
1831
1832 $vcs_used = vcs_exists();
1833 return if (!$vcs_used);
1834
1835 my $cmd = $VCS_cmds{"find_signers_cmd"};
1836 $cmd =~ s/(\$\w+)/$1/eeg; # interpolate $cmd
1837
1838 ($commits, @signers) = vcs_find_signers($cmd);
1839
1840 foreach my $signer (@signers) {
1841 $signer = deduplicate_email($signer);
1842 }
1843
1844 vcs_assign("commit_signer", $commits, @signers);
1845}
1846
1847sub vcs_file_blame {
1848 my ($file) = @_;
1849
1850 my @signers = ();
1851 my @all_commits = ();
1852 my @commits = ();
1853 my $total_commits;
1854 my $total_lines;
1855
1856 $vcs_used = vcs_exists();
1857 return if (!$vcs_used);
1858
1859 @all_commits = vcs_blame($file);
1860 @commits = uniq(@all_commits);
1861 $total_commits = @commits;
1862 $total_lines = @all_commits;
1863
1864 if ($email_git_blame_signatures) {
1865 if (vcs_is_hg()) {
1866 my $commit_count;
1867 my @commit_signers = ();
1868 my $commit = join(" -r ", @commits);
1869 my $cmd;
1870
1871 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1872 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1873
1874 ($commit_count, @commit_signers) = vcs_find_signers($cmd);
1875
1876 push(@signers, @commit_signers);
1877 } else {
1878 foreach my $commit (@commits) {
1879 my $commit_count;
1880 my @commit_signers = ();
1881 my $cmd;
1882
1883 $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1884 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1885
1886 ($commit_count, @commit_signers) = vcs_find_signers($cmd);
1887
1888 push(@signers, @commit_signers);
1889 }
1890 }
1891 }
1892
1893 if ($from_filename) {
1894 if ($output_rolestats) {
1895 my @blame_signers;
1896 if (vcs_is_hg()) {{ # Double brace for last exit
1897 my $commit_count;
1898 my @commit_signers = ();
1899 @commits = uniq(@commits);
1900 @commits = sort(@commits);
1901 my $commit = join(" -r ", @commits);
1902 my $cmd;
1903
1904 $cmd = $VCS_cmds{"find_commit_author_cmd"};
1905 $cmd =~ s/(\$\w+)/$1/eeg; #substitute variables in $cmd
1906
1907 my @lines = ();
1908
1909 @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1910
c6a99b60
MT
1911 last if !@lines;
1912
1913 my @authors = ();
1914 foreach my $line (@lines) {
1915 if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1916 my $author = $1;
1917 $author = deduplicate_email($author);
1918 push(@authors, $author);
1919 }
1920 }
1921
1922 save_commits_by_author(@lines) if ($interactive);
1923 save_commits_by_signer(@lines) if ($interactive);
1924
1925 push(@signers, @authors);
1926 }}
1927 else {
1928 foreach my $commit (@commits) {
1929 my $i;
1930 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
1931 $cmd =~ s/(\$\w+)/$1/eeg; #interpolate $cmd
1932 my @author = vcs_find_author($cmd);
1933 next if !@author;
1934
1935 my $formatted_author = deduplicate_email($author[0]);
1936
1937 my $count = grep(/$commit/, @all_commits);
1938 for ($i = 0; $i < $count ; $i++) {
1939 push(@blame_signers, $formatted_author);
1940 }
1941 }
1942 }
1943 if (@blame_signers) {
1944 vcs_assign("authored lines", $total_lines, @blame_signers);
1945 }
1946 }
1947 foreach my $signer (@signers) {
1948 $signer = deduplicate_email($signer);
1949 }
1950 vcs_assign("commits", $total_commits, @signers);
1951 } else {
1952 foreach my $signer (@signers) {
1953 $signer = deduplicate_email($signer);
1954 }
1955 vcs_assign("modified commits", $total_commits, @signers);
1956 }
1957}
1958
1959sub uniq {
1960 my (@parms) = @_;
1961
1962 my %saw;
1963 @parms = grep(!$saw{$_}++, @parms);
1964 return @parms;
1965}
1966
1967sub sort_and_uniq {
1968 my (@parms) = @_;
1969
1970 my %saw;
1971 @parms = sort @parms;
1972 @parms = grep(!$saw{$_}++, @parms);
1973 return @parms;
1974}
1975
1976sub clean_file_emails {
1977 my (@file_emails) = @_;
1978 my @fmt_emails = ();
1979
1980 foreach my $email (@file_emails) {
1981 $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
1982 my ($name, $address) = parse_email($email);
1983 if ($name eq '"[,\.]"') {
1984 $name = "";
1985 }
1986
1987 my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name);
1988 if (@nw > 2) {
1989 my $first = $nw[@nw - 3];
1990 my $middle = $nw[@nw - 2];
1991 my $last = $nw[@nw - 1];
1992
1993 if (((length($first) == 1 && $first =~ m/[A-Za-z]/) ||
1994 (length($first) == 2 && substr($first, -1) eq ".")) ||
1995 (length($middle) == 1 ||
1996 (length($middle) == 2 && substr($middle, -1) eq "."))) {
1997 $name = "$first $middle $last";
1998 } else {
1999 $name = "$middle $last";
2000 }
2001 }
2002
2003 if (substr($name, -1) =~ /[,\.]/) {
2004 $name = substr($name, 0, length($name) - 1);
2005 } elsif (substr($name, -2) =~ /[,\.]"/) {
2006 $name = substr($name, 0, length($name) - 2) . '"';
2007 }
2008
2009 if (substr($name, 0, 1) =~ /[,\.]/) {
2010 $name = substr($name, 1, length($name) - 1);
2011 } elsif (substr($name, 0, 2) =~ /"[,\.]/) {
2012 $name = '"' . substr($name, 2, length($name) - 2);
2013 }
2014
2015 my $fmt_email = format_email($name, $address, $email_usename);
2016 push(@fmt_emails, $fmt_email);
2017 }
2018 return @fmt_emails;
2019}
2020
2021sub merge_email {
2022 my @lines;
2023 my %saw;
2024
2025 for (@_) {
2026 my ($address, $role) = @$_;
2027 if (!$saw{$address}) {
2028 if ($output_roles) {
2029 push(@lines, "$address ($role)");
2030 } else {
2031 push(@lines, $address);
2032 }
2033 $saw{$address} = 1;
2034 }
2035 }
2036
2037 return @lines;
2038}
2039
2040sub output {
2041 my (@parms) = @_;
2042
2043 if ($output_multiline) {
2044 foreach my $line (@parms) {
2045 print("${line}\n");
2046 }
2047 } else {
2048 print(join($output_separator, @parms));
2049 print("\n");
2050 }
2051}
2052
2053my $rfc822re;
2054
2055sub make_rfc822re {
2056# Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
2057# comment. We must allow for rfc822_lwsp (or comments) after each of these.
2058# This regexp will only work on addresses which have had comments stripped
2059# and replaced with rfc822_lwsp.
2060
2061 my $specials = '()<>@,;:\\\\".\\[\\]';
2062 my $controls = '\\000-\\037\\177';
2063
2064 my $dtext = "[^\\[\\]\\r\\\\]";
2065 my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
2066
2067 my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
2068
2069# Use zero-width assertion to spot the limit of an atom. A simple
2070# $rfc822_lwsp* causes the regexp engine to hang occasionally.
2071 my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
2072 my $word = "(?:$atom|$quoted_string)";
2073 my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
2074
2075 my $sub_domain = "(?:$atom|$domain_literal)";
2076 my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
2077
2078 my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
2079
2080 my $phrase = "$word*";
2081 my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
2082 my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
2083 my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
2084
2085 my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
2086 my $address = "(?:$mailbox|$group)";
2087
2088 return "$rfc822_lwsp*$address";
2089}
2090
2091sub rfc822_strip_comments {
2092 my $s = shift;
2093# Recursively remove comments, and replace with a single space. The simpler
2094# regexps in the Email Addressing FAQ are imperfect - they will miss escaped
2095# chars in atoms, for example.
2096
2097 while ($s =~ s/^((?:[^"\\]|\\.)*
2098 (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
2099 \((?:[^()\\]|\\.)*\)/$1 /osx) {}
2100 return $s;
2101}
2102
2103# valid: returns true if the parameter is an RFC822 valid address
2104#
2105sub rfc822_valid {
2106 my $s = rfc822_strip_comments(shift);
2107
2108 if (!$rfc822re) {
2109 $rfc822re = make_rfc822re();
2110 }
2111
2112 return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
2113}
2114
2115# validlist: In scalar context, returns true if the parameter is an RFC822
2116# valid list of addresses.
2117#
2118# In list context, returns an empty list on failure (an invalid
2119# address was found); otherwise a list whose first element is the
2120# number of addresses found and whose remaining elements are the
2121# addresses. This is needed to disambiguate failure (invalid)
2122# from success with no addresses found, because an empty string is
2123# a valid list.
2124
2125sub rfc822_validlist {
2126 my $s = rfc822_strip_comments(shift);
2127
2128 if (!$rfc822re) {
2129 $rfc822re = make_rfc822re();
2130 }
2131 # * null list items are valid according to the RFC
2132 # * the '1' business is to aid in distinguishing failure from no results
2133
2134 my @r;
2135 if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
2136 $s =~ m/^$rfc822_char*$/) {
2137 while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
2138 push(@r, $1);
2139 }
2140 return wantarray ? (scalar(@r), @r) : 1;
2141 }
2142 return wantarray ? () : 0;
2143}