blob: aadfde1f07669aa590d1bce3c0a334685e88f1ff [file] [log] [blame]
Anas Nashif20483162022-02-25 18:37:47 -05001#!/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
Anas Nashif60271522022-07-18 19:37:31 -040012from github.GithubException import UnknownObjectException
Anas Nashif20483162022-02-25 18:37:47 -050013from collections import defaultdict
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +000014from west.manifest import Manifest
15from west.manifest import ManifestProject
Anas Nashif20483162022-02-25 18:37:47 -050016
17TOP_DIR = os.path.join(os.path.dirname(__file__))
18sys.path.insert(0, os.path.join(TOP_DIR, "scripts"))
19from get_maintainer import Maintainers
20
21def log(s):
22 if args.verbose > 0:
23 print(s, file=sys.stdout)
24
25def parse_args():
26 global args
27 parser = argparse.ArgumentParser(
28 description=__doc__,
Jamie McCraeec704442023-01-04 16:08:36 +000029 formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False)
Anas Nashif20483162022-02-25 18:37:47 -050030
31 parser.add_argument("-M", "--maintainer-file", required=False, default="MAINTAINERS.yml",
32 help="Maintainer file to be used.")
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +000033
34 group = parser.add_mutually_exclusive_group()
35 group.add_argument("-P", "--pull_request", required=False, default=None, type=int,
36 help="Operate on one pull-request only.")
Fabio Baltierib6cbcba2023-08-22 17:04:31 +000037 group.add_argument("-I", "--issue", required=False, default=None, type=int,
38 help="Operate on one issue only.")
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +000039 group.add_argument("-s", "--since", required=False,
40 help="Process pull-requests since date.")
41 group.add_argument("-m", "--modules", action="store_true",
42 help="Process pull-requests from modules.")
Anas Nashif20483162022-02-25 18:37:47 -050043
44 parser.add_argument("-y", "--dry-run", action="store_true", default=False,
45 help="Dry run only.")
46
47 parser.add_argument("-o", "--org", default="zephyrproject-rtos",
48 help="Github organisation")
49
50 parser.add_argument("-r", "--repo", default="zephyr",
51 help="Github repository")
52
53 parser.add_argument("-v", "--verbose", action="count", default=0,
54 help="Verbose Output")
55
56 args = parser.parse_args()
57
58def process_pr(gh, maintainer_file, number):
59
60 gh_repo = gh.get_repo(f"{args.org}/{args.repo}")
61 pr = gh_repo.get_pull(number)
62
63 log(f"working on https://github.com/{args.org}/{args.repo}/pull/{pr.number} : {pr.title}")
64
65 labels = set()
Anas Nashif20483162022-02-25 18:37:47 -050066 area_counter = defaultdict(int)
Anas Nashifd9a300e2023-10-12 10:24:06 +000067 found_maintainers = defaultdict(int)
Anas Nashif20483162022-02-25 18:37:47 -050068
69 num_files = 0
70 all_areas = set()
71 fn = list(pr.get_files())
Anas Nashifd9a300e2023-10-12 10:24:06 +000072
73 # one liner PRs should be trivial
74 if pr.commits == 1 and (pr.additions <= 1 and pr.deletions <= 1):
75 labels = {'trivial'}
76
Anas Nashif20483162022-02-25 18:37:47 -050077 if len(fn) > 500:
78 log(f"Too many files changed ({len(fn)}), skipping....")
79 return
Anas Nashifd9a300e2023-10-12 10:24:06 +000080
81 for changed_file in fn:
Anas Nashif20483162022-02-25 18:37:47 -050082 num_files += 1
Anas Nashifd9a300e2023-10-12 10:24:06 +000083 log(f"file: {changed_file.filename}")
84 areas = maintainer_file.path2areas(changed_file.filename)
Anas Nashif20483162022-02-25 18:37:47 -050085
Anas Nashifd9a300e2023-10-12 10:24:06 +000086 if not areas:
87 continue
Anas Nashif20483162022-02-25 18:37:47 -050088
Anas Nashifd9a300e2023-10-12 10:24:06 +000089 all_areas.update(areas)
90 is_instance = False
91 sorted_areas = sorted(areas, key=lambda x: 'Platform' in x.name, reverse=True)
92 for area in sorted_areas:
93 c = 1 if not is_instance else 0
94
95 area_counter[area] += c
96 labels.update(area.labels)
97 # FIXME: Here we count the same file multiple times if it exists in
98 # multiple areas with same maintainer
99 for area_maintainer in area.maintainers:
100 found_maintainers[area_maintainer] += c
101
102 if 'Platform' in area.name:
103 is_instance = True
104
105 area_counter = dict(sorted(area_counter.items(), key=lambda item: item[1], reverse=True))
106 log(f"Area matches: {area_counter}")
Anas Nashif20483162022-02-25 18:37:47 -0500107 log(f"labels: {labels}")
Anas Nashif20483162022-02-25 18:37:47 -0500108
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900109 # Create a list of collaborators ordered by the area match
110 collab = list()
Anas Nashifd9a300e2023-10-12 10:24:06 +0000111 for area in area_counter:
112 collab += maintainer_file.areas[area.name].maintainers
113 collab += maintainer_file.areas[area.name].collaborators
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900114 collab = list(dict.fromkeys(collab))
115 log(f"collab: {collab}")
116
Anas Nashifd9a300e2023-10-12 10:24:06 +0000117 _all_maintainers = dict(sorted(found_maintainers.items(), key=lambda item: item[1], reverse=True))
Anas Nashif20483162022-02-25 18:37:47 -0500118
119 log(f"Submitted by: {pr.user.login}")
Anas Nashifd9a300e2023-10-12 10:24:06 +0000120 log(f"candidate maintainers: {_all_maintainers}")
Anas Nashif20483162022-02-25 18:37:47 -0500121
Anas Nashifd9a300e2023-10-12 10:24:06 +0000122 maintainers = list(_all_maintainers.keys())
123 assignee = None
Fabio Baltierid06450b2022-10-03 14:51:40 +0000124
Anas Nashifd9a300e2023-10-12 10:24:06 +0000125 # we start with areas with most files changed and pick the maintainer from the first one.
126 # if the first area is an implementation, i.e. driver or platform, we
127 # continue searching for any other areas
128 for area, count in area_counter.items():
129 if count == 0:
130 continue
131 if len(area.maintainers) > 0:
132 assignee = area.maintainers[0]
Anas Nashif20483162022-02-25 18:37:47 -0500133
Anas Nashifd9a300e2023-10-12 10:24:06 +0000134 if 'Platform' not in area.name:
135 break
Anas Nashif20483162022-02-25 18:37:47 -0500136
Anas Nashifd9a300e2023-10-12 10:24:06 +0000137 # if the submitter is the same as the maintainer, check if we have
138 # multiple maintainers
139 if len(maintainers) > 1 and pr.user.login == assignee:
140 log("Submitter is same as Assignee, trying to find another assignee...")
141 aff = list(area_counter.keys())[0]
142 for area in all_areas:
143 if area.name == aff:
144 if len(area.maintainers) > 1:
145 assignee = area.maintainers[1]
146 else:
147 log(f"This area has only one maintainer, keeping assignee as {assignee}")
Anas Nashif20483162022-02-25 18:37:47 -0500148
Anas Nashifd9a300e2023-10-12 10:24:06 +0000149 if assignee:
150 prop = (found_maintainers[assignee] / num_files) * 100
151 log(f"Picked assignee: {assignee} ({prop:.2f}% ownership)")
152 log("+++++++++++++++++++++++++")
Anas Nashif20483162022-02-25 18:37:47 -0500153
154 # Set labels
Fabio Baltieri16d723e2023-01-26 17:03:13 +0000155 if labels:
156 if len(labels) < 10:
157 for l in labels:
158 log(f"adding label {l}...")
159 if not args.dry_run:
160 pr.add_to_labels(l)
161 else:
162 log(f"Too many labels to be applied")
Anas Nashif20483162022-02-25 18:37:47 -0500163
164 if collab:
165 reviewers = []
166 existing_reviewers = set()
167
168 revs = pr.get_reviews()
169 for review in revs:
170 existing_reviewers.add(review.user)
171
172 rl = pr.get_review_requests()
173 page = 0
174 for r in rl:
175 existing_reviewers |= set(r.get_page(page))
176 page += 1
177
178 for c in collab:
Anas Nashif60271522022-07-18 19:37:31 -0400179 try:
180 u = gh.get_user(c)
181 if pr.user != u and gh_repo.has_in_collaborators(u):
182 if u not in existing_reviewers:
183 reviewers.append(c)
184 except UnknownObjectException as e:
185 log(f"Can't get user '{c}', account does not exist anymore? ({e})")
Anas Nashif20483162022-02-25 18:37:47 -0500186
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900187 if len(existing_reviewers) < 15:
188 reviewer_vacancy = 15 - len(existing_reviewers)
189 reviewers = reviewers[:reviewer_vacancy]
190
191 if reviewers:
192 try:
193 log(f"adding reviewers {reviewers}...")
194 if not args.dry_run:
195 pr.create_review_request(reviewers=reviewers)
196 except GithubException:
197 log("cant add reviewer")
198 else:
199 log("not adding reviewers because the existing reviewer count is greater than or "
200 "equal to 15")
Anas Nashif20483162022-02-25 18:37:47 -0500201
202 ms = []
203 # assignees
Anas Nashifd9a300e2023-10-12 10:24:06 +0000204 if assignee and not pr.assignee:
Anas Nashif20483162022-02-25 18:37:47 -0500205 try:
Anas Nashifd9a300e2023-10-12 10:24:06 +0000206 u = gh.get_user(assignee)
Anas Nashif20483162022-02-25 18:37:47 -0500207 ms.append(u)
208 except GithubException:
209 log(f"Error: Unknown user")
210
211 for mm in ms:
212 log(f"Adding assignee {mm}...")
213 if not args.dry_run:
214 pr.add_to_assignees(mm)
Anas Nashifd63c2c42022-06-16 11:25:52 -0400215 else:
216 log("not setting assignee")
Anas Nashif20483162022-02-25 18:37:47 -0500217
218 time.sleep(1)
219
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000220
Fabio Baltierib6cbcba2023-08-22 17:04:31 +0000221def process_issue(gh, maintainer_file, number):
222 gh_repo = gh.get_repo(f"{args.org}/{args.repo}")
223 issue = gh_repo.get_issue(number)
224
225 log(f"Working on {issue.url}: {issue.title}")
226
227 if issue.assignees:
228 print(f"Already assigned {issue.assignees}, bailing out")
229 return
230
231 label_to_maintainer = defaultdict(set)
232 for _, area in maintainer_file.areas.items():
233 if not area.labels:
234 continue
235
236 labels = set()
237 for label in area.labels:
238 labels.add(label.lower())
239 labels = tuple(sorted(labels))
240
241 for maintainer in area.maintainers:
242 label_to_maintainer[labels].add(maintainer)
243
244 # Add extra entries for areas with multiple labels so they match with just
245 # one label if it's specific enough.
246 for areas, maintainers in dict(label_to_maintainer).items():
247 for area in areas:
248 if tuple([area]) not in label_to_maintainer:
249 label_to_maintainer[tuple([area])] = maintainers
250
251 issue_labels = set()
252 for label in issue.labels:
253 label_name = label.name.lower()
254 if tuple([label_name]) not in label_to_maintainer:
255 print(f"Ignoring label: {label}")
256 continue
257 issue_labels.add(label_name)
258 issue_labels = tuple(sorted(issue_labels))
259
260 print(f"Using labels: {issue_labels}")
261
262 if issue_labels not in label_to_maintainer:
263 print(f"no match for the label set, not assigning")
264 return
265
266 for maintainer in label_to_maintainer[issue_labels]:
267 log(f"Adding {maintainer} to {issue.html_url}")
268 if not args.dry_run:
269 issue.add_to_assignees(maintainer)
270
271
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000272def process_modules(gh, maintainers_file):
273 manifest = Manifest.from_file()
274
275 repos = {}
276 for project in manifest.get_projects([]):
277 if not manifest.is_active(project):
278 continue
279
280 if isinstance(project, ManifestProject):
281 continue
282
283 area = f"West project: {project.name}"
284 if area not in maintainers_file.areas:
285 log(f"No area for: {area}")
286 continue
287
288 maintainers = maintainers_file.areas[area].maintainers
289 if not maintainers:
290 log(f"No maintainers for: {area}")
291 continue
292
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000293 collaborators = maintainers_file.areas[area].collaborators
294
295 log(f"Found {area}, maintainers={maintainers}, collaborators={collaborators}")
296
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000297 repo_name = f"{args.org}/{project.name}"
298 repos[repo_name] = maintainers_file.areas[area]
299
300 query = f"is:open is:pr no:assignee"
301 for repo in repos:
302 query += f" repo:{repo}"
303
304 issues = gh.search_issues(query=query)
305 for issue in issues:
306 pull = issue.as_pull_request()
307
308 if pull.draft:
309 continue
310
311 if pull.assignees:
312 log(f"ERROR: {pull.html_url} should have no assignees, found {pull.assignees}")
313 continue
314
315 repo_name = f"{args.org}/{issue.repository.name}"
316 area = repos[repo_name]
317
318 for maintainer in area.maintainers:
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000319 log(f"Assigning {maintainer} to {pull.html_url}")
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000320 if not args.dry_run:
321 pull.add_to_assignees(maintainer)
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000322 pull.create_review_request(maintainer)
323
324 for collaborator in area.collaborators:
325 log(f"Adding {collaborator} to {pull.html_url}")
326 if not args.dry_run:
327 pull.create_review_request(collaborator)
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000328
329
Anas Nashif20483162022-02-25 18:37:47 -0500330def main():
331 parse_args()
332
333 token = os.environ.get('GITHUB_TOKEN', None)
334 if not token:
335 sys.exit('Github token not set in environment, please set the '
336 'GITHUB_TOKEN environment variable and retry.')
337
338 gh = Github(token)
339 maintainer_file = Maintainers(args.maintainer_file)
340
341 if args.pull_request:
342 process_pr(gh, maintainer_file, args.pull_request)
Fabio Baltieri5e786602023-08-25 13:49:27 +0000343 elif args.issue:
Fabio Baltierib6cbcba2023-08-22 17:04:31 +0000344 process_issue(gh, maintainer_file, args.issue)
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000345 elif args.modules:
346 process_modules(gh, maintainer_file)
Anas Nashif20483162022-02-25 18:37:47 -0500347 else:
348 if args.since:
349 since = args.since
350 else:
351 today = datetime.date.today()
352 since = today - datetime.timedelta(days=1)
353
354 common_prs = f'repo:{args.org}/{args.repo} is:open is:pr base:main -is:draft no:assignee created:>{since}'
355 pulls = gh.search_issues(query=f'{common_prs}')
356
357 for issue in pulls:
358 process_pr(gh, maintainer_file, issue.number)
359
360
361if __name__ == "__main__":
362 main()