Add the basic infrastructure to parse MAINTAINERS and generate a list of MaintainerSection objects we can use later.
Add a --validate argument so we can use the script to ensure MAINTAINERS is always parse-able in our CI. Signed-off-by: Alex Bennée <[email protected]> --- scripts/get_maintainer.py | 165 +++++++++++++++++++++++++++++++++++++- 1 file changed, 164 insertions(+), 1 deletion(-) diff --git a/scripts/get_maintainer.py b/scripts/get_maintainer.py index c713f290cc7..7b8ce2b65e3 100755 --- a/scripts/get_maintainer.py +++ b/scripts/get_maintainer.py @@ -10,9 +10,156 @@ # # SPDX-License-Identifier: GPL-2.0-or-later -from argparse import ArgumentParser, ArgumentTypeError +from argparse import ArgumentParser, ArgumentTypeError, BooleanOptionalAction from os import path from pathlib import Path +from enum import StrEnum, auto +from re import compile as re_compile + +# +# Subsystem MAINTAINER entries +# +# The MAINTAINERS file is an unstructured text file where the +# important information is in lines that follow the form: +# +# X: some data +# +# where X is a documented tag and the data is variously an email, +# path, regex or link. Other lines should be ignored except the +# preceding non-blank or underlined line which represents the name of +# the "subsystem" or general area of the project. +# +# A blank line denominates the end of a section. +# + +tag_re = re_compile(r"^([A-Z]):") + + +class UnhandledTag(Exception): + "Exception for unhandled tags" + + +class BadStatus(Exception): + "Exception for unknown status" + + +class Status(StrEnum): + "Maintenance status" + + UNKNOWN = auto() + SUPPORTED = 'Supported' + MAINTAINED = 'Maintained' + ODD_FIXES = 'Odd Fixes' + ORPHAN = 'Orphan' + OBSOLETE = 'Obsolete' + + @classmethod + def _missing_(cls, value): + # _missing_ is only invoked by the enum machinery if 'value' does not + # match any existing enum member's value. + # So, if we reach this point, 'value' is inherently invalid for this enum. + raise BadStatus(f"'{value}' is not a valid maintenance status.") + + +person_re = re_compile(r"^(?P<name>[^<]+?)\s*<(?P<email>[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>\s*(?:@(?P<handle>\w+))?$") + + +class BadPerson(Exception): + "Exception for un-parsable person" + + +class Person: + "Class representing a maintainer or reviewer and their details" + + def __init__(self, info): + match = person_re.search(info) + + if match is None: + raise BadPerson(f"Failed to parse {info}") + + self.name = match.group('name') + self.email = match.group('email') + + +class MaintainerSection: + "Class representing a section of MAINTAINERS" + + def _expand(self, pattern): + if pattern.endswith("/"): + return f"{pattern}*" + return pattern + + def __init__(self, section, entries): + self.section = section + self.status = Status.UNKNOWN + self.maintainers = [] + self.reviewers = [] + self.files = [] + self.files_exclude = [] + self.trees = [] + self.lists = [] + self.web = [] + self.keywords = [] + + for e in entries: + (tag, data) = e.split(": ", 2) + + if tag == "M": + person = Person(data) + self.maintainers.append(person) + elif tag == "R": + person = Person(data) + self.reviewers.append(person) + elif tag == "S": + self.status = Status(data) + elif tag == "L": + self.lists.append(data) + elif tag == 'F': + pat = self._expand(data) + self.files.append(pat) + elif tag == 'W': + self.web.append(data) + elif tag == 'K': + self.keywords.append(data) + elif tag == 'T': + self.trees.append(data) + elif tag == 'X': + pat = self._expand(data) + self.files_exclude.append(pat) + else: + raise UnhandledTag(f"'{tag}' is not understood.") + + + +def read_maintainers(src): + """ + Read the MAINTAINERS file, return a list of MaintainerSection objects. + """ + + mfile = path.join(src, 'MAINTAINERS') + entries = [] + + section = None + fields = [] + + with open(mfile, 'r', encoding='utf-8') as f: + for line in f: + if not line.strip(): # Blank line found, potential end of a section + if section: + new_section = MaintainerSection(section, fields) + entries.append(new_section) + # reset for next section + section = None + fields = [] + elif tag_re.match(line): + fields.append(line.strip()) + else: + if line.startswith("-") or line.startswith("="): + continue + + section = line.strip() + + return entries # @@ -103,6 +250,12 @@ def main(): group.add_argument('-f', '--file', type=valid_file_path, help='path to source file') + # Validate MAINTAINERS + parser.add_argument('--validate', + action=BooleanOptionalAction, + default=None, + help="Just validate MAINTAINERS file") + # We need to know or be told where the root of the source tree is src = find_src_root() @@ -115,6 +268,16 @@ def main(): args = parser.parse_args() + try: + # Now we start by reading the MAINTAINERS file + maint_sections = read_maintainers(args.src) + except Exception as e: + print(f"Error: {e}") + exit(-1) + + if args.validate: + print(f"loaded {len(maint_sections)} from MAINTAINERS") + exit(0) if __name__ == '__main__': -- 2.47.3
