linux/scripts/get_maintainer.pl
<<
>>
Prefs
   1#!/usr/bin/perl -w
   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;
  14
  15my $P = $0;
  16my $V = '0.21';
  17
  18use Getopt::Long qw(:config no_auto_abbrev);
  19
  20my $lk_path = "./";
  21my $email = 1;
  22my $email_usename = 1;
  23my $email_maintainer = 1;
  24my $email_list = 1;
  25my $email_subscriber_list = 0;
  26my $email_git = 1;
  27my $email_git_penguin_chiefs = 0;
  28my $email_git_min_signatures = 1;
  29my $email_git_max_maintainers = 5;
  30my $email_git_min_percent = 5;
  31my $email_git_since = "1-year-ago";
  32my $email_git_blame = 0;
  33my $email_remove_duplicates = 1;
  34my $output_multiline = 1;
  35my $output_separator = ", ";
  36my $scm = 0;
  37my $web = 0;
  38my $subsystem = 0;
  39my $status = 0;
  40my $keywords = 1;
  41my $from_filename = 0;
  42my $pattern_depth = 0;
  43my $version = 0;
  44my $help = 0;
  45
  46my $exit = 0;
  47
  48my @penguin_chief = ();
  49push(@penguin_chief,"Linus Torvalds:torvalds\@linux-foundation.org");
  50#Andrew wants in on most everything - 2009/01/14
  51#push(@penguin_chief,"Andrew Morton:akpm\@linux-foundation.org");
  52
  53my @penguin_chief_names = ();
  54foreach my $chief (@penguin_chief) {
  55    if ($chief =~ m/^(.*):(.*)/) {
  56        my $chief_name = $1;
  57        my $chief_addr = $2;
  58        push(@penguin_chief_names, $chief_name);
  59    }
  60}
  61my $penguin_chiefs = "\(" . join("|",@penguin_chief_names) . "\)";
  62
  63# rfc822 email address - preloaded methods go here.
  64my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
  65my $rfc822_char = '[\\000-\\377]';
  66
  67if (!GetOptions(
  68                'email!' => \$email,
  69                'git!' => \$email_git,
  70                'git-chief-penguins!' => \$email_git_penguin_chiefs,
  71                'git-min-signatures=i' => \$email_git_min_signatures,
  72                'git-max-maintainers=i' => \$email_git_max_maintainers,
  73                'git-min-percent=i' => \$email_git_min_percent,
  74                'git-since=s' => \$email_git_since,
  75                'git-blame!' => \$email_git_blame,
  76                'remove-duplicates!' => \$email_remove_duplicates,
  77                'm!' => \$email_maintainer,
  78                'n!' => \$email_usename,
  79                'l!' => \$email_list,
  80                's!' => \$email_subscriber_list,
  81                'multiline!' => \$output_multiline,
  82                'separator=s' => \$output_separator,
  83                'subsystem!' => \$subsystem,
  84                'status!' => \$status,
  85                'scm!' => \$scm,
  86                'web!' => \$web,
  87                'pattern-depth=i' => \$pattern_depth,
  88                'k|keywords!' => \$keywords,
  89                'f|file' => \$from_filename,
  90                'v|version' => \$version,
  91                'h|help' => \$help,
  92                )) {
  93    usage();
  94    die "$P: invalid argument\n";
  95}
  96
  97if ($help != 0) {
  98    usage();
  99    exit 0;
 100}
 101
 102if ($version != 0) {
 103    print("${P} ${V}\n");
 104    exit 0;
 105}
 106
 107if ($#ARGV < 0) {
 108    usage();
 109    die "$P: argument missing: patchfile or -f file please\n";
 110}
 111
 112if ($output_separator ne ", ") {
 113    $output_multiline = 0;
 114}
 115
 116my $selections = $email + $scm + $status + $subsystem + $web;
 117if ($selections == 0) {
 118    usage();
 119    die "$P:  Missing required option: email, scm, status, subsystem or web\n";
 120}
 121
 122if ($email &&
 123    ($email_maintainer + $email_list + $email_subscriber_list +
 124     $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
 125    usage();
 126    die "$P: Please select at least 1 email option\n";
 127}
 128
 129if (!top_of_kernel_tree($lk_path)) {
 130    die "$P: The current directory does not appear to be "
 131        . "a linux kernel source tree.\n";
 132}
 133
 134## Read MAINTAINERS for type/value pairs
 135
 136my @typevalue = ();
 137my %keyword_hash;
 138
 139open(MAINT, "<${lk_path}MAINTAINERS") || die "$P: Can't open MAINTAINERS\n";
 140while (<MAINT>) {
 141    my $line = $_;
 142
 143    if ($line =~ m/^(\C):\s*(.*)/) {
 144        my $type = $1;
 145        my $value = $2;
 146
 147        ##Filename pattern matching
 148        if ($type eq "F" || $type eq "X") {
 149            $value =~ s@\.@\\\.@g;       ##Convert . to \.
 150            $value =~ s/\*/\.\*/g;       ##Convert * to .*
 151            $value =~ s/\?/\./g;         ##Convert ? to .
 152            ##if pattern is a directory and it lacks a trailing slash, add one
 153            if ((-d $value)) {
 154                $value =~ s@([^/])$@$1/@;
 155            }
 156        } elsif ($type eq "K") {
 157            $keyword_hash{@typevalue} = $value;
 158        }
 159        push(@typevalue, "$type:$value");
 160    } elsif (!/^(\s)*$/) {
 161        $line =~ s/\n$//g;
 162        push(@typevalue, $line);
 163    }
 164}
 165close(MAINT);
 166
 167my %mailmap;
 168
 169if ($email_remove_duplicates) {
 170    open(MAILMAP, "<${lk_path}.mailmap") || warn "$P: Can't open .mailmap\n";
 171    while (<MAILMAP>) {
 172        my $line = $_;
 173
 174        next if ($line =~ m/^\s*#/);
 175        next if ($line =~ m/^\s*$/);
 176
 177        my ($name, $address) = parse_email($line);
 178        $line = format_email($name, $address);
 179
 180        next if ($line =~ m/^\s*$/);
 181
 182        if (exists($mailmap{$name})) {
 183            my $obj = $mailmap{$name};
 184            push(@$obj, $address);
 185        } else {
 186            my @arr = ($address);
 187            $mailmap{$name} = \@arr;
 188        }
 189    }
 190    close(MAILMAP);
 191}
 192
 193## use the filenames on the command line or find the filenames in the patchfiles
 194
 195my @files = ();
 196my @range = ();
 197my @keyword_tvi = ();
 198
 199foreach my $file (@ARGV) {
 200    ##if $file is a directory and it lacks a trailing slash, add one
 201    if ((-d $file)) {
 202        $file =~ s@([^/])$@$1/@;
 203    } elsif (!(-f $file)) {
 204        die "$P: file '${file}' not found\n";
 205    }
 206    if ($from_filename) {
 207        push(@files, $file);
 208        if (-f $file && $keywords) {
 209            open(FILE, "<$file") or die "$P: Can't open ${file}\n";
 210            while (<FILE>) {
 211                my $patch_line = $_;
 212                foreach my $line (keys %keyword_hash) {
 213                    if ($patch_line =~ m/^.*$keyword_hash{$line}/x) {
 214                        push(@keyword_tvi, $line);
 215                    }
 216                }
 217            }
 218            close(FILE);
 219        }
 220    } else {
 221        my $file_cnt = @files;
 222        my $lastfile;
 223        open(PATCH, "<$file") or die "$P: Can't open ${file}\n";
 224        while (<PATCH>) {
 225            my $patch_line = $_;
 226            if (m/^\+\+\+\s+(\S+)/) {
 227                my $filename = $1;
 228                $filename =~ s@^[^/]*/@@;
 229                $filename =~ s@\n@@;
 230                $lastfile = $filename;
 231                push(@files, $filename);
 232            } elsif (m/^\@\@ -(\d+),(\d+)/) {
 233                if ($email_git_blame) {
 234                    push(@range, "$lastfile:$1:$2");
 235                }
 236            } elsif ($keywords) {
 237                foreach my $line (keys %keyword_hash) {
 238                    if ($patch_line =~ m/^[+-].*$keyword_hash{$line}/x) {
 239                        push(@keyword_tvi, $line);
 240                    }
 241                }
 242            }
 243        }
 244        close(PATCH);
 245        if ($file_cnt == @files) {
 246            warn "$P: file '${file}' doesn't appear to be a patch.  "
 247                . "Add -f to options?\n";
 248        }
 249        @files = sort_and_uniq(@files);
 250    }
 251}
 252
 253my @email_to = ();
 254my @list_to = ();
 255my @scm = ();
 256my @web = ();
 257my @subsystem = ();
 258my @status = ();
 259
 260# Find responsible parties
 261
 262foreach my $file (@files) {
 263
 264#Do not match excluded file patterns
 265
 266    my $exclude = 0;
 267    foreach my $line (@typevalue) {
 268        if ($line =~ m/^(\C):\s*(.*)/) {
 269            my $type = $1;
 270            my $value = $2;
 271            if ($type eq 'X') {
 272                if (file_match_pattern($file, $value)) {
 273                    $exclude = 1;
 274                    last;
 275                }
 276            }
 277        }
 278    }
 279
 280    if (!$exclude) {
 281        my $tvi = 0;
 282        my %hash;
 283        foreach my $line (@typevalue) {
 284            if ($line =~ m/^(\C):\s*(.*)/) {
 285                my $type = $1;
 286                my $value = $2;
 287                if ($type eq 'F') {
 288                    if (file_match_pattern($file, $value)) {
 289                        my $value_pd = ($value =~ tr@/@@);
 290                        my $file_pd = ($file  =~ tr@/@@);
 291                        $value_pd++ if (substr($value,-1,1) ne "/");
 292                        if ($pattern_depth == 0 ||
 293                            (($file_pd - $value_pd) < $pattern_depth)) {
 294                            $hash{$tvi} = $value_pd;
 295                        }
 296                    }
 297                }
 298            }
 299            $tvi++;
 300        }
 301        foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
 302            add_categories($line);
 303        }
 304    }
 305
 306    if ($email && $email_git) {
 307        recent_git_signoffs($file);
 308    }
 309
 310    if ($email && $email_git_blame) {
 311        git_assign_blame($file);
 312    }
 313}
 314
 315if ($keywords) {
 316    @keyword_tvi = sort_and_uniq(@keyword_tvi);
 317    foreach my $line (@keyword_tvi) {
 318        add_categories($line);
 319    }
 320}
 321
 322if ($email) {
 323    foreach my $chief (@penguin_chief) {
 324        if ($chief =~ m/^(.*):(.*)/) {
 325            my $email_address;
 326
 327            $email_address = format_email($1, $2);
 328            if ($email_git_penguin_chiefs) {
 329                push(@email_to, $email_address);
 330            } else {
 331                @email_to = grep(!/${email_address}/, @email_to);
 332            }
 333        }
 334    }
 335}
 336
 337if ($email || $email_list) {
 338    my @to = ();
 339    if ($email) {
 340        @to = (@to, @email_to);
 341    }
 342    if ($email_list) {
 343        @to = (@to, @list_to);
 344    }
 345    output(uniq(@to));
 346}
 347
 348if ($scm) {
 349    @scm = uniq(@scm);
 350    output(@scm);
 351}
 352
 353if ($status) {
 354    @status = uniq(@status);
 355    output(@status);
 356}
 357
 358if ($subsystem) {
 359    @subsystem = uniq(@subsystem);
 360    output(@subsystem);
 361}
 362
 363if ($web) {
 364    @web = uniq(@web);
 365    output(@web);
 366}
 367
 368exit($exit);
 369
 370sub file_match_pattern {
 371    my ($file, $pattern) = @_;
 372    if (substr($pattern, -1) eq "/") {
 373        if ($file =~ m@^$pattern@) {
 374            return 1;
 375        }
 376    } else {
 377        if ($file =~ m@^$pattern@) {
 378            my $s1 = ($file =~ tr@/@@);
 379            my $s2 = ($pattern =~ tr@/@@);
 380            if ($s1 == $s2) {
 381                return 1;
 382            }
 383        }
 384    }
 385    return 0;
 386}
 387
 388sub usage {
 389    print <<EOT;
 390usage: $P [options] patchfile
 391       $P [options] -f file|directory
 392version: $V
 393
 394MAINTAINER field selection options:
 395  --email => print email address(es) if any
 396    --git => include recent git \*-by: signers
 397    --git-chief-penguins => include ${penguin_chiefs}
 398    --git-min-signatures => number of signatures required (default: 1)
 399    --git-max-maintainers => maximum maintainers to add (default: 5)
 400    --git-min-percent => minimum percentage of commits required (default: 5)
 401    --git-since => git history to use (default: 1-year-ago)
 402    --git-blame => use git blame to find modified commits for patch or file
 403    --m => include maintainer(s) if any
 404    --n => include name 'Full Name <addr\@domain.tld>'
 405    --l => include list(s) if any
 406    --s => include subscriber only list(s) if any
 407    --remove-duplicates => minimize duplicate email names/addresses
 408  --scm => print SCM tree(s) if any
 409  --status => print status if any
 410  --subsystem => print subsystem name if any
 411  --web => print website(s) if any
 412
 413Output type options:
 414  --separator [, ] => separator for multiple entries on 1 line
 415    using --separator also sets --nomultiline if --separator is not [, ]
 416  --multiline => print 1 entry per line
 417
 418Other options:
 419  --pattern-depth => Number of pattern directory traversals (default: 0 (all))
 420  --keywords => scan patch for keywords (default: 1 (on))
 421  --version => show version
 422  --help => show this help information
 423
 424Default options:
 425  [--email --git --m --n --l --multiline --pattern-depth=0 --remove-duplicates]
 426
 427Notes:
 428  Using "-f directory" may give unexpected results:
 429      Used with "--git", git signators for _all_ files in and below
 430          directory are examined as git recurses directories.
 431          Any specified X: (exclude) pattern matches are _not_ ignored.
 432      Used with "--nogit", directory is used as a pattern match,
 433         no individual file within the directory or subdirectory
 434         is matched.
 435      Used with "--git-blame", does not iterate all files in directory
 436  Using "--git-blame" is slow and may add old committers and authors
 437      that are no longer active maintainers to the output.
 438EOT
 439}
 440
 441sub top_of_kernel_tree {
 442        my ($lk_path) = @_;
 443
 444        if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
 445            $lk_path .= "/";
 446        }
 447        if (   (-f "${lk_path}COPYING")
 448            && (-f "${lk_path}CREDITS")
 449            && (-f "${lk_path}Kbuild")
 450            && (-f "${lk_path}MAINTAINERS")
 451            && (-f "${lk_path}Makefile")
 452            && (-f "${lk_path}README")
 453            && (-d "${lk_path}Documentation")
 454            && (-d "${lk_path}arch")
 455            && (-d "${lk_path}include")
 456            && (-d "${lk_path}drivers")
 457            && (-d "${lk_path}fs")
 458            && (-d "${lk_path}init")
 459            && (-d "${lk_path}ipc")
 460            && (-d "${lk_path}kernel")
 461            && (-d "${lk_path}lib")
 462            && (-d "${lk_path}scripts")) {
 463                return 1;
 464        }
 465        return 0;
 466}
 467
 468sub parse_email {
 469    my ($formatted_email) = @_;
 470
 471    my $name = "";
 472    my $address = "";
 473
 474    if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
 475        $name = $1;
 476        $address = $2;
 477    } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
 478        $address = $1;
 479    } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
 480        $address = $1;
 481    }
 482
 483    $name =~ s/^\s+|\s+$//g;
 484    $name =~ s/^\"|\"$//g;
 485    $address =~ s/^\s+|\s+$//g;
 486
 487    if ($name =~ /[^a-z0-9 \.\-]/i) {    ##has "must quote" chars
 488        $name =~ s/(?<!\\)"/\\"/g;       ##escape quotes
 489        $name = "\"$name\"";
 490    }
 491
 492    return ($name, $address);
 493}
 494
 495sub format_email {
 496    my ($name, $address) = @_;
 497
 498    my $formatted_email;
 499
 500    $name =~ s/^\s+|\s+$//g;
 501    $name =~ s/^\"|\"$//g;
 502    $address =~ s/^\s+|\s+$//g;
 503
 504    if ($name =~ /[^a-z0-9 \.\-]/i) {    ##has "must quote" chars
 505        $name =~ s/(?<!\\)"/\\"/g;       ##escape quotes
 506        $name = "\"$name\"";
 507    }
 508
 509    if ($email_usename) {
 510        if ("$name" eq "") {
 511            $formatted_email = "$address";
 512        } else {
 513            $formatted_email = "$name <${address}>";
 514        }
 515    } else {
 516        $formatted_email = $address;
 517    }
 518
 519    return $formatted_email;
 520}
 521
 522sub find_starting_index {
 523    my ($index) = @_;
 524
 525    while ($index > 0) {
 526        my $tv = $typevalue[$index];
 527        if (!($tv =~ m/^(\C):\s*(.*)/)) {
 528            last;
 529        }
 530        $index--;
 531    }
 532
 533    return $index;
 534}
 535
 536sub find_ending_index {
 537    my ($index) = @_;
 538
 539    while ($index < @typevalue) {
 540        my $tv = $typevalue[$index];
 541        if (!($tv =~ m/^(\C):\s*(.*)/)) {
 542            last;
 543        }
 544        $index++;
 545    }
 546
 547    return $index;
 548}
 549
 550sub add_categories {
 551    my ($index) = @_;
 552
 553    my $i;
 554    my $start = find_starting_index($index);
 555    my $end = find_ending_index($index);
 556
 557    push(@subsystem, $typevalue[$start]);
 558
 559    for ($i = $start + 1; $i < $end; $i++) {
 560        my $tv = $typevalue[$i];
 561        if ($tv =~ m/^(\C):\s*(.*)/) {
 562            my $ptype = $1;
 563            my $pvalue = $2;
 564            if ($ptype eq "L") {
 565                my $list_address = $pvalue;
 566                my $list_additional = "";
 567                if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
 568                    $list_address = $1;
 569                    $list_additional = $2;
 570                }
 571                if ($list_additional =~ m/subscribers-only/) {
 572                    if ($email_subscriber_list) {
 573                        push(@list_to, $list_address);
 574                    }
 575                } else {
 576                    if ($email_list) {
 577                        push(@list_to, $list_address);
 578                    }
 579                }
 580            } elsif ($ptype eq "M") {
 581                my ($name, $address) = parse_email($pvalue);
 582                if ($name eq "") {
 583                    if ($i > 0) {
 584                        my $tv = $typevalue[$i - 1];
 585                        if ($tv =~ m/^(\C):\s*(.*)/) {
 586                            if ($1 eq "P") {
 587                                $name = $2;
 588                                $pvalue = format_email($name, $address);
 589                            }
 590                        }
 591                    }
 592                }
 593                if ($email_maintainer) {
 594                    push_email_addresses($pvalue);
 595                }
 596            } elsif ($ptype eq "T") {
 597                push(@scm, $pvalue);
 598            } elsif ($ptype eq "W") {
 599                push(@web, $pvalue);
 600            } elsif ($ptype eq "S") {
 601                push(@status, $pvalue);
 602            }
 603        }
 604    }
 605}
 606
 607my %email_hash_name;
 608my %email_hash_address;
 609
 610sub email_inuse {
 611    my ($name, $address) = @_;
 612
 613    return 1 if (($name eq "") && ($address eq ""));
 614    return 1 if (($name ne "") && exists($email_hash_name{$name}));
 615    return 1 if (($address ne "") && exists($email_hash_address{$address}));
 616
 617    return 0;
 618}
 619
 620sub push_email_address {
 621    my ($line) = @_;
 622
 623    my ($name, $address) = parse_email($line);
 624
 625    if ($address eq "") {
 626        return 0;
 627    }
 628
 629    if (!$email_remove_duplicates) {
 630        push(@email_to, format_email($name, $address));
 631    } elsif (!email_inuse($name, $address)) {
 632        push(@email_to, format_email($name, $address));
 633        $email_hash_name{$name}++;
 634        $email_hash_address{$address}++;
 635    }
 636
 637    return 1;
 638}
 639
 640sub push_email_addresses {
 641    my ($address) = @_;
 642
 643    my @address_list = ();
 644
 645    if (rfc822_valid($address)) {
 646        push_email_address($address);
 647    } elsif (@address_list = rfc822_validlist($address)) {
 648        my $array_count = shift(@address_list);
 649        while (my $entry = shift(@address_list)) {
 650            push_email_address($entry);
 651        }
 652    } else {
 653        if (!push_email_address($address)) {
 654            warn("Invalid MAINTAINERS address: '" . $address . "'\n");
 655        }
 656    }
 657}
 658
 659sub which {
 660    my ($bin) = @_;
 661
 662    foreach my $path (split(/:/, $ENV{PATH})) {
 663        if (-e "$path/$bin") {
 664            return "$path/$bin";
 665        }
 666    }
 667
 668    return "";
 669}
 670
 671sub mailmap {
 672    my @lines = @_;
 673    my %hash;
 674
 675    foreach my $line (@lines) {
 676        my ($name, $address) = parse_email($line);
 677        if (!exists($hash{$name})) {
 678            $hash{$name} = $address;
 679        } elsif ($address ne $hash{$name}) {
 680            $address = $hash{$name};
 681            $line = format_email($name, $address);
 682        }
 683        if (exists($mailmap{$name})) {
 684            my $obj = $mailmap{$name};
 685            foreach my $map_address (@$obj) {
 686                if (($map_address eq $address) &&
 687                    ($map_address ne $hash{$name})) {
 688                    $line = format_email($name, $hash{$name});
 689                }
 690            }
 691        }
 692    }
 693
 694    return @lines;
 695}
 696
 697sub recent_git_signoffs {
 698    my ($file) = @_;
 699
 700    my $sign_offs = "";
 701    my $cmd = "";
 702    my $output = "";
 703    my $count = 0;
 704    my @lines = ();
 705    my %hash;
 706    my $total_sign_offs;
 707
 708    if (which("git") eq "") {
 709        warn("$P: git not found.  Add --nogit to options?\n");
 710        return;
 711    }
 712    if (!(-d ".git")) {
 713        warn("$P: .git directory not found.  Use a git repository for better results.\n");
 714        warn("$P: perhaps 'git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git'\n");
 715        return;
 716    }
 717
 718    $cmd = "git log --since=${email_git_since} -- ${file}";
 719
 720    $output = `${cmd}`;
 721    $output =~ s/^\s*//gm;
 722
 723    @lines = split("\n", $output);
 724
 725    @lines = grep(/^[-_         a-z]+by:.*\@.*$/i, @lines);
 726    if (!$email_git_penguin_chiefs) {
 727        @lines = grep(!/${penguin_chiefs}/i, @lines);
 728    }
 729    # cut -f2- -d":"
 730    s/.*:\s*(.+)\s*/$1/ for (@lines);
 731
 732    $total_sign_offs = @lines;
 733
 734    if ($email_remove_duplicates) {
 735        @lines = mailmap(@lines);
 736    }
 737
 738    @lines = sort(@lines);
 739
 740    # uniq -c
 741    $hash{$_}++ for @lines;
 742
 743    # sort -rn
 744    foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
 745        my $sign_offs = $hash{$line};
 746        $count++;
 747        last if ($sign_offs < $email_git_min_signatures ||
 748                 $count > $email_git_max_maintainers ||
 749                 $sign_offs * 100 / $total_sign_offs < $email_git_min_percent);
 750        push_email_address($line);
 751    }
 752}
 753
 754sub save_commits {
 755    my ($cmd, @commits) = @_;
 756    my $output;
 757    my @lines = ();
 758
 759    $output = `${cmd}`;
 760
 761    @lines = split("\n", $output);
 762    foreach my $line (@lines) {
 763        if ($line =~ m/^(\w+) /) {
 764            push (@commits, $1);
 765        }
 766    }
 767    return @commits;
 768}
 769
 770sub git_assign_blame {
 771    my ($file) = @_;
 772
 773    my @lines = ();
 774    my @commits = ();
 775    my $cmd;
 776    my $output;
 777    my %hash;
 778    my $total_sign_offs;
 779    my $count;
 780
 781    if (@range) {
 782        foreach my $file_range_diff (@range) {
 783            next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
 784            my $diff_file = $1;
 785            my $diff_start = $2;
 786            my $diff_length = $3;
 787            next if (!("$file" eq "$diff_file"));
 788            $cmd = "git blame -l -L $diff_start,+$diff_length $file";
 789            @commits = save_commits($cmd, @commits);
 790        }
 791    } else {
 792        if (-f $file) {
 793            $cmd = "git blame -l $file";
 794            @commits = save_commits($cmd, @commits);
 795        }
 796    }
 797
 798    $total_sign_offs = 0;
 799    @commits = uniq(@commits);
 800    foreach my $commit (@commits) {
 801        $cmd = "git log -1 ${commit}";
 802
 803        $output = `${cmd}`;
 804        $output =~ s/^\s*//gm;
 805        @lines = split("\n", $output);
 806
 807        @lines = grep(/^[-_     a-z]+by:.*\@.*$/i, @lines);
 808        if (!$email_git_penguin_chiefs) {
 809            @lines = grep(!/${penguin_chiefs}/i, @lines);
 810        }
 811
 812        # cut -f2- -d":"
 813        s/.*:\s*(.+)\s*/$1/ for (@lines);
 814
 815        $total_sign_offs += @lines;
 816
 817        if ($email_remove_duplicates) {
 818            @lines = mailmap(@lines);
 819        }
 820
 821        $hash{$_}++ for @lines;
 822    }
 823
 824    $count = 0;
 825    foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
 826        my $sign_offs = $hash{$line};
 827        $count++;
 828        last if ($sign_offs < $email_git_min_signatures ||
 829                 $count > $email_git_max_maintainers ||
 830                 $sign_offs * 100 / $total_sign_offs < $email_git_min_percent);
 831        push_email_address($line);
 832    }
 833}
 834
 835sub uniq {
 836    my @parms = @_;
 837
 838    my %saw;
 839    @parms = grep(!$saw{$_}++, @parms);
 840    return @parms;
 841}
 842
 843sub sort_and_uniq {
 844    my @parms = @_;
 845
 846    my %saw;
 847    @parms = sort @parms;
 848    @parms = grep(!$saw{$_}++, @parms);
 849    return @parms;
 850}
 851
 852sub output {
 853    my @parms = @_;
 854
 855    if ($output_multiline) {
 856        foreach my $line (@parms) {
 857            print("${line}\n");
 858        }
 859    } else {
 860        print(join($output_separator, @parms));
 861        print("\n");
 862    }
 863}
 864
 865my $rfc822re;
 866
 867sub make_rfc822re {
 868#   Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
 869#   comment.  We must allow for rfc822_lwsp (or comments) after each of these.
 870#   This regexp will only work on addresses which have had comments stripped
 871#   and replaced with rfc822_lwsp.
 872
 873    my $specials = '()<>@,;:\\\\".\\[\\]';
 874    my $controls = '\\000-\\037\\177';
 875
 876    my $dtext = "[^\\[\\]\\r\\\\]";
 877    my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
 878
 879    my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
 880
 881#   Use zero-width assertion to spot the limit of an atom.  A simple
 882#   $rfc822_lwsp* causes the regexp engine to hang occasionally.
 883    my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
 884    my $word = "(?:$atom|$quoted_string)";
 885    my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
 886
 887    my $sub_domain = "(?:$atom|$domain_literal)";
 888    my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
 889
 890    my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
 891
 892    my $phrase = "$word*";
 893    my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
 894    my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
 895    my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
 896
 897    my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
 898    my $address = "(?:$mailbox|$group)";
 899
 900    return "$rfc822_lwsp*$address";
 901}
 902
 903sub rfc822_strip_comments {
 904    my $s = shift;
 905#   Recursively remove comments, and replace with a single space.  The simpler
 906#   regexps in the Email Addressing FAQ are imperfect - they will miss escaped
 907#   chars in atoms, for example.
 908
 909    while ($s =~ s/^((?:[^"\\]|\\.)*
 910                    (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
 911                    \((?:[^()\\]|\\.)*\)/$1 /osx) {}
 912    return $s;
 913}
 914
 915#   valid: returns true if the parameter is an RFC822 valid address
 916#
 917sub rfc822_valid ($) {
 918    my $s = rfc822_strip_comments(shift);
 919
 920    if (!$rfc822re) {
 921        $rfc822re = make_rfc822re();
 922    }
 923
 924    return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
 925}
 926
 927#   validlist: In scalar context, returns true if the parameter is an RFC822
 928#              valid list of addresses.
 929#
 930#              In list context, returns an empty list on failure (an invalid
 931#              address was found); otherwise a list whose first element is the
 932#              number of addresses found and whose remaining elements are the
 933#              addresses.  This is needed to disambiguate failure (invalid)
 934#              from success with no addresses found, because an empty string is
 935#              a valid list.
 936
 937sub rfc822_validlist ($) {
 938    my $s = rfc822_strip_comments(shift);
 939
 940    if (!$rfc822re) {
 941        $rfc822re = make_rfc822re();
 942    }
 943    # * null list items are valid according to the RFC
 944    # * the '1' business is to aid in distinguishing failure from no results
 945
 946    my @r;
 947    if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
 948        $s =~ m/^$rfc822_char*$/) {
 949        while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
 950            push @r, $1;
 951        }
 952        return wantarray ? (scalar(@r), @r) : 1;
 953    }
 954    else {
 955        return wantarray ? () : 0;
 956    }
 957}
 958