## @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))