blob: 269bde82f54938c37b2ab92121a92982d19febd8 [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 Nashifd9a300e2023-10-12 10:24:06 +000081 labels = {'trivial'}
82
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:
149 if area.name == aff:
150 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
184 for c in collab:
Anas Nashif60271522022-07-18 19:37:31 -0400185 try:
186 u = gh.get_user(c)
187 if pr.user != u and gh_repo.has_in_collaborators(u):
188 if u not in existing_reviewers:
189 reviewers.append(c)
190 except UnknownObjectException as e:
191 log(f"Can't get user '{c}', account does not exist anymore? ({e})")
Anas Nashif20483162022-02-25 18:37:47 -0500192
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900193 if len(existing_reviewers) < 15:
194 reviewer_vacancy = 15 - len(existing_reviewers)
195 reviewers = reviewers[:reviewer_vacancy]
196
197 if reviewers:
198 try:
199 log(f"adding reviewers {reviewers}...")
200 if not args.dry_run:
201 pr.create_review_request(reviewers=reviewers)
202 except GithubException:
203 log("cant add reviewer")
204 else:
205 log("not adding reviewers because the existing reviewer count is greater than or "
206 "equal to 15")
Anas Nashif20483162022-02-25 18:37:47 -0500207
208 ms = []
209 # assignees
Anas Nashifd9a300e2023-10-12 10:24:06 +0000210 if assignee and not pr.assignee:
Anas Nashif20483162022-02-25 18:37:47 -0500211 try:
Anas Nashifd9a300e2023-10-12 10:24:06 +0000212 u = gh.get_user(assignee)
Anas Nashif20483162022-02-25 18:37:47 -0500213 ms.append(u)
214 except GithubException:
215 log(f"Error: Unknown user")
216
217 for mm in ms:
218 log(f"Adding assignee {mm}...")
219 if not args.dry_run:
220 pr.add_to_assignees(mm)
Anas Nashifd63c2c42022-06-16 11:25:52 -0400221 else:
222 log("not setting assignee")
Anas Nashif20483162022-02-25 18:37:47 -0500223
224 time.sleep(1)
225
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000226
Fabio Baltierib6cbcba2023-08-22 17:04:31 +0000227def process_issue(gh, maintainer_file, number):
228 gh_repo = gh.get_repo(f"{args.org}/{args.repo}")
229 issue = gh_repo.get_issue(number)
230
231 log(f"Working on {issue.url}: {issue.title}")
232
233 if issue.assignees:
234 print(f"Already assigned {issue.assignees}, bailing out")
235 return
236
237 label_to_maintainer = defaultdict(set)
238 for _, area in maintainer_file.areas.items():
239 if not area.labels:
240 continue
241
242 labels = set()
243 for label in area.labels:
244 labels.add(label.lower())
245 labels = tuple(sorted(labels))
246
247 for maintainer in area.maintainers:
248 label_to_maintainer[labels].add(maintainer)
249
250 # Add extra entries for areas with multiple labels so they match with just
251 # one label if it's specific enough.
252 for areas, maintainers in dict(label_to_maintainer).items():
253 for area in areas:
254 if tuple([area]) not in label_to_maintainer:
255 label_to_maintainer[tuple([area])] = maintainers
256
257 issue_labels = set()
258 for label in issue.labels:
259 label_name = label.name.lower()
260 if tuple([label_name]) not in label_to_maintainer:
261 print(f"Ignoring label: {label}")
262 continue
263 issue_labels.add(label_name)
264 issue_labels = tuple(sorted(issue_labels))
265
266 print(f"Using labels: {issue_labels}")
267
268 if issue_labels not in label_to_maintainer:
269 print(f"no match for the label set, not assigning")
270 return
271
272 for maintainer in label_to_maintainer[issue_labels]:
273 log(f"Adding {maintainer} to {issue.html_url}")
274 if not args.dry_run:
275 issue.add_to_assignees(maintainer)
276
277
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000278def process_modules(gh, maintainers_file):
279 manifest = Manifest.from_file()
280
281 repos = {}
282 for project in manifest.get_projects([]):
283 if not manifest.is_active(project):
284 continue
285
286 if isinstance(project, ManifestProject):
287 continue
288
289 area = f"West project: {project.name}"
290 if area not in maintainers_file.areas:
291 log(f"No area for: {area}")
292 continue
293
294 maintainers = maintainers_file.areas[area].maintainers
295 if not maintainers:
296 log(f"No maintainers for: {area}")
297 continue
298
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000299 collaborators = maintainers_file.areas[area].collaborators
300
301 log(f"Found {area}, maintainers={maintainers}, collaborators={collaborators}")
302
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000303 repo_name = f"{args.org}/{project.name}"
304 repos[repo_name] = maintainers_file.areas[area]
305
306 query = f"is:open is:pr no:assignee"
307 for repo in repos:
308 query += f" repo:{repo}"
309
310 issues = gh.search_issues(query=query)
311 for issue in issues:
312 pull = issue.as_pull_request()
313
314 if pull.draft:
315 continue
316
317 if pull.assignees:
318 log(f"ERROR: {pull.html_url} should have no assignees, found {pull.assignees}")
319 continue
320
321 repo_name = f"{args.org}/{issue.repository.name}"
322 area = repos[repo_name]
323
324 for maintainer in area.maintainers:
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000325 log(f"Assigning {maintainer} to {pull.html_url}")
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000326 if not args.dry_run:
327 pull.add_to_assignees(maintainer)
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000328 pull.create_review_request(maintainer)
329
330 for collaborator in area.collaborators:
331 log(f"Adding {collaborator} to {pull.html_url}")
332 if not args.dry_run:
333 pull.create_review_request(collaborator)
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000334
335
Anas Nashif20483162022-02-25 18:37:47 -0500336def main():
337 parse_args()
338
339 token = os.environ.get('GITHUB_TOKEN', None)
340 if not token:
341 sys.exit('Github token not set in environment, please set the '
342 'GITHUB_TOKEN environment variable and retry.')
343
344 gh = Github(token)
345 maintainer_file = Maintainers(args.maintainer_file)
346
347 if args.pull_request:
348 process_pr(gh, maintainer_file, args.pull_request)
Fabio Baltieri5e786602023-08-25 13:49:27 +0000349 elif args.issue:
Fabio Baltierib6cbcba2023-08-22 17:04:31 +0000350 process_issue(gh, maintainer_file, args.issue)
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000351 elif args.modules:
352 process_modules(gh, maintainer_file)
Anas Nashif20483162022-02-25 18:37:47 -0500353 else:
354 if args.since:
355 since = args.since
356 else:
357 today = datetime.date.today()
358 since = today - datetime.timedelta(days=1)
359
360 common_prs = f'repo:{args.org}/{args.repo} is:open is:pr base:main -is:draft no:assignee created:>{since}'
361 pulls = gh.search_issues(query=f'{common_prs}')
362
363 for issue in pulls:
364 process_pr(gh, maintainer_file, issue.number)
365
366
367if __name__ == "__main__":
368 main()