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 <alex.bennee@linaro.org>
---
v2
- add __str__ function for formatting Person
---
scripts/get_maintainer.py | 168 +++++++++++++++++++++++++++++++++++++-
1 file changed, 167 insertions(+), 1 deletion(-)
diff --git a/scripts/get_maintainer.py b/scripts/get_maintainer.py
index c02bf4f5b0e..696a5b55d8d 100755
--- a/scripts/get_maintainer.py
+++ b/scripts/get_maintainer.py
@@ -10,9 +10,159 @@
#
# 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')
+
+ def __str__(self):
+ return f"{self.name} <{self.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
#
@@ -104,6 +254,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()
@@ -112,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