## @file
# Retrieves the people to request review from on submission of a commit.
#
# Copyright (c) 2019, Linaro Ltd. All rights reserved.
#
# 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.*?)\r*$'),
'file': re.compile(r'^F:\s*(?P.*?)\r*$'),
'list': re.compile(r'^L:\s*(?P.*?)\r*$'),
'maintainer': re.compile(r'^M:\s*(?P.*?)\r*$'),
'reviewer': re.compile(r'^R:\s*(?P.*?)\r*$'),
'status': re.compile(r'^S:\s*(?P.*?)\r*$'),
'tree': re.compile(r'^T:\s*(?P.*?)\r*$'),
'webpage': re.compile(r'^W:\s*(?P.*?)\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 = []
reviewers = []
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']:
# Convert to list if necessary
if isinstance(address, list):
maintainers += address
else:
maintainers += [address]
for address in section['reviewer']:
# Convert to list if necessary
if isinstance(address, list):
reviewers += address
else:
reviewers += [address]
for address in section['list']:
# Convert to list if necessary
if isinstance(address, list):
lists += address
else:
lists += [address]
return {'maintainers': maintainers, 'reviewers': reviewers, 'lists': lists}
def get_maintainers(path, sections, level=0):
"""For 'path', iterates over all sections, returning maintainers
for matching ones."""
maintainers = []
reviewers = []
lists = []
for section in sections:
recipients = get_section_maintainers(path, section)
maintainers += recipients['maintainers']
reviewers += recipients['reviewers']
lists += recipients['lists']
if not maintainers:
# If no match found, look for match for (nonexistent) file
# REPO.working_dir/
print('"%s": no maintainers found, looking for default' % path)
if level == 0:
recipients = get_maintainers('', sections, level=level + 1)
maintainers += recipients['maintainers']
reviewers += recipients['reviewers']
lists += recipients['lists']
else:
print("No maintainers set for project.")
if not maintainers:
return None
return {'maintainers': maintainers, 'reviewers': reviewers, 'lists': 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)
PARSER.add_argument('-g', '--github',
action='store_true',
help='Include GitHub usernames in output',
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)
# Accumulate a sorted list of addresses
ADDRESSES = set([])
for file in FILES:
print(file)
recipients = get_maintainers(file, SECTIONS)
ADDRESSES |= set(recipients['maintainers'] + recipients['reviewers'] + recipients['lists'])
ADDRESSES = list(ADDRESSES)
ADDRESSES.sort()
for address in ADDRESSES:
if '<' in address and '>' in address:
address, github_id = address.split('>', 1)
address = address + '>'
github_id = github_id.strip() if ARGS.github else ''
print(' %s %s' % (address, github_id))