| ## @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 = [] | |
| 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/<default> | |
| print('"%s": no maintainers found, looking for default' % path) | |
| if level == 0: | |
| recipients = get_maintainers('<default>', sections, level=level + 1) | |
| maintainers += recipients['maintainers'] | |
| reviewers += recipients['reviewers'] | |
| lists += recipients['lists'] | |
| else: | |
| print("No <default> 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)) |