Add scripts/get_maintainer.py, a full-parity Python 3.6+ rewrite of
scripts/get_maintainer.pl. This is the first step toward deprecating
Perl in the kernel scripts, following the precedent set by
scripts/kernel-doc.py.
The Python version is a drop-in replacement that produces identical
output across all major operation modes: file lookups, patch parsing,
section listing, interactive mode, and self-test validation.
Benchmark comparison (10 iterations, wall time):
Mode Perl Python
Single file (--nogit) 7.96s 8.00s
Default (with git-fallback) 7.93s 8.09s
Patch (--nogit) 2.60s 2.22s
Self-test (sections, 1x) 2.69s 1.52s
Performance optimizations over a naive port:
- Pre-parsed (type, value) tuples avoid repeated regex matching on the
~24,000 typevalue entries during section traversal
- Literal prefix extraction from F:/X: glob patterns enables fast
string-based short-circuit before regex compilation, reducing regex
compilations from ~9,500 to ~170 per invocation (98% reduction)
- Lazy regex compilation ensures only patterns that survive the prefix
check are compiled
- Pre-compiled regex for the hot-path type:value line format
- Self-test pattern matching uses pre-compiled regexes
Key implementation details:
- All 45+ CLI options supported with Perl-compatible --foo/--nofoo
negation via argparse
- .get_maintainer.conf and .get_maintainer.ignore file loading
- Full VCS integration (git/hg) with command templates using
str.format() instead of Perl eval-based interpolation
- RFC822 email validation ported from the Perl regex builder
- Mailmap support with all 4 entry formats
- Interactive terminal menu with all commands
- stdlib only (no external dependencies), single self-contained file
Signed-off-by: Guido De Rossi <guido.derossi91@gmail.com>
---
scripts/get_maintainer.py | 2393 +++++++++++++++++++++++++++++++++++++
1 file changed, 2393 insertions(+)
create mode 100755 scripts/get_maintainer.py
diff --git a/scripts/get_maintainer.py b/scripts/get_maintainer.py
new file mode 100755
index 000000000000..cc1dab4d4f64
--- /dev/null
+++ b/scripts/get_maintainer.py
@@ -0,0 +1,2393 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Print selected MAINTAINERS information for
+# the files modified in a patch or for a file
+#
+# usage: get_maintainer.py [OPTIONS] <patch>
+# get_maintainer.py [OPTIONS] -f <file>
+#
+# Python rewrite of get_maintainer.pl
+
+import argparse
+import os
+import re
+import shlex
+import shutil
+import subprocess
+import sys
+
+V = '0.26'
+
+# ---- constants ----
+
+penguin_chief = [
+ "Linus Torvalds:torvalds@linux-foundation.org",
+]
+
+penguin_chief_names = []
+for chief in penguin_chief:
+ m = re.match(r'^(.*?):(.*)', chief)
+ if m:
+ penguin_chief_names.append(m.group(1))
+
+penguin_chiefs = "(" + "|".join(re.escape(n) for n in penguin_chief_names) + ")"
+
+signature_tags = [
+ "Signed-off-by:",
+ "Reviewed-by:",
+ "Acked-by:",
+]
+
+signature_pattern = "(" + "|".join(re.escape(t) for t in signature_tags) + ")"
+
+rfc822_lwsp = r"(?:(?:\r\n)?[ \t])"
+rfc822_char = r'[\000-\377]'
+
+# Pre-compiled patterns for hot paths
+_re_type_value = re.compile(r'^([A-Z]):\s*(.*)')
+_re_blank_line = re.compile(r'^\s*$')
+_re_comment_line = re.compile(r'^\s*#')
+
+# ---- VCS command templates ----
+
+VCS_cmds_git = {
+ "available": "git",
+ "find_signers_cmd":
+ 'git log --no-color --follow --since={email_git_since} '
+ '--numstat --no-merges '
+ '--format="GitCommit: %H%n'
+ 'GitAuthor: %an <%ae>%n'
+ 'GitDate: %aD%n'
+ 'GitSubject: %s%n'
+ '%b%n"'
+ ' -- {file}',
+ "find_commit_signers_cmd":
+ 'git log --no-color '
+ '--numstat '
+ '--format="GitCommit: %H%n'
+ 'GitAuthor: %an <%ae>%n'
+ 'GitDate: %aD%n'
+ 'GitSubject: %s%n'
+ '%b%n"'
+ ' -1 {commit}',
+ "find_commit_author_cmd":
+ 'git log --no-color '
+ '--numstat '
+ '--format="GitCommit: %H%n'
+ 'GitAuthor: %an <%ae>%n'
+ 'GitDate: %aD%n'
+ 'GitSubject: %s%n"'
+ ' -1 {commit}',
+ "blame_range_cmd": "git blame -l -L {diff_start},+{diff_length} {file}",
+ "blame_file_cmd": "git blame -l {file}",
+ "commit_pattern": r"^GitCommit: ([0-9a-f]{40})",
+ "blame_commit_pattern": r"^([0-9a-f]+) ",
+ "author_pattern": r"^GitAuthor: (.*)",
+ "subject_pattern": r"^GitSubject: (.*)",
+ "stat_pattern": r"^(\d+)\t(\d+)\t{file}$",
+ "file_exists_cmd": "git ls-files {file}",
+ "list_files_cmd": "git ls-files {file}",
+}
+
+VCS_cmds_hg = {
+ "available": "hg",
+ "find_signers_cmd":
+ "hg log --date={email_hg_since} "
+ "--template='HgCommit: {{node}}\\n"
+ "HgAuthor: {{author}}\\n"
+ "HgSubject: {{desc}}\\n'"
+ " -- {file}",
+ "find_commit_signers_cmd":
+ "hg log "
+ "--template='HgSubject: {{desc}}\\n'"
+ " -r {commit}",
+ "find_commit_author_cmd":
+ "hg log "
+ "--template='HgCommit: {{node}}\\n"
+ "HgAuthor: {{author}}\\n"
+ "HgSubject: {{desc|firstline}}\\n'"
+ " -r {commit}",
+ "blame_range_cmd": "",
+ "blame_file_cmd": "hg blame -n {file}",
+ "commit_pattern": r"^HgCommit: ([0-9a-f]{40})",
+ "blame_commit_pattern": r"^([ 0-9a-f]+):",
+ "author_pattern": r"^HgAuthor: (.*)",
+ "subject_pattern": r"^HgSubject: (.*)",
+ "stat_pattern": r"^(\d+)\t(\d+)\t{file}$",
+ "file_exists_cmd": "hg files {file}",
+ "list_files_cmd": "hg manifest -R {file}",
+}
+
+# ---- global state ----
+
+P = ""
+cur_path = ""
+lk_path = "./"
+
+# Options (with defaults matching the Perl script)
+email = 1
+email_usename = 1
+email_maintainer = 1
+email_reviewer = 1
+email_fixes = 1
+email_list = 1
+email_moderated_list = 1
+email_subscriber_list = 0
+email_git_penguin_chiefs = 0
+email_git = 0
+email_git_all_signature_types = 0
+email_git_blame = 0
+email_git_blame_signatures = 1
+email_git_fallback = 1
+email_git_min_signatures = 1
+email_git_max_maintainers = 5
+email_git_min_percent = 5
+email_git_since = "1-year-ago"
+email_hg_since = "-365"
+interactive = 0
+email_remove_duplicates = 1
+email_use_mailmap = 1
+output_multiline = 1
+output_separator = ", "
+output_roles = 0
+output_rolestats = 1
+output_substatus = None
+output_section_maxlen = 50
+scm = 0
+tree = 1
+web = 0
+bug = 0
+subsystem_opt = 0
+status_opt = 0
+letters = ""
+keywords = 1
+keywords_in_file = 0
+sections = 0
+email_file_emails = 0
+from_filename = 0
+pattern_depth = 0
+self_test = None
+version = 0
+find_maintainer_files = 0
+maintainer_path = None
+vcs_used = 0
+
+# Mutable state
+files = []
+fixes = []
+range_list = []
+keyword_tvi = []
+file_emails = []
+
+commit_author_hash = {}
+commit_signer_hash = {}
+
+typevalue = []
+# Pre-parsed (type_char, value_string) tuples parallel to typevalue.
+# For non-type lines, type_char is None.
+typevalue_parsed = []
+# Lazily compiled regexes for F:/X: patterns, keyed by typevalue index.
+# Sentinel value _UNCOMPILED means the pattern has not been compiled yet.
+_UNCOMPILED = object()
+typevalue_compiled = {}
+# Literal prefix for each F:/X: pattern -- fast string check before regex.
+typevalue_prefix = {}
+keyword_hash = {}
+mfiles = []
+self_test_info = []
+
+mailmap_data = None
+
+email_hash_name = {}
+email_hash_address = {}
+email_to = []
+hash_list_to = {}
+list_to = []
+scm_list = []
+web_list = []
+bug_list = []
+subsystem_list = []
+status_list = []
+substatus_list = []
+deduplicate_name_hash = {}
+deduplicate_address_hash = {}
+
+ignore_emails = []
+
+VCS_cmds = {}
+
+printed_novcs = False
+
+# ---- utility functions ----
+
+def which(bin_name):
+ path = shutil.which(bin_name)
+ return path if path else ""
+
+def which_conf(conf):
+ for path_dir in [".", os.environ.get("HOME", ""), ".scripts"]:
+ p = os.path.join(path_dir, conf)
+ if os.path.exists(p):
+ return p
+ return ""
+
+def top_of_kernel_tree(lk):
+ if lk and not lk.endswith("/"):
+ lk += "/"
+ checks_f = ["COPYING", "CREDITS", "Kbuild", "Makefile", "README"]
+ checks_e = ["MAINTAINERS"]
+ checks_d = ["Documentation", "arch", "include", "drivers", "fs",
+ "init", "ipc", "kernel", "lib", "scripts"]
+ for f in checks_f:
+ if not os.path.isfile(lk + f):
+ return False
+ for f in checks_e:
+ if not os.path.exists(lk + f):
+ return False
+ for d in checks_d:
+ if not os.path.isdir(lk + d):
+ return False
+ return True
+
+def uniq(lst):
+ seen = set()
+ result = []
+ for x in lst:
+ if x not in seen:
+ seen.add(x)
+ result.append(x)
+ return result
+
+def sort_and_uniq(lst):
+ seen = set()
+ result = []
+ for x in sorted(lst):
+ if x not in seen:
+ seen.add(x)
+ result.append(x)
+ return result
+
+# ---- email functions ----
+
+def escape_name(name):
+ if re.search(r'[^\w \-]', name, re.ASCII | re.IGNORECASE):
+ name = name.replace('\\', '\\\\')
+ name = name.replace('"', '\\"')
+ name = '"' + name + '"'
+ return name
+
+def parse_email(formatted_email):
+ name = ""
+ address = ""
+ m = re.match(r'^([^<]+)<(.+@.*)>.*$', formatted_email)
+ if m:
+ name = m.group(1)
+ address = m.group(2)
+ else:
+ m = re.match(r'^\s*<(.+@\S*)>.*$', formatted_email)
+ if m:
+ address = m.group(1)
+ else:
+ m = re.match(r'^(.+@\S*).*$', formatted_email)
+ if m:
+ address = m.group(1)
+ name = name.strip()
+ name = name.strip('"')
+ name = escape_name(name)
+ address = address.strip()
+ return (name, address)
+
+def format_email(name, address, usename):
+ name = name.strip().strip('"')
+ name = escape_name(name)
+ address = address.strip()
+ if usename:
+ if name == "":
+ return address
+ else:
+ return "{} <{}>".format(name, address)
+ else:
+ return address
+
+# ---- RFC822 validation ----
+
+_rfc822re = None
+
+def make_rfc822re():
+ specials = r'()<>@,;:\\".\\[\\]'
+ controls = r'\000-\037\177'
+
+ dtext = r"[^\[\]\r\\]"
+ domain_literal = r"\[(?:" + dtext + r"|\\\\.)* \]" + rfc822_lwsp + "*"
+
+ quoted_string = r'"(?:[^"\r\\]|\\\\.|' + rfc822_lwsp + r')*"' + rfc822_lwsp + "*"
+
+ atom = r"[^" + specials + " " + controls + r"]+(?:" + rfc822_lwsp + r"+|\Z|(?=[\\[\"" + specials + r"]))"
+ word = r"(?:" + atom + r"|" + quoted_string + r")"
+ localpart = word + r"(?:\." + rfc822_lwsp + r"*" + word + r")*"
+
+ sub_domain = r"(?:" + atom + r"|" + domain_literal + r")"
+ domain = sub_domain + r"(?:\." + rfc822_lwsp + r"*" + sub_domain + r")*"
+
+ addr_spec = localpart + r"@" + rfc822_lwsp + r"*" + domain
+
+ phrase = word + r"*"
+ route = r"(?:@" + domain + r"(?:,@" + rfc822_lwsp + r"*" + domain + r")*:" + rfc822_lwsp + r"*)"
+ route_addr = r"\<" + rfc822_lwsp + r"*" + route + r"?" + addr_spec + r"\>" + rfc822_lwsp + r"*"
+ mailbox = r"(?:" + addr_spec + r"|" + phrase + route_addr + r")"
+
+ group = phrase + r":" + rfc822_lwsp + r"*(?:" + mailbox + r"(?:,\s*" + mailbox + r")*)?;\s*"
+ address = r"(?:" + mailbox + r"|" + group + r")"
+
+ return rfc822_lwsp + r"*" + address
+
+def rfc822_strip_comments(s):
+ while True:
+ new_s = re.sub(
+ r'^((?:[^"\\]|\\.)*(?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)\((?:[^()\\]|\\.)*\)',
+ r'\1 ', s, count=1, flags=re.DOTALL)
+ if new_s == s:
+ break
+ s = new_s
+ return s
+
+def rfc822_valid(s):
+ global _rfc822re
+ s = rfc822_strip_comments(s)
+ if _rfc822re is None:
+ _rfc822re = make_rfc822re()
+ if re.match(r'^' + _rfc822re + r'$', s) and re.match(r'^' + rfc822_char + r'*$', s):
+ return True
+ return False
+
+def rfc822_validlist(s):
+ global _rfc822re
+ s = rfc822_strip_comments(s)
+ if _rfc822re is None:
+ _rfc822re = make_rfc822re()
+ if re.match(r'^(?:' + _rfc822re + r')?(?:,(?:' + _rfc822re + r')?)*$', s) and \
+ re.match(r'^' + rfc822_char + r'*$', s):
+ result = []
+ for m in re.finditer(r'(?:^|,' + rfc822_lwsp + r'*)(' + _rfc822re + r')', s):
+ result.append(m.group(1))
+ return result
+ return []
+
+# ---- mailmap functions ----
+
+def read_mailmap():
+ global mailmap_data, email_use_mailmap, lk_path
+ mailmap_data = {"names": {}, "addresses": {}}
+
+ if not email_use_mailmap or not os.path.isfile(lk_path + ".mailmap"):
+ return
+
+ try:
+ with open(lk_path + ".mailmap", "r", encoding="utf-8") as f:
+ for line in f:
+ line = re.sub(r'#.*$', '', line) # strip comments
+ line = line.strip()
+ if not line:
+ continue
+
+ # name1 <mail1> name2 <mail2>
+ m = re.match(r'^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$', line)
+ if m:
+ real_name = m.group(1).rstrip()
+ real_address = m.group(2)
+ wrong_name = m.group(3).rstrip()
+ wrong_address = m.group(4)
+ real_name, real_address = parse_email("{} <{}>".format(real_name, real_address))
+ wrong_name, wrong_address = parse_email("{} <{}>".format(wrong_name, wrong_address))
+ wrong_email = format_email(wrong_name, wrong_address, 1)
+ mailmap_data["names"][wrong_email] = real_name
+ mailmap_data["addresses"][wrong_email] = real_address
+ continue
+
+ # name1 <mail1> <mail2>
+ m = re.match(r'^(.+)<([^>]+)>\s*<([^>]+)>$', line)
+ if m:
+ real_name = m.group(1).rstrip()
+ real_address = m.group(2)
+ wrong_address = m.group(3)
+ real_name, real_address = parse_email("{} <{}>".format(real_name, real_address))
+ mailmap_data["names"][wrong_address] = real_name
+ mailmap_data["addresses"][wrong_address] = real_address
+ continue
+
+ # <mail1> <mail2>
+ m = re.match(r'^<([^>]+)>\s*<([^>]+)>$', line)
+ if m:
+ real_address = m.group(1)
+ wrong_address = m.group(2)
+ mailmap_data["addresses"][wrong_address] = real_address
+ continue
+
+ # name1 <mail1>
+ m = re.match(r'^([^<]+)<([^>]+)>$', line)
+ if m:
+ real_name = m.group(1).rstrip()
+ address = m.group(2)
+ real_name, address = parse_email("{} <{}>".format(real_name, address))
+ mailmap_data["names"][address] = real_name
+ continue
+ except IOError:
+ print("{}: Can't open .mailmap".format(P), file=sys.stderr)
+
+def mailmap_email(line):
+ name, address = parse_email(line)
+ email_str = format_email(name, address, 1)
+ real_name = name
+ real_address = address
+
+ if email_str in mailmap_data["names"] or email_str in mailmap_data["addresses"]:
+ if email_str in mailmap_data["names"]:
+ real_name = mailmap_data["names"][email_str]
+ if email_str in mailmap_data["addresses"]:
+ real_address = mailmap_data["addresses"][email_str]
+ else:
+ if address in mailmap_data["names"]:
+ real_name = mailmap_data["names"][address]
+ if address in mailmap_data["addresses"]:
+ real_address = mailmap_data["addresses"][address]
+
+ return format_email(real_name, real_address, 1)
+
+def mailmap(addresses):
+ mapped = []
+ for line in addresses:
+ mapped.append(mailmap_email(line))
+ if email_use_mailmap:
+ merge_by_realname(mapped)
+ return mapped
+
+def merge_by_realname(emails):
+ address_map = {}
+ for i in range(len(emails)):
+ name, address = parse_email(emails[i])
+ if name in address_map:
+ address = address_map[name]
+ emails[i] = format_email(name, address, 1)
+ else:
+ address_map[name] = address
+
+# ---- MAINTAINERS parsing ----
+
+def read_maintainer_file(filepath):
+ global typevalue, typevalue_parsed, typevalue_compiled
+ global keyword_hash, self_test_info, self_test
+ try:
+ with open(filepath, "r", encoding="utf-8") as f:
+ i = 1
+ for raw_line in f:
+ line = raw_line.rstrip('\n')
+
+ m = _re_type_value.match(line)
+ if m:
+ typ = m.group(1)
+ value = m.group(2)
+
+ if typ in ("F", "X"):
+ value = value.replace('.', '\\.')
+ value = value.replace('**', '\x00')
+ value = value.replace('*', '.*')
+ value = value.replace('?', '.')
+ value = value.replace('\x00', '(?:.*)')
+ if os.path.isdir(value):
+ if not value.endswith('/'):
+ value += '/'
+ # Mark for lazy compilation and extract literal prefix
+ idx = len(typevalue)
+ typevalue_compiled[idx] = _UNCOMPILED
+ # Extract the literal prefix before any regex metachar
+ prefix = []
+ for ch in value:
+ if ch in r'.*+?()[]{}|\\^$':
+ break
+ prefix.append(ch)
+ typevalue_prefix[idx] = ''.join(prefix)
+ elif typ == "K":
+ keyword_hash[len(typevalue)] = value
+
+ typevalue.append("{}:{}".format(typ, value))
+ typevalue_parsed.append((typ, value))
+ elif not _re_blank_line.match(line) and not _re_comment_line.match(line):
+ typevalue.append(line)
+ typevalue_parsed.append((None, line))
+
+ if self_test is not None:
+ self_test_info.append({"file": filepath, "linenr": i, "line": line})
+ i += 1
+ except IOError:
+ print("{}: Can't open MAINTAINERS file '{}'" .format(P, filepath), file=sys.stderr)
+ sys.exit(1)
+
+def read_all_maintainer_files():
+ global mfiles, lk_path, maintainer_path, find_maintainer_files
+ path = lk_path + "MAINTAINERS"
+ if maintainer_path is not None:
+ path = maintainer_path
+ path = os.path.expanduser(path)
+
+ if os.path.isdir(path):
+ if not path.endswith('/'):
+ path += '/'
+ if find_maintainer_files:
+ for root, dirs, fnames in os.walk(path):
+ # skip .git
+ dirs[:] = [d for d in dirs if d != '.git']
+ for fname in fnames:
+ if fname == "MAINTAINERS":
+ mfiles.append(os.path.join(root, fname))
+ else:
+ for fname in os.listdir(path):
+ if not fname.startswith('.'):
+ mfiles.append(path + fname)
+ elif os.path.isfile(path):
+ mfiles.append(path)
+ else:
+ print("{}: MAINTAINER file not found '{}'".format(P, path), file=sys.stderr)
+ sys.exit(1)
+
+ if len(mfiles) == 0:
+ print("{}: No MAINTAINER files found in '{}'".format(P, path), file=sys.stderr)
+ sys.exit(1)
+
+ for filepath in mfiles:
+ read_maintainer_file(filepath)
+
+def maintainers_in_file(filepath):
+ global file_emails, email_file_emails
+ if re.search(r'\bMAINTAINERS$', filepath):
+ return
+ if os.path.isfile(filepath) and (email_file_emails or filepath.endswith('.yaml')):
+ try:
+ with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
+ text = f.read()
+ poss_addr = re.findall(
+ r"[\w\"\' \,\.\+-]*\s*[\,]*\s*[\(\<\{]?[A-Za-z0-9_\.\+-]+@[A-Za-z0-9\.\-]+\.[A-Za-z0-9]+[\)\>\}]?",
+ text)
+ file_emails.extend(clean_file_emails(poss_addr))
+ except IOError:
+ pass
+
+# ---- section navigation ----
+
+def find_first_section():
+ index = 0
+ while index < len(typevalue_parsed):
+ if typevalue_parsed[index][0] is not None:
+ break
+ index += 1
+ return index
+
+def find_starting_index(index):
+ while index > 0:
+ if typevalue_parsed[index][0] is None:
+ break
+ index -= 1
+ return index
+
+def find_ending_index(index):
+ while index < len(typevalue_parsed):
+ if typevalue_parsed[index][0] is None:
+ break
+ index += 1
+ return index
+
+def get_subsystem_name(index):
+ start = find_starting_index(index)
+ sub = typevalue[start]
+ if output_section_maxlen and len(sub) > output_section_maxlen:
+ sub = sub[:output_section_maxlen - 3].rstrip() + "..."
+ return sub
+
+# ---- file matching ----
+
+def _get_compiled(idx):
+ """Lazily compile and cache the regex for typevalue entry at idx."""
+ compiled = typevalue_compiled.get(idx)
+ if compiled is _UNCOMPILED:
+ value = typevalue_parsed[idx][1]
+ try:
+ compiled = re.compile(r'^' + value)
+ except re.error:
+ compiled = None
+ typevalue_compiled[idx] = compiled
+ return compiled
+
+def file_match_pattern(filepath, pattern, compiled=None):
+ if compiled is None:
+ try:
+ compiled = re.compile(r'^' + pattern)
+ except re.error:
+ return False
+ if pattern.endswith("/"):
+ if compiled.search(filepath):
+ return True
+ else:
+ if compiled.search(filepath):
+ s1 = filepath.count('/')
+ s2 = pattern.count('/')
+ if s1 == s2 or '(?:' in pattern:
+ return True
+ return False
+
+# ---- category/role functions ----
+
+def get_maintainer_role(index):
+ start = find_starting_index(index)
+ end = find_ending_index(index)
+ role = "maintainer"
+ sub = get_subsystem_name(index)
+ sts = "unknown"
+
+ for i in range(start + 1, end):
+ typ, value = typevalue_parsed[i]
+ if typ == "S":
+ sts = value
+
+ sts = sts.lower()
+ if sts == "buried alive in reporters":
+ role = "chief penguin"
+
+ return role + ":" + sub
+
+def get_list_role(index):
+ sub = get_subsystem_name(index)
+ if sub == "THE REST":
+ sub = ""
+ return sub
+
+def add_categories(index, suffix):
+ global email_to, hash_list_to, list_to, scm_list, web_list, bug_list
+ global subsystem_list, status_list, substatus_list
+
+ start = find_starting_index(index)
+ end = find_ending_index(index)
+
+ sub = typevalue[start]
+ subsystem_list.append(sub)
+ sts = "Unknown"
+
+ for i in range(start + 1, end):
+ ptype, pvalue = typevalue_parsed[i]
+ if ptype is not None:
+ if ptype == "L":
+ list_address = pvalue
+ list_additional = ""
+ list_role = get_list_role(i)
+
+ if list_role:
+ list_role = ":" + list_role
+
+ m2 = re.match(r'^(\S+)\s+(.*)$', list_address)
+ if m2:
+ list_address = m2.group(1)
+ list_additional = m2.group(2)
+
+ if re.search(r'subscribers-only', list_additional):
+ if email_subscriber_list:
+ if list_address.lower() not in hash_list_to:
+ hash_list_to[list_address.lower()] = 1
+ list_to.append([list_address,
+ "subscriber list{}{}".format(list_role, suffix)])
+ else:
+ if email_list:
+ if list_address.lower() not in hash_list_to:
+ if re.search(r'moderated', list_additional):
+ if email_moderated_list:
+ hash_list_to[list_address.lower()] = 1
+ list_to.append([list_address,
+ "moderated list{}{}".format(list_role, suffix)])
+ else:
+ hash_list_to[list_address.lower()] = 1
+ list_to.append([list_address,
+ "open list{}{}".format(list_role, suffix)])
+
+ elif ptype == "M":
+ if email_maintainer:
+ role = get_maintainer_role(i)
+ push_email_addresses(pvalue, role + suffix)
+ elif ptype == "R":
+ if email_reviewer:
+ subs = get_subsystem_name(i)
+ push_email_addresses(pvalue, "reviewer:" + subs + suffix)
+ elif ptype == "T":
+ scm_list.append(pvalue + suffix)
+ elif ptype == "W":
+ web_list.append(pvalue + suffix)
+ elif ptype == "B":
+ bug_list.append(pvalue + suffix)
+ elif ptype == "S":
+ status_list.append(pvalue + suffix)
+ sts = pvalue
+
+ if sub != "THE REST" and sts != "Maintained":
+ substatus_list.append("{} status: {}{}".format(sub, sts, suffix))
+
+# ---- email management ----
+
+def email_inuse(name, address):
+ if name == "" and address == "":
+ return True
+ if name != "" and name.lower() in email_hash_name:
+ return True
+ if address != "" and address.lower() in email_hash_address:
+ return True
+ return False
+
+def push_email_address(line, role):
+ global email_to, email_hash_name, email_hash_address
+ name, address = parse_email(line)
+ if address == "":
+ return False
+
+ if not email_remove_duplicates:
+ email_to.append([format_email(name, address, email_usename), role])
+ elif not email_inuse(name, address):
+ email_to.append([format_email(name, address, email_usename), role])
+ if name != "":
+ email_hash_name[name.lower()] = email_hash_name.get(name.lower(), 0) + 1
+ email_hash_address[address.lower()] = email_hash_address.get(address.lower(), 0) + 1
+
+ return True
+
+def push_email_addresses(address, role):
+ if rfc822_valid(address):
+ push_email_address(address, role)
+ else:
+ addr_list = rfc822_validlist(address)
+ if addr_list:
+ for entry in addr_list:
+ push_email_address(entry, role)
+ else:
+ if not push_email_address(address, role):
+ print("Invalid MAINTAINERS address: '{}'".format(address), file=sys.stderr)
+
+def add_role(line, role):
+ global email_to
+ name, address = parse_email(line)
+ email_str = format_email(name, address, email_usename)
+
+ for entry in email_to:
+ if email_remove_duplicates:
+ entry_name, entry_address = parse_email(entry[0])
+ if (name == entry_name or address == entry_address) and \
+ (role == "" or role not in entry[1]):
+ if entry[1] == "":
+ entry[1] = role
+ else:
+ entry[1] = "{},{}".format(entry[1], role)
+ else:
+ if email_str == entry[0] and \
+ (role == "" or role not in entry[1]):
+ if entry[1] == "":
+ entry[1] = role
+ else:
+ entry[1] = "{},{}".format(entry[1], role)
+
+def deduplicate_email(email_addr):
+ global deduplicate_name_hash, deduplicate_address_hash
+ name, address = parse_email(email_addr)
+ email_addr = format_email(name, address, 1)
+ email_addr = mailmap_email(email_addr)
+
+ if not email_remove_duplicates:
+ return email_addr
+
+ name, address = parse_email(email_addr)
+ matched = False
+
+ if name != "" and name.lower() in deduplicate_name_hash:
+ name = deduplicate_name_hash[name.lower()][0]
+ address = deduplicate_name_hash[name.lower()][1]
+ matched = True
+ elif address.lower() in deduplicate_address_hash:
+ name = deduplicate_address_hash[address.lower()][0]
+ address = deduplicate_address_hash[address.lower()][1]
+ matched = True
+
+ if not matched:
+ deduplicate_name_hash[name.lower()] = [name, address]
+ deduplicate_address_hash[address.lower()] = [name, address]
+
+ email_addr = format_email(name, address, 1)
+ email_addr = mailmap_email(email_addr)
+ return email_addr
+
+def ignore_email_address(address):
+ for ig in ignore_emails:
+ if ig == address:
+ return True
+ return False
+
+def clean_file_emails(raw_emails):
+ fmt_emails = []
+ for em in raw_emails:
+ em = re.sub(
+ r'[\(\<\{]?([A-Za-z0-9_\.\+-]+@[A-Za-z0-9\.\-]+)[\)\>\}]?',
+ r'<\1>', em)
+ name, address = parse_email(em)
+
+ # Strip quotes
+ if name.startswith('"') and name.endswith('"'):
+ name = name[1:-1]
+
+ # Split into name-like parts
+ nw = re.split(r"[^\w\'\,\.\+\-]", name)
+ nw = [w for w in nw if w and not re.match(r"^[\'\,\.\+\-]$", w)]
+
+ if len(nw) > 2:
+ first = nw[-3]
+ middle = nw[-2]
+ last = nw[-1]
+ if ((len(first) == 1 and re.match(r'\w', first)) or
+ (len(first) == 2 and first.endswith("."))) or \
+ (len(middle) == 1 or
+ (len(middle) == 2 and middle.endswith("."))):
+ name = "{} {} {}".format(first, middle, last)
+ else:
+ name = "{} {}".format(middle, last)
+ else:
+ name = " ".join(nw)
+
+ if name and name[-1] in (',', '.'):
+ name = name[:-1]
+ if name and name[0] in (',', '.'):
+ name = name[1:]
+
+ fmt_emails.append(format_email(name, address, email_usename))
+ return fmt_emails
+
+# ---- VCS integration ----
+
+def git_execute_cmd(cmd):
+ try:
+ output = subprocess.run(cmd, shell=True, capture_output=True,
+ text=True, encoding='utf-8', errors='replace')
+ lines = output.stdout.lstrip().split('\n')
+ # strip leading whitespace from each line (matching Perl)
+ lines = [l.lstrip() for l in lines]
+ return lines
+ except Exception:
+ return []
+
+def hg_execute_cmd(cmd):
+ try:
+ output = subprocess.run(cmd, shell=True, capture_output=True,
+ text=True, encoding='utf-8', errors='replace')
+ return output.stdout.split('\n')
+ except Exception:
+ return []
+
+def vcs_exists():
+ global VCS_cmds, vcs_used, printed_novcs
+ # Try git
+ if which("git") and os.path.exists(".git"):
+ VCS_cmds = VCS_cmds_git
+ vcs_used = 1
+ return 1
+ # Try hg
+ if which("hg") and os.path.isdir(".hg"):
+ VCS_cmds = VCS_cmds_hg
+ vcs_used = 2
+ return 2
+ VCS_cmds = {}
+ if not printed_novcs and email_git:
+ print("{}: No supported VCS found. Add --nogit to options?".format(P), file=sys.stderr)
+ print("Using a git repository produces better results.", file=sys.stderr)
+ print("Try Linus Torvalds' latest git repository using:", file=sys.stderr)
+ print("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git", file=sys.stderr)
+ printed_novcs = True
+ return 0
+
+def vcs_is_git():
+ vcs_exists()
+ return vcs_used == 1
+
+def vcs_is_hg():
+ return vcs_used == 2
+
+def _vcs_exec(cmd):
+ if vcs_used == 1:
+ return git_execute_cmd(cmd)
+ elif vcs_used == 2:
+ return hg_execute_cmd(cmd)
+ return []
+
+def _interpolate_cmd(cmd_template, **kwargs):
+ """Substitute variables in a VCS command template."""
+ result = cmd_template
+ for key, val in kwargs.items():
+ result = result.replace('{' + key + '}', str(val))
+ return result
+
+def extract_formatted_signatures(signature_lines):
+ types = []
+ signers = []
+ for line in signature_lines:
+ # extract type (everything before first ':')
+ m = re.match(r'\s*(.*?):.*', line)
+ typ = m.group(1) if m else ""
+ types.append(typ)
+
+ # extract signer (everything after first ':')
+ m = re.match(r'\s*.*?:\s*(.+)\s*', line)
+ signer = m.group(1).strip() if m else ""
+ signer = deduplicate_email(signer)
+ signers.append(signer)
+
+ return (types, signers)
+
+def vcs_find_signers(cmd, filepath):
+ global signature_pattern
+ lines = _vcs_exec(cmd)
+
+ pattern = VCS_cmds["commit_pattern"]
+ author_pattern = VCS_cmds["author_pattern"]
+ stat_pattern = VCS_cmds["stat_pattern"]
+
+ if filepath:
+ stat_pattern = stat_pattern.replace('{file}', re.escape(filepath))
+ else:
+ stat_pattern = stat_pattern.replace('{file}', '')
+
+ commits = sum(1 for l in lines if re.search(pattern, l))
+ authors = [l for l in lines if re.search(author_pattern, l)]
+ signatures = [l for l in lines if re.search(
+ r'^[ \t]*' + signature_pattern + r'.*@.*$', l)]
+ stats = [l for l in lines if re.search(stat_pattern, l)]
+
+ if not signatures:
+ return (0, signatures, authors, stats)
+
+ if interactive:
+ save_commits_by_author(lines)
+ save_commits_by_signer(lines)
+
+ if not email_git_penguin_chiefs:
+ signatures = [s for s in signatures if not re.search(penguin_chiefs, s, re.IGNORECASE)]
+
+ _, authors_list = extract_formatted_signatures(authors)
+ _, signers_list = extract_formatted_signatures(signatures)
+
+ return (commits, signers_list, authors_list, stats)
+
+def vcs_find_author(cmd):
+ lines = _vcs_exec(cmd)
+
+ if not email_git_penguin_chiefs:
+ lines = [l for l in lines if not re.search(penguin_chiefs, l, re.IGNORECASE)]
+
+ if not lines:
+ return []
+
+ authors = []
+ author_pattern = VCS_cmds["author_pattern"]
+ for line in lines:
+ m = re.search(author_pattern, line)
+ if m:
+ author = m.group(1)
+ name, address = parse_email(author)
+ author = format_email(name, address, 1)
+ authors.append(author)
+
+ if interactive:
+ save_commits_by_author(lines)
+ save_commits_by_signer(lines)
+
+ return authors
+
+def vcs_save_commits(cmd):
+ lines = _vcs_exec(cmd)
+ commits = []
+ blame_pattern = VCS_cmds["blame_commit_pattern"]
+ for line in lines:
+ m = re.search(blame_pattern, line)
+ if m:
+ commits.append(m.group(1))
+ return commits
+
+def vcs_blame(filepath):
+ global range_list
+ commits = []
+ if not os.path.isfile(filepath):
+ return commits
+
+ if range_list and VCS_cmds.get("blame_range_cmd", "") == "":
+ cmd = _interpolate_cmd(VCS_cmds["blame_file_cmd"], file=shlex.quote(filepath))
+ all_commits = vcs_save_commits(cmd)
+
+ for file_range_diff in range_list:
+ m = re.match(r'(.+):(.+):(.+)', file_range_diff)
+ if not m:
+ continue
+ diff_file = m.group(1)
+ diff_start = int(m.group(2))
+ diff_length = int(m.group(3))
+ if filepath != diff_file:
+ continue
+ for i in range(diff_start, diff_start + diff_length):
+ if i < len(all_commits):
+ commits.append(all_commits[i])
+ elif range_list:
+ for file_range_diff in range_list:
+ m = re.match(r'(.+):(.+):(.+)', file_range_diff)
+ if not m:
+ continue
+ diff_file = m.group(1)
+ diff_start = m.group(2)
+ diff_length = m.group(3)
+ if filepath != diff_file:
+ continue
+ cmd = _interpolate_cmd(VCS_cmds["blame_range_cmd"],
+ file=shlex.quote(filepath),
+ diff_start=diff_start,
+ diff_length=diff_length)
+ commits.extend(vcs_save_commits(cmd))
+ else:
+ cmd = _interpolate_cmd(VCS_cmds["blame_file_cmd"], file=shlex.quote(filepath))
+ commits = vcs_save_commits(cmd)
+
+ commits = [c.lstrip('^') for c in commits]
+ return commits
+
+def vcs_file_exists(filepath):
+ v = vcs_exists()
+ if not v:
+ return False
+ cmd = _interpolate_cmd(VCS_cmds["file_exists_cmd"], file=shlex.quote(filepath))
+ cmd += " 2>&1"
+ result = _vcs_exec(cmd)
+ # Check if any non-empty output
+ return any(line.strip() for line in result) if result else False
+
+def vcs_list_files(filepath):
+ v = vcs_exists()
+ if not v:
+ return []
+ cmd = _interpolate_cmd(VCS_cmds["list_files_cmd"], file=shlex.quote(filepath))
+ return _vcs_exec(cmd)
+
+def vcs_assign(role, divisor, lines):
+ if not lines:
+ return
+
+ if divisor <= 0:
+ print("Bad divisor in vcs_assign: {}".format(divisor), file=sys.stderr)
+ divisor = 1
+
+ lines = mailmap(lines)
+
+ if not lines:
+ return
+
+ lines.sort()
+
+ # uniq -c
+ counts = {}
+ for l in lines:
+ counts[l] = counts.get(l, 0) + 1
+
+ count = 0
+ for line in sorted(counts.keys(), key=lambda x: counts[x], reverse=True):
+ sign_offs = counts[line]
+ percent = sign_offs * 100 / divisor
+ if percent > 100:
+ percent = 100
+ if ignore_email_address(line):
+ continue
+ count += 1
+ if sign_offs < email_git_min_signatures or \
+ count > email_git_max_maintainers or \
+ percent < email_git_min_percent:
+ break
+ push_email_address(line, '')
+ if output_rolestats:
+ fmt_percent = "{:.0f}".format(percent)
+ add_role(line, "{}:{}/{}={}%".format(role, sign_offs, divisor, fmt_percent))
+ else:
+ add_role(line, role)
+
+def vcs_file_signoffs(filepath):
+ global vcs_used
+
+ vcs_used = vcs_exists()
+ if not vcs_used:
+ return
+
+ cmd = _interpolate_cmd(VCS_cmds["find_signers_cmd"],
+ email_git_since=email_git_since,
+ email_hg_since=email_hg_since,
+ file=shlex.quote(filepath))
+
+ commits, signers, authors, stats = vcs_find_signers(cmd, filepath)
+
+ for i in range(len(signers)):
+ signers[i] = deduplicate_email(signers[i])
+
+ vcs_assign("commit_signer", commits, signers)
+ vcs_assign("authored", commits, authors)
+
+ if len(authors) == len(stats):
+ stat_pattern = VCS_cmds["stat_pattern"]
+ stat_pattern = stat_pattern.replace('{file}', re.escape(filepath))
+
+ added = 0
+ deleted = 0
+ for i in range(len(stats)):
+ m = re.search(stat_pattern, stats[i])
+ if m:
+ added += int(m.group(1))
+ deleted += int(m.group(2))
+
+ tmp_authors = uniq(authors)
+ tmp_authors = [deduplicate_email(a) for a in tmp_authors]
+ tmp_authors = uniq(tmp_authors)
+
+ list_added = []
+ list_deleted = []
+ for author in tmp_authors:
+ auth_added = 0
+ auth_deleted = 0
+ for i in range(len(stats)):
+ if author == deduplicate_email(authors[i]):
+ m = re.search(stat_pattern, stats[i])
+ if m:
+ auth_added += int(m.group(1))
+ auth_deleted += int(m.group(2))
+ list_added.extend([author] * auth_added)
+ list_deleted.extend([author] * auth_deleted)
+
+ vcs_assign("added_lines", added, list_added)
+ vcs_assign("removed_lines", deleted, list_deleted)
+
+def vcs_file_blame(filepath):
+ global vcs_used
+ vcs_used = vcs_exists()
+ if not vcs_used:
+ return
+
+ all_commits = vcs_blame(filepath)
+ commits = uniq(all_commits)
+ total_commits = len(commits)
+ total_lines = len(all_commits)
+
+ signers = []
+
+ if email_git_blame_signatures:
+ if vcs_is_hg():
+ commit = " -r ".join(commits)
+ cmd = _interpolate_cmd(VCS_cmds["find_commit_signers_cmd"], commit=commit)
+ _, commit_signers, _, _ = vcs_find_signers(cmd, filepath)
+ signers.extend(commit_signers)
+ else:
+ for commit in commits:
+ cmd = _interpolate_cmd(VCS_cmds["find_commit_signers_cmd"], commit=commit)
+ _, commit_signers, _, _ = vcs_find_signers(cmd, filepath)
+ signers.extend(commit_signers)
+
+ if from_filename:
+ if output_rolestats:
+ blame_signers = []
+ if vcs_is_hg():
+ u_commits = sorted(uniq(commits))
+ commit = " -r ".join(u_commits)
+ cmd = _interpolate_cmd(VCS_cmds["find_commit_author_cmd"], commit=commit)
+ lines = _vcs_exec(cmd)
+ if not email_git_penguin_chiefs:
+ lines = [l for l in lines if not re.search(penguin_chiefs, l, re.IGNORECASE)]
+ if lines:
+ author_pattern = VCS_cmds["author_pattern"]
+ for line in lines:
+ m = re.search(author_pattern, line)
+ if m:
+ author = deduplicate_email(m.group(1))
+ signers.append(author)
+ if interactive:
+ save_commits_by_author(lines)
+ save_commits_by_signer(lines)
+ else:
+ for commit in commits:
+ cmd = _interpolate_cmd(VCS_cmds["find_commit_author_cmd"], commit=commit)
+ author_list = vcs_find_author(cmd)
+ if not author_list:
+ continue
+ formatted_author = deduplicate_email(author_list[0])
+ cnt = sum(1 for c in all_commits if commit in c)
+ blame_signers.extend([formatted_author] * cnt)
+
+ if blame_signers:
+ vcs_assign("authored lines", total_lines, blame_signers)
+
+ signers = [deduplicate_email(s) for s in signers]
+ vcs_assign("commits", total_commits, signers)
+ else:
+ signers = [deduplicate_email(s) for s in signers]
+ vcs_assign("modified commits", total_commits, signers)
+
+def vcs_add_commit_signers(commit, desc):
+ if not vcs_exists():
+ return
+
+ cmd = _interpolate_cmd(VCS_cmds["find_commit_signers_cmd"], commit=commit)
+ commit_count, commit_signers, commit_authors, stats = vcs_find_signers(cmd, "")
+
+ for i in range(len(commit_signers)):
+ commit_signers[i] = deduplicate_email(commit_signers[i])
+
+ vcs_assign(desc, 1, commit_signers)
+
+# ---- interactive commit tracking ----
+
+def save_commits_by_author(lines):
+ global commit_author_hash
+ authors = []
+ commits = []
+ subjects = []
+ author_pattern = VCS_cmds["author_pattern"]
+ commit_pattern = VCS_cmds["commit_pattern"]
+ subject_pattern = VCS_cmds["subject_pattern"]
+
+ for line in lines:
+ m = re.search(author_pattern, line)
+ if m:
+ author = deduplicate_email(m.group(1))
+ authors.append(author)
+ m = re.search(commit_pattern, line)
+ if m:
+ commits.append(m.group(1))
+ m = re.search(subject_pattern, line)
+ if m:
+ subjects.append(m.group(1))
+
+ for i in range(len(authors)):
+ if i >= len(commits) or i >= len(subjects):
+ break
+ if authors[i] not in commit_author_hash:
+ commit_author_hash[authors[i]] = []
+ exists = False
+ for ref in commit_author_hash[authors[i]]:
+ if ref[0] == commits[i] and ref[1] == subjects[i]:
+ exists = True
+ break
+ if not exists:
+ commit_author_hash[authors[i]].append([commits[i], subjects[i]])
+
+def save_commits_by_signer(lines):
+ global commit_signer_hash, signature_pattern
+ commit = ""
+ subject = ""
+ commit_pattern = VCS_cmds["commit_pattern"]
+ subject_pattern = VCS_cmds["subject_pattern"]
+
+ for line in lines:
+ m = re.search(commit_pattern, line)
+ if m:
+ commit = m.group(1)
+ m = re.search(subject_pattern, line)
+ if m:
+ subject = m.group(1)
+ if re.search(r'^[ \t]*' + signature_pattern + r'.*@.*$', line):
+ sig_types, sig_signers = extract_formatted_signatures([line])
+ if sig_signers:
+ typ = sig_types[0]
+ signer = deduplicate_email(sig_signers[0])
+ if signer not in commit_signer_hash:
+ commit_signer_hash[signer] = []
+ exists = False
+ for ref in commit_signer_hash[signer]:
+ if ref[0] == commit and ref[1] == subject and ref[2] == typ:
+ exists = True
+ break
+ if not exists:
+ commit_signer_hash[signer].append([commit, subject, typ])
+
+# ---- range checks ----
+
+def range_is_maintained(start, end):
+ for i in range(start, end):
+ typ, value = typevalue_parsed[i]
+ if typ == 'S':
+ if re.search(r'maintain|support', value, re.IGNORECASE):
+ return True
+ return False
+
+def range_has_maintainer(start, end):
+ for i in range(start, end):
+ typ, value = typevalue_parsed[i]
+ if typ == 'M':
+ return True
+ return False
+
+# ---- core orchestration ----
+
+def get_maintainers():
+ global email_hash_name, email_hash_address, commit_author_hash, commit_signer_hash
+ global email_to, hash_list_to, list_to, scm_list, web_list, bug_list
+ global subsystem_list, status_list, substatus_list
+ global deduplicate_name_hash, deduplicate_address_hash
+ global signature_pattern, keyword_tvi, file_emails
+
+ email_hash_name = {}
+ email_hash_address = {}
+ commit_author_hash = {}
+ commit_signer_hash = {}
+ email_to = []
+ hash_list_to = {}
+ list_to = []
+ scm_list = []
+ web_list = []
+ bug_list = []
+ subsystem_list = []
+ status_list = []
+ substatus_list = []
+ deduplicate_name_hash = {}
+ deduplicate_address_hash = {}
+
+ if email_git_all_signature_types:
+ signature_pattern = r"(.+?)[Bb][Yy]:"
+ else:
+ signature_pattern = "(" + "|".join(re.escape(t) for t in signature_tags) + ")"
+
+ exact_pattern_match_hash = {}
+
+ for filepath in files:
+ hash_map = {}
+ tvi = find_first_section()
+ while tvi < len(typevalue):
+ start = find_starting_index(tvi)
+ end = find_ending_index(tvi)
+ exclude = False
+
+ # Check excluded patterns
+ for i in range(start, end):
+ typ, value = typevalue_parsed[i]
+ if typ == 'X':
+ prefix = typevalue_prefix.get(i, '')
+ if prefix and not filepath.startswith(prefix):
+ continue
+ compiled = _get_compiled(i)
+ if compiled is not None and file_match_pattern(filepath, value, compiled):
+ exclude = True
+ break
+
+ if not exclude:
+ for i in range(start, end):
+ typ, value = typevalue_parsed[i]
+ if typ is None:
+ continue
+ if typ == 'F':
+ prefix = typevalue_prefix.get(i, '')
+ if prefix and not filepath.startswith(prefix):
+ continue
+ compiled = _get_compiled(i)
+ if compiled is not None and file_match_pattern(filepath, value, compiled):
+ value_pd = value.count('/')
+ file_pd = filepath.count('/')
+ if not value.endswith('/'):
+ value_pd += 1
+ if re.match(r'^(\.\*|\(\?:\.\*\))', value):
+ value_pd = -1
+ if value_pd >= file_pd and \
+ range_is_maintained(start, end) and \
+ range_has_maintainer(start, end):
+ exact_pattern_match_hash[filepath] = 1
+ if pattern_depth == 0 or \
+ (file_pd - value_pd) < pattern_depth:
+ hash_map[tvi] = value_pd
+ elif typ == 'N':
+ try:
+ if re.search(value, filepath, re.VERBOSE):
+ hash_map[tvi] = 0
+ except re.error:
+ pass
+
+ tvi = end + 1
+
+ for line_idx in sorted(hash_map.keys(), key=lambda x: hash_map[x], reverse=True):
+ add_categories(line_idx, "")
+ if sections:
+ start = find_starting_index(line_idx)
+ end = find_ending_index(line_idx)
+ for i in range(start, end):
+ line = typevalue[i]
+ if re.match(r'^[FX]:', line):
+ # Restore file patterns
+ line = re.sub(r'([^\\])\.([^\*])', r'\1?\2', line)
+ line = re.sub(r'([^\\])\.$', r'\1?', line)
+ line = line.replace('\\.', '.')
+ line = line.replace('(?:.*)', '**')
+ line = line.replace('.*', '*')
+ m2 = re.match(r'^([A-Z]):(.*)', line)
+ if m2:
+ line = "{}:\t{}".format(m2.group(1), m2.group(2))
+ if letters == "" or re.search(m2.group(1), letters, re.IGNORECASE):
+ print(line)
+ else:
+ # Section header lines are always printed
+ print(line)
+ print()
+
+ maintainers_in_file(filepath)
+
+ if keywords:
+ kw_tvi = sort_and_uniq(keyword_tvi)
+ for line_idx in kw_tvi:
+ if line_idx in keyword_hash:
+ add_categories(line_idx, ":Keyword:" + keyword_hash[line_idx])
+
+ for em in email_to + list_to:
+ em[0] = deduplicate_email(em[0])
+
+ for filepath in files:
+ if email and \
+ (email_git or
+ (email_git_fallback and
+ not filepath.endswith('MAINTAINERS') and
+ filepath not in exact_pattern_match_hash)):
+ vcs_file_signoffs(filepath)
+ if email and email_git_blame:
+ vcs_file_blame(filepath)
+
+ if email:
+ for chief in penguin_chief:
+ m = re.match(r'^(.*?):(.*)', chief)
+ if m:
+ email_address = format_email(m.group(1), m.group(2), email_usename)
+ if email_git_penguin_chiefs:
+ email_to.append([email_address, 'chief penguin'])
+ else:
+ email_to[:] = [e for e in email_to
+ if not re.search(re.escape(email_address), e[0])]
+
+ for em in file_emails:
+ em = mailmap_email(em)
+ name, address = parse_email(em)
+ tmp_email = format_email(name, address, email_usename)
+ push_email_address(tmp_email, '')
+ add_role(tmp_email, 'in file')
+
+ for fix in fixes:
+ vcs_add_commit_signers(fix, "blamed_fixes")
+
+ to = []
+ if email or email_list:
+ if email:
+ to.extend(email_to)
+ if email_list:
+ to.extend(list_to)
+
+ if interactive:
+ to = interactive_get_maintainers(to)
+
+ return to
+
+# ---- output functions ----
+
+def merge_email(entries):
+ lines = []
+ saw = {}
+ for entry in entries:
+ address = entry[0]
+ role = entry[1]
+ if address not in saw:
+ if output_roles:
+ lines.append("{} ({})".format(address, role))
+ else:
+ lines.append(address)
+ saw[address] = 1
+ return lines
+
+def output(parms):
+ if output_multiline:
+ for line in parms:
+ print(line)
+ else:
+ print(output_separator.join(parms))
+
+# ---- interactive mode ----
+
+def interactive_get_maintainers(to_list):
+ global interactive, output_rolestats, output_roles, output_substatus
+ global email_git, email_git_fallback, email_git_blame
+ global email_git_blame_signatures, email_git_min_signatures
+ global email_git_max_maintainers, email_git_min_percent
+ global email_git_since, email_hg_since, email_git_all_signature_types
+ global email_file_emails, email_remove_duplicates, email_use_mailmap
+ global keywords, pattern_depth
+
+ vcs_exists()
+
+ selected = {}
+ authored = {}
+ signed = {}
+ count = 0
+ maintained = False
+
+ for entry in to_list:
+ if re.match(r'^(maintainer|supporter)', entry[1], re.IGNORECASE):
+ maintained = True
+ selected[count] = True
+ authored[count] = False
+ signed[count] = False
+ count += 1
+
+ done = False
+ print_options = False
+ redraw = True
+
+ while not done:
+ count = len(to_list)
+ if redraw:
+ sys.stderr.write("\n{:1s} {:2s} {:65s}".format("*", "#", "email/list and role:stats"))
+ if email_git or (email_git_fallback and not maintained) or email_git_blame:
+ sys.stderr.write("auth sign")
+ sys.stderr.write("\n")
+ for idx, entry in enumerate(to_list):
+ em = entry[0]
+ role = entry[1]
+ sel = "*" if selected.get(idx, False) else ""
+ ca = commit_author_hash.get(em, [])
+ cs = commit_signer_hash.get(em, [])
+ auth_count = len(ca)
+ sign_count = len(cs)
+ sys.stderr.write("{:1s} {:2d} {:65s}".format(sel, idx + 1, em))
+ if auth_count > 0 or sign_count > 0:
+ sys.stderr.write("{:4d} {:4d}".format(auth_count, sign_count))
+ sys.stderr.write("\n {}\n".format(role))
+ if authored.get(idx, False):
+ for ref in ca:
+ sys.stderr.write(" Author: {}\n".format(ref[1]))
+ if signed.get(idx, False):
+ for ref in cs:
+ sys.stderr.write(" {}: {}\n".format(ref[2], ref[1]))
+
+ if print_options:
+ print_options = False
+ if vcs_exists():
+ date_ref = email_hg_since if vcs_is_hg() else email_git_since
+ sys.stderr.write("""
+Version Control options:
+g use git history [{}]
+gf use git-fallback [{}]
+b use git blame [{}]
+bs use blame signatures [{}]
+c# minimum commits [{}]
+%# min percent [{}]
+d# history to use [{}]
+x# max maintainers [{}]
+t all signature types [{}]
+m use .mailmap [{}]
+""".format(email_git, email_git_fallback, email_git_blame,
+ email_git_blame_signatures, email_git_min_signatures,
+ email_git_min_percent, date_ref,
+ email_git_max_maintainers, email_git_all_signature_types,
+ email_use_mailmap))
+
+ sys.stderr.write("""
+Additional options:
+0 toggle all
+tm toggle maintainers
+tg toggle git entries
+tl toggle open list entries
+ts toggle subscriber list entries
+f emails in file [{}]
+k keywords in file [{}]
+r remove duplicates [{}]
+p# pattern match depth [{}]
+""".format(email_file_emails, keywords, email_remove_duplicates, pattern_depth))
+
+ sys.stderr.write(
+ "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ")
+
+ try:
+ inp = input()
+ except EOFError:
+ break
+
+ redraw = True
+ rerun = False
+ wishes = re.split(r'[, ]+', inp)
+
+ for nr in wishes:
+ nr = nr.lower().strip()
+ if not nr:
+ continue
+ sel = nr[0]
+ rest = nr[1:]
+ val = 0
+ m = re.match(r'^(\d+)$', rest)
+ if m:
+ val = int(m.group(1))
+
+ if sel == "y":
+ interactive = 0
+ done = True
+ output_rolestats = 0
+ output_roles = 0
+ output_substatus = 0
+ break
+ elif re.match(r'^\d+$', nr):
+ num = int(nr)
+ if 0 < num <= count:
+ selected[num - 1] = not selected.get(num - 1, False)
+ elif sel in ('*', '^'):
+ toggle = (sel == '*')
+ for i in range(count):
+ selected[i] = toggle
+ elif sel == '0':
+ for i in range(count):
+ selected[i] = not selected.get(i, False)
+ elif sel == 't':
+ if rest == 'm':
+ for i in range(count):
+ if re.match(r'^(maintainer|supporter)', to_list[i][1], re.IGNORECASE):
+ selected[i] = not selected.get(i, False)
+ elif rest == 'g':
+ for i in range(count):
+ if re.match(r'^(author|commit|signer)', to_list[i][1], re.IGNORECASE):
+ selected[i] = not selected.get(i, False)
+ elif rest == 'l':
+ for i in range(count):
+ if re.match(r'^(open list)', to_list[i][1], re.IGNORECASE):
+ selected[i] = not selected.get(i, False)
+ elif rest == 's':
+ for i in range(count):
+ if re.match(r'^(subscriber list)', to_list[i][1], re.IGNORECASE):
+ selected[i] = not selected.get(i, False)
+ else:
+ # 't' alone toggles all signature types
+ email_git_all_signature_types = not email_git_all_signature_types
+ rerun = True
+ elif sel == 'a':
+ if val > 0 and val <= count:
+ authored[val - 1] = not authored.get(val - 1, False)
+ elif rest in ('*', '^'):
+ toggle = (rest == '*')
+ for i in range(count):
+ authored[i] = toggle
+ elif sel == 's':
+ if val > 0 and val <= count:
+ signed[val - 1] = not signed.get(val - 1, False)
+ elif rest in ('*', '^'):
+ toggle = (rest == '*')
+ for i in range(count):
+ signed[i] = toggle
+ elif sel == 'o':
+ print_options = True
+ redraw = True
+ elif sel == 'g':
+ if rest == 'f':
+ email_git_fallback = not email_git_fallback
+ else:
+ email_git = not email_git
+ rerun = True
+ elif sel == 'b':
+ if rest == 's':
+ email_git_blame_signatures = not email_git_blame_signatures
+ else:
+ email_git_blame = not email_git_blame
+ rerun = True
+ elif sel == 'c':
+ if val > 0:
+ email_git_min_signatures = val
+ rerun = True
+ elif sel == 'x':
+ if val > 0:
+ email_git_max_maintainers = val
+ rerun = True
+ elif sel == '%':
+ if rest != "" and val >= 0:
+ email_git_min_percent = val
+ rerun = True
+ elif sel == 'd':
+ if vcs_is_git():
+ email_git_since = rest
+ elif vcs_is_hg():
+ email_hg_since = rest
+ rerun = True
+ elif sel == 'f':
+ email_file_emails = not email_file_emails
+ rerun = True
+ elif sel == 'r':
+ email_remove_duplicates = not email_remove_duplicates
+ rerun = True
+ elif sel == 'm':
+ email_use_mailmap = not email_use_mailmap
+ read_mailmap()
+ rerun = True
+ elif sel == 'k':
+ keywords = not keywords
+ rerun = True
+ elif sel == 'p':
+ if rest != "" and val >= 0:
+ pattern_depth = val
+ rerun = True
+ elif sel in ('h', '?'):
+ sys.stderr.write("""
+Interactive mode allows you to select the various maintainers, submitters,
+commit signers and mailing lists that could be CC'd on a patch.
+
+Any *'d entry is selected.
+
+If you have git or hg installed, you can choose to summarize the commit
+history of files in the patch. Also, each line of the current file can
+be matched to its commit author and that commits signers with blame.
+
+Various knobs exist to control the length of time for active commit
+tracking, the maximum number of commit authors and signers to add,
+and such.
+
+Enter selections at the prompt until you are satisfied that the selected
+maintainers are appropriate. You may enter multiple selections separated
+by either commas or spaces.
+
+""")
+ else:
+ sys.stderr.write("invalid option: '{}'\n".format(nr))
+ redraw = False
+
+ if rerun:
+ if email_git_blame:
+ sys.stderr.write("git-blame can be very slow, please have patience...")
+ return get_maintainers()
+
+ # drop not selected entries
+ new_emailto = []
+ for i in range(len(to_list)):
+ if selected.get(i, False):
+ new_emailto.append(to_list[i])
+ return new_emailto
+
+# ---- self-test mode ----
+
+def do_self_test():
+ lsfiles = vcs_list_files(lk_path)
+ good_links = []
+ bad_links = []
+ section_headers = []
+ index = 0
+
+ for x in self_test_info:
+ index += 1
+
+ # Section header duplication and missing section content
+ if (self_test == "" or "sections" in self_test) and \
+ re.match(r'^\S[^:]', x["line"]) and \
+ index < len(self_test_info) and \
+ re.match(r'^([A-Z]):\s*\S', self_test_info[index]["line"]):
+ has_S = False
+ has_F = False
+ has_ML = False
+ sts = ""
+ if any(h.startswith(x["line"]) for h in section_headers):
+ print("{}:{}: warning: duplicate section header\t{}".format(
+ x["file"], x["linenr"], x["line"]))
+ else:
+ section_headers.append(x["line"])
+ nextline = index
+ while nextline < len(self_test_info):
+ m = re.match(r'^([A-Z]):\s*(\S.*)', self_test_info[nextline]["line"])
+ if not m:
+ break
+ typ = m.group(1)
+ val = m.group(2)
+ if typ == "S":
+ has_S = True
+ sts = val
+ elif typ in ("F", "N"):
+ has_F = True
+ elif typ in ("M", "R", "L"):
+ has_ML = True
+ nextline += 1
+ if not has_ML and not re.search(r'orphan|obsolete', sts, re.IGNORECASE):
+ print("{}:{}: warning: section without email address\t{}".format(
+ x["file"], x["linenr"], x["line"]))
+ if not has_S:
+ print("{}:{}: warning: section without status \t{}".format(
+ x["file"], x["linenr"], x["line"]))
+ if not has_F:
+ print("{}:{}: warning: section without file pattern\t{}".format(
+ x["file"], x["linenr"], x["line"]))
+
+ m = _re_type_value.match(x["line"])
+ if not m:
+ continue
+
+ typ = m.group(1)
+ value = m.group(2)
+
+ # Filename pattern matching
+ if typ in ("F", "X") and (self_test == "" or "patterns" in self_test):
+ val = value
+ val = val.replace('.', '\\.')
+ val = val.replace('**', '\x00')
+ val = val.replace('*', '.*')
+ val = val.replace('?', '.')
+ val = val.replace('\x00', '(?:.*)')
+ if os.path.isdir(val):
+ if not val.endswith('/'):
+ val += '/'
+ try:
+ pat = re.compile(r'^' + val)
+ except re.error:
+ continue
+ if not any(pat.search(f) for f in lsfiles):
+ print("{}:{}: warning: no file matches\t{}".format(
+ x["file"], x["linenr"], x["line"]))
+
+ # Link reachability
+ elif typ in ("W", "Q", "B") and \
+ re.match(r'^https?:', value) and \
+ (self_test == "" or "links" in self_test):
+ if value in good_links:
+ continue
+ isbad = False
+ if value in bad_links:
+ isbad = True
+ else:
+ ret = subprocess.run(
+ ["wget", "--spider", "-q", "--no-check-certificate",
+ "--timeout", "10", "--tries", "1", value],
+ capture_output=True)
+ if ret.returncode == 0:
+ good_links.append(value)
+ else:
+ bad_links.append(value)
+ isbad = True
+ if isbad:
+ print("{}:{}: warning: possible bad link\t{}".format(
+ x["file"], x["linenr"], x["line"]))
+
+ # SCM reachability
+ elif typ == "T" and (self_test == "" or "scm" in self_test):
+ if value in good_links:
+ continue
+ isbad = False
+ if value in bad_links:
+ isbad = True
+ elif not re.match(r'^(?:git|quilt|hg)\s+\S', value):
+ print("{}:{}: warning: malformed entry\t{}".format(
+ x["file"], x["linenr"], x["line"]))
+ else:
+ m2 = re.match(r'^git\s+(\S+)(\s+([^\(]+\S+))?', value)
+ if m2:
+ url = m2.group(1)
+ branch = m2.group(3) if m2.group(3) else ""
+ ret = subprocess.run(
+ 'git ls-remote --exit-code -h "{}" {} > /dev/null 2>&1'.format(url, branch),
+ shell=True, capture_output=True)
+ if ret.returncode == 0:
+ good_links.append(value)
+ else:
+ bad_links.append(value)
+ isbad = True
+ else:
+ m2 = re.match(r'^(?:quilt|hg)\s+(https?:\S+)', value)
+ if m2:
+ url = m2.group(1)
+ ret = subprocess.run(
+ ["wget", "--spider", "-q", "--no-check-certificate",
+ "--timeout", "10", "--tries", "1", url],
+ capture_output=True)
+ if ret.returncode == 0:
+ good_links.append(value)
+ else:
+ bad_links.append(value)
+ isbad = True
+ if isbad:
+ print("{}:{}: warning: possible bad link\t{}".format(
+ x["file"], x["linenr"], x["line"]))
+
+# ---- usage ----
+
+def usage():
+ print("""usage: {} [options] patchfile
+ {} [options] -f file|directory
+version: {}
+
+MAINTAINER field selection options:
+ --email => print email address(es) if any
+ --git => include recent git *-by: signers
+ --git-all-signature-types => include signers regardless of signature type
+ or use only {} signers (default: {})
+ --git-fallback => use git when no exact MAINTAINERS pattern (default: {})
+ --git-chief-penguins => include {}
+ --git-min-signatures => number of signatures required (default: {})
+ --git-max-maintainers => maximum maintainers to add (default: {})
+ --git-min-percent => minimum percentage of commits required (default: {})
+ --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: {})
+ --hg-since => hg history to use (default: {})
+ --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
+ --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 (role:subsystem, git-signer, list, etc...)
+ --rolestats => show roles and statistics (commits/total_commits, %)
+ --substatus => show subsystem status if not Maintained (default: match --roles when output is tty)"
+ --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
+ --web => print website(s) if any
+ --bug => print bug reporting info if any
+
+Output type options:
+ --separator [, ] => separator for multiple entries on 1 line
+ using --separator also sets --nomultiline if --separator is not [, ]
+ --multiline => print 1 entry per line
+
+Other options:
+ --pattern-depth => Number of pattern directory traversals (default: 0 (all))
+ --keywords => scan patch for keywords (default: {})
+ --keywords-in-file => scan file for keywords (default: {})
+ --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: {})
+ --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 --tree --nogit --git-fallback --m --r --n --l --multiline
+ --pattern-depth=0 --remove-duplicates --rolestats --keywords]
+
+Notes:
+ Using "-f directory" may give unexpected results:
+ Used with "--git", git signators for _all_ files in and below
+ directory are examined as git recurses directories.
+ Any specified X: (exclude) pattern matches are _not_ ignored.
+ Used with "--nogit", directory is used as a pattern match,
+ no individual file within the directory or subdirectory
+ is matched.
+ Used with "--git-blame", does not iterate all files in directory
+ Using "--git-blame" is slow and may add old committers and authors
+ that are no longer active maintainers to the output.
+ Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
+ other automated tools that expect only ["name"] <email address>
+ may not work because of additional output after <email address>.
+ Using "--rolestats" and "--git-blame" shows the #/total=% commits,
+ not the percentage of the entire file authored. # of commits is
+ not a good measure of amount of code authored. 1 major commit may
+ contain a thousand lines, 5 trivial commits may modify a single line.
+ If git is not installed, but mercurial (hg) is installed and an .hg
+ repository exists, the following options apply to mercurial:
+ --git,
+ --git-min-signatures, --git-max-maintainers, --git-min-percent, and
+ --git-blame
+ Use --hg-since not --git-since to control date selection
+ File ".get_maintainer.conf", if it exists in the linux kernel source root
+ directory, can change whatever get_maintainer defaults are desired.
+ 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>.
+""".format(P, P, V, signature_pattern, email_git_all_signature_types,
+ email_git_fallback, penguin_chiefs,
+ email_git_min_signatures, email_git_max_maintainers,
+ email_git_min_percent, email_git_since, email_hg_since,
+ keywords, keywords_in_file, email_use_mailmap))
+
+# ---- argument parsing helpers ----
+
+def add_negatable_flag(parser, name, dest, default, short=None):
+ """Add --foo/--nofoo/--no-foo flags (Perl Getopt::Long style negation)."""
+ names = ['--' + name]
+ if short:
+ names.insert(0, short)
+ parser.add_argument(*names, dest=dest, action='store_true', default=default)
+ parser.add_argument('--no' + name, '--no-' + name,
+ dest=dest, action='store_false')
+
+# ---- main ----
+
+def main():
+ global P, cur_path, lk_path
+ global email, email_usename, email_maintainer, email_reviewer, email_fixes
+ global email_list, email_moderated_list, email_subscriber_list
+ global email_git_penguin_chiefs, email_git, email_git_all_signature_types
+ global email_git_blame, email_git_blame_signatures, email_git_fallback
+ global email_git_min_signatures, email_git_max_maintainers, email_git_min_percent
+ global email_git_since, email_hg_since, interactive
+ global email_remove_duplicates, email_use_mailmap
+ global output_multiline, output_separator, output_roles, output_rolestats
+ global output_substatus, output_section_maxlen
+ global scm, tree, web, bug, subsystem_opt, status_opt
+ global letters, keywords, keywords_in_file, sections
+ global email_file_emails, from_filename, pattern_depth
+ global self_test, version, find_maintainer_files, maintainer_path
+ global files, fixes, range_list, keyword_tvi, file_emails
+ global ignore_emails
+
+ P = sys.argv[0]
+ cur_path = os.getcwd() + '/'
+
+ # Load config file
+ conf = which_conf(".get_maintainer.conf")
+ conf_args = []
+ if conf and os.path.isfile(conf):
+ try:
+ with open(conf, 'r', encoding='utf-8') as f:
+ for line in f:
+ line = line.rstrip('\n').strip()
+ line = re.sub(r'\s+', ' ', line)
+ if not line or line.startswith('#'):
+ continue
+ for word in line.split():
+ if word.startswith('#'):
+ break
+ conf_args.append(word)
+ except IOError:
+ print("{}: Can't find a readable .get_maintainer.conf file".format(P), file=sys.stderr)
+
+ # Load ignore file
+ ignore_file = which_conf(".get_maintainer.ignore")
+ if ignore_file and os.path.isfile(ignore_file):
+ try:
+ with open(ignore_file, 'r', encoding='utf-8') as f:
+ for line in f:
+ line = line.strip()
+ line = re.sub(r'#.*$', '', line).strip()
+ if not line:
+ continue
+ if rfc822_valid(line):
+ ignore_emails.append(line)
+ except IOError:
+ pass
+
+ # Check for --self-test exclusivity
+ all_args = conf_args + sys.argv[1:]
+ if len(all_args) > 1:
+ for arg in all_args:
+ if re.match(r'^-{1,2}self-test', arg):
+ print("{}: using --self-test does not allow any other option or argument".format(P),
+ file=sys.stderr)
+ sys.exit(1)
+
+ # Build argument parser
+ parser = argparse.ArgumentParser(add_help=False)
+
+ add_negatable_flag(parser, 'email', 'email', True)
+ add_negatable_flag(parser, 'git', 'email_git', False)
+ add_negatable_flag(parser, 'git-all-signature-types', 'email_git_all_signature_types', False)
+ add_negatable_flag(parser, 'git-blame', 'email_git_blame', False)
+ add_negatable_flag(parser, 'git-blame-signatures', 'email_git_blame_signatures', True)
+ add_negatable_flag(parser, 'git-fallback', 'email_git_fallback', True)
+ add_negatable_flag(parser, 'git-chief-penguins', 'email_git_penguin_chiefs', False)
+ parser.add_argument('--git-min-signatures', type=int, default=1)
+ parser.add_argument('--git-max-maintainers', type=int, default=5)
+ parser.add_argument('--git-min-percent', type=int, default=5)
+ parser.add_argument('--git-since', default="1-year-ago")
+ parser.add_argument('--hg-since', default="-365")
+ add_negatable_flag(parser, 'interactive', 'interactive', False, short='-i')
+ add_negatable_flag(parser, 'remove-duplicates', 'email_remove_duplicates', True)
+ add_negatable_flag(parser, 'mailmap', 'email_use_mailmap', True)
+ add_negatable_flag(parser, 'm', 'email_maintainer', True)
+ add_negatable_flag(parser, 'r', 'email_reviewer', True)
+ add_negatable_flag(parser, 'n', 'email_usename', True)
+ add_negatable_flag(parser, 'l', 'email_list', True)
+ add_negatable_flag(parser, 'fixes', 'email_fixes', True)
+ add_negatable_flag(parser, 'moderated', 'email_moderated_list', True)
+ add_negatable_flag(parser, 's', 'email_subscriber_list', False)
+ add_negatable_flag(parser, 'multiline', 'output_multiline', True)
+ add_negatable_flag(parser, 'roles', 'output_roles', False)
+ add_negatable_flag(parser, 'rolestats', 'output_rolestats', True)
+ parser.add_argument('--separator', default=", ")
+ add_negatable_flag(parser, 'subsystem', 'subsystem', False)
+ add_negatable_flag(parser, 'status', 'status', False)
+ add_negatable_flag(parser, 'substatus', 'output_substatus_flag', None)
+ add_negatable_flag(parser, 'scm', 'scm', False)
+ add_negatable_flag(parser, 'tree', 'tree', True)
+ add_negatable_flag(parser, 'web', 'web', False)
+ add_negatable_flag(parser, 'bug', 'bug', False)
+ parser.add_argument('--letters', default="")
+ parser.add_argument('--pattern-depth', type=int, default=0)
+ add_negatable_flag(parser, 'keywords', 'keywords', True, short='-k')
+ add_negatable_flag(parser, 'keywords-in-file', 'keywords_in_file', False)
+ add_negatable_flag(parser, 'sections', 'sections', False)
+ add_negatable_flag(parser, 'file-emails', 'email_file_emails', False)
+ parser.add_argument('-f', '--file', dest='from_filename', action='store_true', default=False)
+ parser.add_argument('--find-maintainer-files', action='store_true', default=False)
+ parser.add_argument('--mpath', '--maintainer-path', dest='maintainer_path', default=None)
+ parser.add_argument('--self-test', dest='self_test', nargs='?', const='', default=None)
+ parser.add_argument('-v', '--version', dest='show_version', action='store_true', default=False)
+ parser.add_argument('-h', '--help', '--usage', dest='show_help', action='store_true', default=False)
+ parser.add_argument('files', nargs='*')
+
+ # Handle negatable --fe alias
+ parser.add_argument('--fe', dest='email_file_emails', action='store_true')
+ parser.add_argument('--kf', dest='keywords_in_file', action='store_true')
+
+ try:
+ args = parser.parse_args(conf_args + sys.argv[1:])
+ except SystemExit:
+ sys.exit(1)
+
+ if args.show_help:
+ usage()
+ sys.exit(0)
+
+ if args.show_version:
+ print("{} {}".format(P, V))
+ sys.exit(0)
+
+ # Apply parsed args to globals
+ email = 1 if args.email else 0
+ email_git = 1 if args.email_git else 0
+ email_git_all_signature_types = 1 if args.email_git_all_signature_types else 0
+ email_git_blame = 1 if args.email_git_blame else 0
+ email_git_blame_signatures = 1 if args.email_git_blame_signatures else 0
+ email_git_fallback = 1 if args.email_git_fallback else 0
+ email_git_penguin_chiefs = 1 if args.email_git_penguin_chiefs else 0
+ email_git_min_signatures = args.git_min_signatures
+ email_git_max_maintainers = args.git_max_maintainers
+ email_git_min_percent = args.git_min_percent
+ email_git_since = args.git_since
+ email_hg_since = args.hg_since
+ interactive = 1 if args.interactive else 0
+ email_remove_duplicates = 1 if args.email_remove_duplicates else 0
+ email_use_mailmap = 1 if args.email_use_mailmap else 0
+ email_maintainer = 1 if args.email_maintainer else 0
+ email_reviewer = 1 if args.email_reviewer else 0
+ email_usename = 1 if args.email_usename else 0
+ email_list = 1 if args.email_list else 0
+ email_fixes = 1 if args.email_fixes else 0
+ email_moderated_list = 1 if args.email_moderated_list else 0
+ email_subscriber_list = 1 if args.email_subscriber_list else 0
+ output_multiline = 1 if args.output_multiline else 0
+ output_roles = 1 if args.output_roles else 0
+ output_rolestats = 1 if args.output_rolestats else 0
+ output_separator = args.separator
+ subsystem_opt = 1 if args.subsystem else 0
+ status_opt = 1 if args.status else 0
+ scm = 1 if args.scm else 0
+ tree = 1 if args.tree else 0
+ web = 1 if args.web else 0
+ bug = 1 if args.bug else 0
+ letters = args.letters
+ pattern_depth = args.pattern_depth
+ keywords = 1 if args.keywords else 0
+ keywords_in_file = 1 if args.keywords_in_file else 0
+ sections = 1 if args.sections else 0
+ email_file_emails = 1 if args.email_file_emails else 0
+ from_filename = 1 if args.from_filename else 0
+ find_maintainer_files = 1 if args.find_maintainer_files else 0
+ maintainer_path = args.maintainer_path
+ self_test = args.self_test
+
+ # Handle substatus special logic
+ if args.output_substatus_flag is None:
+ output_substatus = None
+ else:
+ output_substatus = 1 if args.output_substatus_flag else 0
+
+ if self_test is not None:
+ read_all_maintainer_files()
+ do_self_test()
+ sys.exit(0)
+
+ if output_separator != ", ":
+ output_multiline = 0
+ if interactive:
+ output_rolestats = 1
+ if output_rolestats:
+ output_roles = 1
+
+ if output_substatus is None:
+ output_substatus = 1 if (email and output_roles and sys.stdout.isatty()) else 0
+
+ if sections or letters != "":
+ sections = 1
+ email = 0
+ email_list = 0
+ scm = 0
+ status_opt = 0
+ subsystem_opt = 0
+ web = 0
+ bug = 0
+ keywords = 0
+ keywords_in_file = 0
+ interactive = 0
+ else:
+ selections = email + scm + status_opt + subsystem_opt + web + bug
+ if selections == 0:
+ print("{}: Missing required option: email, scm, status, subsystem, web or bug".format(P),
+ file=sys.stderr)
+ sys.exit(1)
+
+ if email and \
+ (email_maintainer + email_reviewer +
+ email_list + email_subscriber_list +
+ email_git + email_git_penguin_chiefs + email_git_blame) == 0:
+ print("{}: Please select at least 1 email option".format(P), file=sys.stderr)
+ sys.exit(1)
+
+ if tree and not top_of_kernel_tree(lk_path):
+ print("{}: The current directory does not appear to be "
+ "a linux kernel source tree.".format(P), file=sys.stderr)
+ sys.exit(1)
+
+ # Read MAINTAINERS
+ read_all_maintainer_files()
+
+ # Read mailmap
+ read_mailmap()
+
+ # Process input files
+ input_files = args.files if args.files else []
+ if not input_files and not sys.stdin.isatty():
+ input_files = ["&STDIN"]
+ elif not input_files:
+ print("{}: missing patchfile or -f file - use --help if necessary".format(P),
+ file=sys.stderr)
+ sys.exit(1)
+
+ for filepath in input_files:
+ if filepath != "&STDIN":
+ filepath = os.path.normpath(filepath)
+ if os.path.isdir(filepath):
+ if not filepath.endswith('/'):
+ filepath += '/'
+ elif not os.path.isfile(filepath):
+ print("{}: file '{}' not found".format(P, filepath), file=sys.stderr)
+ sys.exit(1)
+
+ file_in_vcs = None
+ if from_filename and vcs_exists():
+ file_in_vcs = vcs_file_exists(filepath)
+ if not file_in_vcs:
+ print("{}: file '{}' not found in version control".format(P, filepath), file=sys.stderr)
+
+ if from_filename or (filepath != "&STDIN" and
+ (file_in_vcs if file_in_vcs is not None else vcs_file_exists(filepath))):
+ # strip absolute or lk_path prefix
+ if filepath.startswith(cur_path):
+ filepath = filepath[len(cur_path):]
+ if filepath.startswith(lk_path) and lk_path != "./":
+ filepath = filepath[len(lk_path):]
+ files.append(filepath)
+ if filepath != "MAINTAINERS" and os.path.isfile(filepath) and keywords and keywords_in_file:
+ try:
+ with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
+ text = f.read()
+ for line_idx in keyword_hash:
+ try:
+ if re.search(keyword_hash[line_idx], text, re.VERBOSE):
+ keyword_tvi.append(line_idx)
+ except re.error:
+ pass
+ except IOError:
+ pass
+ else:
+ file_cnt = len(files)
+ lastfile = None
+
+ if filepath == "&STDIN":
+ patch = sys.stdin
+ else:
+ try:
+ patch = open(filepath, 'r', encoding='utf-8', errors='replace')
+ except IOError:
+ print("{}: Can't open {}: ".format(P, filepath), file=sys.stderr)
+ sys.exit(1)
+
+ patch_prefix = "" # Parsing the intro
+
+ for patch_line in patch:
+ m = re.match(r'^ mode change [0-7]+ => [0-7]+ (\S+)\s*$', patch_line)
+ if m:
+ files.append(m.group(1))
+ continue
+ m = re.match(r'^rename (?:from|to) (\S+)\s*$', patch_line)
+ if m:
+ files.append(m.group(1))
+ continue
+ m = re.match(r'^diff --git a/(\S+) b/(\S+)\s*$', patch_line)
+ if m:
+ files.append(m.group(1))
+ files.append(m.group(2))
+ continue
+ m = re.match(r'^Fixes:\s+([0-9a-fA-F]{6,40})', patch_line)
+ if m:
+ if email_fixes:
+ fixes.append(m.group(1))
+ continue
+ m = re.match(r'^\+\+\+\s+(\S+)', patch_line) or \
+ re.match(r'^---\s+(\S+)', patch_line)
+ if m:
+ filename = m.group(1)
+ filename = re.sub(r'^[^/]*/', '', filename)
+ filename = filename.rstrip('\n')
+ lastfile = filename
+ files.append(filename)
+ patch_prefix = "^[+-].*"
+ continue
+ m = re.match(r'^@@ -(\d+),(\d+)', patch_line)
+ if m:
+ if email_git_blame and lastfile:
+ range_list.append("{}:{}:{}".format(lastfile, m.group(1), m.group(2)))
+ continue
+ if keywords:
+ for line_idx in keyword_hash:
+ try:
+ if re.search(patch_prefix + keyword_hash[line_idx],
+ patch_line, re.VERBOSE):
+ keyword_tvi.append(line_idx)
+ except re.error:
+ pass
+
+ if filepath != "&STDIN":
+ patch.close()
+
+ if file_cnt == len(files):
+ print("{}: file '{}' doesn't appear to be a patch. "
+ "Add -f to options?".format(P, filepath), file=sys.stderr)
+
+ files[:] = sort_and_uniq(files)
+
+ file_emails[:] = uniq(file_emails)
+ fixes[:] = uniq(fixes)
+
+ maintainers = get_maintainers()
+ if maintainers:
+ maintainers = merge_email(maintainers)
+ output(maintainers)
+
+ if scm:
+ scm_out = uniq(scm_list)
+ output(scm_out)
+
+ if output_substatus:
+ ss = uniq(substatus_list)
+ output(ss)
+
+ if status_opt:
+ st = uniq(status_list)
+ output(st)
+
+ if subsystem_opt:
+ ss = uniq(subsystem_list)
+ output(ss)
+
+ if web:
+ w = uniq(web_list)
+ output(w)
+
+ if bug:
+ b = uniq(bug_list)
+ output(b)
+
+if __name__ == "__main__":
+ main()
--
2.52.0
© 2016 - 2026 Red Hat, Inc.