blob: d20919315859212bd86ea7f307ab4a81586c8a8d [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 manifest_change = False
74 for changed_file in fn:
75 if changed_file.filename in ['west.yml','submanifests/optional.yaml']:
76 manifest_change = True
77 break
78
Anas Nashifd9a300e2023-10-12 10:24:06 +000079 # one liner PRs should be trivial
Anas Nashifdebe7fe2023-11-06 13:22:55 +000080 if pr.commits == 1 and (pr.additions <= 1 and pr.deletions <= 1) and not manifest_change:
Anas Nashif0d7d39d2024-01-10 14:10:13 -050081 labels = {'Trivial'}
Anas Nashifd9a300e2023-10-12 10:24:06 +000082
Anas Nashif20483162022-02-25 18:37:47 -050083 if len(fn) > 500:
84 log(f"Too many files changed ({len(fn)}), skipping....")
85 return
Anas Nashifd9a300e2023-10-12 10:24:06 +000086
87 for changed_file in fn:
Anas Nashif20483162022-02-25 18:37:47 -050088 num_files += 1
Anas Nashifd9a300e2023-10-12 10:24:06 +000089 log(f"file: {changed_file.filename}")
90 areas = maintainer_file.path2areas(changed_file.filename)
Anas Nashif20483162022-02-25 18:37:47 -050091
Anas Nashifd9a300e2023-10-12 10:24:06 +000092 if not areas:
93 continue
Anas Nashif20483162022-02-25 18:37:47 -050094
Anas Nashifd9a300e2023-10-12 10:24:06 +000095 all_areas.update(areas)
96 is_instance = False
97 sorted_areas = sorted(areas, key=lambda x: 'Platform' in x.name, reverse=True)
98 for area in sorted_areas:
99 c = 1 if not is_instance else 0
100
101 area_counter[area] += c
102 labels.update(area.labels)
103 # FIXME: Here we count the same file multiple times if it exists in
104 # multiple areas with same maintainer
105 for area_maintainer in area.maintainers:
106 found_maintainers[area_maintainer] += c
107
108 if 'Platform' in area.name:
109 is_instance = True
110
111 area_counter = dict(sorted(area_counter.items(), key=lambda item: item[1], reverse=True))
112 log(f"Area matches: {area_counter}")
Anas Nashif20483162022-02-25 18:37:47 -0500113 log(f"labels: {labels}")
Anas Nashif20483162022-02-25 18:37:47 -0500114
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900115 # Create a list of collaborators ordered by the area match
116 collab = list()
Anas Nashifd9a300e2023-10-12 10:24:06 +0000117 for area in area_counter:
118 collab += maintainer_file.areas[area.name].maintainers
119 collab += maintainer_file.areas[area.name].collaborators
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900120 collab = list(dict.fromkeys(collab))
121 log(f"collab: {collab}")
122
Anas Nashifd9a300e2023-10-12 10:24:06 +0000123 _all_maintainers = dict(sorted(found_maintainers.items(), key=lambda item: item[1], reverse=True))
Anas Nashif20483162022-02-25 18:37:47 -0500124
125 log(f"Submitted by: {pr.user.login}")
Anas Nashifd9a300e2023-10-12 10:24:06 +0000126 log(f"candidate maintainers: {_all_maintainers}")
Anas Nashif20483162022-02-25 18:37:47 -0500127
Anas Nashifd9a300e2023-10-12 10:24:06 +0000128 maintainers = list(_all_maintainers.keys())
129 assignee = None
Fabio Baltierid06450b2022-10-03 14:51:40 +0000130
Anas Nashifd9a300e2023-10-12 10:24:06 +0000131 # we start with areas with most files changed and pick the maintainer from the first one.
132 # if the first area is an implementation, i.e. driver or platform, we
133 # continue searching for any other areas
134 for area, count in area_counter.items():
135 if count == 0:
136 continue
137 if len(area.maintainers) > 0:
138 assignee = area.maintainers[0]
Anas Nashif20483162022-02-25 18:37:47 -0500139
Anas Nashifd9a300e2023-10-12 10:24:06 +0000140 if 'Platform' not in area.name:
141 break
Anas Nashif20483162022-02-25 18:37:47 -0500142
Anas Nashifd9a300e2023-10-12 10:24:06 +0000143 # if the submitter is the same as the maintainer, check if we have
144 # multiple maintainers
145 if len(maintainers) > 1 and pr.user.login == assignee:
146 log("Submitter is same as Assignee, trying to find another assignee...")
147 aff = list(area_counter.keys())[0]
148 for area in all_areas:
Fabio Baltieri49e8e112024-01-16 11:52:11 +0000149 if area == aff:
Anas Nashifd9a300e2023-10-12 10:24:06 +0000150 if len(area.maintainers) > 1:
151 assignee = area.maintainers[1]
152 else:
153 log(f"This area has only one maintainer, keeping assignee as {assignee}")
Anas Nashif20483162022-02-25 18:37:47 -0500154
Anas Nashifd9a300e2023-10-12 10:24:06 +0000155 if assignee:
156 prop = (found_maintainers[assignee] / num_files) * 100
157 log(f"Picked assignee: {assignee} ({prop:.2f}% ownership)")
158 log("+++++++++++++++++++++++++")
Anas Nashif20483162022-02-25 18:37:47 -0500159
160 # Set labels
Fabio Baltieri16d723e2023-01-26 17:03:13 +0000161 if labels:
162 if len(labels) < 10:
163 for l in labels:
164 log(f"adding label {l}...")
165 if not args.dry_run:
166 pr.add_to_labels(l)
167 else:
168 log(f"Too many labels to be applied")
Anas Nashif20483162022-02-25 18:37:47 -0500169
170 if collab:
171 reviewers = []
172 existing_reviewers = set()
173
174 revs = pr.get_reviews()
175 for review in revs:
176 existing_reviewers.add(review.user)
177
178 rl = pr.get_review_requests()
179 page = 0
180 for r in rl:
181 existing_reviewers |= set(r.get_page(page))
182 page += 1
183
Anas Nashif0d7d39d2024-01-10 14:10:13 -0500184 # check for reviewers that remove themselves from list of reviewer and
185 # do not attempt to add them again based on MAINTAINERS file.
186 self_removal = []
187 for event in pr.get_issue_events():
188 if event.event == 'review_request_removed' and event.actor == event.requested_reviewer:
189 self_removal.append(event.actor)
190
191 for collaborator in collab:
Anas Nashif60271522022-07-18 19:37:31 -0400192 try:
Anas Nashif0d7d39d2024-01-10 14:10:13 -0500193 gh_user = gh.get_user(collaborator)
Fabio Baltierifec3cee2024-04-02 08:49:48 +0000194 if pr.user == gh_user or gh_user in existing_reviewers:
195 continue
196 if not gh_repo.has_in_collaborators(gh_user):
197 log(f"Skip '{collaborator}': not in collaborators")
198 continue
199 if gh_user in self_removal:
200 log(f"Skip '{collaborator}': self removed")
201 continue
202 reviewers.append(collaborator)
Anas Nashif60271522022-07-18 19:37:31 -0400203 except UnknownObjectException as e:
Anas Nashif0d7d39d2024-01-10 14:10:13 -0500204 log(f"Can't get user '{collaborator}', account does not exist anymore? ({e})")
Anas Nashif20483162022-02-25 18:37:47 -0500205
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900206 if len(existing_reviewers) < 15:
207 reviewer_vacancy = 15 - len(existing_reviewers)
208 reviewers = reviewers[:reviewer_vacancy]
209
210 if reviewers:
211 try:
212 log(f"adding reviewers {reviewers}...")
213 if not args.dry_run:
214 pr.create_review_request(reviewers=reviewers)
215 except GithubException:
216 log("cant add reviewer")
217 else:
218 log("not adding reviewers because the existing reviewer count is greater than or "
219 "equal to 15")
Anas Nashif20483162022-02-25 18:37:47 -0500220
221 ms = []
222 # assignees
Anas Nashifd9a300e2023-10-12 10:24:06 +0000223 if assignee and not pr.assignee:
Anas Nashif20483162022-02-25 18:37:47 -0500224 try:
Anas Nashifd9a300e2023-10-12 10:24:06 +0000225 u = gh.get_user(assignee)
Anas Nashif20483162022-02-25 18:37:47 -0500226 ms.append(u)
227 except GithubException:
228 log(f"Error: Unknown user")
229
230 for mm in ms:
231 log(f"Adding assignee {mm}...")
232 if not args.dry_run:
233 pr.add_to_assignees(mm)
Anas Nashifd63c2c42022-06-16 11:25:52 -0400234 else:
235 log("not setting assignee")
Anas Nashif20483162022-02-25 18:37:47 -0500236
237 time.sleep(1)
238
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000239
Fabio Baltierib6cbcba2023-08-22 17:04:31 +0000240def process_issue(gh, maintainer_file, number):
241 gh_repo = gh.get_repo(f"{args.org}/{args.repo}")
242 issue = gh_repo.get_issue(number)
243
244 log(f"Working on {issue.url}: {issue.title}")
245
246 if issue.assignees:
247 print(f"Already assigned {issue.assignees}, bailing out")
248 return
249
250 label_to_maintainer = defaultdict(set)
251 for _, area in maintainer_file.areas.items():
252 if not area.labels:
253 continue
254
255 labels = set()
256 for label in area.labels:
257 labels.add(label.lower())
258 labels = tuple(sorted(labels))
259
260 for maintainer in area.maintainers:
261 label_to_maintainer[labels].add(maintainer)
262
263 # Add extra entries for areas with multiple labels so they match with just
264 # one label if it's specific enough.
265 for areas, maintainers in dict(label_to_maintainer).items():
266 for area in areas:
267 if tuple([area]) not in label_to_maintainer:
268 label_to_maintainer[tuple([area])] = maintainers
269
270 issue_labels = set()
271 for label in issue.labels:
272 label_name = label.name.lower()
273 if tuple([label_name]) not in label_to_maintainer:
274 print(f"Ignoring label: {label}")
275 continue
276 issue_labels.add(label_name)
277 issue_labels = tuple(sorted(issue_labels))
278
279 print(f"Using labels: {issue_labels}")
280
281 if issue_labels not in label_to_maintainer:
282 print(f"no match for the label set, not assigning")
283 return
284
285 for maintainer in label_to_maintainer[issue_labels]:
286 log(f"Adding {maintainer} to {issue.html_url}")
287 if not args.dry_run:
288 issue.add_to_assignees(maintainer)
289
290
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000291def process_modules(gh, maintainers_file):
292 manifest = Manifest.from_file()
293
294 repos = {}
295 for project in manifest.get_projects([]):
296 if not manifest.is_active(project):
297 continue
298
299 if isinstance(project, ManifestProject):
300 continue
301
302 area = f"West project: {project.name}"
303 if area not in maintainers_file.areas:
304 log(f"No area for: {area}")
305 continue
306
307 maintainers = maintainers_file.areas[area].maintainers
308 if not maintainers:
309 log(f"No maintainers for: {area}")
310 continue
311
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000312 collaborators = maintainers_file.areas[area].collaborators
313
314 log(f"Found {area}, maintainers={maintainers}, collaborators={collaborators}")
315
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000316 repo_name = f"{args.org}/{project.name}"
317 repos[repo_name] = maintainers_file.areas[area]
318
319 query = f"is:open is:pr no:assignee"
320 for repo in repos:
321 query += f" repo:{repo}"
322
323 issues = gh.search_issues(query=query)
324 for issue in issues:
325 pull = issue.as_pull_request()
326
327 if pull.draft:
328 continue
329
330 if pull.assignees:
331 log(f"ERROR: {pull.html_url} should have no assignees, found {pull.assignees}")
332 continue
333
334 repo_name = f"{args.org}/{issue.repository.name}"
335 area = repos[repo_name]
336
337 for maintainer in area.maintainers:
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000338 log(f"Assigning {maintainer} to {pull.html_url}")
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000339 if not args.dry_run:
340 pull.add_to_assignees(maintainer)
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000341 pull.create_review_request(maintainer)
342
343 for collaborator in area.collaborators:
344 log(f"Adding {collaborator} to {pull.html_url}")
345 if not args.dry_run:
346 pull.create_review_request(collaborator)
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000347
348
Anas Nashif20483162022-02-25 18:37:47 -0500349def main():
350 parse_args()
351
352 token = os.environ.get('GITHUB_TOKEN', None)
353 if not token:
354 sys.exit('Github token not set in environment, please set the '
355 'GITHUB_TOKEN environment variable and retry.')
356
357 gh = Github(token)
358 maintainer_file = Maintainers(args.maintainer_file)
359
360 if args.pull_request:
361 process_pr(gh, maintainer_file, args.pull_request)
Fabio Baltieri5e786602023-08-25 13:49:27 +0000362 elif args.issue:
Fabio Baltierib6cbcba2023-08-22 17:04:31 +0000363 process_issue(gh, maintainer_file, args.issue)
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000364 elif args.modules:
365 process_modules(gh, maintainer_file)
Anas Nashif20483162022-02-25 18:37:47 -0500366 else:
367 if args.since:
368 since = args.since
369 else:
370 today = datetime.date.today()
371 since = today - datetime.timedelta(days=1)
372
373 common_prs = f'repo:{args.org}/{args.repo} is:open is:pr base:main -is:draft no:assignee created:>{since}'
374 pulls = gh.search_issues(query=f'{common_prs}')
375
376 for issue in pulls:
377 process_pr(gh, maintainer_file, issue.number)
378
379
380if __name__ == "__main__":
381 main()