1#!/usr/bin/env python3
2
3# Copyright (c) 2022 Intel Corp.
4# SPDX-License-Identifier: Apache-2.0
5
6import argparse
7import sys
8import os
9import time
10import datetime
11from github import Github, GithubException
12from github.GithubException import UnknownObjectException
13from collections import defaultdict
14
15TOP_DIR = os.path.join(os.path.dirname(__file__))
16sys.path.insert(0, os.path.join(TOP_DIR, "scripts"))
17from get_maintainer import Maintainers
18
19def log(s):
20    if args.verbose > 0:
21        print(s, file=sys.stdout)
22
23def parse_args():
24    global args
25    parser = argparse.ArgumentParser(
26        description=__doc__,
27        formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False)
28
29    parser.add_argument("-M", "--maintainer-file", required=False, default="MAINTAINERS.yml",
30                        help="Maintainer file to be used.")
31    parser.add_argument("-P", "--pull_request", required=False, default=None, type=int,
32                        help="Operate on one pull-request only.")
33    parser.add_argument("-s", "--since", required=False,
34                        help="Process pull-requests since date.")
35
36    parser.add_argument("-y", "--dry-run", action="store_true", default=False,
37                        help="Dry run only.")
38
39    parser.add_argument("-o", "--org", default="zephyrproject-rtos",
40                        help="Github organisation")
41
42    parser.add_argument("-r", "--repo", default="zephyr",
43                        help="Github repository")
44
45    parser.add_argument("-v", "--verbose", action="count", default=0,
46                        help="Verbose Output")
47
48    args = parser.parse_args()
49
50def process_pr(gh, maintainer_file, number):
51
52    gh_repo = gh.get_repo(f"{args.org}/{args.repo}")
53    pr = gh_repo.get_pull(number)
54
55    log(f"working on https://github.com/{args.org}/{args.repo}/pull/{pr.number} : {pr.title}")
56
57    labels = set()
58    area_counter = defaultdict(int)
59    maint = defaultdict(int)
60
61    num_files = 0
62    all_areas = set()
63    fn = list(pr.get_files())
64    if len(fn) > 500:
65        log(f"Too many files changed ({len(fn)}), skipping....")
66        return
67    for f in pr.get_files():
68        num_files += 1
69        log(f"file: {f.filename}")
70        areas = maintainer_file.path2areas(f.filename)
71
72        if areas:
73            all_areas.update(areas)
74            for a in areas:
75                area_counter[a.name] += 1
76                labels.update(a.labels)
77                for p in a.maintainers:
78                    maint[p] += 1
79
80    ac = dict(sorted(area_counter.items(), key=lambda item: item[1], reverse=True))
81    log(f"Area matches: {ac}")
82    log(f"labels: {labels}")
83
84    # Create a list of collaborators ordered by the area match
85    collab = list()
86    for a in ac:
87        collab += maintainer_file.areas[a].maintainers
88        collab += maintainer_file.areas[a].collaborators
89    collab = list(dict.fromkeys(collab))
90    log(f"collab: {collab}")
91
92    sm = dict(sorted(maint.items(), key=lambda item: item[1], reverse=True))
93
94    log(f"Submitted by: {pr.user.login}")
95    log(f"candidate maintainers: {sm}")
96
97    maintainer = "None"
98    maintainers = list(sm.keys())
99
100    prop = 0
101    if maintainers:
102        maintainer = maintainers[0]
103
104        if len(ac) > 1 and list(ac.values())[0] == list(ac.values())[1]:
105            for aa in ac:
106                if 'Documentation' in aa:
107                    log("++ With multiple areas of same weight including docs, take something else other than Documentation as the maintainer")
108                    for a in all_areas:
109                        if (a.name == aa and
110                            a.maintainers and a.maintainers[0] == maintainer and
111                            len(maintainers) > 1):
112                            maintainer = maintainers[1]
113                elif 'Platform' in aa:
114                    log("++ Platform takes precedence over subsystem...")
115                    log(f"Set maintainer of area {aa}")
116                    for a in all_areas:
117                        if a.name == aa:
118                            if a.maintainers:
119                                maintainer = a.maintainers[0]
120                                break
121
122
123        # if the submitter is the same as the maintainer, check if we have
124        # multiple maintainers
125        if pr.user.login == maintainer:
126            log("Submitter is same as Assignee, trying to find another assignee...")
127            aff = list(ac.keys())[0]
128            for a in all_areas:
129                if a.name == aff:
130                    if len(a.maintainers) > 1:
131                        maintainer = a.maintainers[1]
132                    else:
133                        log(f"This area has only one maintainer, keeping assignee as {maintainer}")
134
135        prop = (maint[maintainer] / num_files) * 100
136        if prop < 20:
137            maintainer = "None"
138
139    log(f"Picked maintainer: {maintainer} ({prop:.2f}% ownership)")
140    log("+++++++++++++++++++++++++")
141
142    # Set labels
143    if labels:
144        if len(labels) < 10:
145            for l in labels:
146                log(f"adding label {l}...")
147                if not args.dry_run:
148                    pr.add_to_labels(l)
149        else:
150            log(f"Too many labels to be applied")
151
152    if collab:
153        reviewers = []
154        existing_reviewers = set()
155
156        revs = pr.get_reviews()
157        for review in revs:
158            existing_reviewers.add(review.user)
159
160        rl = pr.get_review_requests()
161        page = 0
162        for r in rl:
163            existing_reviewers |= set(r.get_page(page))
164            page += 1
165
166        for c in collab:
167            try:
168                u = gh.get_user(c)
169                if pr.user != u and gh_repo.has_in_collaborators(u):
170                    if u not in existing_reviewers:
171                        reviewers.append(c)
172            except UnknownObjectException as e:
173                log(f"Can't get user '{c}', account does not exist anymore? ({e})")
174
175        if len(existing_reviewers) < 15:
176            reviewer_vacancy = 15 - len(existing_reviewers)
177            reviewers = reviewers[:reviewer_vacancy]
178
179            if reviewers:
180                try:
181                    log(f"adding reviewers {reviewers}...")
182                    if not args.dry_run:
183                        pr.create_review_request(reviewers=reviewers)
184                except GithubException:
185                    log("cant add reviewer")
186        else:
187            log("not adding reviewers because the existing reviewer count is greater than or "
188                "equal to 15")
189
190    ms = []
191    # assignees
192    if maintainer != 'None' and not pr.assignee:
193        try:
194            u = gh.get_user(maintainer)
195            ms.append(u)
196        except GithubException:
197            log(f"Error: Unknown user")
198
199        for mm in ms:
200            log(f"Adding assignee {mm}...")
201            if not args.dry_run:
202                pr.add_to_assignees(mm)
203    else:
204        log("not setting assignee")
205
206    time.sleep(1)
207
208def main():
209    parse_args()
210
211    token = os.environ.get('GITHUB_TOKEN', None)
212    if not token:
213        sys.exit('Github token not set in environment, please set the '
214                 'GITHUB_TOKEN environment variable and retry.')
215
216    gh = Github(token)
217    maintainer_file = Maintainers(args.maintainer_file)
218
219    if args.pull_request:
220        process_pr(gh, maintainer_file, args.pull_request)
221    else:
222        if args.since:
223            since = args.since
224        else:
225            today = datetime.date.today()
226            since = today - datetime.timedelta(days=1)
227
228        common_prs = f'repo:{args.org}/{args.repo} is:open is:pr base:main -is:draft no:assignee created:>{since}'
229        pulls = gh.search_issues(query=f'{common_prs}')
230
231        for issue in pulls:
232            process_pr(gh, maintainer_file, issue.number)
233
234
235if __name__ == "__main__":
236    main()
237