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