## @file | |
# Retrieves the people to request review from on submission of a commit. | |
# | |
# Copyright (c) 2019, Linaro Ltd. All rights reserved.<BR> | |
# | |
# SPDX-License-Identifier: BSD-2-Clause-Patent | |
# | |
from __future__ import print_function | |
from collections import defaultdict | |
from collections import OrderedDict | |
import argparse | |
import os | |
import re | |
import SetupGit | |
EXPRESSIONS = { | |
'exclude': re.compile(r'^X:\s*(?P<exclude>.*?)\r*$'), | |
'file': re.compile(r'^F:\s*(?P<file>.*?)\r*$'), | |
'list': re.compile(r'^L:\s*(?P<list>.*?)\r*$'), | |
'maintainer': re.compile(r'^M:\s*(?P<maintainer>.*?)\r*$'), | |
'reviewer': re.compile(r'^R:\s*(?P<reviewer>.*?)\r*$'), | |
'status': re.compile(r'^S:\s*(?P<status>.*?)\r*$'), | |
'tree': re.compile(r'^T:\s*(?P<tree>.*?)\r*$'), | |
'webpage': re.compile(r'^W:\s*(?P<webpage>.*?)\r*$') | |
} | |
def printsection(section): | |
"""Prints out the dictionary describing a Maintainers.txt section.""" | |
print('===') | |
for key in section.keys(): | |
print("Key: %s" % key) | |
for item in section[key]: | |
print(' %s' % item) | |
def pattern_to_regex(pattern): | |
"""Takes a string containing regular UNIX path wildcards | |
and returns a string suitable for matching with regex.""" | |
pattern = pattern.replace('.', r'\.') | |
pattern = pattern.replace('?', r'.') | |
pattern = pattern.replace('*', r'.*') | |
if pattern.endswith('/'): | |
pattern += r'.*' | |
elif pattern.endswith('.*'): | |
pattern = pattern[:-2] | |
pattern += r'(?!.*?/.*?)' | |
return pattern | |
def path_in_section(path, section): | |
"""Returns True of False indicating whether the path is covered by | |
the current section.""" | |
if not 'file' in section: | |
return False | |
for pattern in section['file']: | |
regex = pattern_to_regex(pattern) | |
match = re.match(regex, path) | |
if match: | |
# Check if there is an exclude pattern that applies | |
for pattern in section['exclude']: | |
regex = pattern_to_regex(pattern) | |
match = re.match(regex, path) | |
if match: | |
return False | |
return True | |
return False | |
def get_section_maintainers(path, section): | |
"""Returns a list with email addresses to any M: and R: entries | |
matching the provided path in the provided section.""" | |
maintainers = [] | |
lists = [] | |
nowarn_status = ['Supported', 'Maintained'] | |
if path_in_section(path, section): | |
for status in section['status']: | |
if status not in nowarn_status: | |
print('WARNING: Maintained status for "%s" is \'%s\'!' % (path, status)) | |
for address in section['maintainer'], section['reviewer']: | |
# Convert to list if necessary | |
if isinstance(address, list): | |
maintainers += address | |
else: | |
lists += [address] | |
for address in section['list']: | |
# Convert to list if necessary | |
if isinstance(address, list): | |
lists += address | |
else: | |
lists += [address] | |
return maintainers, lists | |
def get_maintainers(path, sections, level=0): | |
"""For 'path', iterates over all sections, returning maintainers | |
for matching ones.""" | |
maintainers = [] | |
lists = [] | |
for section in sections: | |
tmp_maint, tmp_lists = get_section_maintainers(path, section) | |
if tmp_maint: | |
maintainers += tmp_maint | |
if tmp_lists: | |
lists += tmp_lists | |
if not maintainers: | |
# If no match found, look for match for (nonexistent) file | |
# REPO.working_dir/<default> | |
print('"%s": no maintainers found, looking for default' % path) | |
if level == 0: | |
maintainers = get_maintainers('<default>', sections, level=level + 1) | |
else: | |
print("No <default> maintainers set for project.") | |
if not maintainers: | |
return None | |
return maintainers + lists | |
def parse_maintainers_line(line): | |
"""Parse one line of Maintainers.txt, returning any match group and its key.""" | |
for key, expression in EXPRESSIONS.items(): | |
match = expression.match(line) | |
if match: | |
return key, match.group(key) | |
return None, None | |
def parse_maintainers_file(filename): | |
"""Parse the Maintainers.txt from top-level of repo and | |
return a list containing dictionaries of all sections.""" | |
with open(filename, 'r') as text: | |
line = text.readline() | |
sectionlist = [] | |
section = defaultdict(list) | |
while line: | |
key, value = parse_maintainers_line(line) | |
if key and value: | |
section[key].append(value) | |
line = text.readline() | |
# If end of section (end of file, or non-tag line encountered)... | |
if not key or not value or not line: | |
# ...if non-empty, append section to list. | |
if section: | |
sectionlist.append(section.copy()) | |
section.clear() | |
return sectionlist | |
def get_modified_files(repo, args): | |
"""Returns a list of the files modified by the commit specified in 'args'.""" | |
commit = repo.commit(args.commit) | |
return commit.stats.files | |
if __name__ == '__main__': | |
PARSER = argparse.ArgumentParser( | |
description='Retrieves information on who to cc for review on a given commit') | |
PARSER.add_argument('commit', | |
action="store", | |
help='git revision to examine (default: HEAD)', | |
nargs='?', | |
default='HEAD') | |
PARSER.add_argument('-l', '--lookup', | |
help='Find section matches for path LOOKUP', | |
required=False) | |
ARGS = PARSER.parse_args() | |
REPO = SetupGit.locate_repo() | |
CONFIG_FILE = os.path.join(REPO.working_dir, 'Maintainers.txt') | |
SECTIONS = parse_maintainers_file(CONFIG_FILE) | |
if ARGS.lookup: | |
FILES = [ARGS.lookup.replace('\\','/')] | |
else: | |
FILES = get_modified_files(REPO, ARGS) | |
ADDRESSES = [] | |
for file in FILES: | |
print(file) | |
addresslist = get_maintainers(file, SECTIONS) | |
if addresslist: | |
ADDRESSES += addresslist | |
for address in list(OrderedDict.fromkeys(ADDRESSES)): | |
if '<' in address and '>' in address: | |
address = address.split('>', 1)[0] + '>' | |
print(' %s' % address) |