1#!/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 12from github.GithubException import UnknownObjectException 13from collections import defaultdict 14 15TOP_DIR = os.path.join(os.path.dirname(__file__)) 16sys.path.insert(0, os.path.join(TOP_DIR, "scripts")) 17from get_maintainer import Maintainers 18 19def log(s): 20 if args.verbose > 0: 21 print(s, file=sys.stdout) 22 23def parse_args(): 24 global args 25 parser = argparse.ArgumentParser( 26 description=__doc__, 27 formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False) 28 29 parser.add_argument("-M", "--maintainer-file", required=False, default="MAINTAINERS.yml", 30 help="Maintainer file to be used.") 31 parser.add_argument("-P", "--pull_request", required=False, default=None, type=int, 32 help="Operate on one pull-request only.") 33 parser.add_argument("-s", "--since", required=False, 34 help="Process pull-requests since date.") 35 36 parser.add_argument("-y", "--dry-run", action="store_true", default=False, 37 help="Dry run only.") 38 39 parser.add_argument("-o", "--org", default="zephyrproject-rtos", 40 help="Github organisation") 41 42 parser.add_argument("-r", "--repo", default="zephyr", 43 help="Github repository") 44 45 parser.add_argument("-v", "--verbose", action="count", default=0, 46 help="Verbose Output") 47 48 args = parser.parse_args() 49 50def process_pr(gh, maintainer_file, number): 51 52 gh_repo = gh.get_repo(f"{args.org}/{args.repo}") 53 pr = gh_repo.get_pull(number) 54 55 log(f"working on https://github.com/{args.org}/{args.repo}/pull/{pr.number} : {pr.title}") 56 57 labels = set() 58 area_counter = defaultdict(int) 59 maint = defaultdict(int) 60 61 num_files = 0 62 all_areas = set() 63 fn = list(pr.get_files()) 64 if len(fn) > 500: 65 log(f"Too many files changed ({len(fn)}), skipping....") 66 return 67 for f in pr.get_files(): 68 num_files += 1 69 log(f"file: {f.filename}") 70 areas = maintainer_file.path2areas(f.filename) 71 72 if areas: 73 all_areas.update(areas) 74 for a in areas: 75 area_counter[a.name] += 1 76 labels.update(a.labels) 77 for p in a.maintainers: 78 maint[p] += 1 79 80 ac = dict(sorted(area_counter.items(), key=lambda item: item[1], reverse=True)) 81 log(f"Area matches: {ac}") 82 log(f"labels: {labels}") 83 84 # Create a list of collaborators ordered by the area match 85 collab = list() 86 for a in ac: 87 collab += maintainer_file.areas[a].maintainers 88 collab += maintainer_file.areas[a].collaborators 89 collab = list(dict.fromkeys(collab)) 90 log(f"collab: {collab}") 91 92 sm = dict(sorted(maint.items(), key=lambda item: item[1], reverse=True)) 93 94 log(f"Submitted by: {pr.user.login}") 95 log(f"candidate maintainers: {sm}") 96 97 maintainer = "None" 98 maintainers = list(sm.keys()) 99 100 prop = 0 101 if maintainers: 102 maintainer = maintainers[0] 103 104 if len(ac) > 1 and list(ac.values())[0] == list(ac.values())[1]: 105 for aa in ac: 106 if 'Documentation' in aa: 107 log("++ With multiple areas of same weight including docs, take something else other than Documentation as the maintainer") 108 for a in all_areas: 109 if (a.name == aa and 110 a.maintainers and a.maintainers[0] == maintainer and 111 len(maintainers) > 1): 112 maintainer = maintainers[1] 113 elif 'Platform' in aa: 114 log("++ Platform takes precedence over subsystem...") 115 log(f"Set maintainer of area {aa}") 116 for a in all_areas: 117 if a.name == aa: 118 if a.maintainers: 119 maintainer = a.maintainers[0] 120 break 121 122 123 # if the submitter is the same as the maintainer, check if we have 124 # multiple maintainers 125 if pr.user.login == maintainer: 126 log("Submitter is same as Assignee, trying to find another assignee...") 127 aff = list(ac.keys())[0] 128 for a in all_areas: 129 if a.name == aff: 130 if len(a.maintainers) > 1: 131 maintainer = a.maintainers[1] 132 else: 133 log(f"This area has only one maintainer, keeping assignee as {maintainer}") 134 135 prop = (maint[maintainer] / num_files) * 100 136 if prop < 20: 137 maintainer = "None" 138 139 log(f"Picked maintainer: {maintainer} ({prop:.2f}% ownership)") 140 log("+++++++++++++++++++++++++") 141 142 # Set labels 143 if labels: 144 if len(labels) < 10: 145 for l in labels: 146 log(f"adding label {l}...") 147 if not args.dry_run: 148 pr.add_to_labels(l) 149 else: 150 log(f"Too many labels to be applied") 151 152 if collab: 153 reviewers = [] 154 existing_reviewers = set() 155 156 revs = pr.get_reviews() 157 for review in revs: 158 existing_reviewers.add(review.user) 159 160 rl = pr.get_review_requests() 161 page = 0 162 for r in rl: 163 existing_reviewers |= set(r.get_page(page)) 164 page += 1 165 166 for c in collab: 167 try: 168 u = gh.get_user(c) 169 if pr.user != u and gh_repo.has_in_collaborators(u): 170 if u not in existing_reviewers: 171 reviewers.append(c) 172 except UnknownObjectException as e: 173 log(f"Can't get user '{c}', account does not exist anymore? ({e})") 174 175 if len(existing_reviewers) < 15: 176 reviewer_vacancy = 15 - len(existing_reviewers) 177 reviewers = reviewers[:reviewer_vacancy] 178 179 if reviewers: 180 try: 181 log(f"adding reviewers {reviewers}...") 182 if not args.dry_run: 183 pr.create_review_request(reviewers=reviewers) 184 except GithubException: 185 log("cant add reviewer") 186 else: 187 log("not adding reviewers because the existing reviewer count is greater than or " 188 "equal to 15") 189 190 ms = [] 191 # assignees 192 if maintainer != 'None' and not pr.assignee: 193 try: 194 u = gh.get_user(maintainer) 195 ms.append(u) 196 except GithubException: 197 log(f"Error: Unknown user") 198 199 for mm in ms: 200 log(f"Adding assignee {mm}...") 201 if not args.dry_run: 202 pr.add_to_assignees(mm) 203 else: 204 log("not setting assignee") 205 206 time.sleep(1) 207 208def main(): 209 parse_args() 210 211 token = os.environ.get('GITHUB_TOKEN', None) 212 if not token: 213 sys.exit('Github token not set in environment, please set the ' 214 'GITHUB_TOKEN environment variable and retry.') 215 216 gh = Github(token) 217 maintainer_file = Maintainers(args.maintainer_file) 218 219 if args.pull_request: 220 process_pr(gh, maintainer_file, args.pull_request) 221 else: 222 if args.since: 223 since = args.since 224 else: 225 today = datetime.date.today() 226 since = today - datetime.timedelta(days=1) 227 228 common_prs = f'repo:{args.org}/{args.repo} is:open is:pr base:main -is:draft no:assignee created:>{since}' 229 pulls = gh.search_issues(query=f'{common_prs}') 230 231 for issue in pulls: 232 process_pr(gh, maintainer_file, issue.number) 233 234 235if __name__ == "__main__": 236 main() 237