xref: /optee_os/scripts/get_maintainer.py (revision 8eb0262b67709c9644c837e82062540e34e850f4)
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