1## @file 2# Retrieves the people to request review from on submission of a commit. 3# 4# Copyright (c) 2019, Linaro Ltd. All rights reserved.<BR> 5# 6# SPDX-License-Identifier: BSD-2-Clause-Patent 7# 8 9from __future__ import print_function 10from collections import defaultdict 11from collections import OrderedDict 12import argparse 13import os 14import re 15import SetupGit 16 17EXPRESSIONS = { 18 'exclude': re.compile(r'^X:\s*(?P<exclude>.*?)\r*$'), 19 'file': re.compile(r'^F:\s*(?P<file>.*?)\r*$'), 20 'list': re.compile(r'^L:\s*(?P<list>.*?)\r*$'), 21 'maintainer': re.compile(r'^M:\s*(?P<maintainer>.*<.*?>)\r*$'), 22 'reviewer': re.compile(r'^R:\s*(?P<reviewer>.*?)\r*$'), 23 'status': re.compile(r'^S:\s*(?P<status>.*?)\r*$'), 24 'tree': re.compile(r'^T:\s*(?P<tree>.*?)\r*$'), 25 'webpage': re.compile(r'^W:\s*(?P<webpage>.*?)\r*$') 26} 27 28def printsection(section): 29 """Prints out the dictionary describing a Maintainers.txt section.""" 30 print('===') 31 for key in section.keys(): 32 print("Key: %s" % key) 33 for item in section[key]: 34 print(' %s' % item) 35 36def pattern_to_regex(pattern): 37 """Takes a string containing regular UNIX path wildcards 38 and returns a string suitable for matching with regex.""" 39 40 pattern = pattern.replace('.', r'\.') 41 pattern = pattern.replace('?', r'.') 42 pattern = pattern.replace('*', r'.*') 43 44 if pattern.endswith('/'): 45 pattern += r'.*' 46 elif pattern.endswith('.*'): 47 pattern = pattern[:-2] 48 pattern += r'(?!.*?/.*?)' 49 50 return pattern 51 52def path_in_section(path, section): 53 """Returns True of False indicating whether the path is covered by 54 the current section.""" 55 if not 'file' in section: 56 return False 57 58 for pattern in section['file']: 59 regex = pattern_to_regex(pattern) 60 61 match = re.match(regex, path) 62 if match: 63 # Check if there is an exclude pattern that applies 64 for pattern in section['exclude']: 65 regex = pattern_to_regex(pattern) 66 67 match = re.match(regex, path) 68 if match: 69 return False 70 71 return True 72 73 return False 74 75def get_section_maintainers(path, section): 76 """Returns a list with email addresses to any M: and R: entries 77 matching the provided path in the provided section.""" 78 maintainers = [] 79 lists = [] 80 nowarn_status = ['Supported', 'Maintained'] 81 82 if path_in_section(path, section): 83 for status in section['status']: 84 if status not in nowarn_status: 85 print('WARNING: Maintained status for "%s" is \'%s\'!' % (path, status)) 86 for address in section['maintainer'], section['reviewer']: 87 # Convert to list if necessary 88 if isinstance(address, list): 89 maintainers += address 90 else: 91 lists += [address] 92 for address in section['list']: 93 # Convert to list if necessary 94 if isinstance(address, list): 95 lists += address 96 else: 97 lists += [address] 98 99 return maintainers, lists 100 101def get_maintainers(path, sections, level=0): 102 """For 'path', iterates over all sections, returning maintainers 103 for matching ones.""" 104 maintainers = [] 105 lists = [] 106 for section in sections: 107 tmp_maint, tmp_lists = get_section_maintainers(path, section) 108 if tmp_maint: 109 maintainers += tmp_maint 110 if tmp_lists: 111 lists += tmp_lists 112 113 if not maintainers: 114 # If no match found, look for match for (nonexistent) file 115 # REPO.working_dir/<default> 116 print('"%s": no maintainers found, looking for default' % path) 117 if level == 0: 118 maintainers = get_maintainers('<default>', sections, level=level + 1) 119 else: 120 print("No <default> maintainers set for project.") 121 if not maintainers: 122 return None 123 124 return maintainers + lists 125 126def parse_maintainers_line(line): 127 """Parse one line of Maintainers.txt, returning any match group and its key.""" 128 for key, expression in EXPRESSIONS.items(): 129 match = expression.match(line) 130 if match: 131 return key, match.group(key) 132 return None, None 133 134def parse_maintainers_file(filename): 135 """Parse the Maintainers.txt from top-level of repo and 136 return a list containing dictionaries of all sections.""" 137 with open(filename, 'r') as text: 138 line = text.readline() 139 sectionlist = [] 140 section = defaultdict(list) 141 while line: 142 key, value = parse_maintainers_line(line) 143 if key and value: 144 section[key].append(value) 145 146 line = text.readline() 147 # If end of section (end of file, or non-tag line encountered)... 148 if not key or not value or not line: 149 # ...if non-empty, append section to list. 150 if section: 151 sectionlist.append(section.copy()) 152 section.clear() 153 154 return sectionlist 155 156def get_modified_files(repo, args): 157 """Returns a list of the files modified by the commit specified in 'args'.""" 158 commit = repo.commit(args.commit) 159 return commit.stats.files 160 161if __name__ == '__main__': 162 PARSER = argparse.ArgumentParser( 163 description='Retrieves information on who to cc for review on a given commit') 164 PARSER.add_argument('commit', 165 action="store", 166 help='git revision to examine (default: HEAD)', 167 nargs='?', 168 default='HEAD') 169 PARSER.add_argument('-l', '--lookup', 170 help='Find section matches for path LOOKUP', 171 required=False) 172 ARGS = PARSER.parse_args() 173 174 REPO = SetupGit.locate_repo() 175 176 CONFIG_FILE = os.path.join(REPO.working_dir, 'Maintainers.txt') 177 178 SECTIONS = parse_maintainers_file(CONFIG_FILE) 179 180 if ARGS.lookup: 181 FILES = [ARGS.lookup] 182 else: 183 FILES = get_modified_files(REPO, ARGS) 184 185 ADDRESSES = [] 186 187 for file in FILES: 188 print(file) 189 addresslist = get_maintainers(file, SECTIONS) 190 if addresslist: 191 ADDRESSES += addresslist 192 193 for address in list(OrderedDict.fromkeys(ADDRESSES)): 194 print(' %s' % address) 195