1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
|
## @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 = []
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'], section['reviewer']:
# Convert to list if necessary
if isinstance(address, list):
maintainers += address
else:
maintainers += [address]
for address in section['list']:
# Convert to list if necessary
if isinstance(address, list):
lists += address
else:
lists += [address]
return {'maintainers': maintainers, 'lists': lists}
def get_maintainers(path, sections, level=0):
"""For 'path', iterates over all sections, returning maintainers
for matching ones."""
maintainers = []
lists = []
for section in sections:
recipients = get_section_maintainers(path, section)
maintainers += recipients['maintainers']
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']
lists += recipients['lists']
else:
print("No <default> maintainers set for project.")
if not maintainers:
return None
return {'maintainers': maintainers, '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)
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)
ADDRESSES = []
for file in FILES:
print(file)
recipients = get_maintainers(file, SECTIONS)
ADDRESSES += recipients['maintainers'] + recipients['lists']
for address in list(OrderedDict.fromkeys(ADDRESSES)):
if '<' in address and '>' in address:
address = address.split('>', 1)[0] + '>'
print(' %s' % address)
|