blob: 72cbeeacd370d2af0233362fcda5ff795114fb20 [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>.*>)')
20
21
22def get_args():
23 parser = argparse.ArgumentParser(description='Print the maintainers for '
24 'the given source files or directories; '
25 'or for the files modified by a patch or '
26 'a pull request. '
27 '(With -m) Check if a patch or pull '
28 'request is properly Acked/Reviewed for '
29 'merging.')
30 parser.add_argument('-m', '--merge-check', action='store_true',
31 help='use Reviewed-by: and Acked-by: tags found in '
32 'patches to prevent display of information for all '
33 'the approved paths.')
34 parser.add_argument('-p', '--show-paths', action='store_true',
35 help='show all paths that are not approved.')
36 parser.add_argument('-s', '--strict', action='store_true',
37 help='stricter conditions for patch approval check: '
38 'subsystem "THE REST" is ignored for paths that '
39 'match some other subsystem.')
40 parser.add_argument('arg', nargs='*', help='file or patch')
41 parser.add_argument('-f', '--file', action='append',
42 help='treat following argument as a file path, not '
43 'a patch.')
44 parser.add_argument('-g', '--github-pr', action='append', type=int,
45 help='Github pull request ID. The script will '
46 'download the patchset from Github to a temporary '
47 'file and process it.')
48 return parser.parse_args()
49
50
51# Parse MAINTAINERS and return a dictionary of subsystems such as:
52# {'Subsystem name': {'R': ['foo', 'bar'], 'S': ['Maintained'],
53# 'F': [ 'path1', 'path2' ]}, ...}
54def parse_maintainers():
55 subsystems = {}
56 cwd = os.getcwd()
57 parent = os.path.dirname(os.path.realpath(__file__)) + "/../"
58 if (os.path.realpath(cwd) != os.path.realpath(parent)):
59 print("Error: this script must be run from the top-level of the "
60 "optee_os tree")
61 exit(1)
62 with open("MAINTAINERS", "r") as f:
63 start_found = False
64 ss = {}
65 name = ''
66 for line in f:
67 line = line.strip()
68 if not line:
69 continue
70 if not start_found:
71 if line.startswith("----------"):
72 start_found = True
73 continue
74
75 if line[1] == ':':
76 letter = line[0]
77 if (not ss.get(letter)):
78 ss[letter] = []
79 ss[letter].append(line[3:])
80 else:
81 if name:
82 subsystems[name] = ss
83 name = line
84 ss = {}
85 if name:
86 subsystems[name] = ss
87
88 return subsystems
89
90
91# If @path is a patch file, returns the paths touched by the patch as well
92# as the content of the review/ack tags
93def get_paths_from_patchset(patch):
94 paths = []
95 approvers = []
96 try:
97 with open(patch, "r") as f:
98 for line in f:
99 match = re.search(DIFF_GIT_RE, line)
100 if match:
101 p = match.group('path')
102 if p not in paths:
103 paths.append(p)
104 continue
105 match = re.search(REVIEWED_RE, line)
106 if match:
107 a = match.group('approver')
108 if a not in approvers:
109 approvers.append(a)
110 continue
111 match = re.search(ACKED_RE, line)
112 if match:
113 a = match.group('approver')
114 if a not in approvers:
115 approvers.append(a)
116 continue
117 except Exception:
118 pass
119 return (paths, approvers)
120
121
122# Does @path match @pattern?
123# @pattern has the syntax defined in the Linux MAINTAINERS file -- mostly a
124# shell glob pattern, except that a trailing slash means a directory and
125# everything below. Matching can easily be done by converting to a regexp.
126def match_pattern(path, pattern):
127 # Append a trailing slash if path is an existing directory, so that it
128 # matches F: entries such as 'foo/bar/'
129 if not path.endswith('/') and os.path.isdir(path):
130 path += '/'
131 rep = "^" + pattern
132 rep = rep.replace('*', '[^/]+')
133 rep = rep.replace('?', '[^/]')
134 if rep.endswith('/'):
135 rep += '.*'
136 rep += '$'
137 return not not re.match(rep, path)
138
139
140def get_subsystems_for_path(subsystems, path, strict):
141 found = {}
142 for key in subsystems:
143 def inner():
144 excluded = subsystems[key].get('X')
145 if excluded:
146 for pattern in excluded:
147 if match_pattern(path, pattern):
148 return # next key
149 included = subsystems[key].get('F')
150 if not included:
151 return # next key
152 for pattern in included:
153 if match_pattern(path, pattern):
154 found[key] = subsystems[key]
155 inner()
156 if strict and len(found) > 1:
157 found.pop('THE REST', None)
158 return found
159
160
161def get_ss_maintainers(subsys):
162 return subsys.get('M') or []
163
164
165def get_ss_reviewers(subsys):
166 return subsys.get('R') or []
167
168
169def get_ss_approvers(ss):
170 return get_ss_maintainers(ss) + get_ss_reviewers(ss)
171
172
173def approvers_have_approved(approved_by, approvers):
174 for n in approvers:
175 # Ignore anything after the email (Github ID...)
176 n = n.split('>', 1)[0]
177 for m in approved_by:
178 m = m.split('>', 1)[0]
179 if n == m:
180 return True
181 return False
182
183
184def download(pr):
185 url = "https://github.com/OP-TEE/optee_os/pull/{}.patch".format(pr)
186 f = tempfile.NamedTemporaryFile(mode="wb", prefix="pr{}_".format(pr),
187 suffix=".patch", delete=False)
188 print("Downloading {}...".format(url), end='', flush=True)
189 f.write(urlopen(url).read())
190 print(" Done.")
191 return f.name
192
193
194def main():
195 global args
196
197 args = get_args()
198 all_subsystems = parse_maintainers()
199 paths = []
200 downloads = []
201
202 for pr in args.github_pr or []:
203 downloads += [download(pr)]
204
205 for arg in args.arg + downloads:
206 patch_paths = []
207 approved_by = []
208 if os.path.exists(arg):
209 # Try to parse as a patch or patch set
210 (patch_paths, approved_by) = get_paths_from_patchset(arg)
211 if not patch_paths:
212 # Not a patch, consider the path itself
213 # as_posix() cleans the path a little bit (suppress leading ./ and
214 # duplicate slashes...)
215 patch_paths = [PurePath(arg).as_posix()]
216 for path in patch_paths:
217 approved = False
218 if args.merge_check:
219 ss_for_path = get_subsystems_for_path(all_subsystems, path,
220 args.strict)
221 for key in ss_for_path:
222 ss_approvers = get_ss_approvers(ss_for_path[key])
223 if approvers_have_approved(approved_by, ss_approvers):
224 approved = True
225 if not approved:
226 paths += [path]
227
228 for f in downloads:
229 os.remove(f)
230
231 if args.file:
232 paths += args.file
233
234 if (args.show_paths):
235 print(paths)
236
237 ss = {}
238 for path in paths:
239 ss.update(get_subsystems_for_path(all_subsystems, path, args.strict))
240 for key in ss:
241 ss_name = key[:50] + (key[50:] and '...')
242 for name in ss[key].get('M') or []:
243 print("{} (maintainer:{})".format(name, ss_name))
244 for name in ss[key].get('R') or []:
245 print("{} (reviewer:{})".format(name, ss_name))
246
247
248if __name__ == "__main__":
249 main()