xref: /optee_os/scripts/get_maintainer.py (revision 71c9b0780de06ca0e7a8574965c2fa0705d78927)
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.')
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
92716f442dSJerome Forissier# If @patchset is a patchset files and contains 2 patches or more, write
93716f442dSJerome Forissier# individual patches to temporary files and return the paths.
94716f442dSJerome Forissier# Otherwise return [].
95716f442dSJerome Forissierdef split_patchset(patchset):
96716f442dSJerome Forissier    psname = os.path.basename(patchset).replace('.', '_')
97716f442dSJerome Forissier    patchnum = 0
98716f442dSJerome Forissier    of = None
99716f442dSJerome Forissier    ret = []
100716f442dSJerome Forissier    f = None
101716f442dSJerome Forissier    try:
102716f442dSJerome Forissier        f = open(patchset, "r")
103*71c9b078SJerome Forissier    except OSError:
104716f442dSJerome Forissier        return []
105716f442dSJerome Forissier    for line in f:
106716f442dSJerome Forissier        match = re.search(PATCH_START, line)
107716f442dSJerome Forissier        if match:
108716f442dSJerome Forissier            # New patch found: create new file
109716f442dSJerome Forissier            patchnum += 1
110716f442dSJerome Forissier            prefix = "{}_{}_".format(patchnum, psname)
111716f442dSJerome Forissier            of = tempfile.NamedTemporaryFile(mode="w", prefix=prefix,
112716f442dSJerome Forissier                                             suffix=".patch",
113716f442dSJerome Forissier                                             delete=False)
114716f442dSJerome Forissier            ret.append(of.name)
115716f442dSJerome Forissier        if of:
116716f442dSJerome Forissier            of.write(line)
117716f442dSJerome Forissier    if len(ret) >= 2:
118716f442dSJerome Forissier        return ret
119716f442dSJerome Forissier    if len(ret) == 1:
120716f442dSJerome Forissier        os.remove(ret[0])
121716f442dSJerome Forissier    return []
122716f442dSJerome Forissier
123716f442dSJerome 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
126716f442dSJerome 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 = []
233716f442dSJerome Forissier    arglist = []
23428481ff3SJerome Forissier    downloads = []
235716f442dSJerome 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:
241716f442dSJerome Forissier        if os.path.exists(arg):
242716f442dSJerome Forissier            patches = split_patchset(arg)
243716f442dSJerome Forissier            if patches:
244716f442dSJerome Forissier                split_patches += patches
245716f442dSJerome Forissier                continue
246716f442dSJerome Forissier        arglist.append(arg)
247716f442dSJerome Forissier
248716f442dSJerome Forissier    for arg in arglist + split_patches:
24928481ff3SJerome Forissier        patch_paths = []
25028481ff3SJerome Forissier        approved_by = []
25128481ff3SJerome Forissier        if os.path.exists(arg):
252716f442dSJerome Forissier            # Try to parse as a patch
253716f442dSJerome 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
271716f442dSJerome 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