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