From nobody Wed Apr 1 22:00:06 2026 Received: from mail-wm1-f49.google.com (mail-wm1-f49.google.com [209.85.128.49]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 37A643F165D for ; Wed, 1 Apr 2026 14:47:56 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.49 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1775054883; cv=none; b=YyzP7iYJuTuhO1TpGxtov7WI44D1Th3cAqT04XU05dQ5DqAQYjfJFE1wOFOz2SFMDuKN6H30l4sfxKrxsZlrGeio7kP2fs2c1hqYHMF9xxC71QDRMbss2pKT/soZ5P2vjoPeSYkusFq55ekGbSj3UA32HxawQUnKR+5iGPwWlQs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1775054883; c=relaxed/simple; bh=tdiwdLvqudQEDZnVeHEOTSgoik2UvPlpugmg9Jz8WII=; h=From:To:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=H2KrNLnvYD1tVh5Sa4sHJpBGyCFT3xU5/M9z7csDN53zwzMc3Z5remvWBFgpZ9o0bAVjnX4vBd2SRo7TS1UtWQP3kVO6/TcMX4pMKnZIK2p9x6R+NJKzt3tnla31VXlrNyWmBBbCU+GTz6mFic6NnPbaInX5r9mQnoINXh2Tj/I= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=m4r3a2pZ; arc=none smtp.client-ip=209.85.128.49 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="m4r3a2pZ" Received: by mail-wm1-f49.google.com with SMTP id 5b1f17b1804b1-486fc4725f0so13483715e9.1 for ; Wed, 01 Apr 2026 07:47:56 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1775054875; x=1775659675; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to; bh=t2dIUngSfbQfLIy8bJws/+ZY8TXN1Mkp/WGt72plMlc=; b=m4r3a2pZXynhzKYpWpT692g/qVaMC1ZcGz909cTcMgehn+Cq/DoXrgn/CbDK/Ug0zz qWu1iINZ9SZbblk57D2R5jM3fZ1KBYfn0KnMsjm+3ad3Y/J70XPfcRNpCDiGTwKUmn+G xQ8dZtV2mK5CULhjhZF5CZytOzeUh4snlqEU8K6NgW2IPHNrfaxrqCrL2gNWbcZxWLa9 bSjKZD6b677GDr7Bm6ALnOaRH4ONdxP/EXvKCQjcM/RWo6A1tS1Psq4Nso468zKOiNvt DxvwsQ8KN1Kili3dTHe13P7rqPqeBtvPW6TsdFK1cSulyJzKSIJPzkjf8lZ9/HhBjO3W MzZQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1775054875; x=1775659675; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to; bh=t2dIUngSfbQfLIy8bJws/+ZY8TXN1Mkp/WGt72plMlc=; b=ajcxTmcMEkmp8k9E9WjyKRx03T8LNTYyPmi7hIbj4mcztDjQGOYeIepIjtsR4117Ue KIKkzXZhpcqzNakXTTDBXZ8QrJPzBrwvTMHTEmEiFYXWH5HCrkka/P56yntdQhExa7Pg NEyA/u935JvTqfD/c16+LYXD//vfgRJwfOx4X5f9ltgXV4pFOaGVPU+G871LIrksDEx4 CjNoaHwcLcOvOAV3XbzG3bISu/ZH1iNHdhvgXxWxfA+bhl8x0LDBLL4nluljyO+sCXQz V9p+9jE7MAH/nOKdnV9KLYDR8syl+QBvD5eG2A2oqYKoqtBimoTWvI/bqMXWSGDlB4kp 7UJw== X-Gm-Message-State: AOJu0YwbEWmbSlM1ScHjjMmAyJMtWBmhIIaEPf5pHpKKSEJtBl/YpW+5 s84ROf6aXAA7Mav1+THY1K80tNcqfLqZJKPKDHKtOxP6umSTVmFp9V89aLFv0XRNmHY= X-Gm-Gg: ATEYQzyadEVhMaVmWxKEfNEroQDGgz48MkE45KJnwHgLlc0VquZkw1wp2qMfLfC/QMI 1nPJ2D7xo5npGcb0uNNznbkrM85cZ/yonXV6xZFc/UA/DE28HeT+859CeBxPZP3WV+trW9tZnc6 aPyjokK5knGOFR/JgK5WAjxoOHC6uKB4+wRXgbbwEyhIEVE0wxSfZOqd1ALNLE0o+oESSGW7uKy /PM/Bap6QgloCSsHc1wGcQWYJ6Cw6C0SnmbQuuPofy6K99iytl8XsG9pyHtXMTvu3necQM4jw/+ ZIQjzFn88i866OXgVCKqbwBKCBYJ6biGVWlyiA3ZOxNN/I/xcgLBIti0TltaWfg0O/V/zamBooF zWAyrhMutll1i/R/JIY1RO4KgRVX14u1AD5mwVDHvzsaGNuUdUGATKzA1LO5NKNjdMvBWktGWQx tRhyMbmH+5j45gswYq+Dd9O1i+A+jBlCo2WkFF/6DtYYfsQOpKVVzSydqLWAxyUw== X-Received: by 2002:a05:600c:5303:b0:477:9890:9ab8 with SMTP id 5b1f17b1804b1-48883566129mr64146415e9.3.1775054874846; Wed, 01 Apr 2026 07:47:54 -0700 (PDT) Received: from fedorarm (net-2-37-83-250.cust.vodafonedsl.it. [2.37.83.250]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-4887c8b6230sm45753505e9.24.2026.04.01.07.47.53 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 01 Apr 2026 07:47:54 -0700 (PDT) From: Guido De Rossi To: LKML Subject: [PATCH 1/2] get_maintainer: rewrite in Python Date: Wed, 1 Apr 2026 16:47:22 +0200 Message-ID: <20260401144723.44406-2-guido.derossi91@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260401144723.44406-1-guido.derossi91@gmail.com> References: <20260401144723.44406-1-guido.derossi91@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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 --- 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] +# get_maintainer.py [OPTIONS] -f +# +# Python rewrite of get_maintainer.pl + +import argparse +import os +import re +import shlex +import shutil +import subprocess +import sys + +V =3D '0.26' + +# ---- constants ---- + +penguin_chief =3D [ + "Linus Torvalds:torvalds@linux-foundation.org", +] + +penguin_chief_names =3D [] +for chief in penguin_chief: + m =3D re.match(r'^(.*?):(.*)', chief) + if m: + penguin_chief_names.append(m.group(1)) + +penguin_chiefs =3D "(" + "|".join(re.escape(n) for n in penguin_chief_name= s) + ")" + +signature_tags =3D [ + "Signed-off-by:", + "Reviewed-by:", + "Acked-by:", +] + +signature_pattern =3D "(" + "|".join(re.escape(t) for t in signature_tags)= + ")" + +rfc822_lwsp =3D r"(?:(?:\r\n)?[ \t])" +rfc822_char =3D r'[\000-\377]' + +# Pre-compiled patterns for hot paths +_re_type_value =3D re.compile(r'^([A-Z]):\s*(.*)') +_re_blank_line =3D re.compile(r'^\s*$') +_re_comment_line =3D re.compile(r'^\s*#') + +# ---- VCS command templates ---- + +VCS_cmds_git =3D { + "available": "git", + "find_signers_cmd": + 'git log --no-color --follow --since=3D{email_git_since} ' + '--numstat --no-merges ' + '--format=3D"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=3D"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=3D"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 =3D { + "available": "hg", + "find_signers_cmd": + "hg log --date=3D{email_hg_since} " + "--template=3D'HgCommit: {{node}}\\n" + "HgAuthor: {{author}}\\n" + "HgSubject: {{desc}}\\n'" + " -- {file}", + "find_commit_signers_cmd": + "hg log " + "--template=3D'HgSubject: {{desc}}\\n'" + " -r {commit}", + "find_commit_author_cmd": + "hg log " + "--template=3D'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 =3D "" +cur_path =3D "" +lk_path =3D "./" + +# Options (with defaults matching the Perl script) +email =3D 1 +email_usename =3D 1 +email_maintainer =3D 1 +email_reviewer =3D 1 +email_fixes =3D 1 +email_list =3D 1 +email_moderated_list =3D 1 +email_subscriber_list =3D 0 +email_git_penguin_chiefs =3D 0 +email_git =3D 0 +email_git_all_signature_types =3D 0 +email_git_blame =3D 0 +email_git_blame_signatures =3D 1 +email_git_fallback =3D 1 +email_git_min_signatures =3D 1 +email_git_max_maintainers =3D 5 +email_git_min_percent =3D 5 +email_git_since =3D "1-year-ago" +email_hg_since =3D "-365" +interactive =3D 0 +email_remove_duplicates =3D 1 +email_use_mailmap =3D 1 +output_multiline =3D 1 +output_separator =3D ", " +output_roles =3D 0 +output_rolestats =3D 1 +output_substatus =3D None +output_section_maxlen =3D 50 +scm =3D 0 +tree =3D 1 +web =3D 0 +bug =3D 0 +subsystem_opt =3D 0 +status_opt =3D 0 +letters =3D "" +keywords =3D 1 +keywords_in_file =3D 0 +sections =3D 0 +email_file_emails =3D 0 +from_filename =3D 0 +pattern_depth =3D 0 +self_test =3D None +version =3D 0 +find_maintainer_files =3D 0 +maintainer_path =3D None +vcs_used =3D 0 + +# Mutable state +files =3D [] +fixes =3D [] +range_list =3D [] +keyword_tvi =3D [] +file_emails =3D [] + +commit_author_hash =3D {} +commit_signer_hash =3D {} + +typevalue =3D [] +# Pre-parsed (type_char, value_string) tuples parallel to typevalue. +# For non-type lines, type_char is None. +typevalue_parsed =3D [] +# Lazily compiled regexes for F:/X: patterns, keyed by typevalue index. +# Sentinel value _UNCOMPILED means the pattern has not been compiled yet. +_UNCOMPILED =3D object() +typevalue_compiled =3D {} +# Literal prefix for each F:/X: pattern -- fast string check before regex. +typevalue_prefix =3D {} +keyword_hash =3D {} +mfiles =3D [] +self_test_info =3D [] + +mailmap_data =3D None + +email_hash_name =3D {} +email_hash_address =3D {} +email_to =3D [] +hash_list_to =3D {} +list_to =3D [] +scm_list =3D [] +web_list =3D [] +bug_list =3D [] +subsystem_list =3D [] +status_list =3D [] +substatus_list =3D [] +deduplicate_name_hash =3D {} +deduplicate_address_hash =3D {} + +ignore_emails =3D [] + +VCS_cmds =3D {} + +printed_novcs =3D False + +# ---- utility functions ---- + +def which(bin_name): + path =3D shutil.which(bin_name) + return path if path else "" + +def which_conf(conf): + for path_dir in [".", os.environ.get("HOME", ""), ".scripts"]: + p =3D 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 +=3D "/" + checks_f =3D ["COPYING", "CREDITS", "Kbuild", "Makefile", "README"] + checks_e =3D ["MAINTAINERS"] + checks_d =3D ["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 =3D set() + result =3D [] + for x in lst: + if x not in seen: + seen.add(x) + result.append(x) + return result + +def sort_and_uniq(lst): + seen =3D set() + result =3D [] + 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 =3D name.replace('\\', '\\\\') + name =3D name.replace('"', '\\"') + name =3D '"' + name + '"' + return name + +def parse_email(formatted_email): + name =3D "" + address =3D "" + m =3D re.match(r'^([^<]+)<(.+@.*)>.*$', formatted_email) + if m: + name =3D m.group(1) + address =3D m.group(2) + else: + m =3D re.match(r'^\s*<(.+@\S*)>.*$', formatted_email) + if m: + address =3D m.group(1) + else: + m =3D re.match(r'^(.+@\S*).*$', formatted_email) + if m: + address =3D m.group(1) + name =3D name.strip() + name =3D name.strip('"') + name =3D escape_name(name) + address =3D address.strip() + return (name, address) + +def format_email(name, address, usename): + name =3D name.strip().strip('"') + name =3D escape_name(name) + address =3D address.strip() + if usename: + if name =3D=3D "": + return address + else: + return "{} <{}>".format(name, address) + else: + return address + +# ---- RFC822 validation ---- + +_rfc822re =3D None + +def make_rfc822re(): + specials =3D r'()<>@,;:\\".\\[\\]' + controls =3D r'\000-\037\177' + + dtext =3D r"[^\[\]\r\\]" + domain_literal =3D r"\[(?:" + dtext + r"|\\\\.)* \]" + rfc822_lwsp + "= *" + + quoted_string =3D r'"(?:[^"\r\\]|\\\\.|' + rfc822_lwsp + r')*"' + rfc8= 22_lwsp + "*" + + atom =3D r"[^" + specials + " " + controls + r"]+(?:" + rfc822_lwsp + = r"+|\Z|(?=3D[\\[\"" + specials + r"]))" + word =3D r"(?:" + atom + r"|" + quoted_string + r")" + localpart =3D word + r"(?:\." + rfc822_lwsp + r"*" + word + r")*" + + sub_domain =3D r"(?:" + atom + r"|" + domain_literal + r")" + domain =3D sub_domain + r"(?:\." + rfc822_lwsp + r"*" + sub_domain + r= ")*" + + addr_spec =3D localpart + r"@" + rfc822_lwsp + r"*" + domain + + phrase =3D word + r"*" + route =3D r"(?:@" + domain + r"(?:,@" + rfc822_lwsp + r"*" + domain + = r")*:" + rfc822_lwsp + r"*)" + route_addr =3D r"\<" + rfc822_lwsp + r"*" + route + r"?" + addr_spec += r"\>" + rfc822_lwsp + r"*" + mailbox =3D r"(?:" + addr_spec + r"|" + phrase + route_addr + r")" + + group =3D phrase + r":" + rfc822_lwsp + r"*(?:" + mailbox + r"(?:,\s*"= + mailbox + r")*)?;\s*" + address =3D r"(?:" + mailbox + r"|" + group + r")" + + return rfc822_lwsp + r"*" + address + +def rfc822_strip_comments(s): + while True: + new_s =3D re.sub( + r'^((?:[^"\\]|\\.)*(?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)\((?:= [^()\\]|\\.)*\)', + r'\1 ', s, count=3D1, flags=3Dre.DOTALL) + if new_s =3D=3D s: + break + s =3D new_s + return s + +def rfc822_valid(s): + global _rfc822re + s =3D rfc822_strip_comments(s) + if _rfc822re is None: + _rfc822re =3D make_rfc822re() + if re.match(r'^' + _rfc822re + r'$', s) and re.match(r'^' + rfc822_cha= r + r'*$', s): + return True + return False + +def rfc822_validlist(s): + global _rfc822re + s =3D rfc822_strip_comments(s) + if _rfc822re is None: + _rfc822re =3D make_rfc822re() + if re.match(r'^(?:' + _rfc822re + r')?(?:,(?:' + _rfc822re + r')?)*$',= s) and \ + re.match(r'^' + rfc822_char + r'*$', s): + result =3D [] + 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 =3D {"names": {}, "addresses": {}} + + if not email_use_mailmap or not os.path.isfile(lk_path + ".mailmap"): + return + + try: + with open(lk_path + ".mailmap", "r", encoding=3D"utf-8") as f: + for line in f: + line =3D re.sub(r'#.*$', '', line) # strip comments + line =3D line.strip() + if not line: + continue + + # name1 name2 + m =3D re.match(r'^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$', line) + if m: + real_name =3D m.group(1).rstrip() + real_address =3D m.group(2) + wrong_name =3D m.group(3).rstrip() + wrong_address =3D m.group(4) + real_name, real_address =3D parse_email("{} <{}>".form= at(real_name, real_address)) + wrong_name, wrong_address =3D parse_email("{} <{}>".fo= rmat(wrong_name, wrong_address)) + wrong_email =3D format_email(wrong_name, wrong_address= , 1) + mailmap_data["names"][wrong_email] =3D real_name + mailmap_data["addresses"][wrong_email] =3D real_address + continue + + # name1 + m =3D re.match(r'^(.+)<([^>]+)>\s*<([^>]+)>$', line) + if m: + real_name =3D m.group(1).rstrip() + real_address =3D m.group(2) + wrong_address =3D m.group(3) + real_name, real_address =3D parse_email("{} <{}>".form= at(real_name, real_address)) + mailmap_data["names"][wrong_address] =3D real_name + mailmap_data["addresses"][wrong_address] =3D real_addr= ess + continue + + # + m =3D re.match(r'^<([^>]+)>\s*<([^>]+)>$', line) + if m: + real_address =3D m.group(1) + wrong_address =3D m.group(2) + mailmap_data["addresses"][wrong_address] =3D real_addr= ess + continue + + # name1 + m =3D re.match(r'^([^<]+)<([^>]+)>$', line) + if m: + real_name =3D m.group(1).rstrip() + address =3D m.group(2) + real_name, address =3D parse_email("{} <{}>".format(re= al_name, address)) + mailmap_data["names"][address] =3D real_name + continue + except IOError: + print("{}: Can't open .mailmap".format(P), file=3Dsys.stderr) + +def mailmap_email(line): + name, address =3D parse_email(line) + email_str =3D format_email(name, address, 1) + real_name =3D name + real_address =3D address + + if email_str in mailmap_data["names"] or email_str in mailmap_data["ad= dresses"]: + if email_str in mailmap_data["names"]: + real_name =3D mailmap_data["names"][email_str] + if email_str in mailmap_data["addresses"]: + real_address =3D mailmap_data["addresses"][email_str] + else: + if address in mailmap_data["names"]: + real_name =3D mailmap_data["names"][address] + if address in mailmap_data["addresses"]: + real_address =3D mailmap_data["addresses"][address] + + return format_email(real_name, real_address, 1) + +def mailmap(addresses): + mapped =3D [] + 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 =3D {} + for i in range(len(emails)): + name, address =3D parse_email(emails[i]) + if name in address_map: + address =3D address_map[name] + emails[i] =3D format_email(name, address, 1) + else: + address_map[name] =3D 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=3D"utf-8") as f: + i =3D 1 + for raw_line in f: + line =3D raw_line.rstrip('\n') + + m =3D _re_type_value.match(line) + if m: + typ =3D m.group(1) + value =3D m.group(2) + + if typ in ("F", "X"): + value =3D value.replace('.', '\\.') + value =3D value.replace('**', '\x00') + value =3D value.replace('*', '.*') + value =3D value.replace('?', '.') + value =3D value.replace('\x00', '(?:.*)') + if os.path.isdir(value): + if not value.endswith('/'): + value +=3D '/' + # Mark for lazy compilation and extract literal pr= efix + idx =3D len(typevalue) + typevalue_compiled[idx] =3D _UNCOMPILED + # Extract the literal prefix before any regex meta= char + prefix =3D [] + for ch in value: + if ch in r'.*+?()[]{}|\\^$': + break + prefix.append(ch) + typevalue_prefix[idx] =3D ''.join(prefix) + elif typ =3D=3D "K": + keyword_hash[len(typevalue)] =3D value + + typevalue.append("{}:{}".format(typ, value)) + typevalue_parsed.append((typ, value)) + elif not _re_blank_line.match(line) and not _re_comment_li= ne.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 +=3D 1 + except IOError: + print("{}: Can't open MAINTAINERS file '{}'" .format(P, filepath),= file=3Dsys.stderr) + sys.exit(1) + +def read_all_maintainer_files(): + global mfiles, lk_path, maintainer_path, find_maintainer_files + path =3D lk_path + "MAINTAINERS" + if maintainer_path is not None: + path =3D maintainer_path + path =3D os.path.expanduser(path) + + if os.path.isdir(path): + if not path.endswith('/'): + path +=3D '/' + if find_maintainer_files: + for root, dirs, fnames in os.walk(path): + # skip .git + dirs[:] =3D [d for d in dirs if d !=3D '.git'] + for fname in fnames: + if fname =3D=3D "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= =3Dsys.stderr) + sys.exit(1) + + if len(mfiles) =3D=3D 0: + print("{}: No MAINTAINER files found in '{}'".format(P, path), fil= e=3Dsys.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.endswit= h('.yaml')): + try: + with open(filepath, 'r', encoding=3D'utf-8', errors=3D'replace= ') as f: + text =3D f.read() + poss_addr =3D 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 =3D 0 + while index < len(typevalue_parsed): + if typevalue_parsed[index][0] is not None: + break + index +=3D 1 + return index + +def find_starting_index(index): + while index > 0: + if typevalue_parsed[index][0] is None: + break + index -=3D 1 + return index + +def find_ending_index(index): + while index < len(typevalue_parsed): + if typevalue_parsed[index][0] is None: + break + index +=3D 1 + return index + +def get_subsystem_name(index): + start =3D find_starting_index(index) + sub =3D typevalue[start] + if output_section_maxlen and len(sub) > output_section_maxlen: + sub =3D 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 =3D typevalue_compiled.get(idx) + if compiled is _UNCOMPILED: + value =3D typevalue_parsed[idx][1] + try: + compiled =3D re.compile(r'^' + value) + except re.error: + compiled =3D None + typevalue_compiled[idx] =3D compiled + return compiled + +def file_match_pattern(filepath, pattern, compiled=3DNone): + if compiled is None: + try: + compiled =3D re.compile(r'^' + pattern) + except re.error: + return False + if pattern.endswith("/"): + if compiled.search(filepath): + return True + else: + if compiled.search(filepath): + s1 =3D filepath.count('/') + s2 =3D pattern.count('/') + if s1 =3D=3D s2 or '(?:' in pattern: + return True + return False + +# ---- category/role functions ---- + +def get_maintainer_role(index): + start =3D find_starting_index(index) + end =3D find_ending_index(index) + role =3D "maintainer" + sub =3D get_subsystem_name(index) + sts =3D "unknown" + + for i in range(start + 1, end): + typ, value =3D typevalue_parsed[i] + if typ =3D=3D "S": + sts =3D value + + sts =3D sts.lower() + if sts =3D=3D "buried alive in reporters": + role =3D "chief penguin" + + return role + ":" + sub + +def get_list_role(index): + sub =3D get_subsystem_name(index) + if sub =3D=3D "THE REST": + sub =3D "" + 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 =3D find_starting_index(index) + end =3D find_ending_index(index) + + sub =3D typevalue[start] + subsystem_list.append(sub) + sts =3D "Unknown" + + for i in range(start + 1, end): + ptype, pvalue =3D typevalue_parsed[i] + if ptype is not None: + if ptype =3D=3D "L": + list_address =3D pvalue + list_additional =3D "" + list_role =3D get_list_role(i) + + if list_role: + list_role =3D ":" + list_role + + m2 =3D re.match(r'^(\S+)\s+(.*)$', list_address) + if m2: + list_address =3D m2.group(1) + list_additional =3D 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()] =3D 1 + list_to.append([list_address, + "subscriber list{}{}".format(l= ist_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()] =3D= 1 + list_to.append([list_address, + "moderated list{}{}".f= ormat(list_role, suffix)]) + else: + hash_list_to[list_address.lower()] =3D 1 + list_to.append([list_address, + "open list{}{}".format(lis= t_role, suffix)]) + + elif ptype =3D=3D "M": + if email_maintainer: + role =3D get_maintainer_role(i) + push_email_addresses(pvalue, role + suffix) + elif ptype =3D=3D "R": + if email_reviewer: + subs =3D get_subsystem_name(i) + push_email_addresses(pvalue, "reviewer:" + subs + suff= ix) + elif ptype =3D=3D "T": + scm_list.append(pvalue + suffix) + elif ptype =3D=3D "W": + web_list.append(pvalue + suffix) + elif ptype =3D=3D "B": + bug_list.append(pvalue + suffix) + elif ptype =3D=3D "S": + status_list.append(pvalue + suffix) + sts =3D pvalue + + if sub !=3D "THE REST" and sts !=3D "Maintained": + substatus_list.append("{} status: {}{}".format(sub, sts, suffix)) + +# ---- email management ---- + +def email_inuse(name, address): + if name =3D=3D "" and address =3D=3D "": + return True + if name !=3D "" and name.lower() in email_hash_name: + return True + if address !=3D "" 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 =3D parse_email(line) + if address =3D=3D "": + 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 !=3D "": + email_hash_name[name.lower()] =3D email_hash_name.get(name.low= er(), 0) + 1 + email_hash_address[address.lower()] =3D email_hash_address.get(add= ress.lower(), 0) + 1 + + return True + +def push_email_addresses(address, role): + if rfc822_valid(address): + push_email_address(address, role) + else: + addr_list =3D 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=3Dsys.stderr) + +def add_role(line, role): + global email_to + name, address =3D parse_email(line) + email_str =3D format_email(name, address, email_usename) + + for entry in email_to: + if email_remove_duplicates: + entry_name, entry_address =3D parse_email(entry[0]) + if (name =3D=3D entry_name or address =3D=3D entry_address) an= d \ + (role =3D=3D "" or role not in entry[1]): + if entry[1] =3D=3D "": + entry[1] =3D role + else: + entry[1] =3D "{},{}".format(entry[1], role) + else: + if email_str =3D=3D entry[0] and \ + (role =3D=3D "" or role not in entry[1]): + if entry[1] =3D=3D "": + entry[1] =3D role + else: + entry[1] =3D "{},{}".format(entry[1], role) + +def deduplicate_email(email_addr): + global deduplicate_name_hash, deduplicate_address_hash + name, address =3D parse_email(email_addr) + email_addr =3D format_email(name, address, 1) + email_addr =3D mailmap_email(email_addr) + + if not email_remove_duplicates: + return email_addr + + name, address =3D parse_email(email_addr) + matched =3D False + + if name !=3D "" and name.lower() in deduplicate_name_hash: + name =3D deduplicate_name_hash[name.lower()][0] + address =3D deduplicate_name_hash[name.lower()][1] + matched =3D True + elif address.lower() in deduplicate_address_hash: + name =3D deduplicate_address_hash[address.lower()][0] + address =3D deduplicate_address_hash[address.lower()][1] + matched =3D True + + if not matched: + deduplicate_name_hash[name.lower()] =3D [name, address] + deduplicate_address_hash[address.lower()] =3D [name, address] + + email_addr =3D format_email(name, address, 1) + email_addr =3D mailmap_email(email_addr) + return email_addr + +def ignore_email_address(address): + for ig in ignore_emails: + if ig =3D=3D address: + return True + return False + +def clean_file_emails(raw_emails): + fmt_emails =3D [] + for em in raw_emails: + em =3D re.sub( + r'[\(\<\{]?([A-Za-z0-9_\.\+-]+@[A-Za-z0-9\.\-]+)[\)\>\}]?', + r'<\1>', em) + name, address =3D parse_email(em) + + # Strip quotes + if name.startswith('"') and name.endswith('"'): + name =3D name[1:-1] + + # Split into name-like parts + nw =3D re.split(r"[^\w\'\,\.\+\-]", name) + nw =3D [w for w in nw if w and not re.match(r"^[\'\,\.\+\-]$", w)] + + if len(nw) > 2: + first =3D nw[-3] + middle =3D nw[-2] + last =3D nw[-1] + if ((len(first) =3D=3D 1 and re.match(r'\w', first)) or + (len(first) =3D=3D 2 and first.endswith("."))) or \ + (len(middle) =3D=3D 1 or + (len(middle) =3D=3D 2 and middle.endswith("."))): + name =3D "{} {} {}".format(first, middle, last) + else: + name =3D "{} {}".format(middle, last) + else: + name =3D " ".join(nw) + + if name and name[-1] in (',', '.'): + name =3D name[:-1] + if name and name[0] in (',', '.'): + name =3D name[1:] + + fmt_emails.append(format_email(name, address, email_usename)) + return fmt_emails + +# ---- VCS integration ---- + +def git_execute_cmd(cmd): + try: + output =3D subprocess.run(cmd, shell=3DTrue, capture_output=3DTrue, + text=3DTrue, encoding=3D'utf-8', errors=3D= 'replace') + lines =3D output.stdout.lstrip().split('\n') + # strip leading whitespace from each line (matching Perl) + lines =3D [l.lstrip() for l in lines] + return lines + except Exception: + return [] + +def hg_execute_cmd(cmd): + try: + output =3D subprocess.run(cmd, shell=3DTrue, capture_output=3DTrue, + text=3DTrue, encoding=3D'utf-8', errors=3D= '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 =3D VCS_cmds_git + vcs_used =3D 1 + return 1 + # Try hg + if which("hg") and os.path.isdir(".hg"): + VCS_cmds =3D VCS_cmds_hg + vcs_used =3D 2 + return 2 + VCS_cmds =3D {} + if not printed_novcs and email_git: + print("{}: No supported VCS found. Add --nogit to options?".forma= t(P), file=3Dsys.stderr) + print("Using a git repository produces better results.", file=3Dsy= s.stderr) + print("Try Linus Torvalds' latest git repository using:", file=3Ds= ys.stderr) + print("git clone git://git.kernel.org/pub/scm/linux/kernel/git/tor= valds/linux.git", file=3Dsys.stderr) + printed_novcs =3D True + return 0 + +def vcs_is_git(): + vcs_exists() + return vcs_used =3D=3D 1 + +def vcs_is_hg(): + return vcs_used =3D=3D 2 + +def _vcs_exec(cmd): + if vcs_used =3D=3D 1: + return git_execute_cmd(cmd) + elif vcs_used =3D=3D 2: + return hg_execute_cmd(cmd) + return [] + +def _interpolate_cmd(cmd_template, **kwargs): + """Substitute variables in a VCS command template.""" + result =3D cmd_template + for key, val in kwargs.items(): + result =3D result.replace('{' + key + '}', str(val)) + return result + +def extract_formatted_signatures(signature_lines): + types =3D [] + signers =3D [] + for line in signature_lines: + # extract type (everything before first ':') + m =3D re.match(r'\s*(.*?):.*', line) + typ =3D m.group(1) if m else "" + types.append(typ) + + # extract signer (everything after first ':') + m =3D re.match(r'\s*.*?:\s*(.+)\s*', line) + signer =3D m.group(1).strip() if m else "" + signer =3D deduplicate_email(signer) + signers.append(signer) + + return (types, signers) + +def vcs_find_signers(cmd, filepath): + global signature_pattern + lines =3D _vcs_exec(cmd) + + pattern =3D VCS_cmds["commit_pattern"] + author_pattern =3D VCS_cmds["author_pattern"] + stat_pattern =3D VCS_cmds["stat_pattern"] + + if filepath: + stat_pattern =3D stat_pattern.replace('{file}', re.escape(filepath= )) + else: + stat_pattern =3D stat_pattern.replace('{file}', '') + + commits =3D sum(1 for l in lines if re.search(pattern, l)) + authors =3D [l for l in lines if re.search(author_pattern, l)] + signatures =3D [l for l in lines if re.search( + r'^[ \t]*' + signature_pattern + r'.*@.*$', l)] + stats =3D [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 =3D [s for s in signatures if not re.search(penguin_chi= efs, s, re.IGNORECASE)] + + _, authors_list =3D extract_formatted_signatures(authors) + _, signers_list =3D extract_formatted_signatures(signatures) + + return (commits, signers_list, authors_list, stats) + +def vcs_find_author(cmd): + lines =3D _vcs_exec(cmd) + + if not email_git_penguin_chiefs: + lines =3D [l for l in lines if not re.search(penguin_chiefs, l, re= .IGNORECASE)] + + if not lines: + return [] + + authors =3D [] + author_pattern =3D VCS_cmds["author_pattern"] + for line in lines: + m =3D re.search(author_pattern, line) + if m: + author =3D m.group(1) + name, address =3D parse_email(author) + author =3D 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 =3D _vcs_exec(cmd) + commits =3D [] + blame_pattern =3D VCS_cmds["blame_commit_pattern"] + for line in lines: + m =3D re.search(blame_pattern, line) + if m: + commits.append(m.group(1)) + return commits + +def vcs_blame(filepath): + global range_list + commits =3D [] + if not os.path.isfile(filepath): + return commits + + if range_list and VCS_cmds.get("blame_range_cmd", "") =3D=3D "": + cmd =3D _interpolate_cmd(VCS_cmds["blame_file_cmd"], file=3Dshlex.= quote(filepath)) + all_commits =3D vcs_save_commits(cmd) + + for file_range_diff in range_list: + m =3D re.match(r'(.+):(.+):(.+)', file_range_diff) + if not m: + continue + diff_file =3D m.group(1) + diff_start =3D int(m.group(2)) + diff_length =3D int(m.group(3)) + if filepath !=3D 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 =3D re.match(r'(.+):(.+):(.+)', file_range_diff) + if not m: + continue + diff_file =3D m.group(1) + diff_start =3D m.group(2) + diff_length =3D m.group(3) + if filepath !=3D diff_file: + continue + cmd =3D _interpolate_cmd(VCS_cmds["blame_range_cmd"], + file=3Dshlex.quote(filepath), + diff_start=3Ddiff_start, + diff_length=3Ddiff_length) + commits.extend(vcs_save_commits(cmd)) + else: + cmd =3D _interpolate_cmd(VCS_cmds["blame_file_cmd"], file=3Dshlex.= quote(filepath)) + commits =3D vcs_save_commits(cmd) + + commits =3D [c.lstrip('^') for c in commits] + return commits + +def vcs_file_exists(filepath): + v =3D vcs_exists() + if not v: + return False + cmd =3D _interpolate_cmd(VCS_cmds["file_exists_cmd"], file=3Dshlex.quo= te(filepath)) + cmd +=3D " 2>&1" + result =3D _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 =3D vcs_exists() + if not v: + return [] + cmd =3D _interpolate_cmd(VCS_cmds["list_files_cmd"], file=3Dshlex.quot= e(filepath)) + return _vcs_exec(cmd) + +def vcs_assign(role, divisor, lines): + if not lines: + return + + if divisor <=3D 0: + print("Bad divisor in vcs_assign: {}".format(divisor), file=3Dsys.= stderr) + divisor =3D 1 + + lines =3D mailmap(lines) + + if not lines: + return + + lines.sort() + + # uniq -c + counts =3D {} + for l in lines: + counts[l] =3D counts.get(l, 0) + 1 + + count =3D 0 + for line in sorted(counts.keys(), key=3Dlambda x: counts[x], reverse= =3DTrue): + sign_offs =3D counts[line] + percent =3D sign_offs * 100 / divisor + if percent > 100: + percent =3D 100 + if ignore_email_address(line): + continue + count +=3D 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 =3D "{:.0f}".format(percent) + add_role(line, "{}:{}/{}=3D{}%".format(role, sign_offs, diviso= r, fmt_percent)) + else: + add_role(line, role) + +def vcs_file_signoffs(filepath): + global vcs_used + + vcs_used =3D vcs_exists() + if not vcs_used: + return + + cmd =3D _interpolate_cmd(VCS_cmds["find_signers_cmd"], + email_git_since=3Demail_git_since, + email_hg_since=3Demail_hg_since, + file=3Dshlex.quote(filepath)) + + commits, signers, authors, stats =3D vcs_find_signers(cmd, filepath) + + for i in range(len(signers)): + signers[i] =3D deduplicate_email(signers[i]) + + vcs_assign("commit_signer", commits, signers) + vcs_assign("authored", commits, authors) + + if len(authors) =3D=3D len(stats): + stat_pattern =3D VCS_cmds["stat_pattern"] + stat_pattern =3D stat_pattern.replace('{file}', re.escape(filepath= )) + + added =3D 0 + deleted =3D 0 + for i in range(len(stats)): + m =3D re.search(stat_pattern, stats[i]) + if m: + added +=3D int(m.group(1)) + deleted +=3D int(m.group(2)) + + tmp_authors =3D uniq(authors) + tmp_authors =3D [deduplicate_email(a) for a in tmp_authors] + tmp_authors =3D uniq(tmp_authors) + + list_added =3D [] + list_deleted =3D [] + for author in tmp_authors: + auth_added =3D 0 + auth_deleted =3D 0 + for i in range(len(stats)): + if author =3D=3D deduplicate_email(authors[i]): + m =3D re.search(stat_pattern, stats[i]) + if m: + auth_added +=3D int(m.group(1)) + auth_deleted +=3D 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 =3D vcs_exists() + if not vcs_used: + return + + all_commits =3D vcs_blame(filepath) + commits =3D uniq(all_commits) + total_commits =3D len(commits) + total_lines =3D len(all_commits) + + signers =3D [] + + if email_git_blame_signatures: + if vcs_is_hg(): + commit =3D " -r ".join(commits) + cmd =3D _interpolate_cmd(VCS_cmds["find_commit_signers_cmd"], = commit=3Dcommit) + _, commit_signers, _, _ =3D vcs_find_signers(cmd, filepath) + signers.extend(commit_signers) + else: + for commit in commits: + cmd =3D _interpolate_cmd(VCS_cmds["find_commit_signers_cmd= "], commit=3Dcommit) + _, commit_signers, _, _ =3D vcs_find_signers(cmd, filepath) + signers.extend(commit_signers) + + if from_filename: + if output_rolestats: + blame_signers =3D [] + if vcs_is_hg(): + u_commits =3D sorted(uniq(commits)) + commit =3D " -r ".join(u_commits) + cmd =3D _interpolate_cmd(VCS_cmds["find_commit_author_cmd"= ], commit=3Dcommit) + lines =3D _vcs_exec(cmd) + if not email_git_penguin_chiefs: + lines =3D [l for l in lines if not re.search(penguin_c= hiefs, l, re.IGNORECASE)] + if lines: + author_pattern =3D VCS_cmds["author_pattern"] + for line in lines: + m =3D re.search(author_pattern, line) + if m: + author =3D 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 =3D _interpolate_cmd(VCS_cmds["find_commit_author_= cmd"], commit=3Dcommit) + author_list =3D vcs_find_author(cmd) + if not author_list: + continue + formatted_author =3D deduplicate_email(author_list[0]) + cnt =3D 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 =3D [deduplicate_email(s) for s in signers] + vcs_assign("commits", total_commits, signers) + else: + signers =3D [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 =3D _interpolate_cmd(VCS_cmds["find_commit_signers_cmd"], commit= =3Dcommit) + commit_count, commit_signers, commit_authors, stats =3D vcs_find_signe= rs(cmd, "") + + for i in range(len(commit_signers)): + commit_signers[i] =3D 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 =3D [] + commits =3D [] + subjects =3D [] + author_pattern =3D VCS_cmds["author_pattern"] + commit_pattern =3D VCS_cmds["commit_pattern"] + subject_pattern =3D VCS_cmds["subject_pattern"] + + for line in lines: + m =3D re.search(author_pattern, line) + if m: + author =3D deduplicate_email(m.group(1)) + authors.append(author) + m =3D re.search(commit_pattern, line) + if m: + commits.append(m.group(1)) + m =3D re.search(subject_pattern, line) + if m: + subjects.append(m.group(1)) + + for i in range(len(authors)): + if i >=3D len(commits) or i >=3D len(subjects): + break + if authors[i] not in commit_author_hash: + commit_author_hash[authors[i]] =3D [] + exists =3D False + for ref in commit_author_hash[authors[i]]: + if ref[0] =3D=3D commits[i] and ref[1] =3D=3D subjects[i]: + exists =3D 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 =3D "" + subject =3D "" + commit_pattern =3D VCS_cmds["commit_pattern"] + subject_pattern =3D VCS_cmds["subject_pattern"] + + for line in lines: + m =3D re.search(commit_pattern, line) + if m: + commit =3D m.group(1) + m =3D re.search(subject_pattern, line) + if m: + subject =3D m.group(1) + if re.search(r'^[ \t]*' + signature_pattern + r'.*@.*$', line): + sig_types, sig_signers =3D extract_formatted_signatures([line]) + if sig_signers: + typ =3D sig_types[0] + signer =3D deduplicate_email(sig_signers[0]) + if signer not in commit_signer_hash: + commit_signer_hash[signer] =3D [] + exists =3D False + for ref in commit_signer_hash[signer]: + if ref[0] =3D=3D commit and ref[1] =3D=3D subject and = ref[2] =3D=3D typ: + exists =3D True + break + if not exists: + commit_signer_hash[signer].append([commit, subject, ty= p]) + +# ---- range checks ---- + +def range_is_maintained(start, end): + for i in range(start, end): + typ, value =3D typevalue_parsed[i] + if typ =3D=3D '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 =3D typevalue_parsed[i] + if typ =3D=3D '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 =3D {} + email_hash_address =3D {} + commit_author_hash =3D {} + commit_signer_hash =3D {} + email_to =3D [] + hash_list_to =3D {} + list_to =3D [] + scm_list =3D [] + web_list =3D [] + bug_list =3D [] + subsystem_list =3D [] + status_list =3D [] + substatus_list =3D [] + deduplicate_name_hash =3D {} + deduplicate_address_hash =3D {} + + if email_git_all_signature_types: + signature_pattern =3D r"(.+?)[Bb][Yy]:" + else: + signature_pattern =3D "(" + "|".join(re.escape(t) for t in signatu= re_tags) + ")" + + exact_pattern_match_hash =3D {} + + for filepath in files: + hash_map =3D {} + tvi =3D find_first_section() + while tvi < len(typevalue): + start =3D find_starting_index(tvi) + end =3D find_ending_index(tvi) + exclude =3D False + + # Check excluded patterns + for i in range(start, end): + typ, value =3D typevalue_parsed[i] + if typ =3D=3D 'X': + prefix =3D typevalue_prefix.get(i, '') + if prefix and not filepath.startswith(prefix): + continue + compiled =3D _get_compiled(i) + if compiled is not None and file_match_pattern(filepat= h, value, compiled): + exclude =3D True + break + + if not exclude: + for i in range(start, end): + typ, value =3D typevalue_parsed[i] + if typ is None: + continue + if typ =3D=3D 'F': + prefix =3D typevalue_prefix.get(i, '') + if prefix and not filepath.startswith(prefix): + continue + compiled =3D _get_compiled(i) + if compiled is not None and file_match_pattern(fil= epath, value, compiled): + value_pd =3D value.count('/') + file_pd =3D filepath.count('/') + if not value.endswith('/'): + value_pd +=3D 1 + if re.match(r'^(\.\*|\(\?:\.\*\))', value): + value_pd =3D -1 + if value_pd >=3D file_pd and \ + range_is_maintained(start, end) and \ + range_has_maintainer(start, end): + exact_pattern_match_hash[filepath] =3D 1 + if pattern_depth =3D=3D 0 or \ + (file_pd - value_pd) < pattern_depth: + hash_map[tvi] =3D value_pd + elif typ =3D=3D 'N': + try: + if re.search(value, filepath, re.VERBOSE): + hash_map[tvi] =3D 0 + except re.error: + pass + + tvi =3D end + 1 + + for line_idx in sorted(hash_map.keys(), key=3Dlambda x: hash_map[x= ], reverse=3DTrue): + add_categories(line_idx, "") + if sections: + start =3D find_starting_index(line_idx) + end =3D find_ending_index(line_idx) + for i in range(start, end): + line =3D typevalue[i] + if re.match(r'^[FX]:', line): + # Restore file patterns + line =3D re.sub(r'([^\\])\.([^\*])', r'\1?\2', lin= e) + line =3D re.sub(r'([^\\])\.$', r'\1?', line) + line =3D line.replace('\\.', '.') + line =3D line.replace('(?:.*)', '**') + line =3D line.replace('.*', '*') + m2 =3D re.match(r'^([A-Z]):(.*)', line) + if m2: + line =3D "{}:\t{}".format(m2.group(1), m2.group(2)) + if letters =3D=3D "" or re.search(m2.group(1), let= ters, re.IGNORECASE): + print(line) + else: + # Section header lines are always printed + print(line) + print() + + maintainers_in_file(filepath) + + if keywords: + kw_tvi =3D 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_i= dx]) + + for em in email_to + list_to: + em[0] =3D 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 =3D re.match(r'^(.*?):(.*)', chief) + if m: + email_address =3D format_email(m.group(1), m.group(2), ema= il_usename) + if email_git_penguin_chiefs: + email_to.append([email_address, 'chief penguin']) + else: + email_to[:] =3D [e for e in email_to + if not re.search(re.escape(email_addres= s), e[0])] + + for em in file_emails: + em =3D mailmap_email(em) + name, address =3D parse_email(em) + tmp_email =3D 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 =3D [] + if email or email_list: + if email: + to.extend(email_to) + if email_list: + to.extend(list_to) + + if interactive: + to =3D interactive_get_maintainers(to) + + return to + +# ---- output functions ---- + +def merge_email(entries): + lines =3D [] + saw =3D {} + for entry in entries: + address =3D entry[0] + role =3D entry[1] + if address not in saw: + if output_roles: + lines.append("{} ({})".format(address, role)) + else: + lines.append(address) + saw[address] =3D 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 =3D {} + authored =3D {} + signed =3D {} + count =3D 0 + maintained =3D False + + for entry in to_list: + if re.match(r'^(maintainer|supporter)', entry[1], re.IGNORECASE): + maintained =3D True + selected[count] =3D True + authored[count] =3D False + signed[count] =3D False + count +=3D 1 + + done =3D False + print_options =3D False + redraw =3D True + + while not done: + count =3D len(to_list) + if redraw: + sys.stderr.write("\n{:1s} {:2s} {:65s}".format("*", "#", "emai= l/list and role:stats")) + if email_git or (email_git_fallback and not maintained) or ema= il_git_blame: + sys.stderr.write("auth sign") + sys.stderr.write("\n") + for idx, entry in enumerate(to_list): + em =3D entry[0] + role =3D entry[1] + sel =3D "*" if selected.get(idx, False) else "" + ca =3D commit_author_hash.get(em, []) + cs =3D commit_signer_hash.get(em, []) + auth_count =3D len(ca) + sign_count =3D 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], re= f[1])) + + if print_options: + print_options =3D False + if vcs_exists(): + date_ref =3D 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_d= epth)) + + sys.stderr.write( + "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(option= s), Y(approve): ") + + try: + inp =3D input() + except EOFError: + break + + redraw =3D True + rerun =3D False + wishes =3D re.split(r'[, ]+', inp) + + for nr in wishes: + nr =3D nr.lower().strip() + if not nr: + continue + sel =3D nr[0] + rest =3D nr[1:] + val =3D 0 + m =3D re.match(r'^(\d+)$', rest) + if m: + val =3D int(m.group(1)) + + if sel =3D=3D "y": + interactive =3D 0 + done =3D True + output_rolestats =3D 0 + output_roles =3D 0 + output_substatus =3D 0 + break + elif re.match(r'^\d+$', nr): + num =3D int(nr) + if 0 < num <=3D count: + selected[num - 1] =3D not selected.get(num - 1, False) + elif sel in ('*', '^'): + toggle =3D (sel =3D=3D '*') + for i in range(count): + selected[i] =3D toggle + elif sel =3D=3D '0': + for i in range(count): + selected[i] =3D not selected.get(i, False) + elif sel =3D=3D 't': + if rest =3D=3D 'm': + for i in range(count): + if re.match(r'^(maintainer|supporter)', to_list[i]= [1], re.IGNORECASE): + selected[i] =3D not selected.get(i, False) + elif rest =3D=3D 'g': + for i in range(count): + if re.match(r'^(author|commit|signer)', to_list[i]= [1], re.IGNORECASE): + selected[i] =3D not selected.get(i, False) + elif rest =3D=3D 'l': + for i in range(count): + if re.match(r'^(open list)', to_list[i][1], re.IGN= ORECASE): + selected[i] =3D not selected.get(i, False) + elif rest =3D=3D 's': + for i in range(count): + if re.match(r'^(subscriber list)', to_list[i][1], = re.IGNORECASE): + selected[i] =3D not selected.get(i, False) + else: + # 't' alone toggles all signature types + email_git_all_signature_types =3D not email_git_all_si= gnature_types + rerun =3D True + elif sel =3D=3D 'a': + if val > 0 and val <=3D count: + authored[val - 1] =3D not authored.get(val - 1, False) + elif rest in ('*', '^'): + toggle =3D (rest =3D=3D '*') + for i in range(count): + authored[i] =3D toggle + elif sel =3D=3D 's': + if val > 0 and val <=3D count: + signed[val - 1] =3D not signed.get(val - 1, False) + elif rest in ('*', '^'): + toggle =3D (rest =3D=3D '*') + for i in range(count): + signed[i] =3D toggle + elif sel =3D=3D 'o': + print_options =3D True + redraw =3D True + elif sel =3D=3D 'g': + if rest =3D=3D 'f': + email_git_fallback =3D not email_git_fallback + else: + email_git =3D not email_git + rerun =3D True + elif sel =3D=3D 'b': + if rest =3D=3D 's': + email_git_blame_signatures =3D not email_git_blame_sig= natures + else: + email_git_blame =3D not email_git_blame + rerun =3D True + elif sel =3D=3D 'c': + if val > 0: + email_git_min_signatures =3D val + rerun =3D True + elif sel =3D=3D 'x': + if val > 0: + email_git_max_maintainers =3D val + rerun =3D True + elif sel =3D=3D '%': + if rest !=3D "" and val >=3D 0: + email_git_min_percent =3D val + rerun =3D True + elif sel =3D=3D 'd': + if vcs_is_git(): + email_git_since =3D rest + elif vcs_is_hg(): + email_hg_since =3D rest + rerun =3D True + elif sel =3D=3D 'f': + email_file_emails =3D not email_file_emails + rerun =3D True + elif sel =3D=3D 'r': + email_remove_duplicates =3D not email_remove_duplicates + rerun =3D True + elif sel =3D=3D 'm': + email_use_mailmap =3D not email_use_mailmap + read_mailmap() + rerun =3D True + elif sel =3D=3D 'k': + keywords =3D not keywords + rerun =3D True + elif sel =3D=3D 'p': + if rest !=3D "" and val >=3D 0: + pattern_depth =3D val + rerun =3D 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 =3D 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 =3D [] + 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 =3D vcs_list_files(lk_path) + good_links =3D [] + bad_links =3D [] + section_headers =3D [] + index =3D 0 + + for x in self_test_info: + index +=3D 1 + + # Section header duplication and missing section content + if (self_test =3D=3D "" 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 =3D False + has_F =3D False + has_ML =3D False + sts =3D "" + if any(h.startswith(x["line"]) for h in section_headers): + print("{}:{}: warning: duplicate section header\t{}".forma= t( + x["file"], x["linenr"], x["line"])) + else: + section_headers.append(x["line"]) + nextline =3D index + while nextline < len(self_test_info): + m =3D re.match(r'^([A-Z]):\s*(\S.*)', self_test_info[nextl= ine]["line"]) + if not m: + break + typ =3D m.group(1) + val =3D m.group(2) + if typ =3D=3D "S": + has_S =3D True + sts =3D val + elif typ in ("F", "N"): + has_F =3D True + elif typ in ("M", "R", "L"): + has_ML =3D True + nextline +=3D 1 + if not has_ML and not re.search(r'orphan|obsolete', sts, re.IG= NORECASE): + 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{}".f= ormat( + x["file"], x["linenr"], x["line"])) + + m =3D _re_type_value.match(x["line"]) + if not m: + continue + + typ =3D m.group(1) + value =3D m.group(2) + + # Filename pattern matching + if typ in ("F", "X") and (self_test =3D=3D "" or "patterns" in sel= f_test): + val =3D value + val =3D val.replace('.', '\\.') + val =3D val.replace('**', '\x00') + val =3D val.replace('*', '.*') + val =3D val.replace('?', '.') + val =3D val.replace('\x00', '(?:.*)') + if os.path.isdir(val): + if not val.endswith('/'): + val +=3D '/' + try: + pat =3D 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 =3D=3D "" or "links" in self_test): + if value in good_links: + continue + isbad =3D False + if value in bad_links: + isbad =3D True + else: + ret =3D subprocess.run( + ["wget", "--spider", "-q", "--no-check-certificate", + "--timeout", "10", "--tries", "1", value], + capture_output=3DTrue) + if ret.returncode =3D=3D 0: + good_links.append(value) + else: + bad_links.append(value) + isbad =3D True + if isbad: + print("{}:{}: warning: possible bad link\t{}".format( + x["file"], x["linenr"], x["line"])) + + # SCM reachability + elif typ =3D=3D "T" and (self_test =3D=3D "" or "scm" in self_test= ): + if value in good_links: + continue + isbad =3D False + if value in bad_links: + isbad =3D 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 =3D re.match(r'^git\s+(\S+)(\s+([^\(]+\S+))?', value) + if m2: + url =3D m2.group(1) + branch =3D m2.group(3) if m2.group(3) else "" + ret =3D subprocess.run( + 'git ls-remote --exit-code -h "{}" {} > /dev/null = 2>&1'.format(url, branch), + shell=3DTrue, capture_output=3DTrue) + if ret.returncode =3D=3D 0: + good_links.append(value) + else: + bad_links.append(value) + isbad =3D True + else: + m2 =3D re.match(r'^(?:quilt|hg)\s+(https?:\S+)', value) + if m2: + url =3D m2.group(1) + ret =3D subprocess.run( + ["wget", "--spider", "-q", "--no-check-certifi= cate", + "--timeout", "10", "--tries", "1", url], + capture_output=3DTrue) + if ret.returncode =3D=3D 0: + good_links.append(value) + else: + bad_links.append(value) + isbad =3D 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 =3D> print email address(es) if any + --git =3D> include recent git *-by: signers + --git-all-signature-types =3D> include signers regardless of signature= type + or use only {} signers (default: {}) + --git-fallback =3D> use git when no exact MAINTAINERS pattern (default= : {}) + --git-chief-penguins =3D> include {} + --git-min-signatures =3D> number of signatures required (default: {}) + --git-max-maintainers =3D> maximum maintainers to add (default: {}) + --git-min-percent =3D> minimum percentage of commits required (default= : {}) + --git-blame =3D> use git blame to find modified commits for patch or f= ile + --git-blame-signatures =3D> when used with --git-blame, also include a= ll commit signers + --git-since =3D> git history to use (default: {}) + --hg-since =3D> hg history to use (default: {}) + --interactive =3D> display a menu (mostly useful if used with the --gi= t option) + --m =3D> include maintainer(s) if any + --r =3D> include reviewer(s) if any + --n =3D> include name 'Full Name ' + --l =3D> include list(s) if any + --moderated =3D> include moderated lists(s) if any (default: true) + --s =3D> include subscriber only list(s) if any (default: false) + --remove-duplicates =3D> minimize duplicate email names/addresses + --roles =3D> show roles (role:subsystem, git-signer, list, etc...) + --rolestats =3D> show roles and statistics (commits/total_commits, %) + --substatus =3D> show subsystem status if not Maintained (default: mat= ch --roles when output is tty)" + --file-emails =3D> add email addresses found in -f file (default: 0 (o= ff)) + --fixes =3D> for patches, add signatures of commits with 'Fixes: ' (default: 1 (on)) + --scm =3D> print SCM tree(s) if any + --status =3D> print status if any + --subsystem =3D> print subsystem name if any + --web =3D> print website(s) if any + --bug =3D> print bug reporting info if any + +Output type options: + --separator [, ] =3D> separator for multiple entries on 1 line + using --separator also sets --nomultiline if --separator is not [, ] + --multiline =3D> print 1 entry per line + +Other options: + --pattern-depth =3D> Number of pattern directory traversals (default: 0 = (all)) + --keywords =3D> scan patch for keywords (default: {}) + --keywords-in-file =3D> scan file for keywords (default: {}) + --sections =3D> print all of the subsystem sections with pattern matches + --letters =3D> print all matching 'letter' types from all matching secti= ons + --mailmap =3D> use .mailmap file (default: {}) + --no-tree =3D> run without a kernel tree + --self-test =3D> show potential issues with MAINTAINERS file content + --version =3D> show version + --help =3D> show this help information + +Default options: + [--email --tree --nogit --git-fallback --m --r --n --l --multiline + --pattern-depth=3D0 --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"] + may not work because of additional output after . + Using "--rolestats" and "--git-blame" shows the #/total=3D% 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 -- are --no and --no-. +""".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=3DNone): + """Add --foo/--nofoo/--no-foo flags (Perl Getopt::Long style negation)= .""" + names =3D ['--' + name] + if short: + names.insert(0, short) + parser.add_argument(*names, dest=3Ddest, action=3D'store_true', defaul= t=3Ddefault) + parser.add_argument('--no' + name, '--no-' + name, + dest=3Ddest, action=3D'store_false') + +# ---- main ---- + +def main(): + global P, cur_path, lk_path + global email, email_usename, email_maintainer, email_reviewer, email_f= ixes + global email_list, email_moderated_list, email_subscriber_list + global email_git_penguin_chiefs, email_git, email_git_all_signature_ty= pes + 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_rolest= ats + 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 =3D sys.argv[0] + cur_path =3D os.getcwd() + '/' + + # Load config file + conf =3D which_conf(".get_maintainer.conf") + conf_args =3D [] + if conf and os.path.isfile(conf): + try: + with open(conf, 'r', encoding=3D'utf-8') as f: + for line in f: + line =3D line.rstrip('\n').strip() + line =3D 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".fo= rmat(P), file=3Dsys.stderr) + + # Load ignore file + ignore_file =3D which_conf(".get_maintainer.ignore") + if ignore_file and os.path.isfile(ignore_file): + try: + with open(ignore_file, 'r', encoding=3D'utf-8') as f: + for line in f: + line =3D line.strip() + line =3D 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 =3D 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 opti= on or argument".format(P), + file=3Dsys.stderr) + sys.exit(1) + + # Build argument parser + parser =3D argparse.ArgumentParser(add_help=3DFalse) + + 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_s= ignature_types', False) + add_negatable_flag(parser, 'git-blame', 'email_git_blame', False) + add_negatable_flag(parser, 'git-blame-signatures', 'email_git_blame_si= gnatures', True) + add_negatable_flag(parser, 'git-fallback', 'email_git_fallback', True) + add_negatable_flag(parser, 'git-chief-penguins', 'email_git_penguin_ch= iefs', False) + parser.add_argument('--git-min-signatures', type=3Dint, default=3D1) + parser.add_argument('--git-max-maintainers', type=3Dint, default=3D5) + parser.add_argument('--git-min-percent', type=3Dint, default=3D5) + parser.add_argument('--git-since', default=3D"1-year-ago") + parser.add_argument('--hg-since', default=3D"-365") + add_negatable_flag(parser, 'interactive', 'interactive', False, short= =3D'-i') + add_negatable_flag(parser, 'remove-duplicates', 'email_remove_duplicat= es', 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=3D", ") + 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=3D"") + parser.add_argument('--pattern-depth', type=3Dint, default=3D0) + add_negatable_flag(parser, 'keywords', 'keywords', True, short=3D'-k') + add_negatable_flag(parser, 'keywords-in-file', 'keywords_in_file', Fal= se) + add_negatable_flag(parser, 'sections', 'sections', False) + add_negatable_flag(parser, 'file-emails', 'email_file_emails', False) + parser.add_argument('-f', '--file', dest=3D'from_filename', action=3D'= store_true', default=3DFalse) + parser.add_argument('--find-maintainer-files', action=3D'store_true', = default=3DFalse) + parser.add_argument('--mpath', '--maintainer-path', dest=3D'maintainer= _path', default=3DNone) + parser.add_argument('--self-test', dest=3D'self_test', nargs=3D'?', co= nst=3D'', default=3DNone) + parser.add_argument('-v', '--version', dest=3D'show_version', action= =3D'store_true', default=3DFalse) + parser.add_argument('-h', '--help', '--usage', dest=3D'show_help', act= ion=3D'store_true', default=3DFalse) + parser.add_argument('files', nargs=3D'*') + + # Handle negatable --fe alias + parser.add_argument('--fe', dest=3D'email_file_emails', action=3D'stor= e_true') + parser.add_argument('--kf', dest=3D'keywords_in_file', action=3D'store= _true') + + try: + args =3D 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 =3D 1 if args.email else 0 + email_git =3D 1 if args.email_git else 0 + email_git_all_signature_types =3D 1 if args.email_git_all_signature_ty= pes else 0 + email_git_blame =3D 1 if args.email_git_blame else 0 + email_git_blame_signatures =3D 1 if args.email_git_blame_signatures el= se 0 + email_git_fallback =3D 1 if args.email_git_fallback else 0 + email_git_penguin_chiefs =3D 1 if args.email_git_penguin_chiefs else 0 + email_git_min_signatures =3D args.git_min_signatures + email_git_max_maintainers =3D args.git_max_maintainers + email_git_min_percent =3D args.git_min_percent + email_git_since =3D args.git_since + email_hg_since =3D args.hg_since + interactive =3D 1 if args.interactive else 0 + email_remove_duplicates =3D 1 if args.email_remove_duplicates else 0 + email_use_mailmap =3D 1 if args.email_use_mailmap else 0 + email_maintainer =3D 1 if args.email_maintainer else 0 + email_reviewer =3D 1 if args.email_reviewer else 0 + email_usename =3D 1 if args.email_usename else 0 + email_list =3D 1 if args.email_list else 0 + email_fixes =3D 1 if args.email_fixes else 0 + email_moderated_list =3D 1 if args.email_moderated_list else 0 + email_subscriber_list =3D 1 if args.email_subscriber_list else 0 + output_multiline =3D 1 if args.output_multiline else 0 + output_roles =3D 1 if args.output_roles else 0 + output_rolestats =3D 1 if args.output_rolestats else 0 + output_separator =3D args.separator + subsystem_opt =3D 1 if args.subsystem else 0 + status_opt =3D 1 if args.status else 0 + scm =3D 1 if args.scm else 0 + tree =3D 1 if args.tree else 0 + web =3D 1 if args.web else 0 + bug =3D 1 if args.bug else 0 + letters =3D args.letters + pattern_depth =3D args.pattern_depth + keywords =3D 1 if args.keywords else 0 + keywords_in_file =3D 1 if args.keywords_in_file else 0 + sections =3D 1 if args.sections else 0 + email_file_emails =3D 1 if args.email_file_emails else 0 + from_filename =3D 1 if args.from_filename else 0 + find_maintainer_files =3D 1 if args.find_maintainer_files else 0 + maintainer_path =3D args.maintainer_path + self_test =3D args.self_test + + # Handle substatus special logic + if args.output_substatus_flag is None: + output_substatus =3D None + else: + output_substatus =3D 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 !=3D ", ": + output_multiline =3D 0 + if interactive: + output_rolestats =3D 1 + if output_rolestats: + output_roles =3D 1 + + if output_substatus is None: + output_substatus =3D 1 if (email and output_roles and sys.stdout.i= satty()) else 0 + + if sections or letters !=3D "": + sections =3D 1 + email =3D 0 + email_list =3D 0 + scm =3D 0 + status_opt =3D 0 + subsystem_opt =3D 0 + web =3D 0 + bug =3D 0 + keywords =3D 0 + keywords_in_file =3D 0 + interactive =3D 0 + else: + selections =3D email + scm + status_opt + subsystem_opt + web + bug + if selections =3D=3D 0: + print("{}: Missing required option: email, scm, status, subsys= tem, web or bug".format(P), + file=3Dsys.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) =3D=3D 0: + print("{}: Please select at least 1 email option".format(P), file= =3Dsys.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=3Dsys.stderr) + sys.exit(1) + + # Read MAINTAINERS + read_all_maintainer_files() + + # Read mailmap + read_mailmap() + + # Process input files + input_files =3D args.files if args.files else [] + if not input_files and not sys.stdin.isatty(): + input_files =3D ["&STDIN"] + elif not input_files: + print("{}: missing patchfile or -f file - use --help if necessary"= .format(P), + file=3Dsys.stderr) + sys.exit(1) + + for filepath in input_files: + if filepath !=3D "&STDIN": + filepath =3D os.path.normpath(filepath) + if os.path.isdir(filepath): + if not filepath.endswith('/'): + filepath +=3D '/' + elif not os.path.isfile(filepath): + print("{}: file '{}' not found".format(P, filepath), file= =3Dsys.stderr) + sys.exit(1) + + file_in_vcs =3D None + if from_filename and vcs_exists(): + file_in_vcs =3D vcs_file_exists(filepath) + if not file_in_vcs: + print("{}: file '{}' not found in version control".format(= P, filepath), file=3Dsys.stderr) + + if from_filename or (filepath !=3D "&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 =3D filepath[len(cur_path):] + if filepath.startswith(lk_path) and lk_path !=3D "./": + filepath =3D filepath[len(lk_path):] + files.append(filepath) + if filepath !=3D "MAINTAINERS" and os.path.isfile(filepath) an= d keywords and keywords_in_file: + try: + with open(filepath, 'r', encoding=3D'utf-8', errors=3D= 'replace') as f: + text =3D 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 =3D len(files) + lastfile =3D None + + if filepath =3D=3D "&STDIN": + patch =3D sys.stdin + else: + try: + patch =3D open(filepath, 'r', encoding=3D'utf-8', erro= rs=3D'replace') + except IOError: + print("{}: Can't open {}: ".format(P, filepath), file= =3Dsys.stderr) + sys.exit(1) + + patch_prefix =3D "" # Parsing the intro + + for patch_line in patch: + m =3D re.match(r'^ mode change [0-7]+ =3D> [0-7]+ (\S+)\s*= $', patch_line) + if m: + files.append(m.group(1)) + continue + m =3D re.match(r'^rename (?:from|to) (\S+)\s*$', patch_lin= e) + if m: + files.append(m.group(1)) + continue + m =3D re.match(r'^diff --git a/(\S+) b/(\S+)\s*$', patch_l= ine) + if m: + files.append(m.group(1)) + files.append(m.group(2)) + continue + m =3D re.match(r'^Fixes:\s+([0-9a-fA-F]{6,40})', patch_lin= e) + if m: + if email_fixes: + fixes.append(m.group(1)) + continue + m =3D re.match(r'^\+\+\+\s+(\S+)', patch_line) or \ + re.match(r'^---\s+(\S+)', patch_line) + if m: + filename =3D m.group(1) + filename =3D re.sub(r'^[^/]*/', '', filename) + filename =3D filename.rstrip('\n') + lastfile =3D filename + files.append(filename) + patch_prefix =3D "^[+-].*" + continue + m =3D re.match(r'^@@ -(\d+),(\d+)', patch_line) + if m: + if email_git_blame and lastfile: + range_list.append("{}:{}:{}".format(lastfile, m.gr= oup(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 !=3D "&STDIN": + patch.close() + + if file_cnt =3D=3D len(files): + print("{}: file '{}' doesn't appear to be a patch. " + "Add -f to options?".format(P, filepath), file=3Dsys= .stderr) + + files[:] =3D sort_and_uniq(files) + + file_emails[:] =3D uniq(file_emails) + fixes[:] =3D uniq(fixes) + + maintainers =3D get_maintainers() + if maintainers: + maintainers =3D merge_email(maintainers) + output(maintainers) + + if scm: + scm_out =3D uniq(scm_list) + output(scm_out) + + if output_substatus: + ss =3D uniq(substatus_list) + output(ss) + + if status_opt: + st =3D uniq(status_list) + output(st) + + if subsystem_opt: + ss =3D uniq(subsystem_list) + output(ss) + + if web: + w =3D uniq(web_list) + output(w) + + if bug: + b =3D uniq(bug_list) + output(b) + +if __name__ =3D=3D "__main__": + main() --=20 2.52.0