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>.*>)') 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() 250