blob: f9fa1769a8d7bdea3c88b6eb0ccb8f3b5e69c45e [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
Anas Nashifdebe7fe2023-11-06 13:22:55 +000073 for changed_file in fn:
74 if changed_file.filename in ['west.yml','submanifests/optional.yaml']:
Anas Nashifdebe7fe2023-11-06 13:22:55 +000075 break
76
Martin Jägerc1a3bfb2024-06-04 14:42:32 +020077 if pr.commits == 1 and (pr.additions <= 1 and pr.deletions <= 1):
78 labels = {'size: XS'}
Anas Nashifd9a300e2023-10-12 10:24:06 +000079
Anas Nashif20483162022-02-25 18:37:47 -050080 if len(fn) > 500:
81 log(f"Too many files changed ({len(fn)}), skipping....")
82 return
Anas Nashifd9a300e2023-10-12 10:24:06 +000083
84 for changed_file in fn:
Anas Nashif20483162022-02-25 18:37:47 -050085 num_files += 1
Anas Nashifd9a300e2023-10-12 10:24:06 +000086 log(f"file: {changed_file.filename}")
87 areas = maintainer_file.path2areas(changed_file.filename)
Anas Nashif20483162022-02-25 18:37:47 -050088
Anas Nashifd9a300e2023-10-12 10:24:06 +000089 if not areas:
90 continue
Anas Nashif20483162022-02-25 18:37:47 -050091
Anas Nashifd9a300e2023-10-12 10:24:06 +000092 all_areas.update(areas)
93 is_instance = False
94 sorted_areas = sorted(areas, key=lambda x: 'Platform' in x.name, reverse=True)
95 for area in sorted_areas:
96 c = 1 if not is_instance else 0
97
98 area_counter[area] += c
99 labels.update(area.labels)
100 # FIXME: Here we count the same file multiple times if it exists in
101 # multiple areas with same maintainer
102 for area_maintainer in area.maintainers:
103 found_maintainers[area_maintainer] += c
104
105 if 'Platform' in area.name:
106 is_instance = True
107
108 area_counter = dict(sorted(area_counter.items(), key=lambda item: item[1], reverse=True))
109 log(f"Area matches: {area_counter}")
Anas Nashif20483162022-02-25 18:37:47 -0500110 log(f"labels: {labels}")
Anas Nashif20483162022-02-25 18:37:47 -0500111
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900112 # Create a list of collaborators ordered by the area match
113 collab = list()
Anas Nashifd9a300e2023-10-12 10:24:06 +0000114 for area in area_counter:
115 collab += maintainer_file.areas[area.name].maintainers
116 collab += maintainer_file.areas[area.name].collaborators
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900117 collab = list(dict.fromkeys(collab))
118 log(f"collab: {collab}")
119
Anas Nashifd9a300e2023-10-12 10:24:06 +0000120 _all_maintainers = dict(sorted(found_maintainers.items(), key=lambda item: item[1], reverse=True))
Anas Nashif20483162022-02-25 18:37:47 -0500121
122 log(f"Submitted by: {pr.user.login}")
Anas Nashifd9a300e2023-10-12 10:24:06 +0000123 log(f"candidate maintainers: {_all_maintainers}")
Anas Nashif20483162022-02-25 18:37:47 -0500124
Anas Nashif1a46b7e2024-06-18 07:27:46 -0400125 assignees = []
Anas Nashif913426e2024-06-25 21:56:57 -0400126 tmp_assignees = []
Fabio Baltierid06450b2022-10-03 14:51:40 +0000127
Anas Nashifd9a300e2023-10-12 10:24:06 +0000128 # we start with areas with most files changed and pick the maintainer from the first one.
129 # if the first area is an implementation, i.e. driver or platform, we
Anas Nashif913426e2024-06-25 21:56:57 -0400130 # continue searching for any other areas involved
Anas Nashifd9a300e2023-10-12 10:24:06 +0000131 for area, count in area_counter.items():
132 if count == 0:
133 continue
134 if len(area.maintainers) > 0:
Anas Nashif913426e2024-06-25 21:56:57 -0400135 tmp_assignees = area.maintainers
136 if pr.user.login in area.maintainers:
137 # submitter = assignee, try to pick next area and
138 # assign someone else other than the submitter
139 continue
140 else:
141 assignees = area.maintainers
Anas Nashif20483162022-02-25 18:37:47 -0500142
Anas Nashifd9a300e2023-10-12 10:24:06 +0000143 if 'Platform' not in area.name:
144 break
Anas Nashif20483162022-02-25 18:37:47 -0500145
Anas Nashif913426e2024-06-25 21:56:57 -0400146 if tmp_assignees and not assignees:
147 assignees = tmp_assignees
148
Anas Nashif1a46b7e2024-06-18 07:27:46 -0400149 if assignees:
150 prop = (found_maintainers[assignees[0]] / num_files) * 100
151 log(f"Picked assignees: {assignees} ({prop:.2f}% ownership)")
Anas Nashifd9a300e2023-10-12 10:24:06 +0000152 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
Anas Nashif0d7d39d2024-01-10 14:10:13 -0500178 # check for reviewers that remove themselves from list of reviewer and
179 # do not attempt to add them again based on MAINTAINERS file.
180 self_removal = []
181 for event in pr.get_issue_events():
182 if event.event == 'review_request_removed' and event.actor == event.requested_reviewer:
183 self_removal.append(event.actor)
184
185 for collaborator in collab:
Anas Nashif60271522022-07-18 19:37:31 -0400186 try:
Anas Nashif0d7d39d2024-01-10 14:10:13 -0500187 gh_user = gh.get_user(collaborator)
Fabio Baltierifec3cee2024-04-02 08:49:48 +0000188 if pr.user == gh_user or gh_user in existing_reviewers:
189 continue
190 if not gh_repo.has_in_collaborators(gh_user):
191 log(f"Skip '{collaborator}': not in collaborators")
192 continue
193 if gh_user in self_removal:
194 log(f"Skip '{collaborator}': self removed")
195 continue
196 reviewers.append(collaborator)
Anas Nashif60271522022-07-18 19:37:31 -0400197 except UnknownObjectException as e:
Anas Nashif0d7d39d2024-01-10 14:10:13 -0500198 log(f"Can't get user '{collaborator}', account does not exist anymore? ({e})")
Anas Nashif20483162022-02-25 18:37:47 -0500199
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900200 if len(existing_reviewers) < 15:
201 reviewer_vacancy = 15 - len(existing_reviewers)
202 reviewers = reviewers[:reviewer_vacancy]
203
204 if reviewers:
205 try:
206 log(f"adding reviewers {reviewers}...")
207 if not args.dry_run:
208 pr.create_review_request(reviewers=reviewers)
209 except GithubException:
210 log("cant add reviewer")
211 else:
212 log("not adding reviewers because the existing reviewer count is greater than or "
213 "equal to 15")
Anas Nashif20483162022-02-25 18:37:47 -0500214
215 ms = []
216 # assignees
Anas Nashif1a46b7e2024-06-18 07:27:46 -0400217 if assignees and not pr.assignee:
Anas Nashif20483162022-02-25 18:37:47 -0500218 try:
Anas Nashif1a46b7e2024-06-18 07:27:46 -0400219 for assignee in assignees:
220 u = gh.get_user(assignee)
221 ms.append(u)
Anas Nashif20483162022-02-25 18:37:47 -0500222 except GithubException:
223 log(f"Error: Unknown user")
224
225 for mm in ms:
226 log(f"Adding assignee {mm}...")
227 if not args.dry_run:
228 pr.add_to_assignees(mm)
Anas Nashifd63c2c42022-06-16 11:25:52 -0400229 else:
230 log("not setting assignee")
Anas Nashif20483162022-02-25 18:37:47 -0500231
232 time.sleep(1)
233
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000234
Fabio Baltierib6cbcba2023-08-22 17:04:31 +0000235def process_issue(gh, maintainer_file, number):
236 gh_repo = gh.get_repo(f"{args.org}/{args.repo}")
237 issue = gh_repo.get_issue(number)
238
239 log(f"Working on {issue.url}: {issue.title}")
240
241 if issue.assignees:
242 print(f"Already assigned {issue.assignees}, bailing out")
243 return
244
245 label_to_maintainer = defaultdict(set)
246 for _, area in maintainer_file.areas.items():
247 if not area.labels:
248 continue
249
250 labels = set()
251 for label in area.labels:
252 labels.add(label.lower())
253 labels = tuple(sorted(labels))
254
255 for maintainer in area.maintainers:
256 label_to_maintainer[labels].add(maintainer)
257
258 # Add extra entries for areas with multiple labels so they match with just
259 # one label if it's specific enough.
260 for areas, maintainers in dict(label_to_maintainer).items():
261 for area in areas:
262 if tuple([area]) not in label_to_maintainer:
263 label_to_maintainer[tuple([area])] = maintainers
264
265 issue_labels = set()
266 for label in issue.labels:
267 label_name = label.name.lower()
268 if tuple([label_name]) not in label_to_maintainer:
269 print(f"Ignoring label: {label}")
270 continue
271 issue_labels.add(label_name)
272 issue_labels = tuple(sorted(issue_labels))
273
274 print(f"Using labels: {issue_labels}")
275
276 if issue_labels not in label_to_maintainer:
277 print(f"no match for the label set, not assigning")
278 return
279
280 for maintainer in label_to_maintainer[issue_labels]:
281 log(f"Adding {maintainer} to {issue.html_url}")
282 if not args.dry_run:
283 issue.add_to_assignees(maintainer)
284
285
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000286def process_modules(gh, maintainers_file):
287 manifest = Manifest.from_file()
288
289 repos = {}
290 for project in manifest.get_projects([]):
291 if not manifest.is_active(project):
292 continue
293
294 if isinstance(project, ManifestProject):
295 continue
296
297 area = f"West project: {project.name}"
298 if area not in maintainers_file.areas:
299 log(f"No area for: {area}")
300 continue
301
302 maintainers = maintainers_file.areas[area].maintainers
303 if not maintainers:
304 log(f"No maintainers for: {area}")
305 continue
306
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000307 collaborators = maintainers_file.areas[area].collaborators
308
309 log(f"Found {area}, maintainers={maintainers}, collaborators={collaborators}")
310
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000311 repo_name = f"{args.org}/{project.name}"
312 repos[repo_name] = maintainers_file.areas[area]
313
314 query = f"is:open is:pr no:assignee"
315 for repo in repos:
316 query += f" repo:{repo}"
317
318 issues = gh.search_issues(query=query)
319 for issue in issues:
320 pull = issue.as_pull_request()
321
322 if pull.draft:
323 continue
324
325 if pull.assignees:
326 log(f"ERROR: {pull.html_url} should have no assignees, found {pull.assignees}")
327 continue
328
329 repo_name = f"{args.org}/{issue.repository.name}"
330 area = repos[repo_name]
331
332 for maintainer in area.maintainers:
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000333 log(f"Assigning {maintainer} to {pull.html_url}")
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000334 if not args.dry_run:
335 pull.add_to_assignees(maintainer)
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000336 pull.create_review_request(maintainer)
337
338 for collaborator in area.collaborators:
339 log(f"Adding {collaborator} to {pull.html_url}")
340 if not args.dry_run:
341 pull.create_review_request(collaborator)
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000342
343
Anas Nashif20483162022-02-25 18:37:47 -0500344def main():
345 parse_args()
346
347 token = os.environ.get('GITHUB_TOKEN', None)
348 if not token:
349 sys.exit('Github token not set in environment, please set the '
350 'GITHUB_TOKEN environment variable and retry.')
351
352 gh = Github(token)
353 maintainer_file = Maintainers(args.maintainer_file)
354
355 if args.pull_request:
356 process_pr(gh, maintainer_file, args.pull_request)
Fabio Baltieri5e786602023-08-25 13:49:27 +0000357 elif args.issue:
Fabio Baltierib6cbcba2023-08-22 17:04:31 +0000358 process_issue(gh, maintainer_file, args.issue)
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000359 elif args.modules:
360 process_modules(gh, maintainer_file)
Anas Nashif20483162022-02-25 18:37:47 -0500361 else:
362 if args.since:
363 since = args.since
364 else:
365 today = datetime.date.today()
366 since = today - datetime.timedelta(days=1)
367
368 common_prs = f'repo:{args.org}/{args.repo} is:open is:pr base:main -is:draft no:assignee created:>{since}'
369 pulls = gh.search_issues(query=f'{common_prs}')
370
371 for issue in pulls:
372 process_pr(gh, maintainer_file, issue.number)
373
374
375if __name__ == "__main__":
376 main()