128481ff3SJerome Forissier#!/usr/bin/env python3 228481ff3SJerome Forissier# 328481ff3SJerome Forissier# Copyright (c) 2019, Linaro Limited 428481ff3SJerome Forissier# 528481ff3SJerome Forissier# SPDX-License-Identifier: BSD-2-Clause 628481ff3SJerome Forissier 728481ff3SJerome Forissierfrom pathlib import PurePath 828481ff3SJerome Forissierfrom urllib.request import urlopen 928481ff3SJerome Forissier 1028481ff3SJerome Forissierimport argparse 1128481ff3SJerome Forissierimport glob 1228481ff3SJerome Forissierimport os 1328481ff3SJerome Forissierimport re 1428481ff3SJerome Forissierimport tempfile 1528481ff3SJerome Forissier 1628481ff3SJerome Forissier 1728481ff3SJerome ForissierDIFF_GIT_RE = re.compile(r'^diff --git a/(?P<path>.*) ') 1828481ff3SJerome ForissierREVIEWED_RE = re.compile(r'^Reviewed-by: (?P<approver>.*>)') 1928481ff3SJerome ForissierACKED_RE = re.compile(r'^Acked-by: (?P<approver>.*>)') 20*716f442dSJerome ForissierPATCH_START = re.compile(r'^From [0-9a-f]{40}') 2128481ff3SJerome Forissier 2228481ff3SJerome Forissier 2328481ff3SJerome Forissierdef get_args(): 2428481ff3SJerome Forissier parser = argparse.ArgumentParser(description='Print the maintainers for ' 2528481ff3SJerome Forissier 'the given source files or directories; ' 2628481ff3SJerome Forissier 'or for the files modified by a patch or ' 2728481ff3SJerome Forissier 'a pull request. ' 2828481ff3SJerome Forissier '(With -m) Check if a patch or pull ' 2928481ff3SJerome Forissier 'request is properly Acked/Reviewed for ' 3028481ff3SJerome Forissier 'merging.') 3128481ff3SJerome Forissier parser.add_argument('-m', '--merge-check', action='store_true', 3228481ff3SJerome Forissier help='use Reviewed-by: and Acked-by: tags found in ' 3328481ff3SJerome Forissier 'patches to prevent display of information for all ' 3428481ff3SJerome Forissier 'the approved paths.') 3528481ff3SJerome Forissier parser.add_argument('-p', '--show-paths', action='store_true', 3628481ff3SJerome Forissier help='show all paths that are not approved.') 3728481ff3SJerome Forissier parser.add_argument('-s', '--strict', action='store_true', 3828481ff3SJerome Forissier help='stricter conditions for patch approval check: ' 3928481ff3SJerome Forissier 'subsystem "THE REST" is ignored for paths that ' 4028481ff3SJerome Forissier 'match some other subsystem.') 4128481ff3SJerome Forissier parser.add_argument('arg', nargs='*', help='file or patch') 4228481ff3SJerome Forissier parser.add_argument('-f', '--file', action='append', 4328481ff3SJerome Forissier help='treat following argument as a file path, not ' 4428481ff3SJerome Forissier 'a patch.') 4528481ff3SJerome Forissier parser.add_argument('-g', '--github-pr', action='append', type=int, 4628481ff3SJerome Forissier help='Github pull request ID. The script will ' 4728481ff3SJerome Forissier 'download the patchset from Github to a temporary ' 4828481ff3SJerome Forissier 'file and process it.') 4928481ff3SJerome Forissier return parser.parse_args() 5028481ff3SJerome Forissier 5128481ff3SJerome Forissier 5228481ff3SJerome Forissier# Parse MAINTAINERS and return a dictionary of subsystems such as: 5328481ff3SJerome Forissier# {'Subsystem name': {'R': ['foo', 'bar'], 'S': ['Maintained'], 5428481ff3SJerome Forissier# 'F': [ 'path1', 'path2' ]}, ...} 5528481ff3SJerome Forissierdef parse_maintainers(): 5628481ff3SJerome Forissier subsystems = {} 5728481ff3SJerome Forissier cwd = os.getcwd() 5828481ff3SJerome Forissier parent = os.path.dirname(os.path.realpath(__file__)) + "/../" 5928481ff3SJerome Forissier if (os.path.realpath(cwd) != os.path.realpath(parent)): 6028481ff3SJerome Forissier print("Error: this script must be run from the top-level of the " 6128481ff3SJerome Forissier "optee_os tree") 6228481ff3SJerome Forissier exit(1) 6328481ff3SJerome Forissier with open("MAINTAINERS", "r") as f: 6428481ff3SJerome Forissier start_found = False 6528481ff3SJerome Forissier ss = {} 6628481ff3SJerome Forissier name = '' 6728481ff3SJerome Forissier for line in f: 6828481ff3SJerome Forissier line = line.strip() 6928481ff3SJerome Forissier if not line: 7028481ff3SJerome Forissier continue 7128481ff3SJerome Forissier if not start_found: 7228481ff3SJerome Forissier if line.startswith("----------"): 7328481ff3SJerome Forissier start_found = True 7428481ff3SJerome Forissier continue 7528481ff3SJerome Forissier 7628481ff3SJerome Forissier if line[1] == ':': 7728481ff3SJerome Forissier letter = line[0] 7828481ff3SJerome Forissier if (not ss.get(letter)): 7928481ff3SJerome Forissier ss[letter] = [] 8028481ff3SJerome Forissier ss[letter].append(line[3:]) 8128481ff3SJerome Forissier else: 8228481ff3SJerome Forissier if name: 8328481ff3SJerome Forissier subsystems[name] = ss 8428481ff3SJerome Forissier name = line 8528481ff3SJerome Forissier ss = {} 8628481ff3SJerome Forissier if name: 8728481ff3SJerome Forissier subsystems[name] = ss 8828481ff3SJerome Forissier 8928481ff3SJerome Forissier return subsystems 9028481ff3SJerome Forissier 9128481ff3SJerome Forissier 92*716f442dSJerome Forissier# If @patchset is a patchset files and contains 2 patches or more, write 93*716f442dSJerome Forissier# individual patches to temporary files and return the paths. 94*716f442dSJerome Forissier# Otherwise return []. 95*716f442dSJerome Forissierdef split_patchset(patchset): 96*716f442dSJerome Forissier psname = os.path.basename(patchset).replace('.', '_') 97*716f442dSJerome Forissier patchnum = 0 98*716f442dSJerome Forissier of = None 99*716f442dSJerome Forissier ret = [] 100*716f442dSJerome Forissier f = None 101*716f442dSJerome Forissier try: 102*716f442dSJerome Forissier f = open(patchset, "r") 103*716f442dSJerome Forissier except OsError: 104*716f442dSJerome Forissier return [] 105*716f442dSJerome Forissier for line in f: 106*716f442dSJerome Forissier match = re.search(PATCH_START, line) 107*716f442dSJerome Forissier if match: 108*716f442dSJerome Forissier # New patch found: create new file 109*716f442dSJerome Forissier patchnum += 1 110*716f442dSJerome Forissier prefix = "{}_{}_".format(patchnum, psname) 111*716f442dSJerome Forissier of = tempfile.NamedTemporaryFile(mode="w", prefix=prefix, 112*716f442dSJerome Forissier suffix=".patch", 113*716f442dSJerome Forissier delete=False) 114*716f442dSJerome Forissier ret.append(of.name) 115*716f442dSJerome Forissier if of: 116*716f442dSJerome Forissier of.write(line) 117*716f442dSJerome Forissier if len(ret) >= 2: 118*716f442dSJerome Forissier return ret 119*716f442dSJerome Forissier if len(ret) == 1: 120*716f442dSJerome Forissier os.remove(ret[0]) 121*716f442dSJerome Forissier return [] 122*716f442dSJerome Forissier 123*716f442dSJerome Forissier 12428481ff3SJerome Forissier# If @path is a patch file, returns the paths touched by the patch as well 12528481ff3SJerome Forissier# as the content of the review/ack tags 126*716f442dSJerome Forissierdef get_paths_from_patch(patch): 12728481ff3SJerome Forissier paths = [] 12828481ff3SJerome Forissier approvers = [] 12928481ff3SJerome Forissier try: 13028481ff3SJerome Forissier with open(patch, "r") as f: 13128481ff3SJerome Forissier for line in f: 13228481ff3SJerome Forissier match = re.search(DIFF_GIT_RE, line) 13328481ff3SJerome Forissier if match: 13428481ff3SJerome Forissier p = match.group('path') 13528481ff3SJerome Forissier if p not in paths: 13628481ff3SJerome Forissier paths.append(p) 13728481ff3SJerome Forissier continue 13828481ff3SJerome Forissier match = re.search(REVIEWED_RE, line) 13928481ff3SJerome Forissier if match: 14028481ff3SJerome Forissier a = match.group('approver') 14128481ff3SJerome Forissier if a not in approvers: 14228481ff3SJerome Forissier approvers.append(a) 14328481ff3SJerome Forissier continue 14428481ff3SJerome Forissier match = re.search(ACKED_RE, line) 14528481ff3SJerome Forissier if match: 14628481ff3SJerome Forissier a = match.group('approver') 14728481ff3SJerome Forissier if a not in approvers: 14828481ff3SJerome Forissier approvers.append(a) 14928481ff3SJerome Forissier continue 15028481ff3SJerome Forissier except Exception: 15128481ff3SJerome Forissier pass 15228481ff3SJerome Forissier return (paths, approvers) 15328481ff3SJerome Forissier 15428481ff3SJerome Forissier 15528481ff3SJerome Forissier# Does @path match @pattern? 15628481ff3SJerome Forissier# @pattern has the syntax defined in the Linux MAINTAINERS file -- mostly a 15728481ff3SJerome Forissier# shell glob pattern, except that a trailing slash means a directory and 15828481ff3SJerome Forissier# everything below. Matching can easily be done by converting to a regexp. 15928481ff3SJerome Forissierdef match_pattern(path, pattern): 16028481ff3SJerome Forissier # Append a trailing slash if path is an existing directory, so that it 16128481ff3SJerome Forissier # matches F: entries such as 'foo/bar/' 16228481ff3SJerome Forissier if not path.endswith('/') and os.path.isdir(path): 16328481ff3SJerome Forissier path += '/' 16428481ff3SJerome Forissier rep = "^" + pattern 16528481ff3SJerome Forissier rep = rep.replace('*', '[^/]+') 16628481ff3SJerome Forissier rep = rep.replace('?', '[^/]') 16728481ff3SJerome Forissier if rep.endswith('/'): 16828481ff3SJerome Forissier rep += '.*' 16928481ff3SJerome Forissier rep += '$' 17028481ff3SJerome Forissier return not not re.match(rep, path) 17128481ff3SJerome Forissier 17228481ff3SJerome Forissier 17328481ff3SJerome Forissierdef get_subsystems_for_path(subsystems, path, strict): 17428481ff3SJerome Forissier found = {} 17528481ff3SJerome Forissier for key in subsystems: 17628481ff3SJerome Forissier def inner(): 17728481ff3SJerome Forissier excluded = subsystems[key].get('X') 17828481ff3SJerome Forissier if excluded: 17928481ff3SJerome Forissier for pattern in excluded: 18028481ff3SJerome Forissier if match_pattern(path, pattern): 18128481ff3SJerome Forissier return # next key 18228481ff3SJerome Forissier included = subsystems[key].get('F') 18328481ff3SJerome Forissier if not included: 18428481ff3SJerome Forissier return # next key 18528481ff3SJerome Forissier for pattern in included: 18628481ff3SJerome Forissier if match_pattern(path, pattern): 18728481ff3SJerome Forissier found[key] = subsystems[key] 18828481ff3SJerome Forissier inner() 18928481ff3SJerome Forissier if strict and len(found) > 1: 19028481ff3SJerome Forissier found.pop('THE REST', None) 19128481ff3SJerome Forissier return found 19228481ff3SJerome Forissier 19328481ff3SJerome Forissier 19428481ff3SJerome Forissierdef get_ss_maintainers(subsys): 19528481ff3SJerome Forissier return subsys.get('M') or [] 19628481ff3SJerome Forissier 19728481ff3SJerome Forissier 19828481ff3SJerome Forissierdef get_ss_reviewers(subsys): 19928481ff3SJerome Forissier return subsys.get('R') or [] 20028481ff3SJerome Forissier 20128481ff3SJerome Forissier 20228481ff3SJerome Forissierdef get_ss_approvers(ss): 20328481ff3SJerome Forissier return get_ss_maintainers(ss) + get_ss_reviewers(ss) 20428481ff3SJerome Forissier 20528481ff3SJerome Forissier 20628481ff3SJerome Forissierdef approvers_have_approved(approved_by, approvers): 20728481ff3SJerome Forissier for n in approvers: 20828481ff3SJerome Forissier # Ignore anything after the email (Github ID...) 20928481ff3SJerome Forissier n = n.split('>', 1)[0] 21028481ff3SJerome Forissier for m in approved_by: 21128481ff3SJerome Forissier m = m.split('>', 1)[0] 21228481ff3SJerome Forissier if n == m: 21328481ff3SJerome Forissier return True 21428481ff3SJerome Forissier return False 21528481ff3SJerome Forissier 21628481ff3SJerome Forissier 21728481ff3SJerome Forissierdef download(pr): 21828481ff3SJerome Forissier url = "https://github.com/OP-TEE/optee_os/pull/{}.patch".format(pr) 21928481ff3SJerome Forissier f = tempfile.NamedTemporaryFile(mode="wb", prefix="pr{}_".format(pr), 22028481ff3SJerome Forissier suffix=".patch", delete=False) 22128481ff3SJerome Forissier print("Downloading {}...".format(url), end='', flush=True) 22228481ff3SJerome Forissier f.write(urlopen(url).read()) 22328481ff3SJerome Forissier print(" Done.") 22428481ff3SJerome Forissier return f.name 22528481ff3SJerome Forissier 22628481ff3SJerome Forissier 22728481ff3SJerome Forissierdef main(): 22828481ff3SJerome Forissier global args 22928481ff3SJerome Forissier 23028481ff3SJerome Forissier args = get_args() 23128481ff3SJerome Forissier all_subsystems = parse_maintainers() 23228481ff3SJerome Forissier paths = [] 233*716f442dSJerome Forissier arglist = [] 23428481ff3SJerome Forissier downloads = [] 235*716f442dSJerome Forissier split_patches = [] 23628481ff3SJerome Forissier 23728481ff3SJerome Forissier for pr in args.github_pr or []: 23828481ff3SJerome Forissier downloads += [download(pr)] 23928481ff3SJerome Forissier 24028481ff3SJerome Forissier for arg in args.arg + downloads: 241*716f442dSJerome Forissier if os.path.exists(arg): 242*716f442dSJerome Forissier patches = split_patchset(arg) 243*716f442dSJerome Forissier if patches: 244*716f442dSJerome Forissier split_patches += patches 245*716f442dSJerome Forissier continue 246*716f442dSJerome Forissier arglist.append(arg) 247*716f442dSJerome Forissier 248*716f442dSJerome Forissier for arg in arglist + split_patches: 24928481ff3SJerome Forissier patch_paths = [] 25028481ff3SJerome Forissier approved_by = [] 25128481ff3SJerome Forissier if os.path.exists(arg): 252*716f442dSJerome Forissier # Try to parse as a patch 253*716f442dSJerome Forissier (patch_paths, approved_by) = get_paths_from_patch(arg) 25428481ff3SJerome Forissier if not patch_paths: 25528481ff3SJerome Forissier # Not a patch, consider the path itself 25628481ff3SJerome Forissier # as_posix() cleans the path a little bit (suppress leading ./ and 25728481ff3SJerome Forissier # duplicate slashes...) 25828481ff3SJerome Forissier patch_paths = [PurePath(arg).as_posix()] 25928481ff3SJerome Forissier for path in patch_paths: 26028481ff3SJerome Forissier approved = False 26128481ff3SJerome Forissier if args.merge_check: 26228481ff3SJerome Forissier ss_for_path = get_subsystems_for_path(all_subsystems, path, 26328481ff3SJerome Forissier args.strict) 26428481ff3SJerome Forissier for key in ss_for_path: 26528481ff3SJerome Forissier ss_approvers = get_ss_approvers(ss_for_path[key]) 26628481ff3SJerome Forissier if approvers_have_approved(approved_by, ss_approvers): 26728481ff3SJerome Forissier approved = True 26828481ff3SJerome Forissier if not approved: 26928481ff3SJerome Forissier paths += [path] 27028481ff3SJerome Forissier 271*716f442dSJerome Forissier for f in downloads + split_patches: 27228481ff3SJerome Forissier os.remove(f) 27328481ff3SJerome Forissier 27428481ff3SJerome Forissier if args.file: 27528481ff3SJerome Forissier paths += args.file 27628481ff3SJerome Forissier 27728481ff3SJerome Forissier if (args.show_paths): 27828481ff3SJerome Forissier print(paths) 27928481ff3SJerome Forissier 28028481ff3SJerome Forissier ss = {} 28128481ff3SJerome Forissier for path in paths: 28228481ff3SJerome Forissier ss.update(get_subsystems_for_path(all_subsystems, path, args.strict)) 28328481ff3SJerome Forissier for key in ss: 28428481ff3SJerome Forissier ss_name = key[:50] + (key[50:] and '...') 28528481ff3SJerome Forissier for name in ss[key].get('M') or []: 28628481ff3SJerome Forissier print("{} (maintainer:{})".format(name, ss_name)) 28728481ff3SJerome Forissier for name in ss[key].get('R') or []: 28828481ff3SJerome Forissier print("{} (reviewer:{})".format(name, ss_name)) 28928481ff3SJerome Forissier 29028481ff3SJerome Forissier 29128481ff3SJerome Forissierif __name__ == "__main__": 29228481ff3SJerome Forissier main() 293