]> git.ipfire.org Git - thirdparty/u-boot.git/blobdiff - scripts/get_maintainer.pl
Merge tag 'xilinx-for-v2024.04-rc1-v2' of https://source.denx.de/u-boot/custodians...
[thirdparty/u-boot.git] / scripts / get_maintainer.pl
index 4707dfd18e6f5e1045c03e139b3c5a2d94ad5916..6c58578e9885d66d582b731c50ba031b33f5ab4a 100755 (executable)
@@ -1,4 +1,6 @@
-#!/usr/bin/perl -w
+#!/usr/bin/env perl
+# SPDX-License-Identifier: GPL-2.0
+#
 # (c) 2007, Joe Perches <joe@perches.com>
 #           created from checkpatch.pl
 #
@@ -7,22 +9,27 @@
 #
 # usage: perl scripts/get_maintainer.pl [OPTIONS] <patch>
 #        perl scripts/get_maintainer.pl [OPTIONS] -f <file>
-#
-# Licensed under the terms of the GNU GPL License version 2
 
+use warnings;
 use strict;
 
 my $P = $0;
 my $V = '0.26';
 
 use Getopt::Long qw(:config no_auto_abbrev);
+use Cwd;
 use File::Find;
+use File::Spec::Functions;
 
+my $cur_path = fastgetcwd() . '/';
 my $lk_path = "./";
 my $email = 1;
 my $email_usename = 1;
 my $email_maintainer = 1;
+my $email_reviewer = 1;
+my $email_fixes = 1;
 my $email_list = 1;
+my $email_moderated_list = 1;
 my $email_subscriber_list = 0;
 my $email_git_penguin_chiefs = 0;
 my $email_git = 0;
@@ -42,22 +49,33 @@ my $output_multiline = 1;
 my $output_separator = ", ";
 my $output_roles = 0;
 my $output_rolestats = 1;
+my $output_section_maxlen = 50;
 my $scm = 0;
+my $tree = 1;
 my $web = 0;
 my $subsystem = 0;
 my $status = 0;
+my $letters = "";
 my $keywords = 1;
 my $sections = 0;
-my $file_emails = 0;
+my $email_file_emails = 0;
 my $from_filename = 0;
 my $pattern_depth = 0;
+my $self_test = undef;
 my $version = 0;
 my $help = 0;
-
+my $find_maintainer_files = 0;
+my $maintainer_path;
 my $vcs_used = 0;
 
 my $exit = 0;
 
+my @files = ();
+my @fixes = ();                        # If a patch description includes Fixes: lines
+my @range = ();
+my @keyword_tvi = ();
+my @file_emails = ();
+
 my %commit_author_hash;
 my %commit_signer_hash;
 
