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 and reviewers)' 52 '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 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 235def 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 243def main(): 244 global args 245 246 args = get_args() 247 248 if args.release_to: 249 show_release_to() 250 return 251 252 all_subsystems = parse_maintainers() 253 paths = [] 254 arglist = [] 255 downloads = [] 256 split_patches = [] 257 258 for pr in args.github_pr or []: 259 downloads += [download(pr)] 260 261 for arg in args.arg + downloads: 262 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: 270 patch_paths = [] 271 approved_by = [] 272 if os.path.exists(arg): 273 # Try to parse as a patch 274 (patch_paths, approved_by) = get_paths_from_patch(arg) 275 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 292 for f in downloads + split_patches: 293 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() 314