qemu/scripts/get_maintainer.pl
<<
>>
Prefs
   1#!/usr/bin/env perl
   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;
  14use warnings;
  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;
  25my $email_reviewer = 1;
  26my $email_list = 1;
  27my $email_subscriber_list = 0;
  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
  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
  72my $signature_pattern = "\(" . join("|", @signature_tags) . "\)";
  73
  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 "") && (-e ".git")',
  85    "find_signers_cmd" =>
  86        "git log --no-color --follow --since=\$email_git_since " .
  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,
 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,
 185                'r!' => \$email_reviewer,
 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 &&
 244    ($email_maintainer + $email_reviewer +
 245     $email_list + $email_subscriber_list +
 246     $email_git + $email_git_blame) == 0) {
 247    die "$P: Please select at least 1 email option\n";
 248}
 249
 250if (!top_of_tree($lk_path)) {
 251    die "$P: The current directory does not appear to be "
 252        . "a QEMU source tree.\n";
 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
 265    if ($line =~ m/^(.):\s*(.*)/) {
 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)
 320
 321        if (/^([^<]+)<([^>]+)>$/) {
 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
 329        } elsif (/^<([^>]+)>\s*<([^>]+)>$/) {
 330            my $real_address = $1;
 331            my $wrong_address = $2;
 332
 333            $mailmap->{addresses}->{$wrong_address} = $real_address;
 334
 335        } elsif (/^(.+)<([^>]+)>\s*<([^>]+)>$/) {
 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
 346        } elsif (/^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$/) {
 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/@;
 384        } elsif (!(stat $file)) {
 385            die "$P: file '${file}' not found: $!\n";
 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 = ();
 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];
 500        if ($line =~ m/^(.):\s*(.*)/) {
 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];
 518        if ($line =~ m/^(.):\s*(.*)/) {
 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];
 567                if ($line =~ m/^(.):\s*(.*)/) {
 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];
 582                    if ($line =~ m/^(.):\s*(.*)/) {
 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
 641    if ($email) {
 642        if (! $interactive) {
 643            $email_git_fallback = 0 if @email_to > 0 || $email_git || $email_git_blame;
 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
 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            }
 659        }
 660
 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)
 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
 725    --r => include reviewer(s) if any
 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:
 752  [--email --nogit --git-fallback --m --r --n --l --multiline --pattern-depth=0
 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
 779  File ".get_maintainer.conf", if it exists in the QEMU source root
 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
 787sub top_of_tree {
 788    my ($lk_path) = @_;
 789
 790    if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
 791        $lk_path .= "/";
 792    }
 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        && (-d "${lk_path}linux-user/")
 799        && (-d "${lk_path}softmmu/")) {
 800        return 1;
 801    }
 802    return 0;
 803}
 804
 805sub parse_email {
 806    my ($formatted_email) = @_;
 807
 808    my $name = "";
 809    my $address = "";
 810
 811    if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
 812        $name = $1;
 813        $address = $2;
 814    } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
 815        $address = $1;
 816    } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
 817        $address = $1;
 818    }
 819
 820    $name =~ s/^\s+|\s+$//g;
 821    $name =~ s/^\"|\"$//g;
 822    $address =~ s/^\s+|\s+$//g;
 823
 824    if ($name =~ /[^\w \-]/i) {          ##has "must quote" chars
 825        $name =~ s/(?<!\\)"/\\"/g;       ##escape quotes
 826        $name = "\"$name\"";
 827    }
 828
 829    return ($name, $address);
 830}
 831
 832sub format_email {
 833    my ($name, $address, $usename) = @_;
 834
 835    my $formatted_email;
 836
 837    $name =~ s/^\s+|\s+$//g;
 838    $name =~ s/^\"|\"$//g;
 839    $address =~ s/^\s+|\s+$//g;
 840
 841    if ($name =~ /[^\w \-]/i) {          ##has "must quote" chars
 842        $name =~ s/(?<!\\)"/\\"/g;       ##escape quotes
 843        $name = "\"$name\"";
 844    }
 845
 846    if ($usename) {
 847        if ("$name" eq "") {
 848            $formatted_email = "$address";
 849        } else {
 850            $formatted_email = "$name <$address>";
 851        }
 852    } else {
 853        $formatted_email = $address;
 854    }
 855
 856    return $formatted_email;
 857}
 858
 859sub find_first_section {
 860    my $index = 0;
 861
 862    while ($index < @typevalue) {
 863        my $tv = $typevalue[$index];
 864        if (($tv =~ m/^(.):\s*(.*)/)) {
 865            last;
 866        }
 867        $index++;
 868    }
 869
 870    return $index;
 871}
 872
 873sub find_starting_index {
 874    my ($index) = @_;
 875
 876    while ($index > 0) {
 877        my $tv = $typevalue[$index];
 878        if (!($tv =~ m/^(.):\s*(.*)/)) {
 879            last;
 880        }
 881        $index--;
 882    }
 883
 884    return $index;
 885}
 886
 887sub find_ending_index {
 888    my ($index) = @_;
 889
 890    while ($index < @typevalue) {
 891        my $tv = $typevalue[$index];
 892        if (!($tv =~ m/^(.):\s*(.*)/)) {
 893            last;
 894        }
 895        $index++;
 896    }
 897
 898    return $index;
 899}
 900
 901sub get_subsystem_name {
 902    my ($index) = @_;
 903
 904    my $start = find_starting_index($index);
 905
 906    my $subsystem = $typevalue[$start];
 907    if (length($subsystem) > 20) {
 908        $subsystem = substr($subsystem, 0, 17);
 909        $subsystem =~ s/\s*$//;
 910        $subsystem = $subsystem . "...";
 911    }
 912    return $subsystem;
 913}
 914
 915sub get_maintainer_role {
 916    my ($index) = @_;
 917
 918    my $i;
 919    my $start = find_starting_index($index);
 920    my $end = find_ending_index($index);
 921
 922    my $role = "unknown";
 923    my $subsystem = get_subsystem_name($index);
 924
 925    for ($i = $start + 1; $i < $end; $i++) {
 926        my $tv = $typevalue[$i];
 927        if ($tv =~ m/^(.):\s*(.*)/) {
 928            my $ptype = $1;
 929            my $pvalue = $2;
 930            if ($ptype eq "S") {
 931                $role = $pvalue;
 932            }
 933        }
 934    }
 935
 936    $role = lc($role);
 937    if      ($role eq "supported") {
 938        $role = "supporter";
 939    } elsif ($role eq "maintained") {
 940        $role = "maintainer";
 941    } elsif ($role eq "odd fixes") {
 942        $role = "odd fixer";
 943    } elsif ($role eq "orphan") {
 944        $role = "orphan minder";
 945    } elsif ($role eq "obsolete") {
 946        $role = "obsolete minder";
 947    } elsif ($role eq "buried alive in reporters") {
 948        $role = "chief penguin";
 949    }
 950
 951    return $role . ":" . $subsystem;
 952}
 953
 954sub get_list_role {
 955    my ($index) = @_;
 956
 957    my $subsystem = get_subsystem_name($index);
 958
 959    if ($subsystem eq "THE REST") {
 960        $subsystem = "";
 961    }
 962
 963    return $subsystem;
 964}
 965
 966sub add_categories {
 967    my ($index) = @_;
 968
 969    my $i;
 970    my $start = find_starting_index($index);
 971    my $end = find_ending_index($index);
 972
 973    push(@subsystem, $typevalue[$start]);
 974
 975    for ($i = $start + 1; $i < $end; $i++) {
 976        my $tv = $typevalue[$i];
 977        if ($tv =~ m/^(.):\s*(.*)/) {
 978            my $ptype = $1;
 979            my $pvalue = $2;
 980            if ($ptype eq "L") {
 981                my $list_address = $pvalue;
 982                my $list_additional = "";
 983                my $list_role = get_list_role($i);
 984
 985                if ($list_role ne "") {
 986                    $list_role = ":" . $list_role;
 987                }
 988                if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
 989                    $list_address = $1;
 990                    $list_additional = $2;
 991                }
 992                if ($list_additional =~ m/subscribers-only/) {
 993                    if ($email_subscriber_list) {
 994                        if (!$hash_list_to{lc($list_address)}) {
 995                            $hash_list_to{lc($list_address)} = 1;
 996                            push(@list_to, [$list_address,
 997                                            "subscriber list${list_role}"]);
 998                        }
 999                    }
1000                } else {
1001                    if ($email_list) {
1002                        if (!$hash_list_to{lc($list_address)}) {
1003                            $hash_list_to{lc($list_address)} = 1;
1004                            if ($list_additional =~ m/moderated/) {
1005                                push(@list_to, [$list_address,
1006                                                "moderated list${list_role}"]);
1007                            } else {
1008                                push(@list_to, [$list_address,
1009                                                "open list${list_role}"]);
1010                            }
1011                        }
1012                    }
1013                }
1014            } elsif ($ptype eq "M") {
1015                my ($name, $address) = parse_email($pvalue);
1016                if ($name eq "") {
1017                    if ($i > 0) {
1018                        my $tv = $typevalue[$i - 1];
1019                        if ($tv =~ m/^(.):\s*(.*)/) {
1020                            if ($1 eq "P") {
1021                                $name = $2;
1022                                $pvalue = format_email($name, $address, $email_usename);
1023                            }
1024                        }
1025                    }
1026                }
1027                if ($email_maintainer) {
1028                    my $role = get_maintainer_role($i);
1029                    push_email_addresses($pvalue, $role);
1030                }
1031            } elsif ($ptype eq "R") {
1032                my ($name, $address) = parse_email($pvalue);
1033                if ($name eq "") {
1034                    if ($i > 0) {
1035                        my $tv = $typevalue[$i - 1];
1036                        if ($tv =~ m/^(.):\s*(.*)/) {
1037                            if ($1 eq "P") {
1038                                $name = $2;
1039                                $pvalue = format_email($name, $address, $email_usename);
1040                            }
1041                        }
1042                    }
1043                }
1044                if ($email_reviewer) {
1045                    my $subsystem = get_subsystem_name($i);
1046                    push_email_addresses($pvalue, "reviewer:$subsystem");
1047                }
1048            } elsif ($ptype eq "T") {
1049                push(@scm, $pvalue);
1050            } elsif ($ptype eq "W") {
1051                push(@web, $pvalue);
1052            } elsif ($ptype eq "S") {
1053                push(@status, $pvalue);
1054            }
1055        }
1056    }
1057}
1058
1059sub email_inuse {
1060    my ($name, $address) = @_;
1061
1062    return 1 if (($name eq "") && ($address eq ""));
1063    return 1 if (($name ne "") && exists($email_hash_name{lc($name)}));
1064    return 1 if (($address ne "") && exists($email_hash_address{lc($address)}));
1065
1066    return 0;
1067}
1068
1069sub push_email_address {
1070    my ($line, $role) = @_;
1071
1072    my ($name, $address) = parse_email($line);
1073
1074    if ($address eq "") {
1075        return 0;
1076    }
1077
1078    if (!$email_remove_duplicates) {
1079        push(@email_to, [format_email($name, $address, $email_usename), $role]);
1080    } elsif (!email_inuse($name, $address)) {
1081        push(@email_to, [format_email($name, $address, $email_usename), $role]);
1082        $email_hash_name{lc($name)}++ if ($name ne "");
1083        $email_hash_address{lc($address)}++;
1084    }
1085
1086    return 1;
1087}
1088
1089sub push_email_addresses {
1090    my ($address, $role) = @_;
1091
1092    my @address_list = ();
1093
1094    if (rfc822_valid($address)) {
1095        push_email_address($address, $role);
1096    } elsif (@address_list = rfc822_validlist($address)) {
1097        my $array_count = shift(@address_list);
1098        while (my $entry = shift(@address_list)) {
1099            push_email_address($entry, $role);
1100        }
1101    } else {
1102        if (!push_email_address($address, $role)) {
1103            warn("Invalid MAINTAINERS address: '" . $address . "'\n");
1104        }
1105    }
1106}
1107
1108sub add_role {
1109    my ($line, $role) = @_;
1110
1111    my ($name, $address) = parse_email($line);
1112    my $email = format_email($name, $address, $email_usename);
1113
1114    foreach my $entry (@email_to) {
1115        if ($email_remove_duplicates) {
1116            my ($entry_name, $entry_address) = parse_email($entry->[0]);
1117            if (($name eq $entry_name || $address eq $entry_address)
1118                && ($role eq "" || !($entry->[1] =~ m/$role/))
1119            ) {
1120                if ($entry->[1] eq "") {
1121                    $entry->[1] = "$role";
1122                } else {
1123                    $entry->[1] = "$entry->[1],$role";
1124                }
1125            }
1126        } else {
1127            if ($email eq $entry->[0]
1128                && ($role eq "" || !($entry->[1] =~ m/$role/))
1129            ) {
1130                if ($entry->[1] eq "") {
1131                    $entry->[1] = "$role";
1132                } else {
1133                    $entry->[1] = "$entry->[1],$role";
1134                }
1135            }
1136        }
1137    }
1138}
1139
1140sub which {
1141    my ($bin) = @_;
1142
1143    foreach my $path (split(/:/, $ENV{PATH})) {
1144        if (-e "$path/$bin") {
1145            return "$path/$bin";
1146        }
1147    }
1148
1149    return "";
1150}
1151
1152sub which_conf {
1153    my ($conf) = @_;
1154
1155    foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
1156        if (-e "$path/$conf") {
1157            return "$path/$conf";
1158        }
1159    }
1160
1161    return "";
1162}
1163
1164sub mailmap_email {
1165    my ($line) = @_;
1166
1167    my ($name, $address) = parse_email($line);
1168    my $email = format_email($name, $address, 1);
1169    my $real_name = $name;
1170    my $real_address = $address;
1171
1172    if (exists $mailmap->{names}->{$email} ||
1173        exists $mailmap->{addresses}->{$email}) {
1174        if (exists $mailmap->{names}->{$email}) {
1175            $real_name = $mailmap->{names}->{$email};
1176        }
1177        if (exists $mailmap->{addresses}->{$email}) {
1178            $real_address = $mailmap->{addresses}->{$email};
1179        }
1180    } else {
1181        if (exists $mailmap->{names}->{$address}) {
1182            $real_name = $mailmap->{names}->{$address};
1183        }
1184        if (exists $mailmap->{addresses}->{$address}) {
1185            $real_address = $mailmap->{addresses}->{$address};
1186        }
1187    }
1188    return format_email($real_name, $real_address, 1);
1189}
1190
1191sub mailmap {
1192    my (@addresses) = @_;
1193
1194    my @mapped_emails = ();
1195    foreach my $line (@addresses) {
1196        push(@mapped_emails, mailmap_email($line));
1197    }
1198    merge_by_realname(@mapped_emails) if ($email_use_mailmap);
1199    return @mapped_emails;
1200}
1201
1202sub merge_by_realname {
1203    my %address_map;
1204    my (@emails) = @_;
1205
1206    foreach my $email (@emails) {
1207        my ($name, $address) = parse_email($email);
1208        if (exists $address_map{$name}) {
1209            $address = $address_map{$name};
1210            $email = format_email($name, $address, 1);
1211        } else {
1212            $address_map{$name} = $address;
1213        }
1214    }
1215}
1216
1217sub git_execute_cmd {
1218    my ($cmd) = @_;
1219    my @lines = ();
1220
1221    my $output = `$cmd`;
1222    $output =~ s/^\s*//gm;
1223    @lines = split("\n", $output);
1224
1225    return @lines;
1226}
1227
1228sub hg_execute_cmd {
1229    my ($cmd) = @_;
1230    my @lines = ();
1231
1232    my $output = `$cmd`;
1233    @lines = split("\n", $output);
1234
1235    return @lines;
1236}
1237
1238sub extract_formatted_signatures {
1239    my (@signature_lines) = @_;
1240
1241    my @type = @signature_lines;
1242
1243    s/\s*(.*):.*/$1/ for (@type);
1244
1245    # cut -f2- -d":"
1246    s/\s*.*:\s*(.+)\s*/$1/ for (@signature_lines);
1247
1248## Reformat email addresses (with names) to avoid badly written signatures
1249
1250    foreach my $signer (@signature_lines) {
1251        $signer = deduplicate_email($signer);
1252    }
1253
1254    return (\@type, \@signature_lines);
1255}
1256
1257sub vcs_find_signers {
1258    my ($cmd) = @_;
1259    my $commits;
1260    my @lines = ();
1261    my @signatures = ();
1262
1263    @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1264
1265    my $pattern = $VCS_cmds{"commit_pattern"};
1266
1267    $commits = grep(/$pattern/, @lines);        # of commits
1268
1269    @signatures = grep(/^[ \t]*${signature_pattern}.*\@.*$/, @lines);
1270
1271    return (0, @signatures) if !@signatures;
1272
1273    save_commits_by_author(@lines) if ($interactive);
1274    save_commits_by_signer(@lines) if ($interactive);
1275
1276    my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1277
1278    return ($commits, @$signers_ref);
1279}
1280
1281sub vcs_find_author {
1282    my ($cmd) = @_;
1283    my @lines = ();
1284
1285    @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1286
1287    return @lines if !@lines;
1288
1289    my @authors = ();
1290    foreach my $line (@lines) {
1291        if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1292            my $author = $1;
1293            my ($name, $address) = parse_email($author);
1294            $author = format_email($name, $address, 1);
1295            push(@authors, $author);
1296        }
1297    }
1298
1299    save_commits_by_author(@lines) if ($interactive);
1300    save_commits_by_signer(@lines) if ($interactive);
1301
1302    return @authors;
1303}
1304
1305sub vcs_save_commits {
1306    my ($cmd) = @_;
1307    my @lines = ();
1308    my @commits = ();
1309
1310    @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1311
1312    foreach my $line (@lines) {
1313        if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1314            push(@commits, $1);
1315        }
1316    }
1317
1318    return @commits;
1319}
1320
1321sub vcs_blame {
1322    my ($file) = @_;
1323    my $cmd;
1324    my @commits = ();
1325
1326    return @commits if (!(-f $file));
1327
1328    if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1329        my @all_commits = ();
1330
1331        $cmd = $VCS_cmds{"blame_file_cmd"};
1332        $cmd =~ s/(\$\w+)/$1/eeg;               #interpolate $cmd
1333        @all_commits = vcs_save_commits($cmd);
1334
1335        foreach my $file_range_diff (@range) {
1336            next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1337            my $diff_file = $1;
1338            my $diff_start = $2;
1339            my $diff_length = $3;
1340            next if ("$file" ne "$diff_file");
1341            for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1342                push(@commits, $all_commits[$i]);
1343            }
1344        }
1345    } elsif (@range) {
1346        foreach my $file_range_diff (@range) {
1347            next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1348            my $diff_file = $1;
1349            my $diff_start = $2;
1350            my $diff_length = $3;
1351            next if ("$file" ne "$diff_file");
1352            $cmd = $VCS_cmds{"blame_range_cmd"};
1353            $cmd =~ s/(\$\w+)/$1/eeg;           #interpolate $cmd
1354            push(@commits, vcs_save_commits($cmd));
1355        }
1356    } else {
1357        $cmd = $VCS_cmds{"blame_file_cmd"};
1358        $cmd =~ s/(\$\w+)/$1/eeg;               #interpolate $cmd
1359        @commits = vcs_save_commits($cmd);
1360    }
1361
1362    foreach my $commit (@commits) {
1363        $commit =~ s/^\^//g;
1364    }
1365
1366    return @commits;
1367}
1368
1369my $printed_novcs = 0;
1370sub vcs_exists {
1371    %VCS_cmds = %VCS_cmds_git;
1372    return 1 if eval $VCS_cmds{"available"};
1373    %VCS_cmds = %VCS_cmds_hg;
1374    return 2 if eval $VCS_cmds{"available"};
1375    %VCS_cmds = ();
1376    if (!$printed_novcs) {
1377        warn("$P: No supported VCS found.  Add --nogit to options?\n");
1378        warn("Using a git repository produces better results.\n");
1379        warn("Try latest git repository using:\n");
1380        warn("git clone https://gitlab.com/qemu-project/qemu.git\n");
1381        $printed_novcs = 1;
1382    }
1383    return 0;
1384}
1385
1386sub vcs_is_git {
1387    vcs_exists();
1388    return $vcs_used == 1;
1389}
1390
1391sub vcs_is_hg {
1392    return $vcs_used == 2;
1393}
1394
1395sub interactive_get_maintainers {
1396    my ($list_ref) = @_;
1397    my @list = @$list_ref;
1398
1399    vcs_exists();
1400
1401    my %selected;
1402    my %authored;
1403    my %signed;
1404    my $count = 0;
1405    my $maintained = 0;
1406    foreach my $entry (@list) {
1407        $maintained = 1 if ($entry->[1] =~ /^(maintainer|supporter)/i);
1408        $selected{$count} = 1;
1409        $authored{$count} = 0;
1410        $signed{$count} = 0;
1411        $count++;
1412    }
1413
1414    #menu loop
1415    my $done = 0;
1416    my $print_options = 0;
1417    my $redraw = 1;
1418    while (!$done) {
1419        $count = 0;
1420        if ($redraw) {
1421            printf STDERR "\n%1s %2s %-65s",
1422                          "*", "#", "email/list and role:stats";
1423            if ($email_git ||
1424                ($email_git_fallback && !$maintained) ||
1425                $email_git_blame) {
1426                print STDERR "auth sign";
1427            }
1428            print STDERR "\n";
1429            foreach my $entry (@list) {
1430                my $email = $entry->[0];
1431                my $role = $entry->[1];
1432                my $sel = "";
1433                $sel = "*" if ($selected{$count});
1434                my $commit_author = $commit_author_hash{$email};
1435                my $commit_signer = $commit_signer_hash{$email};
1436                my $authored = 0;
1437                my $signed = 0;
1438                $authored++ for (@{$commit_author});
1439                $signed++ for (@{$commit_signer});
1440                printf STDERR "%1s %2d %-65s", $sel, $count + 1, $email;
1441                printf STDERR "%4d %4d", $authored, $signed
1442                    if ($authored > 0 || $signed > 0);
1443                printf STDERR "\n     %s\n", $role;
1444                if ($authored{$count}) {
1445                    my $commit_author = $commit_author_hash{$email};
1446                    foreach my $ref (@{$commit_author}) {
1447                        print STDERR "     Author: @{$ref}[1]\n";
1448                    }
1449                }
1450                if ($signed{$count}) {
1451                    my $commit_signer = $commit_signer_hash{$email};
1452                    foreach my $ref (@{$commit_signer}) {
1453                        print STDERR "     @{$ref}[2]: @{$ref}[1]\n";
1454                    }
1455                }
1456
1457                $count++;
1458            }
1459        }
1460        my $date_ref = \$email_git_since;
1461        $date_ref = \$email_hg_since if (vcs_is_hg());
1462        if ($print_options) {
1463            $print_options = 0;
1464            if (vcs_exists()) {
1465                print STDERR <<EOT
1466
1467Version Control options:
1468g  use git history      [$email_git]
1469gf use git-fallback     [$email_git_fallback]
1470b  use git blame        [$email_git_blame]
1471bs use blame signatures [$email_git_blame_signatures]
1472c# minimum commits      [$email_git_min_signatures]
1473%# min percent          [$email_git_min_percent]
1474d# history to use       [$$date_ref]
1475x# max maintainers      [$email_git_max_maintainers]
1476t  all signature types  [$email_git_all_signature_types]
1477m  use .mailmap         [$email_use_mailmap]
1478EOT
1479            }
1480            print STDERR <<EOT
1481
1482Additional options:
14830  toggle all
1484tm toggle maintainers
1485tg toggle git entries
1486tl toggle open list entries
1487ts toggle subscriber list entries
1488f  emails in file       [$file_emails]
1489k  keywords in file     [$keywords]
1490r  remove duplicates    [$email_remove_duplicates]
1491p# pattern match depth  [$pattern_depth]
1492EOT
1493        }
1494        print STDERR
1495"\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ";
1496
1497        my $input = <STDIN>;
1498        chomp($input);
1499
1500        $redraw = 1;
1501        my $rerun = 0;
1502        my @wish = split(/[, ]+/, $input);
1503        foreach my $nr (@wish) {
1504            $nr = lc($nr);
1505            my $sel = substr($nr, 0, 1);
1506            my $str = substr($nr, 1);
1507            my $val = 0;
1508            $val = $1 if $str =~ /^(\d+)$/;
1509
1510            if ($sel eq "y") {
1511                $interactive = 0;
1512                $done = 1;
1513                $output_rolestats = 0;
1514                $output_roles = 0;
1515                last;
1516            } elsif ($nr =~ /^\d+$/ && $nr > 0 && $nr <= $count) {
1517                $selected{$nr - 1} = !$selected{$nr - 1};
1518            } elsif ($sel eq "*" || $sel eq '^') {
1519                my $toggle = 0;
1520                $toggle = 1 if ($sel eq '*');
1521                for (my $i = 0; $i < $count; $i++) {
1522                    $selected{$i} = $toggle;
1523                }
1524            } elsif ($sel eq "0") {
1525                for (my $i = 0; $i < $count; $i++) {
1526                    $selected{$i} = !$selected{$i};
1527                }
1528            } elsif ($sel eq "t") {
1529                if (lc($str) eq "m") {
1530                    for (my $i = 0; $i < $count; $i++) {
1531                        $selected{$i} = !$selected{$i}
1532                            if ($list[$i]->[1] =~ /^(maintainer|supporter)/i);
1533                    }
1534                } elsif (lc($str) eq "g") {
1535                    for (my $i = 0; $i < $count; $i++) {
1536                        $selected{$i} = !$selected{$i}
1537                            if ($list[$i]->[1] =~ /^(author|commit|signer)/i);
1538                    }
1539                } elsif (lc($str) eq "l") {
1540                    for (my $i = 0; $i < $count; $i++) {
1541                        $selected{$i} = !$selected{$i}
1542                            if ($list[$i]->[1] =~ /^(open list)/i);
1543                    }
1544                } elsif (lc($str) eq "s") {
1545                    for (my $i = 0; $i < $count; $i++) {
1546                        $selected{$i} = !$selected{$i}
1547                            if ($list[$i]->[1] =~ /^(subscriber list)/i);
1548                    }
1549                }
1550            } elsif ($sel eq "a") {
1551                if ($val > 0 && $val <= $count) {
1552                    $authored{$val - 1} = !$authored{$val - 1};
1553                } elsif ($str eq '*' || $str eq '^') {
1554                    my $toggle = 0;
1555                    $toggle = 1 if ($str eq '*');
1556                    for (my $i = 0; $i < $count; $i++) {
1557                        $authored{$i} = $toggle;
1558                    }
1559                }
1560            } elsif ($sel eq "s") {
1561                if ($val > 0 && $val <= $count) {
1562                    $signed{$val - 1} = !$signed{$val - 1};
1563                } elsif ($str eq '*' || $str eq '^') {
1564                    my $toggle = 0;
1565                    $toggle = 1 if ($str eq '*');
1566                    for (my $i = 0; $i < $count; $i++) {
1567                        $signed{$i} = $toggle;
1568                    }
1569                }
1570            } elsif ($sel eq "o") {
1571                $print_options = 1;
1572                $redraw = 1;
1573            } elsif ($sel eq "g") {
1574                if ($str eq "f") {
1575                    bool_invert(\$email_git_fallback);
1576                } else {
1577                    bool_invert(\$email_git);
1578                }
1579                $rerun = 1;
1580            } elsif ($sel eq "b") {
1581                if ($str eq "s") {
1582                    bool_invert(\$email_git_blame_signatures);
1583                } else {
1584                    bool_invert(\$email_git_blame);
1585                }
1586                $rerun = 1;
1587            } elsif ($sel eq "c") {
1588                if ($val > 0) {
1589                    $email_git_min_signatures = $val;
1590                    $rerun = 1;
1591                }
1592            } elsif ($sel eq "x") {
1593                if ($val > 0) {
1594                    $email_git_max_maintainers = $val;
1595                    $rerun = 1;
1596                }
1597            } elsif ($sel eq "%") {
1598                if ($str ne "" && $val >= 0) {
1599                    $email_git_min_percent = $val;
1600                    $rerun = 1;
1601                }
1602            } elsif ($sel eq "d") {
1603                if (vcs_is_git()) {
1604                    $email_git_since = $str;
1605                } elsif (vcs_is_hg()) {
1606                    $email_hg_since = $str;
1607                }
1608                $rerun = 1;
1609            } elsif ($sel eq "t") {
1610                bool_invert(\$email_git_all_signature_types);
1611                $rerun = 1;
1612            } elsif ($sel eq "f") {
1613                bool_invert(\$file_emails);
1614                $rerun = 1;
1615            } elsif ($sel eq "r") {
1616                bool_invert(\$email_remove_duplicates);
1617                $rerun = 1;
1618            } elsif ($sel eq "m") {
1619                bool_invert(\$email_use_mailmap);
1620                read_mailmap();
1621                $rerun = 1;
1622            } elsif ($sel eq "k") {
1623                bool_invert(\$keywords);
1624                $rerun = 1;
1625            } elsif ($sel eq "p") {
1626                if ($str ne "" && $val >= 0) {
1627                    $pattern_depth = $val;
1628                    $rerun = 1;
1629                }
1630            } elsif ($sel eq "h" || $sel eq "?") {
1631                print STDERR <<EOT
1632
1633Interactive mode allows you to select the various maintainers, submitters,
1634commit signers and mailing lists that could be CC'd on a patch.
1635
1636Any *'d entry is selected.
1637
1638If you have git or hg installed, you can choose to summarize the commit
1639history of files in the patch.  Also, each line of the current file can
1640be matched to its commit author and that commits signers with blame.
1641
1642Various knobs exist to control the length of time for active commit
1643tracking, the maximum number of commit authors and signers to add,
1644and such.
1645
1646Enter selections at the prompt until you are satisfied that the selected
1647maintainers are appropriate.  You may enter multiple selections separated
1648by either commas or spaces.
1649
1650EOT
1651            } else {
1652                print STDERR "invalid option: '$nr'\n";
1653                $redraw = 0;
1654            }
1655        }
1656        if ($rerun) {
1657            print STDERR "git-blame can be very slow, please have patience..."
1658                if ($email_git_blame);
1659            goto &get_maintainers;
1660        }
1661    }
1662
1663    #drop not selected entries
1664    $count = 0;
1665    my @new_emailto = ();
1666    foreach my $entry (@list) {
1667        if ($selected{$count}) {
1668            push(@new_emailto, $list[$count]);
1669        }
1670        $count++;
1671    }
1672    return @new_emailto;
1673}
1674
1675sub bool_invert {
1676    my ($bool_ref) = @_;
1677
1678    if ($$bool_ref) {
1679        $$bool_ref = 0;
1680    } else {
1681        $$bool_ref = 1;
1682    }
1683}
1684
1685sub deduplicate_email {
1686    my ($email) = @_;
1687
1688    my $matched = 0;
1689    my ($name, $address) = parse_email($email);
1690    $email = format_email($name, $address, 1);
1691    $email = mailmap_email($email);
1692
1693    return $email if (!$email_remove_duplicates);
1694
1695    ($name, $address) = parse_email($email);
1696
1697    if ($name ne "" && $deduplicate_name_hash{lc($name)}) {
1698        $name = $deduplicate_name_hash{lc($name)}->[0];
1699        $address = $deduplicate_name_hash{lc($name)}->[1];
1700        $matched = 1;
1701    } elsif ($deduplicate_address_hash{lc($address)}) {
1702        $name = $deduplicate_address_hash{lc($address)}->[0];
1703        $address = $deduplicate_address_hash{lc($address)}->[1];
1704        $matched = 1;
1705    }
1706    if (!$matched) {
1707        $deduplicate_name_hash{lc($name)} = [ $name, $address ];
1708        $deduplicate_address_hash{lc($address)} = [ $name, $address ];
1709    }
1710    $email = format_email($name, $address, 1);
1711    $email = mailmap_email($email);
1712    return $email;
1713}
1714
1715sub save_commits_by_author {
1716    my (@lines) = @_;
1717
1718    my @authors = ();
1719    my @commits = ();
1720    my @subjects = ();
1721
1722    foreach my $line (@lines) {
1723        if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1724            my $author = $1;
1725            $author = deduplicate_email($author);
1726            push(@authors, $author);
1727        }
1728        push(@commits, $1) if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1729        push(@subjects, $1) if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1730    }
1731
1732    for (my $i = 0; $i < @authors; $i++) {
1733        my $exists = 0;
1734        foreach my $ref(@{$commit_author_hash{$authors[$i]}}) {
1735            if (@{$ref}[0] eq $commits[$i] &&
1736                @{$ref}[1] eq $subjects[$i]) {
1737                $exists = 1;
1738                last;
1739            }
1740        }
1741        if (!$exists) {
1742            push(@{$commit_author_hash{$authors[$i]}},
1743                 [ ($commits[$i], $subjects[$i]) ]);
1744        }
1745    }
1746}
1747
1748sub save_commits_by_signer {
1749    my (@lines) = @_;
1750
1751    my $commit = "";
1752    my $subject = "";
1753
1754    foreach my $line (@lines) {
1755        $commit = $1 if ($line =~ m/$VCS_cmds{"commit_pattern"}/);
1756        $subject = $1 if ($line =~ m/$VCS_cmds{"subject_pattern"}/);
1757        if ($line =~ /^[ \t]*${signature_pattern}.*\@.*$/) {
1758            my @signatures = ($line);
1759            my ($types_ref, $signers_ref) = extract_formatted_signatures(@signatures);
1760            my @types = @$types_ref;
1761            my @signers = @$signers_ref;
1762
1763            my $type = $types[0];
1764            my $signer = $signers[0];
1765
1766            $signer = deduplicate_email($signer);
1767
1768            my $exists = 0;
1769            foreach my $ref(@{$commit_signer_hash{$signer}}) {
1770                if (@{$ref}[0] eq $commit &&
1771                    @{$ref}[1] eq $subject &&
1772                    @{$ref}[2] eq $type) {
1773                    $exists = 1;
1774                    last;
1775                }
1776            }
1777            if (!$exists) {
1778                push(@{$commit_signer_hash{$signer}},
1779                     [ ($commit, $subject, $type) ]);
1780            }
1781        }
1782    }
1783}
1784
1785sub vcs_assign {
1786    my ($role, $divisor, @lines) = @_;
1787
1788    my %hash;
1789    my $count = 0;
1790
1791    return if (@lines <= 0);
1792
1793    if ($divisor <= 0) {
1794        warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
1795        $divisor = 1;
1796    }
1797
1798    @lines = mailmap(@lines);
1799
1800    return if (@lines <= 0);
1801
1802    @lines = sort(@lines);
1803
1804    # uniq -c
1805    $hash{$_}++ for @lines;
1806
1807    # sort -rn
1808    foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
1809        my $sign_offs = $hash{$line};
1810        my $percent = $sign_offs * 100 / $divisor;
1811
1812        $percent = 100 if ($percent > 100);
1813        $count++;
1814        last if ($sign_offs < $email_git_min_signatures ||
1815                 $count > $email_git_max_maintainers ||
1816                 $percent < $email_git_min_percent);
1817        push_email_address($line, '');
1818        if ($output_rolestats) {
1819            my $fmt_percent = sprintf("%.0f", $percent);
1820            add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%");
1821        } else {
1822            add_role($line, $role);
1823        }
1824    }
1825}
1826
1827sub vcs_file_signoffs {
1828    my ($file) = @_;
1829
1830    my @signers = ();
1831    my $commits;
1832
1833    $vcs_used = vcs_exists();
1834    return if (!$vcs_used);
1835
1836    my $cmd = $VCS_cmds{"find_signers_cmd"};
1837    $cmd =~ s/(\$\w+)/$1/eeg;           # interpolate $cmd
1838
1839    ($commits, @signers) = vcs_find_signers($cmd);
1840
1841    foreach my $signer (@signers) {
1842        $signer = deduplicate_email($signer);
1843    }
1844
1845    vcs_assign("commit_signer", $commits, @signers);
1846}
1847
1848sub vcs_file_blame {
1849    my ($file) = @_;
1850
1851    my @signers = ();
1852    my @all_commits = ();
1853    my @commits = ();
1854    my $total_commits;
1855    my $total_lines;
1856
1857    $vcs_used = vcs_exists();
1858    return if (!$vcs_used);
1859
1860    @all_commits = vcs_blame($file);
1861    @commits = uniq(@all_commits);
1862    $total_commits = @commits;
1863    $total_lines = @all_commits;
1864
1865    if ($email_git_blame_signatures) {
1866        if (vcs_is_hg()) {
1867            my $commit_count;
1868            my @commit_signers = ();
1869            my $commit = join(" -r ", @commits);
1870            my $cmd;
1871
1872            $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1873            $cmd =~ s/(\$\w+)/$1/eeg;   #substitute variables in $cmd
1874
1875            ($commit_count, @commit_signers) = vcs_find_signers($cmd);
1876
1877            push(@signers, @commit_signers);
1878        } else {
1879            foreach my $commit (@commits) {
1880                my $commit_count;
1881                my @commit_signers = ();
1882                my $cmd;
1883
1884                $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1885                $cmd =~ s/(\$\w+)/$1/eeg;       #substitute variables in $cmd
1886
1887                ($commit_count, @commit_signers) = vcs_find_signers($cmd);
1888
1889                push(@signers, @commit_signers);
1890            }
1891        }
1892    }
1893
1894    if ($from_filename) {
1895        if ($output_rolestats) {
1896            my @blame_signers;
1897            if (vcs_is_hg()) {{         # Double brace for last exit
1898                my $commit_count;
1899                my @commit_signers = ();
1900                @commits = uniq(@commits);
1901                @commits = sort(@commits);
1902                my $commit = join(" -r ", @commits);
1903                my $cmd;
1904
1905                $cmd = $VCS_cmds{"find_commit_author_cmd"};
1906                $cmd =~ s/(\$\w+)/$1/eeg;       #substitute variables in $cmd
1907
1908                my @lines = ();
1909
1910                @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1911
1912                last if !@lines;
1913
1914                my @authors = ();
1915                foreach my $line (@lines) {
1916                    if ($line =~ m/$VCS_cmds{"author_pattern"}/) {
1917                        my $author = $1;
1918                        $author = deduplicate_email($author);
1919                        push(@authors, $author);
1920                    }
1921                }
1922
1923                save_commits_by_author(@lines) if ($interactive);
1924                save_commits_by_signer(@lines) if ($interactive);
1925
1926                push(@signers, @authors);
1927            }}
1928            else {
1929                foreach my $commit (@commits) {
1930                    my $i;
1931                    my $cmd = $VCS_cmds{"find_commit_author_cmd"};
1932                    $cmd =~ s/(\$\w+)/$1/eeg;   #interpolate $cmd
1933                    my @author = vcs_find_author($cmd);
1934                    next if !@author;
1935
1936                    my $formatted_author = deduplicate_email($author[0]);
1937
1938                    my $count = grep(/$commit/, @all_commits);
1939                    for ($i = 0; $i < $count ; $i++) {
1940                        push(@blame_signers, $formatted_author);
1941                    }
1942                }
1943            }
1944            if (@blame_signers) {
1945                vcs_assign("authored lines", $total_lines, @blame_signers);
1946            }
1947        }
1948        foreach my $signer (@signers) {
1949            $signer = deduplicate_email($signer);
1950        }
1951        vcs_assign("commits", $total_commits, @signers);
1952    } else {
1953        foreach my $signer (@signers) {
1954            $signer = deduplicate_email($signer);
1955        }
1956        vcs_assign("modified commits", $total_commits, @signers);
1957    }
1958}
1959
1960sub uniq {
1961    my (@parms) = @_;
1962
1963    my %saw;
1964    @parms = grep(!$saw{$_}++, @parms);
1965    return @parms;
1966}
1967
1968sub sort_and_uniq {
1969    my (@parms) = @_;
1970
1971    my %saw;
1972    @parms = sort @parms;
1973    @parms = grep(!$saw{$_}++, @parms);
1974    return @parms;
1975}
1976
1977sub clean_file_emails {
1978    my (@file_emails) = @_;
1979    my @fmt_emails = ();
1980
1981    foreach my $email (@file_emails) {
1982        $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
1983        my ($name, $address) = parse_email($email);
1984        if ($name eq '"[,\.]"') {
1985            $name = "";
1986        }
1987
1988        my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name);
1989        if (@nw > 2) {
1990            my $first = $nw[@nw - 3];
1991            my $middle = $nw[@nw - 2];
1992            my $last = $nw[@nw - 1];
1993
1994            if (((length($first) == 1 && $first =~ m/[A-Za-z]/) ||
1995                 (length($first) == 2 && substr($first, -1) eq ".")) ||
1996                (length($middle) == 1 ||
1997                 (length($middle) == 2 && substr($middle, -1) eq "."))) {
1998                $name = "$first $middle $last";
1999            } else {
2000                $name = "$middle $last";
2001            }
2002        }
2003
2004        if (substr($name, -1) =~ /[,\.]/) {
2005            $name = substr($name, 0, length($name) - 1);
2006        } elsif (substr($name, -2) =~ /[,\.]"/) {
2007            $name = substr($name, 0, length($name) - 2) . '"';
2008        }
2009
2010        if (substr($name, 0, 1) =~ /[,\.]/) {
2011            $name = substr($name, 1, length($name) - 1);
2012        } elsif (substr($name, 0, 2) =~ /"[,\.]/) {
2013            $name = '"' . substr($name, 2, length($name) - 2);
2014        }
2015
2016        my $fmt_email = format_email($name, $address, $email_usename);
2017        push(@fmt_emails, $fmt_email);
2018    }
2019    return @fmt_emails;
2020}
2021
2022sub merge_email {
2023    my @lines;
2024    my %saw;
2025
2026    for (@_) {
2027        my ($address, $role) = @$_;
2028        if (!$saw{$address}) {
2029            if ($output_roles) {
2030                push(@lines, "$address ($role)");
2031            } else {
2032                push(@lines, $address);
2033            }
2034            $saw{$address} = 1;
2035        }
2036    }
2037
2038    return @lines;
2039}
2040
2041sub output {
2042    my (@parms) = @_;
2043
2044    if ($output_multiline) {
2045        foreach my $line (@parms) {
2046            print("${line}\n");
2047        }
2048    } else {
2049        print(join($output_separator, @parms));
2050        print("\n");
2051    }
2052}
2053
2054my $rfc822re;
2055
2056sub make_rfc822re {
2057#   Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
2058#   comment.  We must allow for rfc822_lwsp (or comments) after each of these.
2059#   This regexp will only work on addresses which have had comments stripped
2060#   and replaced with rfc822_lwsp.
2061
2062    my $specials = '()<>@,;:\\\\".\\[\\]';
2063    my $controls = '\\000-\\037\\177';
2064
2065    my $dtext = "[^\\[\\]\\r\\\\]";
2066    my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
2067
2068    my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
2069
2070#   Use zero-width assertion to spot the limit of an atom.  A simple
2071#   $rfc822_lwsp* causes the regexp engine to hang occasionally.
2072    my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
2073    my $word = "(?:$atom|$quoted_string)";
2074    my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
2075
2076    my $sub_domain = "(?:$atom|$domain_literal)";
2077    my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
2078
2079    my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
2080
2081    my $phrase = "$word*";
2082    my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
2083    my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
2084    my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
2085
2086    my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
2087    my $address = "(?:$mailbox|$group)";
2088
2089    return "$rfc822_lwsp*$address";
2090}
2091
2092sub rfc822_strip_comments {
2093    my $s = shift;
2094#   Recursively remove comments, and replace with a single space.  The simpler
2095#   regexps in the Email Addressing FAQ are imperfect - they will miss escaped
2096#   chars in atoms, for example.
2097
2098    while ($s =~ s/^((?:[^"\\]|\\.)*
2099                    (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
2100                    \((?:[^()\\]|\\.)*\)/$1 /osx) {}
2101    return $s;
2102}
2103
2104#   valid: returns true if the parameter is an RFC822 valid address
2105#
2106sub rfc822_valid {
2107    my $s = rfc822_strip_comments(shift);
2108
2109    if (!$rfc822re) {
2110        $rfc822re = make_rfc822re();
2111    }
2112
2113    return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
2114}
2115
2116#   validlist: In scalar context, returns true if the parameter is an RFC822
2117#              valid list of addresses.
2118#
2119#              In list context, returns an empty list on failure (an invalid
2120#              address was found); otherwise a list whose first element is the
2121#              number of addresses found and whose remaining elements are the
2122#              addresses.  This is needed to disambiguate failure (invalid)
2123#              from success with no addresses found, because an empty string is
2124#              a valid list.
2125
2126sub rfc822_validlist {
2127    my $s = rfc822_strip_comments(shift);
2128
2129    if (!$rfc822re) {
2130        $rfc822re = make_rfc822re();
2131    }
2132    # * null list items are valid according to the RFC
2133    # * the '1' business is to aid in distinguishing failure from no results
2134
2135    my @r;
2136    if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
2137        $s =~ m/^$rfc822_char*$/) {
2138        while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
2139            push(@r, $1);
2140        }
2141        return wantarray ? (scalar(@r), @r) : 1;
2142    }
2143    return wantarray ? () : 0;
2144}
2145