| #!/usr/bin/env python3 |
| # |
| # Copyright (c) 2019, Arm Limited. All rights reserved. |
| # |
| # SPDX-License-Identifier: BSD-3-Clause |
| # |
| # |
| # Assigns reviewers according to maintainers file. |
| |
| import argparse |
| import os |
| from pygerrit2 import GerritRestAPI, HTTPBasicAuth |
| import re |
| |
| DEFAULT_GERRIT_URL = 'https://review.trustedfirmware.org' |
| DEFAULT_GERRIT_PROJECT_NAME = 'TF-A/trusted-firmware-a' |
| DEFAULT_MAINTAINERS_FILE_NAME = 'maintainers.rst' |
| |
| # Commit message is returned in a file list, ignore it |
| COMMIT_MSG_FILE = '/COMMIT_MSG' |
| |
| def connect_to_gerrit(gerrit_url, gerrit_user, gerrit_password): |
| ''' |
| Connect to Gerrit server. |
| The password is not a plaintext password, |
| it can be obtained from Profile/Settings/HTTP Password page. |
| Returns GerritRestAPI class. |
| ''' |
| |
| auth = HTTPBasicAuth(gerrit_user, gerrit_password) |
| return GerritRestAPI(url=gerrit_url, auth=auth) |
| |
| |
| def get_open_changes(rest_api, project_name): |
| ''' |
| Get list of open reviews for the project. |
| ''' |
| |
| # Pass DETAILED_ACCOUNTS to get owner username |
| return rest_api.get("/changes/?q=status:open%20project:" + project_name + "&o=DETAILED_ACCOUNTS") |
| |
| |
| def get_files(rest_api, change_id): |
| ''' |
| Get list of changed files for the review. |
| Commit message is removed from the list. |
| ''' |
| |
| files_list = rest_api.get("/changes/" + change_id + "/revisions/current/files/") |
| del files_list[COMMIT_MSG_FILE] |
| |
| return files_list |
| |
| |
| def add_reviewer(rest_api, change_id, username, dry_run): |
| ''' |
| Add reviewer to the review. |
| ''' |
| |
| endpoint = "/changes/" + change_id + "/reviewers" |
| kwargs = {"data": {"reviewer": username}} |
| |
| # Exception is thrown if username is wrong, so just print it |
| try: |
| if not dry_run: |
| rest_api.post(endpoint, **kwargs) |
| except Exception as e: |
| print(" Add reviewer failed, username: " + str(username)) |
| print(" " + str(e)) |
| else: |
| print(" Reviewer added, username: " + str(username)) |
| |
| |
| def parse_maintainers_file(file_path): |
| ''' |
| Parse maintainers file. |
| Returns a dictionary {file_path:set{user1, user2, ...}} |
| ''' |
| |
| f = open(file_path, encoding='utf8') |
| file_text = f.read() |
| f.close() |
| |
| FILE_PREFIX = "\n:F: " |
| |
| regex = r"^:G: `(?P<user>.*)`_$(?P<paths>(" + FILE_PREFIX + r".*$)+)" |
| matches = re.finditer(regex, file_text, re.MULTILINE) |
| |
| # Create a dictionary {file_path:set{user1, user2, ...}} for faster search |
| result_dict = {} |
| |
| for match in matches: |
| user_name = match.group("user") |
| |
| paths = match.group("paths").split(FILE_PREFIX) |
| paths.remove("") |
| |
| # Fill the dictionary |
| for path in paths: |
| if path not in result_dict: |
| result_dict[path] = set() |
| |
| result_dict[path].add(user_name) |
| |
| return result_dict |
| |
| |
| def get_file_maintainers(file_path, maintainers_dictionary): |
| ''' |
| Returns a set of usernames(mainteiners) for the file. |
| ''' |
| |
| maintainers = set() |
| |
| file = file_path |
| |
| # Get maintainers of the file |
| maintainers_set = maintainers_dictionary.get(file) |
| if maintainers_set: |
| maintainers.update(maintainers_set) |
| |
| # Get maintainers of the directories |
| while (file > "/"): |
| # Get upper directory on each step. |
| file = os.path.dirname(file) |
| path_to_check = file + "/" |
| |
| maintainers_set = maintainers_dictionary.get(path_to_check) |
| if maintainers_set: |
| maintainers.update(maintainers_set) |
| |
| return maintainers |
| |
| |
| def assign_reviewers(rest_api, maintainers_dictionary, change, dry_run): |
| ''' |
| Assign maintainers to the review. |
| ''' |
| |
| # It looks like some accounts may not have username |
| owner_username = None |
| if ('username' in change['owner']): |
| owner_username = change['owner']['username'] |
| |
| print("\nChange: " + str(change['id'])) |
| print(" Topic: " + str(change.get('topic'))) |
| print(" Owner: " + str(owner_username)) |
| |
| change_maintainers = set() |
| |
| # Get list of all files in the change |
| files = get_files(rest_api, change['id']) |
| |
| for file in files: |
| # Get all maintainers of the file |
| file_maintainers = get_file_maintainers(file, maintainers_dictionary) |
| |
| if (len(file_maintainers) > 0): |
| print(" File: " + file + " maintainers: " + str(file_maintainers)) |
| |
| change_maintainers.update(file_maintainers) |
| |
| # Don't add owner even if he is a maintainer |
| change_maintainers.discard(owner_username) |
| |
| for maintainer in change_maintainers: |
| add_reviewer(rest_api, change['id'], maintainer, dry_run) |
| |
| |
| def parse_cmd_line(): |
| |
| parser = argparse.ArgumentParser( |
| description="Gerrit bot", |
| epilog=""" |
| Assigns reviewers according to maintainers file. |
| """ |
| ) |
| |
| required_group = parser.add_argument_group('required arguments') |
| |
| parser.add_argument("--url", "-u", |
| help = """ |
| Gerrit URL (default: %(default)s) |
| """, |
| default = DEFAULT_GERRIT_URL) |
| |
| parser.add_argument("--project", "-p", |
| help = """ |
| Project name (default: %(default)s). |
| """, |
| default = DEFAULT_GERRIT_PROJECT_NAME) |
| |
| parser.add_argument("--maintainers", "-m", |
| help = """ |
| Path to maintainers file (default: %(default)s). |
| """, |
| default = DEFAULT_MAINTAINERS_FILE_NAME) |
| |
| parser.add_argument("--dry-run", |
| help = """ |
| Check maintainers, but don't add them (default: %(default)s). |
| """, |
| action='store_true', |
| default = False) |
| |
| required_group.add_argument("--user", |
| help = """ |
| Gerrit user. |
| """, |
| required = True) |
| |
| required_group.add_argument("--password", |
| help=""" |
| Gerrit HTTP password. |
| This is NOT a plaintext password. |
| But the value from Profile/Settings/HTTP Password |
| """, |
| required = True) |
| |
| return parser.parse_args() |
| |
| |
| if __name__ == '__main__': |
| |
| args = parse_cmd_line() |
| |
| maintainers_dict = parse_maintainers_file(args.maintainers) |
| rest = connect_to_gerrit(args.url, args.user, args.password) |
| changes = get_open_changes(rest, args.project) |
| |
| for change in changes: |
| assign_reviewers(rest, maintainers_dict, change, args.dry_run) |