Zelalem | 917b43e | 2020-08-04 11:39:55 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # Copyright (c) 2019, Arm Limited. All rights reserved. |
| 4 | # |
| 5 | # SPDX-License-Identifier: BSD-3-Clause |
| 6 | # |
| 7 | # |
| 8 | # Assigns reviewers according to maintainers file. |
| 9 | |
| 10 | import argparse |
| 11 | import os |
| 12 | from pygerrit2 import GerritRestAPI, HTTPBasicAuth |
| 13 | import re |
| 14 | |
| 15 | DEFAULT_GERRIT_URL = 'https://review.trustedfirmware.org' |
| 16 | DEFAULT_GERRIT_PROJECT_NAME = 'TF-A/trusted-firmware-a' |
| 17 | DEFAULT_MAINTAINERS_FILE_NAME = 'maintainers.rst' |
| 18 | |
| 19 | # Commit message is returned in a file list, ignore it |
| 20 | COMMIT_MSG_FILE = '/COMMIT_MSG' |
| 21 | |
| 22 | def connect_to_gerrit(gerrit_url, gerrit_user, gerrit_password): |
| 23 | ''' |
| 24 | Connect to Gerrit server. |
| 25 | The password is not a plaintext password, |
| 26 | it can be obtained from Profile/Settings/HTTP Password page. |
| 27 | Returns GerritRestAPI class. |
| 28 | ''' |
| 29 | |
| 30 | auth = HTTPBasicAuth(gerrit_user, gerrit_password) |
| 31 | return GerritRestAPI(url=gerrit_url, auth=auth) |
| 32 | |
| 33 | |
| 34 | def get_open_changes(rest_api, project_name): |
| 35 | ''' |
| 36 | Get list of open reviews for the project. |
| 37 | ''' |
| 38 | |
| 39 | # Pass DETAILED_ACCOUNTS to get owner username |
| 40 | return rest_api.get("/changes/?q=status:open%20project:" + project_name + "&o=DETAILED_ACCOUNTS") |
| 41 | |
| 42 | |
| 43 | def get_files(rest_api, change_id): |
| 44 | ''' |
| 45 | Get list of changed files for the review. |
| 46 | Commit message is removed from the list. |
| 47 | ''' |
| 48 | |
| 49 | files_list = rest_api.get("/changes/" + change_id + "/revisions/current/files/") |
| 50 | del files_list[COMMIT_MSG_FILE] |
| 51 | |
| 52 | return files_list |
| 53 | |
| 54 | |
| 55 | def add_reviewer(rest_api, change_id, username, dry_run): |
| 56 | ''' |
| 57 | Add reviewer to the review. |
| 58 | ''' |
| 59 | |
| 60 | endpoint = "/changes/" + change_id + "/reviewers" |
| 61 | kwargs = {"data": {"reviewer": username}} |
| 62 | |
| 63 | # Exception is thrown if username is wrong, so just print it |
| 64 | try: |
| 65 | if not dry_run: |
| 66 | rest_api.post(endpoint, **kwargs) |
| 67 | except Exception as e: |
| 68 | print(" Add reviewer failed, username: " + str(username)) |
| 69 | print(" " + str(e)) |
| 70 | else: |
| 71 | print(" Reviewer added, username: " + str(username)) |
| 72 | |
| 73 | |
| 74 | def parse_maintainers_file(file_path): |
| 75 | ''' |
| 76 | Parse maintainers file. |
| 77 | Returns a dictionary {file_path:set{user1, user2, ...}} |
| 78 | ''' |
| 79 | |
| 80 | f = open(file_path, encoding='utf8') |
| 81 | file_text = f.read() |
| 82 | f.close() |
| 83 | |
| 84 | FILE_PREFIX = "\n:F: " |
| 85 | |
| 86 | regex = r"^:G: `(?P<user>.*)`_$(?P<paths>(" + FILE_PREFIX + r".*$)+)" |
| 87 | matches = re.finditer(regex, file_text, re.MULTILINE) |
| 88 | |
| 89 | # Create a dictionary {file_path:set{user1, user2, ...}} for faster search |
| 90 | result_dict = {} |
| 91 | |
| 92 | for match in matches: |
| 93 | user_name = match.group("user") |
| 94 | |
| 95 | paths = match.group("paths").split(FILE_PREFIX) |
| 96 | paths.remove("") |
| 97 | |
| 98 | # Fill the dictionary |
| 99 | for path in paths: |
| 100 | if path not in result_dict: |
| 101 | result_dict[path] = set() |
| 102 | |
| 103 | result_dict[path].add(user_name) |
| 104 | |
| 105 | return result_dict |
| 106 | |
| 107 | |
| 108 | def get_file_maintainers(file_path, maintainers_dictionary): |
| 109 | ''' |
| 110 | Returns a set of usernames(mainteiners) for the file. |
| 111 | ''' |
| 112 | |
| 113 | maintainers = set() |
| 114 | |
| 115 | file = file_path |
| 116 | |
| 117 | # Get maintainers of the file |
| 118 | maintainers_set = maintainers_dictionary.get(file) |
| 119 | if maintainers_set: |
| 120 | maintainers.update(maintainers_set) |
| 121 | |
| 122 | # Get maintainers of the directories |
| 123 | while (file > "/"): |
| 124 | # Get upper directory on each step. |
| 125 | file = os.path.dirname(file) |
| 126 | path_to_check = file + "/" |
| 127 | |
| 128 | maintainers_set = maintainers_dictionary.get(path_to_check) |
| 129 | if maintainers_set: |
| 130 | maintainers.update(maintainers_set) |
| 131 | |
| 132 | return maintainers |
| 133 | |
| 134 | |
| 135 | def assign_reviewers(rest_api, maintainers_dictionary, change, dry_run): |
| 136 | ''' |
| 137 | Assign maintainers to the review. |
| 138 | ''' |
| 139 | |
| 140 | # It looks like some accounts may not have username |
| 141 | owner_username = None |
| 142 | if ('username' in change['owner']): |
| 143 | owner_username = change['owner']['username'] |
| 144 | |
| 145 | print("\nChange: " + str(change['id'])) |
| 146 | print(" Topic: " + str(change.get('topic'))) |
| 147 | print(" Owner: " + str(owner_username)) |
| 148 | |
| 149 | change_maintainers = set() |
| 150 | |
| 151 | # Get list of all files in the change |
| 152 | files = get_files(rest_api, change['id']) |
| 153 | |
| 154 | for file in files: |
| 155 | # Get all maintainers of the file |
| 156 | file_maintainers = get_file_maintainers(file, maintainers_dictionary) |
| 157 | |
| 158 | if (len(file_maintainers) > 0): |
| 159 | print(" File: " + file + " maintainers: " + str(file_maintainers)) |
| 160 | |
| 161 | change_maintainers.update(file_maintainers) |
| 162 | |
| 163 | # Don't add owner even if he is a maintainer |
| 164 | change_maintainers.discard(owner_username) |
| 165 | |
| 166 | for maintainer in change_maintainers: |
| 167 | add_reviewer(rest_api, change['id'], maintainer, dry_run) |
| 168 | |
| 169 | |
| 170 | def parse_cmd_line(): |
| 171 | |
| 172 | parser = argparse.ArgumentParser( |
| 173 | description="Gerrit bot", |
| 174 | epilog=""" |
| 175 | Assigns reviewers according to maintainers file. |
| 176 | """ |
| 177 | ) |
| 178 | |
| 179 | required_group = parser.add_argument_group('required arguments') |
| 180 | |
| 181 | parser.add_argument("--url", "-u", |
| 182 | help = """ |
| 183 | Gerrit URL (default: %(default)s) |
| 184 | """, |
| 185 | default = DEFAULT_GERRIT_URL) |
| 186 | |
| 187 | parser.add_argument("--project", "-p", |
| 188 | help = """ |
| 189 | Project name (default: %(default)s). |
| 190 | """, |
| 191 | default = DEFAULT_GERRIT_PROJECT_NAME) |
| 192 | |
| 193 | parser.add_argument("--maintainers", "-m", |
| 194 | help = """ |
| 195 | Path to maintainers file (default: %(default)s). |
| 196 | """, |
| 197 | default = DEFAULT_MAINTAINERS_FILE_NAME) |
| 198 | |
| 199 | parser.add_argument("--dry-run", |
| 200 | help = """ |
| 201 | Check maintainers, but don't add them (default: %(default)s). |
| 202 | """, |
| 203 | action='store_true', |
| 204 | default = False) |
| 205 | |
| 206 | required_group.add_argument("--user", |
| 207 | help = """ |
| 208 | Gerrit user. |
| 209 | """, |
| 210 | required = True) |
| 211 | |
| 212 | required_group.add_argument("--password", |
| 213 | help=""" |
| 214 | Gerrit HTTP password. |
| 215 | This is NOT a plaintext password. |
| 216 | But the value from Profile/Settings/HTTP Password |
| 217 | """, |
| 218 | required = True) |
| 219 | |
| 220 | return parser.parse_args() |
| 221 | |
| 222 | |
| 223 | if __name__ == '__main__': |
| 224 | |
| 225 | args = parse_cmd_line() |
| 226 | |
| 227 | maintainers_dict = parse_maintainers_file(args.maintainers) |
| 228 | rest = connect_to_gerrit(args.url, args.user, args.password) |
| 229 | changes = get_open_changes(rest, args.project) |
| 230 | |
| 231 | for change in changes: |
| 232 | assign_reviewers(rest, maintainers_dict, change, args.dry_run) |