blob: 2a4575eb3c40312c4615b6b2a2bbaa4976e56647 [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)
67 maint = defaultdict(int)
68
69 num_files = 0
70 all_areas = set()
71 fn = list(pr.get_files())
72 if len(fn) > 500:
73 log(f"Too many files changed ({len(fn)}), skipping....")
74 return
75 for f in pr.get_files():
76 num_files += 1
77 log(f"file: {f.filename}")
78 areas = maintainer_file.path2areas(f.filename)
79
80 if areas:
81 all_areas.update(areas)
82 for a in areas:
83 area_counter[a.name] += 1
84 labels.update(a.labels)
Anas Nashif20483162022-02-25 18:37:47 -050085 for p in a.maintainers:
86 maint[p] += 1
87
88 ac = dict(sorted(area_counter.items(), key=lambda item: item[1], reverse=True))
89 log(f"Area matches: {ac}")
90 log(f"labels: {labels}")
Anas Nashif20483162022-02-25 18:37:47 -050091
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +090092 # Create a list of collaborators ordered by the area match
93 collab = list()
94 for a in ac:
95 collab += maintainer_file.areas[a].maintainers
96 collab += maintainer_file.areas[a].collaborators
97 collab = list(dict.fromkeys(collab))
98 log(f"collab: {collab}")
99
Anas Nashif20483162022-02-25 18:37:47 -0500100 sm = dict(sorted(maint.items(), key=lambda item: item[1], reverse=True))
101
102 log(f"Submitted by: {pr.user.login}")
103 log(f"candidate maintainers: {sm}")
104
Fabio Baltierid06450b2022-10-03 14:51:40 +0000105 maintainer = "None"
106 maintainers = list(sm.keys())
107
Anas Nashif20483162022-02-25 18:37:47 -0500108 prop = 0
Fabio Baltierid06450b2022-10-03 14:51:40 +0000109 if maintainers:
110 maintainer = maintainers[0]
Anas Nashif20483162022-02-25 18:37:47 -0500111
112 if len(ac) > 1 and list(ac.values())[0] == list(ac.values())[1]:
Anas Nashif20483162022-02-25 18:37:47 -0500113 for aa in ac:
114 if 'Documentation' in aa:
115 log("++ With multiple areas of same weight including docs, take something else other than Documentation as the maintainer")
116 for a in all_areas:
Fabio Baltierid06450b2022-10-03 14:51:40 +0000117 if (a.name == aa and
118 a.maintainers and a.maintainers[0] == maintainer and
119 len(maintainers) > 1):
120 maintainer = maintainers[1]
Anas Nashif20483162022-02-25 18:37:47 -0500121 elif 'Platform' in aa:
Fabio Baltieri279ab432022-10-03 15:00:10 +0000122 log("++ Platform takes precedence over subsystem...")
Anas Nashif20483162022-02-25 18:37:47 -0500123 log(f"Set maintainer of area {aa}")
124 for a in all_areas:
125 if a.name == aa:
126 if a.maintainers:
127 maintainer = a.maintainers[0]
128 break
129
130
131 # if the submitter is the same as the maintainer, check if we have
132 # multiple maintainers
133 if pr.user.login == maintainer:
134 log("Submitter is same as Assignee, trying to find another assignee...")
135 aff = list(ac.keys())[0]
136 for a in all_areas:
137 if a.name == aff:
138 if len(a.maintainers) > 1:
139 maintainer = a.maintainers[1]
140 else:
141 log(f"This area has only one maintainer, keeping assignee as {maintainer}")
142
143 prop = (maint[maintainer] / num_files) * 100
144 if prop < 20:
145 maintainer = "None"
Fabio Baltierid06450b2022-10-03 14:51:40 +0000146
Anas Nashif20483162022-02-25 18:37:47 -0500147 log(f"Picked maintainer: {maintainer} ({prop:.2f}% ownership)")
148 log("+++++++++++++++++++++++++")
149
150 # Set labels
Fabio Baltieri16d723e2023-01-26 17:03:13 +0000151 if labels:
152 if len(labels) < 10:
153 for l in labels:
154 log(f"adding label {l}...")
155 if not args.dry_run:
156 pr.add_to_labels(l)
157 else:
158 log(f"Too many labels to be applied")
Anas Nashif20483162022-02-25 18:37:47 -0500159
160 if collab:
161 reviewers = []
162 existing_reviewers = set()
163
164 revs = pr.get_reviews()
165 for review in revs:
166 existing_reviewers.add(review.user)
167
168 rl = pr.get_review_requests()
169 page = 0
170 for r in rl:
171 existing_reviewers |= set(r.get_page(page))
172 page += 1
173
174 for c in collab:
Anas Nashif60271522022-07-18 19:37:31 -0400175 try:
176 u = gh.get_user(c)
177 if pr.user != u and gh_repo.has_in_collaborators(u):
178 if u not in existing_reviewers:
179 reviewers.append(c)
180 except UnknownObjectException as e:
181 log(f"Can't get user '{c}', account does not exist anymore? ({e})")
Anas Nashif20483162022-02-25 18:37:47 -0500182
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900183 if len(existing_reviewers) < 15:
184 reviewer_vacancy = 15 - len(existing_reviewers)
185 reviewers = reviewers[:reviewer_vacancy]
186
187 if reviewers:
188 try:
189 log(f"adding reviewers {reviewers}...")
190 if not args.dry_run:
191 pr.create_review_request(reviewers=reviewers)
192 except GithubException:
193 log("cant add reviewer")
194 else:
195 log("not adding reviewers because the existing reviewer count is greater than or "
196 "equal to 15")
Anas Nashif20483162022-02-25 18:37:47 -0500197
198 ms = []
199 # assignees
Anas Nashifd63c2c42022-06-16 11:25:52 -0400200 if maintainer != 'None' and not pr.assignee:
Anas Nashif20483162022-02-25 18:37:47 -0500201 try:
202 u = gh.get_user(maintainer)
203 ms.append(u)
204 except GithubException:
205 log(f"Error: Unknown user")
206
207 for mm in ms:
208 log(f"Adding assignee {mm}...")
209 if not args.dry_run:
210 pr.add_to_assignees(mm)
Anas Nashifd63c2c42022-06-16 11:25:52 -0400211 else:
212 log("not setting assignee")
Anas Nashif20483162022-02-25 18:37:47 -0500213
214 time.sleep(1)
215
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000216
Fabio Baltierib6cbcba2023-08-22 17:04:31 +0000217def process_issue(gh, maintainer_file, number):
218 gh_repo = gh.get_repo(f"{args.org}/{args.repo}")
219 issue = gh_repo.get_issue(number)
220
221 log(f"Working on {issue.url}: {issue.title}")
222
223 if issue.assignees:
224 print(f"Already assigned {issue.assignees}, bailing out")
225 return
226
227 label_to_maintainer = defaultdict(set)
228 for _, area in maintainer_file.areas.items():
229 if not area.labels:
230 continue
231
232 labels = set()
233 for label in area.labels:
234 labels.add(label.lower())
235 labels = tuple(sorted(labels))
236
237 for maintainer in area.maintainers:
238 label_to_maintainer[labels].add(maintainer)
239
240 # Add extra entries for areas with multiple labels so they match with just
241 # one label if it's specific enough.
242 for areas, maintainers in dict(label_to_maintainer).items():
243 for area in areas:
244 if tuple([area]) not in label_to_maintainer:
245 label_to_maintainer[tuple([area])] = maintainers
246
247 issue_labels = set()
248 for label in issue.labels:
249 label_name = label.name.lower()
250 if tuple([label_name]) not in label_to_maintainer:
251 print(f"Ignoring label: {label}")
252 continue
253 issue_labels.add(label_name)
254 issue_labels = tuple(sorted(issue_labels))
255
256 print(f"Using labels: {issue_labels}")
257
258 if issue_labels not in label_to_maintainer:
259 print(f"no match for the label set, not assigning")
260 return
261
262 for maintainer in label_to_maintainer[issue_labels]:
263 log(f"Adding {maintainer} to {issue.html_url}")
264 if not args.dry_run:
265 issue.add_to_assignees(maintainer)
266
267
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000268def process_modules(gh, maintainers_file):
269 manifest = Manifest.from_file()
270
271 repos = {}
272 for project in manifest.get_projects([]):
273 if not manifest.is_active(project):
274 continue
275
276 if isinstance(project, ManifestProject):
277 continue
278
279 area = f"West project: {project.name}"
280 if area not in maintainers_file.areas:
281 log(f"No area for: {area}")
282 continue
283
284 maintainers = maintainers_file.areas[area].maintainers
285 if not maintainers:
286 log(f"No maintainers for: {area}")
287 continue
288
289 log(f"Found {area}, maintainers={maintainers}")
290 repo_name = f"{args.org}/{project.name}"
291 repos[repo_name] = maintainers_file.areas[area]
292
293 query = f"is:open is:pr no:assignee"
294 for repo in repos:
295 query += f" repo:{repo}"
296
297 issues = gh.search_issues(query=query)
298 for issue in issues:
299 pull = issue.as_pull_request()
300
301 if pull.draft:
302 continue
303
304 if pull.assignees:
305 log(f"ERROR: {pull.html_url} should have no assignees, found {pull.assignees}")
306 continue
307
308 repo_name = f"{args.org}/{issue.repository.name}"
309 area = repos[repo_name]
310
311 for maintainer in area.maintainers:
312 log(f"Adding {maintainer} to {pull.html_url}")
313 if not args.dry_run:
314 pull.add_to_assignees(maintainer)
315
316
Anas Nashif20483162022-02-25 18:37:47 -0500317def main():
318 parse_args()
319
320 token = os.environ.get('GITHUB_TOKEN', None)
321 if not token:
322 sys.exit('Github token not set in environment, please set the '
323 'GITHUB_TOKEN environment variable and retry.')
324
325 gh = Github(token)
326 maintainer_file = Maintainers(args.maintainer_file)
327
328 if args.pull_request:
329 process_pr(gh, maintainer_file, args.pull_request)
Fabio Baltieri5e786602023-08-25 13:49:27 +0000330 elif args.issue:
Fabio Baltierib6cbcba2023-08-22 17:04:31 +0000331 process_issue(gh, maintainer_file, args.issue)
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000332 elif args.modules:
333 process_modules(gh, maintainer_file)
Anas Nashif20483162022-02-25 18:37:47 -0500334 else:
335 if args.since:
336 since = args.since
337 else:
338 today = datetime.date.today()
339 since = today - datetime.timedelta(days=1)
340
341 common_prs = f'repo:{args.org}/{args.repo} is:open is:pr base:main -is:draft no:assignee created:>{since}'
342 pulls = gh.search_issues(query=f'{common_prs}')
343
344 for issue in pulls:
345 process_pr(gh, maintainer_file, issue.number)
346
347
348if __name__ == "__main__":
349 main()