1#!/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>.*>)') 20PATCH_START = re.compile(r'^From [0-9a-f]{40}') 21 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.') 49 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, reviewers ' 52 'and OP-TEE mailing list(s)) and exit.') 53 return parser.parse_args() 54 55 56def 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 65# 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 = {} 70 check_cwd() 71 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 100# 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") 111 except OSError: 112 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 132# If @path is a patch file, returns the paths touched by the patch as well 133# as the content of the review/ack tags 134def get_paths_from_patch(patch): 135 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 get_ss_lists(subsys): 215 return subsys.get('L') or [] 216 217 218def approvers_have_approved(approved_by, approvers): 219 for n in approvers: 220 # Ignore anything after the email (Github ID...) 221 n = n.split('>', 1)[0] 222 for m in approved_by: 223 m = m.split('>', 1)[0] 224 if n == m: 225 return True 226 return False 227 228 229def download(pr): 230 url = "https://github.com/OP-TEE/optee_os/pull/{}.patch".format(pr) 231 f = tempfile.NamedTemporaryFile(mode="wb", prefix="pr{}_".format(pr), 232 suffix=".patch", delete=False) 233 print("Downloading {}...".format(url), end='', flush=True) 234 f.write(urlopen(url).read()) 235 print(" Done.") 236 return f.name 237 238 239def show_release_to(subsystems): 240 check_cwd() 241 with open("MAINTAINERS", "r") as f: 242 emails = sorted(set(re.findall(r'[RM]:\t(.*[\w]*<[\w\.-]+@[\w\.-]+>)', 243 f.read()))) 244 emails += get_ss_lists(subsystems["THE REST"]) 245 print(*emails, sep=', ') 246 247 248def main(): 249 global args 250 251 args = get_args() 252 253 all_subsystems = parse_maintainers() 254 255 if args.release_to: 256 show_release_to(all_subsystems) 257 return 258 259 paths = [] 260 arglist = [] 261 downloads = [] 262 split_patches = [] 263 264 for pr in args.github_pr or []: 265 downloads += [download(pr)] 266 267 for arg in args.arg + downloads: 268 if os.path.exists(arg): 269 patches = split_patchset(arg) 270 if patches: 271 split_patches += patches 272 continue 273 arglist.append(arg) 274 275 for arg in arglist + split_patches: 276 patch_paths = [] 277 approved_by = [] 278 if os.path.exists(arg): 279 # Try to parse as a patch 280 (patch_paths, approved_by) = get_paths_from_patch(arg) 281 if not patch_paths: 282 # Not a patch, consider the path itself 283 # as_posix() cleans the path a little bit (suppress leading ./ and 284 # duplicate slashes...) 285 patch_paths = [PurePath(arg).as_posix()] 286 for path in patch_paths: 287 approved = False 288 if args.merge_check: 289 ss_for_path = get_subsystems_for_path(all_subsystems, path, 290 args.strict) 291 for key in ss_for_path: 292 ss_approvers = get_ss_approvers(ss_for_path[key]) 293 if approvers_have_approved(approved_by, ss_approvers): 294 approved = True 295 if not approved: 296 paths += [path] 297 298 for f in downloads + split_patches: 299 os.remove(f) 300 301 if args.file: 302 paths += args.file 303 304 if (args.show_paths): 305 print(paths) 306 307 ss = {} 308 for path in paths: 309 ss.update(get_subsystems_for_path(all_subsystems, path, args.strict)) 310 for key in ss: 311 ss_name = key[:50] + (key[50:] and '...') 312 for name in ss[key].get('M') or []: 313 print("{} (maintainer:{})".format(name, ss_name)) 314 for name in ss[key].get('R') or []: 315 print("{} (reviewer:{})".format(name, ss_name)) 316 317 318if __name__ == "__main__": 319 main() 320