@@ -128,6 +146,8 @@ my %VCS_cmds_git = (
     "author_pattern" => "^GitAuthor: (.*)",
     "subject_pattern" => "^GitSubject: (.*)",
     "stat_pattern" => "^(\\d+)\\t(\\d+)\\t\$file\$",
+    "file_exists_cmd" => "git ls-files \$file",
+    "list_files_cmd" => "git ls-files \$file",
 );
 
 my %VCS_cmds_hg = (
@@ -156,6 +176,8 @@ my %VCS_cmds_hg = (
     "author_pattern" => "^HgAuthor: (.*)",
     "subject_pattern" => "^HgSubject: (.*)",
     "stat_pattern" => "^(\\d+)\t(\\d+)\t\$file\$",
+    "file_exists_cmd" => "hg files \$file",
+    "list_files_cmd" => "hg manifest -R \$file",
 );
 
 my $conf = which_conf(".get_maintainer.conf");
@@ -184,6 +206,35 @@ if (-f $conf) {
     unshift(@ARGV, @conf_args) if @conf_args;
 }
 
+my @ignore_emails = ();
+my $ignore_file = which_conf(".get_maintainer.ignore");
+if (-f $ignore_file) {
+    open(my $ignore, '<', "$ignore_file")
+       or warn "$P: Can't find a readable .get_maintainer.ignore file $!\n";
+    while (<$ignore>) {
+       my $line = $_;
+
+       $line =~ s/\s*\n?$//;
+       $line =~ s/^\s*//;
+       $line =~ s/\s+$//;
+       $line =~ s/#.*$//;
+
+       next if ($line =~ m/^\s*$/);
+       if (rfc822_valid($line)) {
+           push(@ignore_emails, $line);
+       }
+    }
+    close($ignore);
+}
+
+if ($#ARGV > 0) {
+    foreach (@ARGV) {
+        if ($_ =~ /^-{1,2}self-test(?:=|$)/) {
+            die "$P: using --self-test does not allow any other option or argument\n";
+        }
+    }
+}
+
 if (!GetOptions(
                'email!' => \$email,
                'git!' => \$email_git,
@@ -201,8 +252,11 @@ if (!GetOptions(
                'remove-duplicates!' => \$email_remove_duplicates,
                'mailmap!' => \$email_use_mailmap,
                'm!' => \$email_maintainer,
+               'r!' => \$email_reviewer,
                'n!' => \$email_usename,
                'l!' => \$email_list,
+               'fixes!' => \$email_fixes,
+               'moderated!' => \$email_moderated_list,
                's!' => \$email_subscriber_list,
                'multiline!' => \$output_multiline,
                'roles!' => \$output_roles,
@@ -211,12 +265,17 @@ if (!GetOptions(
                'subsystem!' => \$subsystem,
                'status!' => \$status,
                'scm!' => \$scm,
+               'tree!' => \$tree,
                'web!' => \$web,
+               'letters=s' => \$letters,
                'pattern-depth=i' => \$pattern_depth,
                'k|keywords!' => \$keywords,
                'sections!' => \$sections,
-               'fe|file-emails!' => \$file_emails,
+               'fe|file-emails!' => \$email_file_emails,
                'f|file' => \$from_filename,
+               'find-maintainer-files' => \$find_maintainer_files,
+               'mpath|maintainer-path=s' => \$maintainer_path,
+               'self-test:s' => \$self_test,
                'v|version' => \$version,
                'h|help|usage' => \$help,
                )) {
@@ -233,6 +292,12 @@ if ($version != 0) {
     exit 0;
 }
 
+if (defined $self_test) {
+    read_all_maintainer_files();
+    self_test();
+    exit 0;
+}
+
 if (-t STDIN && !@ARGV) {
     # We're talking to a terminal, but have no command line arguments.
     die "$P: missing patchfile or -f file - use --help if necessary\n";
@@ -242,7 +307,8 @@ $output_multiline = 0 if ($output_separator ne ", ");
 $output_rolestats = 1 if ($interactive);
 $output_roles = 1 if ($output_rolestats);
 
-if ($sections) {
+if ($sections || $letters ne "") {
+    $sections = 1;
     $email = 0;
     $email_list = 0;
     $scm = 0;
@@ -259,46 +325,35 @@ if ($sections) {
 }
 
 if ($email &&
-    ($email_maintainer + $email_list + $email_subscriber_list +
+    ($email_maintainer + $email_reviewer +
+     $email_list + $email_subscriber_list +
      $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
     die "$P: Please select at least 1 email option\n";
 }
 
-if (!top_of_kernel_tree($lk_path)) {
+if ($tree && !top_of_kernel_tree($lk_path)) {
     die "$P: The current directory does not appear to be "
-       . "a linux kernel source tree.\n";
+       . "a U-Boot source tree.\n";
 }
 
 ## Read MAINTAINERS for type/value pairs
 
 my @typevalue = ();
 my %keyword_hash;
+my @mfiles = ();
+my @self_test_info = ();
 
-my @maint_files = ();
-push(@maint_files, "${lk_path}MAINTAINERS");
-
-sub maint_wanted {
-    return unless $_ =~ /^MAINTAINERS/;
-    push(@maint_files, "$File::Find::name");
-}
-
-File::Find::find(\&maint_wanted, "${lk_path}board");
-
-foreach my $maint_file (@maint_files) {
-    my $maint;
-    open ($maint, '<', "$maint_file")
-       or die "$P: Can't open $maint_file: $!\n";
-    read_maintainers($maint);
-    close($maint);
-}
-
-sub read_maintainers {
-    my ($maint) = @_;
+sub read_maintainer_file {
+    my ($file) = @_;
 
+    open (my $maint, '<', "$file")
+       or die "$P: Can't open MAINTAINERS file '$file': $!\n";
+    my $i = 1;
     while (<$maint>) {
        my $line = $_;
+       chomp $line;
 
-       if ($line =~ m/^(\C):\s*(.*)/) {
+       if ($line =~ m/^([A-Z]):\s*(.*)/) {
            my $type = $1;
            my $value = $2;
 
@@ -315,13 +370,80 @@ sub read_maintainers {
                $keyword_hash{@typevalue} = $value;
            }
            push(@typevalue, "$type:$value");
-       } elsif (!/^(\s)*$/) {
-           $line =~ s/\n$//g;
+       } elsif (!(/^\s*$/ || /^\s*\#/)) {
            push(@typevalue, $line);
        }
+       if (defined $self_test) {
+           push(@self_test_info, {file=>$file, linenr=>$i, line=>$line});
+       }
+       $i++;
     }
+    close($maint);
 }
 
+sub find_is_maintainer_file {
+    my ($file) = $_;
+    return if ($file !~ m@/MAINTAINERS$@);
+    $file = $File::Find::name;
+    return if (! -f $file);
+    push(@mfiles, $file);
+}
+
+sub find_ignore_git {
+    return grep { $_ !~ /^\.git$/; } @_;
+}
+
+read_all_maintainer_files();
+
+sub read_all_maintainer_files {
+    my $path = "${lk_path}MAINTAINERS";
+    if (defined $maintainer_path) {
+       $path = $maintainer_path;
+       # Perl Cookbook tilde expansion if necessary
+       $path =~ s@^~([^/]*)@ $1 ? (getpwnam($1))[7] : ( $ENV{HOME} || $ENV{LOGDIR} || (getpwuid($<))[7])@ex;
+    }
+
+    if (-d $path) {
+       $path .= '/' if ($path !~ m@/$@);
+       if ($find_maintainer_files) {
+           find( { wanted => \&find_is_maintainer_file,
+                   preprocess => \&find_ignore_git,
+                   no_chdir => 1,
+               }, "$path");
+       } else {
+           opendir(DIR, "$path") or die $!;
+           my @files = readdir(DIR);
+           closedir(DIR);
+           foreach my $file (@files) {
+               push(@mfiles, "$path$file") if ($file !~ /^\./);
+           }
+       }
+    } elsif (-f "$path") {
+       push(@mfiles, "$path");
+    } else {
+       die "$P: MAINTAINER file not found '$path'\n";
+    }
+    die "$P: No MAINTAINER files found in '$path'\n" if (scalar(@mfiles) == 0);
+    foreach my $file (@mfiles) {
+       read_maintainer_file("$file");
+    }
+}
+
+sub maintainers_in_file {
+    my ($file) = @_;
+
+    return if ($file =~ m@\bMAINTAINERS$@);
+
+    if (-f $file && ($email_file_emails || $file =~ /\.yaml$/)) {
+       open(my $f, '<', $file)
+           or die "$P: Can't open $file: $!\n";
+       my $text = do { local($/) ; <$f> };
+       close($f);
+
+       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;
+       push(@file_emails, clean_file_emails(@poss_addr));
+    }
+}
 
 #
 # Read mail address map
@@ -403,17 +525,13 @@ sub read_mailmap {
 
 ## use the filenames on the command line or find the filenames in the patchfiles
 
-my @files = ();
-my @range = ();
-my @keyword_tvi = ();
-my @file_emails = ();
-
 if (!@ARGV) {
     push(@ARGV, "&STDIN");
 }
 
 foreach my $file (@ARGV) {
     if ($file ne "&STDIN") {
+       $file = canonpath($file);
        ##if $file is a directory and it lacks a trailing slash, add one
        if ((-d $file)) {
            $file =~ s@([^/])$@$1/@;
@@ -421,9 +539,14 @@ foreach my $file (@ARGV) {
            die "$P: file '${file}' not found\n";
        }
     }
-    if ($from_filename) {
+    if ($from_filename && (vcs_exists() && !vcs_file_exists($file))) {
+       warn "$P: file '$file' not found in version control $!\n";
+    }
+    if ($from_filename || ($file ne "&STDIN" && vcs_file_exists($file))) {
+       $file =~ s/^\Q${cur_path}\E//;  #strip any absolute path
+       $file =~ s/^\Q${lk_path}\E//;   #or the path to the lk tree
        push(@files, $file);
-       if ($file ne "MAINTAINERS" && -f $file && ($keywords || $file_emails)) {
+       if ($file ne "MAINTAINERS" && -f $file && $keywords) {
            open(my $f, '<', $file)
                or die "$P: Can't open $file: $!\n";
            my $text = do { local($/) ; <$f> };
@@ -435,10 +558,6 @@ foreach my $file (@ARGV) {
                    }
                }
            }
-           if ($file_emails) {
-               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;
-               push(@file_emails, clean_file_emails(@poss_addr));
-           }
        }
     } else {
        my $file_cnt = @files;
@@ -456,7 +575,20 @@ foreach my $file (@ARGV) {
 
        while (<$patch>) {
            my $patch_line = $_;
-           if (m/^\+\+\+\s+(\S+)/ or m/^---\s+(\S+)/) {
+           if (m/^ mode change [0-7]+ => [0-7]+ (\S+)\s*$/) {
+               my $filename = $1;
+               push(@files, $filename);
+           } elsif (m/^rename (?:from|to) (\S+)\s*$/) {
+               my $filename = $1;
+               push(@files, $filename);
+           } elsif (m/^diff --git a\/(\S+) b\/(\S+)\s*$/) {
+               my $filename1 = $1;
+               my $filename2 = $2;
+               push(@files, $filename1);
+               push(@files, $filename2);
+           } elsif (m/^Fixes:\s+([0-9a-fA-F]{6,40})/) {
+               push(@fixes, $1) if ($email_fixes);
+           } elsif (m/^\+\+\+\s+(\S+)/ or m/^---\s+(\S+)/) {
                my $filename = $1;
                $filename =~ s@^[^/]*/@@;
                $filename =~ s@\n@@;
@@ -486,6 +618,7 @@ foreach my $file (@ARGV) {
 }
 
 @file_emails = uniq(@file_emails);
+@fixes = uniq(@fixes);
 
 my %email_hash_name;
 my %email_hash_address;
@@ -500,7 +633,6 @@ my %deduplicate_name_hash = ();
 my %deduplicate_address_hash = ();
 
 my @maintainers = get_maintainers();
-
 if (@maintainers) {
     @maintainers = merge_email(@maintainers);
     output(@maintainers);
@@ -528,12 +660,151 @@ if ($web) {
 
 exit($exit);
 
+sub self_test {
+    my @lsfiles = ();
+    my @good_links = ();
+    my @bad_links = ();
+    my @section_headers = ();
+    my $index = 0;
+
+    @lsfiles = vcs_list_files($lk_path);
+
+    for my $x (@self_test_info) {
+       $index++;
+
+       ## Section header duplication and missing section content
+       if (($self_test eq "" || $self_test =~ /\bsections\b/) &&
+           $x->{line} =~ /^\S[^:]/ &&
+           defined $self_test_info[$index] &&
+           $self_test_info[$index]->{line} =~ /^([A-Z]):\s*\S/) {
+           my $has_S = 0;
+           my $has_F = 0;
+           my $has_ML = 0;
+           my $status = "";
+           if (grep(m@^\Q$x->{line}\E@, @section_headers)) {
+               print("$x->{file}:$x->{linenr}: warning: duplicate section header\t$x->{line}\n");
+           } else {
+               push(@section_headers, $x->{line});
+           }
+           my $nextline = $index;
+           while (defined $self_test_info[$nextline] &&
+                  $self_test_info[$nextline]->{line} =~ /^([A-Z]):\s*(\S.*)/) {
+               my $type = $1;
+               my $value = $2;
+               if ($type eq "S") {
+                   $has_S = 1;
+                   $status = $value;
+               } elsif ($type eq "F" || $type eq "N") {
+                   $has_F = 1;
+               } elsif ($type eq "M" || $type eq "R" || $type eq "L") {
+                   $has_ML = 1;
+               }
+               $nextline++;
+           }
+           if (!$has_ML && $status !~ /orphan|obsolete/i) {
+               print("$x->{file}:$x->{linenr}: warning: section without email address\t$x->{line}\n");
+           }
+           if (!$has_S) {
+               print("$x->{file}:$x->{linenr}: warning: section without status \t$x->{line}\n");
+           }
+           if (!$has_F) {
+               print("$x->{file}:$x->{linenr}: warning: section without file pattern\t$x->{line}\n");
+           }
+       }
+
+       next if ($x->{line} !~ /^([A-Z]):\s*(.*)/);
+
+       my $type = $1;
+       my $value = $2;
+
+       ## Filename pattern matching
+       if (($type eq "F" || $type eq "X") &&
+           ($self_test eq "" || $self_test =~ /\bpatterns\b/)) {
+           $value =~ s@\.@\\\.@g;       ##Convert . to \.
+           $value =~ s/\*/\.\*/g;       ##Convert * to .*
+           $value =~ s/\?/\./g;         ##Convert ? to .
+           ##if pattern is a directory and it lacks a trailing slash, add one
+           if ((-d $value)) {
+               $value =~ s@([^/])$@$1/@;
+           }
+           if (!grep(m@^$value@, @lsfiles)) {
+               print("$x->{file}:$x->{linenr}: warning: no file matches\t$x->{line}\n");
+           }
+
+       ## Link reachability
+       } elsif (($type eq "W" || $type eq "Q" || $type eq "B") &&
+                $value =~ /^https?:/ &&
+                ($self_test eq "" || $self_test =~ /\blinks\b/)) {
+           next if (grep(m@^\Q$value\E$@, @good_links));
+           my $isbad = 0;
+           if (grep(m@^\Q$value\E$@, @bad_links)) {
+               $isbad = 1;
+           } else {
+               my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $value`;
+               if ($? == 0) {
+                   push(@good_links, $value);
+               } else {
+                   push(@bad_links, $value);
+                   $isbad = 1;
+               }
+           }
+           if ($isbad) {
+               print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
+           }
+
+       ## SCM reachability
+       } elsif ($type eq "T" &&
+                ($self_test eq "" || $self_test =~ /\bscm\b/)) {
+           next if (grep(m@^\Q$value\E$@, @good_links));
+           my $isbad = 0;
+           if (grep(m@^\Q$value\E$@, @bad_links)) {
+               $isbad = 1;
+            } elsif ($value !~ /^(?:git|quilt|hg)\s+\S/) {
+               print("$x->{file}:$x->{linenr}: warning: malformed entry\t$x->{line}\n");
+           } elsif ($value =~ /^git\s+(\S+)(\s+([^\(]+\S+))?/) {
+               my $url = $1;
+               my $branch = "";
+               $branch = $3 if $3;
+               my $output = `git ls-remote --exit-code -h "$url" $branch > /dev/null 2>&1`;
+               if ($? == 0) {
+                   push(@good_links, $value);
+               } else {
+                   push(@bad_links, $value);
+                   $isbad = 1;
+               }
+           } elsif ($value =~ /^(?:quilt|hg)\s+(https?:\S+)/) {
+               my $url = $1;
+               my $output = `wget --spider -q --no-check-certificate --timeout 10 --tries 1 $url`;
+               if ($? == 0) {
+                   push(@good_links, $value);
+               } else {
+                   push(@bad_links, $value);
+                   $isbad = 1;
+               }
+           }
+           if ($isbad) {
+               print("$x->{file}:$x->{linenr}: warning: possible bad link\t$x->{line}\n");
+           }
+       }
+    }
+}
+
+sub ignore_email_address {
+    my ($address) = @_;
+
+    foreach my $ignore (@ignore_emails) {
+       return 1 if ($ignore eq $address);
+    }
+
+    return 0;
+}
+
 sub range_is_maintained {
     my ($start, $end) = @_;
 
     for (my $i = $start; $i < $end; $i++) {
        my $line = $typevalue[$i];
-       if ($line =~ m/^(\C):\s*(.*)/) {
+       if ($line =~ m/^([A-Z]):\s*(.*)/) {
            my $type = $1;
            my $value = $2;
            if ($type eq 'S') {
@@ -551,7 +822,7 @@ sub range_has_maintainer {
 
     for (my $i = $start; $i < $end; $i++) {
        my $line = $typevalue[$i];
-       if ($line =~ m/^(\C):\s*(.*)/) {
+       if ($line =~ m/^([A-Z]):\s*(.*)/) {
            my $type = $1;
            my $value = $2;
            if ($type eq 'M') {
@@ -600,7 +871,7 @@ sub get_maintainers {
 
            for ($i = $start; $i < $end; $i++) {
                my $line = $typevalue[$i];
-               if ($line =~ m/^(\C):\s*(.*)/) {
+               if ($line =~ m/^([A-Z]):\s*(.*)/) {
                    my $type = $1;
                    my $value = $2;
                    if ($type eq 'X') {
@@ -615,7 +886,7 @@ sub get_maintainers {
            if (!$exclude) {
                for ($i = $start; $i < $end; $i++) {
                    my $line = $typevalue[$i];
-                   if ($line =~ m/^(\C):\s*(.*)/) {
+                   if ($line =~ m/^([A-Z]):\s*(.*)/) {
                        my $type = $1;
                        my $value = $2;
                        if ($type eq 'F') {
@@ -659,12 +930,16 @@ sub get_maintainers {
                        $line =~ s/\\\./\./g;           ##Convert \. to .
                        $line =~ s/\.\*/\*/g;           ##Convert .* to *
                    }
-                   $line =~ s/^([A-Z]):/$1:\t/g;
-                   print("$line\n");
+                   my $count = $line =~ s/^([A-Z]):/$1:\t/g;
+                   if ($letters eq "" || (!$count || $letters =~ /$1/i)) {
+                       print("$line\n");
+                   }
                }
                print("\n");
            }
        }
+
+       maintainers_in_file($file);
     }
 
     if ($keywords) {
@@ -680,8 +955,10 @@ sub get_maintainers {
 
     foreach my $file (@files) {
        if ($email &&
-           ($email_git || ($email_git_fallback &&
-                           !$exact_pattern_match_hash{$file}))) {
+           ($email_git ||
+            ($email_git_fallback &&
+             $file !~ /MAINTAINERS$/ &&
+             !$exact_pattern_match_hash{$file}))) {
            vcs_file_signoffs($file);
        }
        if ($email && $email_git_blame) {
@@ -704,6 +981,7 @@ sub get_maintainers {
        }
 
        foreach my $email (@file_emails) {
+           $email = mailmap_email($email);
            my ($name, $address) = parse_email($email);
 
            my $tmp_email = format_email($name, $address, $email_usename);
@@ -712,6 +990,10 @@ sub get_maintainers {
        }
     }
 
+    foreach my $fix (@fixes) {
+       vcs_add_commit_signers($fix, "blamed_fixes");
+    }
+
     my @to = ();
     if ($email || $email_list) {
        if ($email) {
@@ -764,17 +1046,21 @@ MAINTAINER field selection options:
     --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
     --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
     --git-blame => use git blame to find modified commits for patch or file
+    --git-blame-signatures => when used with --git-blame, also include all commit signers
     --git-since => git history to use (default: $email_git_since)
     --hg-since => hg history to use (default: $email_hg_since)
     --interactive => display a menu (mostly useful if used with the --git option)
     --m => include maintainer(s) if any
+    --r => include reviewer(s) if any
     --n => include name 'Full Name <addr\@domain.tld>'
     --l => include list(s) if any
-    --s => include subscriber only list(s) if any
+    --moderated => include moderated lists(s) if any (default: true)
+    --s => include subscriber only list(s) if any (default: false)
     --remove-duplicates => minimize duplicate email names/addresses
     --roles => show roles (status:subsystem, git-signer, list, etc...)
     --rolestats => show roles and statistics (commits/total_commits, %)
     --file-emails => add email addresses found in -f file (default: 0 (off))
+    --fixes => for patches, add signatures of commits with 'Fixes: <commit>' (default: 1 (on))
   --scm => print SCM tree(s) if any
   --status => print status if any
   --subsystem => print subsystem name if any
@@ -789,13 +1075,16 @@ Other options:
   --pattern-depth => Number of pattern directory traversals (default: 0 (all))
   --keywords => scan patch for keywords (default: $keywords)
   --sections => print all of the subsystem sections with pattern matches
+  --letters => print all matching 'letter' types from all matching sections
   --mailmap => use .mailmap file (default: $email_use_mailmap)
+  --no-tree => run without a kernel tree
+  --self-test => show potential issues with MAINTAINERS file content
   --version => show version
   --help => show this help information
 
 Default options:
-  [--email --nogit --git-fallback --m --n --l --multiline -pattern-depth=0
-   --remove-duplicates --rolestats]
+  [--email --tree --nogit --git-fallback --m --r --n --l --multiline
+   --pattern-depth=0 --remove-duplicates --rolestats]
 
 Notes:
   Using "-f directory" may give unexpected results:
@@ -826,6 +1115,9 @@ Notes:
       Entries in this file can be any command line argument.
       This file is prepended to any additional command line arguments.
       Multiple lines and # comments are allowed.
+  Most options have both positive and negative forms.
+      The negative forms for --<foo> are --no<foo> and --no-<foo>.
+
 EOT
 }
 
@@ -836,7 +1128,7 @@ sub top_of_kernel_tree {
        $lk_path .= "/";
     }
     if (   (-f "${lk_path}Kbuild")
-       && (-f "${lk_path}MAINTAINERS")
+       && (-e "${lk_path}MAINTAINERS")
        && (-f "${lk_path}Makefile")
        && (-f "${lk_path}README")
        && (-d "${lk_path}arch")
@@ -917,7 +1209,7 @@ sub find_first_section {
 
     while ($index < @typevalue) {
        my $tv = $typevalue[$index];
-       if (($tv =~ m/^(\C):\s*(.*)/)) {
+       if (($tv =~ m/^([A-Z]):\s*(.*)/)) {
            last;
        }
        $index++;
@@ -931,7 +1223,7 @@ sub find_starting_index {
 
     while ($index > 0) {
        my $tv = $typevalue[$index];
-       if (!($tv =~ m/^(\C):\s*(.*)/)) {
+       if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
            last;
        }
        $index--;
@@ -945,7 +1237,7 @@ sub find_ending_index {
 
     while ($index < @typevalue) {
        my $tv = $typevalue[$index];
-       if (!($tv =~ m/^(\C):\s*(.*)/)) {
+       if (!($tv =~ m/^([A-Z]):\s*(.*)/)) {
            last;
        }
        $index++;
@@ -954,24 +1246,33 @@ sub find_ending_index {
     return $index;
 }
 
-sub get_maintainer_role {
+sub get_subsystem_name {
     my ($index) = @_;
 
-    my $i;
     my $start = find_starting_index($index);
-    my $end = find_ending_index($index);
 
-    my $role = "unknown";
     my $subsystem = $typevalue[$start];
-    if (length($subsystem) > 20) {
-       $subsystem = substr($subsystem, 0, 17);
+    if ($output_section_maxlen && length($subsystem) > $output_section_maxlen) {
+       $subsystem = substr($subsystem, 0, $output_section_maxlen - 3);
        $subsystem =~ s/\s*$//;
        $subsystem = $subsystem . "...";
     }
+    return $subsystem;
+}
+
+sub get_maintainer_role {
+    my ($index) = @_;
+
+    my $i;
+    my $start = find_starting_index($index);
+    my $end = find_ending_index($index);
+
+    my $role = "unknown";
+    my $subsystem = get_subsystem_name($index);
 
     for ($i = $start + 1; $i < $end; $i++) {
        my $tv = $typevalue[$i];
-       if ($tv =~ m/^(\C):\s*(.*)/) {
+       if ($tv =~ m/^([A-Z]):\s*(.*)/) {
            my $ptype = $1;
            my $pvalue = $2;
            if ($ptype eq "S") {
@@ -1001,16 +1302,7 @@ sub get_maintainer_role {
 sub get_list_role {
     my ($index) = @_;
 
-    my $i;
-    my $start = find_starting_index($index);
-    my $end = find_ending_index($index);
-
-    my $subsystem = $typevalue[$start];
-    if (length($subsystem) > 20) {
-       $subsystem = substr($subsystem, 0, 17);
-       $subsystem =~ s/\s*$//;
-       $subsystem = $subsystem . "...";
-    }
+    my $subsystem = get_subsystem_name($index);
 
     if ($subsystem eq "THE REST") {
        $subsystem = "";
@@ -1030,7 +1322,7 @@ sub add_categories {
 
     for ($i = $start + 1; $i < $end; $i++) {
        my $tv = $typevalue[$i];
-       if ($tv =~ m/^(\C):\s*(.*)/) {
+       if ($tv =~ m/^([A-Z]):\s*(.*)/) {
            my $ptype = $1;
            my $pvalue = $2;
            if ($ptype eq "L") {
@@ -1056,11 +1348,14 @@ sub add_categories {
                } else {
                    if ($email_list) {
                        if (!$hash_list_to{lc($list_address)}) {
-                           $hash_list_to{lc($list_address)} = 1;
                            if ($list_additional =~ m/moderated/) {
-                               push(@list_to, [$list_address,
-                                               "moderated list${list_role}"]);
+                               if ($email_moderated_list) {
+                                   $hash_list_to{lc($list_address)} = 1;
+                                   push(@list_to, [$list_address,
+                                                   "moderated list${list_role}"]);
+                               }
                            } else {
+                               $hash_list_to{lc($list_address)} = 1;
                                push(@list_to, [$list_address,
                                                "open list${list_role}"]);
                            }
@@ -1068,22 +1363,15 @@ sub add_categories {
                    }
                }
            } elsif ($ptype eq "M") {
-               my ($name, $address) = parse_email($pvalue);
-               if ($name eq "") {
-                   if ($i > 0) {
-                       my $tv = $typevalue[$i - 1];
-                       if ($tv =~ m/^(\C):\s*(.*)/) {
-                           if ($1 eq "P") {
-                               $name = $2;
-                               $pvalue = format_email($name, $address, $email_usename);
-                           }
-                       }
-                   }
-               }
                if ($email_maintainer) {
                    my $role = get_maintainer_role($i);
                    push_email_addresses($pvalue, $role);
                }
+           } elsif ($ptype eq "R") {
+               if ($email_reviewer) {
+                   my $subsystem = get_subsystem_name($i);
+                   push_email_addresses($pvalue, "reviewer:$subsystem");
+               }
            } elsif ($ptype eq "T") {
                push(@scm, $pvalue);
            } elsif ($ptype eq "W") {
@@ -1431,7 +1719,7 @@ sub vcs_exists {
     %VCS_cmds = %VCS_cmds_hg;
     return 2 if eval $VCS_cmds{"available"};
     %VCS_cmds = ();
-    if (!$printed_novcs) {
+    if (!$printed_novcs && $email_git) {
        warn("$P: No supported VCS found.  Add --nogit to options?\n");
        warn("Using a git repository produces better results.\n");
        warn("Try Linus Torvalds' latest git repository using:\n");
@@ -1450,6 +1738,32 @@ sub vcs_is_hg {
     return $vcs_used == 2;
 }
 
+sub vcs_add_commit_signers {
+    return if (!vcs_exists());
+
+    my ($commit, $desc) = @_;
+    my $commit_count = 0;
+    my $commit_authors_ref;
+    my $commit_signers_ref;
+    my $stats_ref;
+    my @commit_authors = ();
+    my @commit_signers = ();
+    my $cmd;
+
+    $cmd = $VCS_cmds{"find_commit_signers_cmd"};
+    $cmd =~ s/(\$\w+)/$1/eeg;  #substitute variables in $cmd
+
+    ($commit_count, $commit_signers_ref, $commit_authors_ref, $stats_ref) = vcs_find_signers($cmd, "");
+    @commit_authors = @{$commit_authors_ref} if defined $commit_authors_ref;
+    @commit_signers = @{$commit_signers_ref} if defined $commit_signers_ref;
+
+    foreach my $signer (@commit_signers) {
+       $signer = deduplicate_email($signer);
+    }
+
+    vcs_assign($desc, 1, @commit_signers);
+}
+
 sub interactive_get_maintainers {
     my ($list_ref) = @_;
     my @list = @$list_ref;
@@ -1543,7 +1857,7 @@ tm toggle maintainers
 tg toggle git entries
 tl toggle open list entries
 ts toggle subscriber list entries
-f  emails in file       [$file_emails]
+f  emails in file       [$email_file_emails]
 k  keywords in file     [$keywords]
 r  remove duplicates    [$email_remove_duplicates]
 p# pattern match depth  [$pattern_depth]
@@ -1668,7 +1982,7 @@ EOT
                bool_invert(\$email_git_all_signature_types);
                $rerun = 1;
            } elsif ($sel eq "f") {
-               bool_invert(\$file_emails);
+               bool_invert(\$email_file_emails);
                $rerun = 1;
            } elsif ($sel eq "r") {
                bool_invert(\$email_remove_duplicates);
@@ -1868,6 +2182,7 @@ sub vcs_assign {
        my $percent = $sign_offs * 100 / $divisor;
 
        $percent = 100 if ($percent > 100);
+       next if (ignore_email_address($line));
        $count++;
        last if ($sign_offs < $email_git_min_signatures ||
                 $count > $email_git_max_maintainers ||
@@ -2082,6 +2397,41 @@ sub vcs_file_blame {
     }
 }
 
+sub vcs_file_exists {
+    my ($file) = @_;
+
+    my $exists;
+
+    my $vcs_used = vcs_exists();
+    return 0 if (!$vcs_used);
+
+    my $cmd = $VCS_cmds{"file_exists_cmd"};
+    $cmd =~ s/(\$\w+)/$1/eeg;          # interpolate $cmd
+    $cmd .= " 2>&1";
+    $exists = &{$VCS_cmds{"execute_cmd"}}($cmd);
+
+    return 0 if ($? != 0);
+
+    return $exists;
+}
+
+sub vcs_list_files {
+    my ($file) = @_;
+
+    my @lsfiles = ();
+
+    my $vcs_used = vcs_exists();
+    return 0 if (!$vcs_used);
+
+    my $cmd = $VCS_cmds{"list_files_cmd"};
+    $cmd =~ s/(\$\w+)/$1/eeg;   # interpolate $cmd
+    @lsfiles = &{$VCS_cmds{"execute_cmd"}}($cmd);
+
+    return () if ($? != 0);
+
+    return @lsfiles;
+}
+
 sub uniq {
     my (@parms) = @_;