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