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