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>.*>)') 20716f442dSJerome 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.') 4972ec5fdeSJerome Forissier parser.add_argument('-r', '--release-to', action='store_true', 5072ec5fdeSJerome Forissier help='show all the recipients to be used in release ' 51*8eb0262bSJerome Forissier 'announcement emails (i.e., maintainers, reviewers ' 52*8eb0262bSJerome Forissier 'and OP-TEE mailing list(s)) and exit.') 5328481ff3SJerome Forissier return parser.parse_args() 5428481ff3SJerome Forissier 5528481ff3SJerome Forissier 5672ec5fdeSJerome Forissierdef check_cwd(): 5772ec5fdeSJerome Forissier cwd = os.getcwd() 5872ec5fdeSJerome Forissier parent = os.path.dirname(os.path.realpath(__file__)) + "/../" 5972ec5fdeSJerome Forissier if (os.path.realpath(cwd) != os.path.realpath(parent)): 6072ec5fdeSJerome Forissier print("Error: this script must be run from the top-level of the " 6172ec5fdeSJerome Forissier "optee_os tree") 6272ec5fdeSJerome Forissier exit(1) 6372ec5fdeSJerome Forissier 6472ec5fdeSJerome Forissier 6528481ff3SJerome Forissier# Parse MAINTAINERS and return a dictionary of subsystems such as: 6628481ff3SJerome Forissier# {'Subsystem name': {'R': ['foo', 'bar'], 'S': ['Maintained'], 6728481ff3SJerome Forissier# 'F': [ 'path1', 'path2' ]}, ...} 6828481ff3SJerome Forissierdef parse_maintainers(): 6928481ff3SJerome Forissier subsystems = {} 7072ec5fdeSJerome Forissier check_cwd() 7128481ff3SJerome Forissier with open("MAINTAINERS", "r") as f: 7228481ff3SJerome Forissier start_found = False 7328481ff3SJerome Forissier ss = {} 7428481ff3SJerome Forissier name = '' 7528481ff3SJerome Forissier for line in f: 7628481ff3SJerome Forissier line = line.strip() 7728481ff3SJerome Forissier if not line: 7828481ff3SJerome Forissier continue 7928481ff3SJerome Forissier if not start_found: 8028481ff3SJerome Forissier if line.startswith("----------"): 8128481ff3SJerome Forissier start_found = True 8228481ff3SJerome Forissier continue 8328481ff3SJerome Forissier 8428481ff3SJerome Forissier if line[1] == ':': 8528481ff3SJerome Forissier letter = line[0] 8628481ff3SJerome Forissier if (not ss.get(letter)): 8728481ff3SJerome Forissier ss[letter] = [] 8828481ff3SJerome Forissier ss[letter].append(line[3:]) 8928481ff3SJerome Forissier else: 9028481ff3SJerome Forissier if name: 9128481ff3SJerome Forissier subsystems[name] = ss 9228481ff3SJerome Forissier name = line 9328481ff3SJerome Forissier ss = {} 9428481ff3SJerome Forissier if name: 9528481ff3SJerome Forissier subsystems[name] = ss 9628481ff3SJerome Forissier 9728481ff3SJerome Forissier return subsystems 9828481ff3SJerome Forissier 9928481ff3SJerome Forissier 100716f442dSJerome Forissier# If @patchset is a patchset files and contains 2 patches or more, write 101716f442dSJerome Forissier# individual patches to temporary files and return the paths. 102716f442dSJerome Forissier# Otherwise return []. 103716f442dSJerome Forissierdef split_patchset(patchset): 104716f442dSJerome Forissier psname = os.path.basename(patchset).replace('.', '_') 105716f442dSJerome Forissier patchnum = 0 106716f442dSJerome Forissier of = None 107716f442dSJerome Forissier ret = [] 108716f442dSJerome Forissier f = None 109716f442dSJerome Forissier try: 110716f442dSJerome Forissier f = open(patchset, "r") 11171c9b078SJerome Forissier except OSError: 112716f442dSJerome Forissier return [] 113716f442dSJerome Forissier for line in f: 114716f442dSJerome Forissier match = re.search(PATCH_START, line) 115716f442dSJerome Forissier if match: 116716f442dSJerome Forissier # New patch found: create new file 117716f442dSJerome Forissier patchnum += 1 118716f442dSJerome Forissier prefix = "{}_{}_".format(patchnum, psname) 119716f442dSJerome Forissier of = tempfile.NamedTemporaryFile(mode="w", prefix=prefix, 120716f442dSJerome Forissier suffix=".patch", 121716f442dSJerome Forissier delete=False) 122716f442dSJerome Forissier ret.append(of.name) 123716f442dSJerome Forissier if of: 124716f442dSJerome Forissier of.write(line) 125716f442dSJerome Forissier if len(ret) >= 2: 126716f442dSJerome Forissier return ret 127716f442dSJerome Forissier if len(ret) == 1: 128716f442dSJerome Forissier os.remove(ret[0]) 129716f442dSJerome Forissier return [] 130716f442dSJerome Forissier 131716f442dSJerome Forissier 13228481ff3SJerome Forissier# If @path is a patch file, returns the paths touched by the patch as well 13328481ff3SJerome Forissier# as the content of the review/ack tags 134716f442dSJerome Forissierdef get_paths_from_patch(patch): 13528481ff3SJerome Forissier paths = [] 13628481ff3SJerome Forissier approvers = [] 13728481ff3SJerome Forissier try: 13828481ff3SJerome Forissier with open(patch, "r") as f: 13928481ff3SJerome Forissier for line in f: 14028481ff3SJerome Forissier match = re.search(DIFF_GIT_RE, line) 14128481ff3SJerome Forissier if match: 14228481ff3SJerome Forissier p = match.group('path') 14328481ff3SJerome Forissier if p not in paths: 14428481ff3SJerome Forissier paths.append(p) 14528481ff3SJerome Forissier continue 14628481ff3SJerome Forissier match = re.search(REVIEWED_RE, line) 14728481ff3SJerome Forissier if match: 14828481ff3SJerome Forissier a = match.group('approver') 14928481ff3SJerome Forissier if a not in approvers: 15028481ff3SJerome Forissier approvers.append(a) 15128481ff3SJerome Forissier continue 15228481ff3SJerome Forissier match = re.search(ACKED_RE, line) 15328481ff3SJerome Forissier if match: 15428481ff3SJerome Forissier a = match.group('approver') 15528481ff3SJerome Forissier if a not in approvers: 15628481ff3SJerome Forissier approvers.append(a) 15728481ff3SJerome Forissier continue 15828481ff3SJerome Forissier except Exception: 15928481ff3SJerome Forissier pass 16028481ff3SJerome Forissier return (paths, approvers) 16128481ff3SJerome Forissier 16228481ff3SJerome Forissier 16328481ff3SJerome Forissier# Does @path match @pattern? 16428481ff3SJerome Forissier# @pattern has the syntax defined in the Linux MAINTAINERS file -- mostly a 16528481ff3SJerome Forissier# shell glob pattern, except that a trailing slash means a directory and 16628481ff3SJerome Forissier# everything below. Matching can easily be done by converting to a regexp. 16728481ff3SJerome Forissierdef match_pattern(path, pattern): 16828481ff3SJerome Forissier # Append a trailing slash if path is an existing directory, so that it 16928481ff3SJerome Forissier # matches F: entries such as 'foo/bar/' 17028481ff3SJerome Forissier if not path.endswith('/') and os.path.isdir(path): 17128481ff3SJerome Forissier path += '/' 17228481ff3SJerome Forissier rep = "^" + pattern 17328481ff3SJerome Forissier rep = rep.replace('*', '[^/]+') 17428481ff3SJerome Forissier rep = rep.replace('?', '[^/]') 17528481ff3SJerome Forissier if rep.endswith('/'): 17628481ff3SJerome Forissier rep += '.*' 17728481ff3SJerome Forissier rep += '$' 17828481ff3SJerome Forissier return not not re.match(rep, path) 17928481ff3SJerome Forissier 18028481ff3SJerome Forissier 18128481ff3SJerome Forissierdef get_subsystems_for_path(subsystems, path, strict): 18228481ff3SJerome Forissier found = {} 18328481ff3SJerome Forissier for key in subsystems: 18428481ff3SJerome Forissier def inner(): 18528481ff3SJerome Forissier excluded = subsystems[key].get('X') 18628481ff3SJerome Forissier if excluded: 18728481ff3SJerome Forissier for pattern in excluded: 18828481ff3SJerome Forissier if match_pattern(path, pattern): 18928481ff3SJerome Forissier return # next key 19028481ff3SJerome Forissier included = subsystems[key].get('F') 19128481ff3SJerome Forissier if not included: 19228481ff3SJerome Forissier return # next key 19328481ff3SJerome Forissier for pattern in included: 19428481ff3SJerome Forissier if match_pattern(path, pattern): 19528481ff3SJerome Forissier found[key] = subsystems[key] 19628481ff3SJerome Forissier inner() 19728481ff3SJerome Forissier if strict and len(found) > 1: 19828481ff3SJerome Forissier found.pop('THE REST', None) 19928481ff3SJerome Forissier return found 20028481ff3SJerome Forissier 20128481ff3SJerome Forissier 20228481ff3SJerome Forissierdef get_ss_maintainers(subsys): 20328481ff3SJerome Forissier return subsys.get('M') or [] 20428481ff3SJerome Forissier 20528481ff3SJerome Forissier 20628481ff3SJerome Forissierdef get_ss_reviewers(subsys): 20728481ff3SJerome Forissier return subsys.get('R') or [] 20828481ff3SJerome Forissier 20928481ff3SJerome Forissier 21028481ff3SJerome Forissierdef get_ss_approvers(ss): 21128481ff3SJerome Forissier return get_ss_maintainers(ss) + get_ss_reviewers(ss) 21228481ff3SJerome Forissier 21328481ff3SJerome Forissier 214*8eb0262bSJerome Forissierdef get_ss_lists(subsys): 215*8eb0262bSJerome Forissier return subsys.get('L') or [] 216*8eb0262bSJerome Forissier 217*8eb0262bSJerome Forissier 21828481ff3SJerome Forissierdef approvers_have_approved(approved_by, approvers): 21928481ff3SJerome Forissier for n in approvers: 22028481ff3SJerome Forissier # Ignore anything after the email (Github ID...) 22128481ff3SJerome Forissier n = n.split('>', 1)[0] 22228481ff3SJerome Forissier for m in approved_by: 22328481ff3SJerome Forissier m = m.split('>', 1)[0] 22428481ff3SJerome Forissier if n == m: 22528481ff3SJerome Forissier return True 22628481ff3SJerome Forissier return False 22728481ff3SJerome Forissier 22828481ff3SJerome Forissier 22928481ff3SJerome Forissierdef download(pr): 23028481ff3SJerome Forissier url = "https://github.com/OP-TEE/optee_os/pull/{}.patch".format(pr) 23128481ff3SJerome Forissier f = tempfile.NamedTemporaryFile(mode="wb", prefix="pr{}_".format(pr), 23228481ff3SJerome Forissier suffix=".patch", delete=False) 23328481ff3SJerome Forissier print("Downloading {}...".format(url), end='', flush=True) 23428481ff3SJerome Forissier f.write(urlopen(url).read()) 23528481ff3SJerome Forissier print(" Done.") 23628481ff3SJerome Forissier return f.name 23728481ff3SJerome Forissier 23828481ff3SJerome Forissier 239*8eb0262bSJerome Forissierdef show_release_to(subsystems): 24072ec5fdeSJerome Forissier check_cwd() 24172ec5fdeSJerome Forissier with open("MAINTAINERS", "r") as f: 24272ec5fdeSJerome Forissier emails = sorted(set(re.findall(r'[RM]:\t(.*[\w]*<[\w\.-]+@[\w\.-]+>)', 24372ec5fdeSJerome Forissier f.read()))) 244*8eb0262bSJerome Forissier emails += get_ss_lists(subsystems["THE REST"]) 24572ec5fdeSJerome Forissier print(*emails, sep=', ') 24672ec5fdeSJerome Forissier 24772ec5fdeSJerome Forissier 24828481ff3SJerome Forissierdef main(): 24928481ff3SJerome Forissier global args 25028481ff3SJerome Forissier 25128481ff3SJerome Forissier args = get_args() 25272ec5fdeSJerome Forissier 253*8eb0262bSJerome Forissier all_subsystems = parse_maintainers() 254*8eb0262bSJerome Forissier 25572ec5fdeSJerome Forissier if args.release_to: 256*8eb0262bSJerome Forissier show_release_to(all_subsystems) 25772ec5fdeSJerome Forissier return 25872ec5fdeSJerome Forissier 25928481ff3SJerome Forissier paths = [] 260716f442dSJerome Forissier arglist = [] 26128481ff3SJerome Forissier downloads = [] 262716f442dSJerome Forissier split_patches = [] 26328481ff3SJerome Forissier 26428481ff3SJerome Forissier for pr in args.github_pr or []: 26528481ff3SJerome Forissier downloads += [download(pr)] 26628481ff3SJerome Forissier 26728481ff3SJerome Forissier for arg in args.arg + downloads: 268716f442dSJerome Forissier if os.path.exists(arg): 269716f442dSJerome Forissier patches = split_patchset(arg) 270716f442dSJerome Forissier if patches: 271716f442dSJerome Forissier split_patches += patches 272716f442dSJerome Forissier continue 273716f442dSJerome Forissier arglist.append(arg) 274716f442dSJerome Forissier 275716f442dSJerome Forissier for arg in arglist + split_patches: 27628481ff3SJerome Forissier patch_paths = [] 27728481ff3SJerome Forissier approved_by = [] 27828481ff3SJerome Forissier if os.path.exists(arg): 279716f442dSJerome Forissier # Try to parse as a patch 280716f442dSJerome Forissier (patch_paths, approved_by) = get_paths_from_patch(arg) 28128481ff3SJerome Forissier if not patch_paths: 28228481ff3SJerome Forissier # Not a patch, consider the path itself 28328481ff3SJerome Forissier # as_posix() cleans the path a little bit (suppress leading ./ and 28428481ff3SJerome Forissier # duplicate slashes...) 28528481ff3SJerome Forissier patch_paths = [PurePath(arg).as_posix()] 28628481ff3SJerome Forissier for path in patch_paths: 28728481ff3SJerome Forissier approved = False 28828481ff3SJerome Forissier if args.merge_check: 28928481ff3SJerome Forissier ss_for_path = get_subsystems_for_path(all_subsystems, path, 29028481ff3SJerome Forissier args.strict) 29128481ff3SJerome Forissier for key in ss_for_path: 29228481ff3SJerome Forissier ss_approvers = get_ss_approvers(ss_for_path[key]) 29328481ff3SJerome Forissier if approvers_have_approved(approved_by, ss_approvers): 29428481ff3SJerome Forissier approved = True 29528481ff3SJerome Forissier if not approved: 29628481ff3SJerome Forissier paths += [path] 29728481ff3SJerome Forissier 298716f442dSJerome Forissier for f in downloads + split_patches: 29928481ff3SJerome Forissier os.remove(f) 30028481ff3SJerome Forissier 30128481ff3SJerome Forissier if args.file: 30228481ff3SJerome Forissier paths += args.file 30328481ff3SJerome Forissier 30428481ff3SJerome Forissier if (args.show_paths): 30528481ff3SJerome Forissier print(paths) 30628481ff3SJerome Forissier 30728481ff3SJerome Forissier ss = {} 30828481ff3SJerome Forissier for path in paths: 30928481ff3SJerome Forissier ss.update(get_subsystems_for_path(all_subsystems, path, args.strict)) 31028481ff3SJerome Forissier for key in ss: 31128481ff3SJerome Forissier ss_name = key[:50] + (key[50:] and '...') 31228481ff3SJerome Forissier for name in ss[key].get('M') or []: 31328481ff3SJerome Forissier print("{} (maintainer:{})".format(name, ss_name)) 31428481ff3SJerome Forissier for name in ss[key].get('R') or []: 31528481ff3SJerome Forissier print("{} (reviewer:{})".format(name, ss_name)) 31628481ff3SJerome Forissier 31728481ff3SJerome Forissier 31828481ff3SJerome Forissierif __name__ == "__main__": 31928481ff3SJerome Forissier main() 320