blob: 3431fea7471f9e0357c45da5053e0dcf9acc4369 [file] [log] [blame]
Jerome Forissier28481ff2019-08-09 10:46:02 +02001#!/usr/bin/env python3
2#
3# Copyright (c) 2019, Linaro Limited
4#
5# SPDX-License-Identifier: BSD-2-Clause
6
7from pathlib import PurePath
8from urllib.request import urlopen
9
10import argparse
11import glob
12import os
13import re
14import tempfile
15
16
17DIFF_GIT_RE = re.compile(r'^diff --git a/(?P<path>.*) ')
18REVIEWED_RE = re.compile(r'^Reviewed-by: (?P<approver>.*>)')
19ACKED_RE = re.compile(r'^Acked-by: (?P<approver>.*>)')
Jerome Forissier716f4422019-08-12 16:43:02 +020020PATCH_START = re.compile(r'^From [0-9a-f]{40}')
Jerome Forissier28481ff2019-08-09 10:46:02 +020021
22
23def get_args():
24 parser = argparse.ArgumentParser(description='Print the maintainers for '
25 'the given source files or directories; '
26 'or for the files modified by a patch or '
27 'a pull request. '
28 '(With -m) Check if a patch or pull '
29 'request is properly Acked/Reviewed for '
30 'merging.')
31 parser.add_argument('-m', '--merge-check', action='store_true',
32 help='use Reviewed-by: and Acked-by: tags found in '
33 'patches to prevent display of information for all '
34 'the approved paths.')
35 parser.add_argument('-p', '--show-paths', action='store_true',
36 help='show all paths that are not approved.')
37 parser.add_argument('-s', '--strict', action='store_true',
38 help='stricter conditions for patch approval check: '
39 'subsystem "THE REST" is ignored for paths that '
40 'match some other subsystem.')
41 parser.add_argument('arg', nargs='*', help='file or patch')
42 parser.add_argument('-f', '--file', action='append',
43 help='treat following argument as a file path, not '
44 'a patch.')
45 parser.add_argument('-g', '--github-pr', action='append', type=int,
46 help='Github pull request ID. The script will '
47 'download the patchset from Github to a temporary '
48 'file and process it.')
Jerome Forissier72ec5fd2020-02-20 08:20:15 +010049 parser.add_argument('-r', '--release-to', action='store_true',
50 help='show all the recipients to be used in release '
51 'announcement emails (i.e., maintainers and reviewers)'
52 'and exit.')
Jerome Forissier28481ff2019-08-09 10:46:02 +020053 return parser.parse_args()
54
55
Jerome Forissier72ec5fd2020-02-20 08:20:15 +010056def check_cwd():
57 cwd = os.getcwd()
58 parent = os.path.dirname(os.path.realpath(__file__)) + "/../"
59 if (os.path.realpath(cwd) != os.path.realpath(parent)):
60 print("Error: this script must be run from the top-level of the "
61 "optee_os tree")
62 exit(1)
63
64
Jerome Forissier28481ff2019-08-09 10:46:02 +020065# Parse MAINTAINERS and return a dictionary of subsystems such as:
66# {'Subsystem name': {'R': ['foo', 'bar'], 'S': ['Maintained'],
67# 'F': [ 'path1', 'path2' ]}, ...}
68def parse_maintainers():
69 subsystems = {}
Jerome Forissier72ec5fd2020-02-20 08:20:15 +010070 check_cwd()
Jerome Forissier28481ff2019-08-09 10:46:02 +020071 with open("MAINTAINERS", "r") as f:
72 start_found = False
73 ss = {}
74 name = ''
75 for line in f:
76 line = line.strip()
77 if not line:
78 continue
79 if not start_found:
80 if line.startswith("----------"):
81 start_found = True
82 continue
83
84 if line[1] == ':':
85 letter = line[0]
86 if (not ss.get(letter)):
87 ss[letter] = []
88 ss[letter].append(line[3:])
89 else:
90 if name:
91 subsystems[name] = ss
92 name = line
93 ss = {}
94 if name:
95 subsystems[name] = ss
96
97 return subsystems
98
99
Jerome Forissier716f4422019-08-12 16:43:02 +0200100# If @patchset is a patchset files and contains 2 patches or more, write
101# individual patches to temporary files and return the paths.
102# Otherwise return [].
103def split_patchset(patchset):
104 psname = os.path.basename(patchset).replace('.', '_')
105 patchnum = 0
106 of = None
107 ret = []
108 f = None
109 try:
110 f = open(patchset, "r")
Jerome Forissier71c9b072019-09-03 08:32:38 +0200111 except OSError:
Jerome Forissier716f4422019-08-12 16:43:02 +0200112 return []
113 for line in f:
114 match = re.search(PATCH_START, line)
115 if match:
116 # New patch found: create new file
117 patchnum += 1
118 prefix = "{}_{}_".format(patchnum, psname)
119 of = tempfile.NamedTemporaryFile(mode="w", prefix=prefix,
120 suffix=".patch",
121 delete=False)
122 ret.append(of.name)
123 if of:
124 of.write(line)
125 if len(ret) >= 2:
126 return ret
127 if len(ret) == 1:
128 os.remove(ret[0])
129 return []
130
131
Jerome Forissier28481ff2019-08-09 10:46:02 +0200132# If @path is a patch file, returns the paths touched by the patch as well
133# as the content of the review/ack tags
Jerome Forissier716f4422019-08-12 16:43:02 +0200134def get_paths_from_patch(patch):
Jerome Forissier28481ff2019-08-09 10:46:02 +0200135 paths = []
136 approvers = []
137 try:
138 with open(patch, "r") as f:
139 for line in f:
140 match = re.search(DIFF_GIT_RE, line)
141 if match:
142 p = match.group('path')
143 if p not in paths:
144 paths.append(p)
145 continue
146 match = re.search(REVIEWED_RE, line)
147 if match:
148 a = match.group('approver')
149 if a not in approvers:
150 approvers.append(a)
151 continue
152 match = re.search(ACKED_RE, line)
153 if match:
154 a = match.group('approver')
155 if a not in approvers:
156 approvers.append(a)
157 continue
158 except Exception:
159 pass
160 return (paths, approvers)
161
162
163# Does @path match @pattern?
164# @pattern has the syntax defined in the Linux MAINTAINERS file -- mostly a
165# shell glob pattern, except that a trailing slash means a directory and
166# everything below. Matching can easily be done by converting to a regexp.
167def match_pattern(path, pattern):
168 # Append a trailing slash if path is an existing directory, so that it
169 # matches F: entries such as 'foo/bar/'
170 if not path.endswith('/') and os.path.isdir(path):
171 path += '/'
172 rep = "^" + pattern
173 rep = rep.replace('*', '[^/]+')
174 rep = rep.replace('?', '[^/]')
175 if rep.endswith('/'):
176 rep += '.*'
177 rep += '$'
178 return not not re.match(rep, path)
179
180
181def get_subsystems_for_path(subsystems, path, strict):
182 found = {}
183 for key in subsystems:
184 def inner():
185 excluded = subsystems[key].get('X')
186 if excluded:
187 for pattern in excluded:
188 if match_pattern(path, pattern):
189 return # next key
190 included = subsystems[key].get('F')
191 if not included:
192 return # next key
193 for pattern in included:
194 if match_pattern(path, pattern):
195 found[key] = subsystems[key]
196 inner()
197 if strict and len(found) > 1:
198 found.pop('THE REST', None)
199 return found
200
201
202def get_ss_maintainers(subsys):
203 return subsys.get('M') or []
204
205
206def get_ss_reviewers(subsys):
207 return subsys.get('R') or []
208
209
210def get_ss_approvers(ss):
211 return get_ss_maintainers(ss) + get_ss_reviewers(ss)
212
213
214def approvers_have_approved(approved_by, approvers):
215 for n in approvers:
216 # Ignore anything after the email (Github ID...)
217 n = n.split('>', 1)[0]
218 for m in approved_by:
219 m = m.split('>', 1)[0]
220 if n == m:
221 return True
222 return False
223
224
225def download(pr):
226 url = "https://github.com/OP-TEE/optee_os/pull/{}.patch".format(pr)
227 f = tempfile.NamedTemporaryFile(mode="wb", prefix="pr{}_".format(pr),
228 suffix=".patch", delete=False)
229 print("Downloading {}...".format(url), end='', flush=True)
230 f.write(urlopen(url).read())
231 print(" Done.")
232 return f.name
233
234
Jerome Forissier72ec5fd2020-02-20 08:20:15 +0100235def show_release_to():
236 check_cwd()
237 with open("MAINTAINERS", "r") as f:
238 emails = sorted(set(re.findall(r'[RM]:\t(.*[\w]*<[\w\.-]+@[\w\.-]+>)',
239 f.read())))
240 print(*emails, sep=', ')
241
242
Jerome Forissier28481ff2019-08-09 10:46:02 +0200243def main():
244 global args
245
246 args = get_args()
Jerome Forissier72ec5fd2020-02-20 08:20:15 +0100247
248 if args.release_to:
249 show_release_to()
250 return
251
Jerome Forissier28481ff2019-08-09 10:46:02 +0200252 all_subsystems = parse_maintainers()
253 paths = []
Jerome Forissier716f4422019-08-12 16:43:02 +0200254 arglist = []
Jerome Forissier28481ff2019-08-09 10:46:02 +0200255 downloads = []
Jerome Forissier716f4422019-08-12 16:43:02 +0200256 split_patches = []
Jerome Forissier28481ff2019-08-09 10:46:02 +0200257
258 for pr in args.github_pr or []:
259 downloads += [download(pr)]
260
261 for arg in args.arg + downloads:
Jerome Forissier716f4422019-08-12 16:43:02 +0200262 if os.path.exists(arg):
263 patches = split_patchset(arg)
264 if patches:
265 split_patches += patches
266 continue
267 arglist.append(arg)
268
269 for arg in arglist + split_patches:
Jerome Forissier28481ff2019-08-09 10:46:02 +0200270 patch_paths = []
271 approved_by = []
272 if os.path.exists(arg):
Jerome Forissier716f4422019-08-12 16:43:02 +0200273 # Try to parse as a patch
274 (patch_paths, approved_by) = get_paths_from_patch(arg)
Jerome Forissier28481ff2019-08-09 10:46:02 +0200275 if not patch_paths:
276 # Not a patch, consider the path itself
277 # as_posix() cleans the path a little bit (suppress leading ./ and
278 # duplicate slashes...)
279 patch_paths = [PurePath(arg).as_posix()]
280 for path in patch_paths:
281 approved = False
282 if args.merge_check:
283 ss_for_path = get_subsystems_for_path(all_subsystems, path,
284 args.strict)
285 for key in ss_for_path:
286 ss_approvers = get_ss_approvers(ss_for_path[key])
287 if approvers_have_approved(approved_by, ss_approvers):
288 approved = True
289 if not approved:
290 paths += [path]
291
Jerome Forissier716f4422019-08-12 16:43:02 +0200292 for f in downloads + split_patches:
Jerome Forissier28481ff2019-08-09 10:46:02 +0200293 os.remove(f)
294
295 if args.file:
296 paths += args.file
297
298 if (args.show_paths):
299 print(paths)
300
301 ss = {}
302 for path in paths:
303 ss.update(get_subsystems_for_path(all_subsystems, path, args.strict))
304 for key in ss:
305 ss_name = key[:50] + (key[50:] and '...')
306 for name in ss[key].get('M') or []:
307 print("{} (maintainer:{})".format(name, ss_name))
308 for name in ss[key].get('R') or []:
309 print("{} (reviewer:{})".format(name, ss_name))
310
311
312if __name__ == "__main__":
313 main()