322 lines · python
1#!/usr/bin/env python32# ===-- commit-access-review.py --------------------------------------------===#3#4# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.5# See https://llvm.org/LICENSE.txt for license information.6# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception7#8# ===------------------------------------------------------------------------===#9#10# ===------------------------------------------------------------------------===#11 12import datetime13import github14import re15import requests16import time17import sys18import re19 20 21class User:22 THRESHOLD = 523 24 def __init__(self, name, triage_list):25 self.name = name26 self.authored = 027 self.merged = 028 self.reviewed = 029 self.triage_list = triage_list30 31 def add_authored(self, val=1):32 self.authored += val33 if self.meets_threshold():34 print(self.name, "meets the threshold with authored commits")35 del self.triage_list[self.name]36 37 def set_authored(self, val):38 self.authored = 039 self.add_authored(val)40 41 def add_merged(self, val=1):42 self.merged += val43 if self.meets_threshold():44 print(self.name, "meets the threshold with merged commits")45 del self.triage_list[self.name]46 47 def add_reviewed(self, val=1):48 self.reviewed += val49 if self.meets_threshold():50 print(self.name, "meets the threshold with reviewed commits")51 del self.triage_list[self.name]52 53 def get_total(self):54 return self.authored + self.merged + self.reviewed55 56 def meets_threshold(self):57 return self.get_total() >= self.THRESHOLD58 59 def __repr__(self):60 return "{} : a: {} m: {} r: {}".format(61 self.name, self.authored, self.merged, self.reviewed62 )63 64 65def check_manual_requests(66 gh: github.Github, start_date: datetime.datetime67) -> list[str]:68 """69 Return a list of users who have been asked since ``start_date`` if they70 want to keep their commit access or if they have applied for commit71 access since ``start_date``72 """73 74 query = """75 query ($query: String!, $after: String) {76 search(query: $query, type: ISSUE, first: 100, after: $after) {77 nodes {78 ... on Issue {79 author {80 login81 }82 body83 }84 }85 pageInfo {86 hasNextPage87 endCursor88 }89 }90 }91 """92 formatted_start_date = start_date.strftime("%Y-%m-%dT%H:%M:%S")93 variables = {94 "query": f"type:issue created:>{formatted_start_date} org:llvm repo:llvm-project label:infra:commit-access,infra:commit-access-request"95 }96 97 has_next_page = True98 users = []99 while has_next_page:100 res_header, res_data = gh._Github__requester.graphql_query(101 query=query, variables=variables102 )103 data = res_data["data"]104 for issue in data["search"]["nodes"]:105 users.extend([user[1:] for user in re.findall("@[^ ,\n]+", issue["body"])])106 if issue["author"]:107 users.append(issue["author"]["login"])108 has_next_page = data["search"]["pageInfo"]["hasNextPage"]109 if has_next_page:110 variables["after"] = data["search"]["pageInfo"]["endCursor"]111 return users112 113 114def get_num_commits(gh: github.Github, user: str, start_date: datetime.datetime) -> int:115 """116 Get number of commits that ``user`` has been made since ``start_date`.117 """118 variables = {119 "owner": "llvm",120 "user": user,121 "start_date": start_date.strftime("%Y-%m-%dT%H:%M:%S"),122 }123 124 user_query = """125 query ($user: String!) {126 user(login: $user) {127 id128 }129 }130 """131 132 res_header, res_data = gh._Github__requester.graphql_query(133 query=user_query, variables=variables134 )135 data = res_data["data"]136 variables["user_id"] = data["user"]["id"]137 138 query = """139 query ($owner: String!, $user_id: ID!, $start_date: GitTimestamp!){140 organization(login: $owner) {141 teams(query: "llvm-committers" first:1) {142 nodes {143 repositories {144 nodes {145 ref(qualifiedName: "main") {146 target {147 ... on Commit {148 history(since: $start_date, author: {id: $user_id }) {149 totalCount150 }151 }152 }153 }154 }155 }156 }157 }158 }159 }160 """161 count = 0162 res_header, res_data = gh._Github__requester.graphql_query(163 query=query, variables=variables164 )165 data = res_data["data"]166 for repo in data["organization"]["teams"]["nodes"][0]["repositories"]["nodes"]:167 count += int(repo["ref"]["target"]["history"]["totalCount"])168 if count >= User.THRESHOLD:169 break170 return count171 172 173def get_review_count(174 gh: github.Github, user: str, start_date: datetime.datetime175) -> int:176 """177 Return the number of reviews that ``user`` has done since ``start_date``.178 """179 query = """180 query ($query: String!) {181 search(query: $query, type: ISSUE, first: 5) {182 issueCount183 }184 }185 """186 formatted_start_date = start_date.strftime("%Y-%m-%dT%H:%M:%S")187 variables = {188 "owner": "llvm",189 "repo": "llvm-project",190 "user": user,191 "query": f"type:pr commenter:{user} -author:{user} merged:>{formatted_start_date} org:llvm",192 }193 194 res_header, res_data = gh._Github__requester.graphql_query(195 query=query, variables=variables196 )197 data = res_data["data"]198 return int(data["search"]["issueCount"])199 200 201def count_prs(gh: github.Github, triage_list: dict, start_date: datetime.datetime):202 """203 Fetch all the merged PRs for the project since ``start_date`` and update204 ``triage_list`` with the number of PRs merged for each user.205 """206 207 query = """208 query ($query: String!, $after: String) {209 search(query: $query, type: ISSUE, first: 100, after: $after) {210 issueCount,211 nodes {212 ... on PullRequest {213 author {214 login215 }216 mergedBy {217 login218 }219 }220 }221 pageInfo {222 hasNextPage223 endCursor224 }225 }226 }227 """228 date_begin = start_date229 date_end = None230 while date_begin < datetime.datetime.now():231 date_end = date_begin + datetime.timedelta(days=7)232 formatted_date_begin = date_begin.strftime("%Y-%m-%dT%H:%M:%S")233 formatted_date_end = date_end.strftime("%Y-%m-%dT%H:%M:%S")234 variables = {235 "query": f"type:pr is:merged merged:{formatted_date_begin}..{formatted_date_end} org:llvm",236 }237 has_next_page = True238 while has_next_page:239 print(variables)240 res_header, res_data = gh._Github__requester.graphql_query(241 query=query, variables=variables242 )243 data = res_data["data"]244 for pr in data["search"]["nodes"]:245 # Users can be None if the user has been deleted.246 if not pr["author"]:247 continue248 author = pr["author"]["login"]249 if author in triage_list:250 triage_list[author].add_authored()251 252 if not pr["mergedBy"]:253 continue254 merger = pr["mergedBy"]["login"]255 if author == merger:256 continue257 if merger not in triage_list:258 continue259 triage_list[merger].add_merged()260 261 has_next_page = data["search"]["pageInfo"]["hasNextPage"]262 if has_next_page:263 variables["after"] = data["search"]["pageInfo"]["endCursor"]264 date_begin = date_end265 266 267def main():268 token = sys.argv[1]269 gh = github.Github(login_or_token=token)270 org = gh.get_organization("llvm")271 repo = org.get_repo("llvm-project")272 one_year_ago = datetime.datetime.now() - datetime.timedelta(days=365)273 triage_list = {}274 for collaborator in repo.get_collaborators(permission="push"):275 triage_list[collaborator.login] = User(collaborator.login, triage_list)276 277 print("Start:", len(triage_list), "triagers")278 # Step 0 Check if users have requested commit access in the last year.279 for user in check_manual_requests(gh, one_year_ago):280 if user in triage_list:281 print(user, "requested commit access in the last year.")282 del triage_list[user]283 print("After Request Check:", len(triage_list), "triagers")284 285 # Step 1 count all PRs authored or merged286 count_prs(gh, triage_list, one_year_ago)287 288 print("After PRs:", len(triage_list), "triagers")289 290 if len(triage_list) == 0:291 sys.exit(0)292 293 # Step 2 check for reviews294 for user in list(triage_list.keys()):295 review_count = get_review_count(gh, user, one_year_ago)296 triage_list[user].add_reviewed(review_count)297 298 print("After Reviews:", len(triage_list), "triagers")299 300 if len(triage_list) == 0:301 sys.exit(0)302 303 # Step 3 check for number of commits304 for user in list(triage_list.keys()):305 num_commits = get_num_commits(gh, user, one_year_ago)306 # Override the total number of commits to not double count commits and307 # authored PRs.308 triage_list[user].set_authored(num_commits)309 310 print("After Commits:", len(triage_list), "triagers")311 312 print("Complete:", len(triage_list), "triagers")313 314 with open("triagers.log", "w") as triagers_log:315 for user in triage_list:316 print(triage_list[user].__repr__())317 triagers_log.write(user + "\n")318 319 320if __name__ == "__main__":321 main()322