]>
Commit | Line | Data |
---|---|---|
7d0a56c4 LL |
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 | ||
9 | from __future__ import print_function | |
10 | from collections import defaultdict | |
11 | from collections import OrderedDict | |
12 | import argparse | |
13 | import os | |
14 | import re | |
15 | import SetupGit | |
16 | ||
17 | EXPRESSIONS = { | |
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 | ||
28 | def 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 | ||
36 | def 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 | ||
52 | def 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 | ||
75 | def 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 | ||
81 | if path_in_section(path, section): | |
82 | for address in section['maintainer'], section['reviewer']: | |
83 | # Convert to list if necessary | |
84 | if isinstance(address, list): | |
85 | maintainers += address | |
86 | else: | |
87 | lists += [address] | |
88 | for address in section['list']: | |
89 | # Convert to list if necessary | |
90 | if isinstance(address, list): | |
91 | lists += address | |
92 | else: | |
93 | lists += [address] | |
94 | ||
95 | return maintainers, lists | |
96 | ||
97 | def get_maintainers(path, sections, level=0): | |
98 | """For 'path', iterates over all sections, returning maintainers | |
99 | for matching ones.""" | |
100 | maintainers = [] | |
101 | lists = [] | |
102 | for section in sections: | |
103 | tmp_maint, tmp_lists = get_section_maintainers(path, section) | |
104 | if tmp_maint: | |
105 | maintainers += tmp_maint | |
106 | if tmp_lists: | |
107 | lists += tmp_lists | |
108 | ||
109 | if not maintainers: | |
110 | # If no match found, look for match for (nonexistent) file | |
111 | # REPO.working_dir/<default> | |
112 | print('"%s": no maintainers found, looking for default' % path) | |
113 | if level == 0: | |
114 | maintainers = get_maintainers('<default>', sections, level=level + 1) | |
115 | else: | |
116 | print("No <default> maintainers set for project.") | |
117 | if not maintainers: | |
118 | return None | |
119 | ||
120 | return maintainers + lists | |
121 | ||
122 | def parse_maintainers_line(line): | |
123 | """Parse one line of Maintainers.txt, returning any match group and its key.""" | |
124 | for key, expression in EXPRESSIONS.items(): | |
125 | match = expression.match(line) | |
126 | if match: | |
127 | return key, match.group(key) | |
128 | return None, None | |
129 | ||
130 | def parse_maintainers_file(filename): | |
131 | """Parse the Maintainers.txt from top-level of repo and | |
132 | return a list containing dictionaries of all sections.""" | |
133 | with open(filename, 'r') as text: | |
134 | line = text.readline() | |
135 | sectionlist = [] | |
136 | section = defaultdict(list) | |
137 | while line: | |
138 | key, value = parse_maintainers_line(line) | |
139 | if key and value: | |
140 | section[key].append(value) | |
141 | ||
142 | line = text.readline() | |
143 | # If end of section (end of file, or non-tag line encountered)... | |
144 | if not key or not value or not line: | |
145 | # ...if non-empty, append section to list. | |
146 | if section: | |
147 | sectionlist.append(section.copy()) | |
148 | section.clear() | |
149 | ||
150 | return sectionlist | |
151 | ||
152 | def get_modified_files(repo, args): | |
153 | """Returns a list of the files modified by the commit specified in 'args'.""" | |
154 | commit = repo.commit(args.commit) | |
155 | return commit.stats.files | |
156 | ||
157 | if __name__ == '__main__': | |
158 | PARSER = argparse.ArgumentParser( | |
159 | description='Retrieves information on who to cc for review on a given commit') | |
160 | PARSER.add_argument('commit', | |
161 | action="store", | |
162 | help='git revision to examine (default: HEAD)', | |
163 | nargs='?', | |
164 | default='HEAD') | |
165 | PARSER.add_argument('-l', '--lookup', | |
166 | help='Find section matches for path LOOKUP', | |
167 | required=False) | |
168 | ARGS = PARSER.parse_args() | |
169 | ||
170 | REPO = SetupGit.locate_repo() | |
171 | ||
172 | CONFIG_FILE = os.path.join(REPO.working_dir, 'Maintainers.txt') | |
173 | ||
174 | SECTIONS = parse_maintainers_file(CONFIG_FILE) | |
175 | ||
176 | if ARGS.lookup: | |
177 | FILES = [ARGS.lookup] | |
178 | else: | |
179 | FILES = get_modified_files(REPO, ARGS) | |
180 | ||
181 | ADDRESSES = [] | |
182 | ||
183 | for file in FILES: | |
184 | print(file) | |
185 | addresslist = get_maintainers(file, SECTIONS) | |
186 | if addresslist: | |
187 | ADDRESSES += addresslist | |
188 | ||
189 | for address in list(OrderedDict.fromkeys(ADDRESSES)): | |
190 | print(' %s' % address) |