blob: 1bad974f5430a3b632277699b1c3963855bc28ff [file] [log] [blame]
Zelalem917b43e2020-08-04 11:39:55 -05001#!/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
10import argparse
11import os
12from pygerrit2 import GerritRestAPI, HTTPBasicAuth
13import re
14
15DEFAULT_GERRIT_URL = 'https://review.trustedfirmware.org'
16DEFAULT_GERRIT_PROJECT_NAME = 'TF-A/trusted-firmware-a'
17DEFAULT_MAINTAINERS_FILE_NAME = 'maintainers.rst'
18
19# Commit message is returned in a file list, ignore it
20COMMIT_MSG_FILE = '/COMMIT_MSG'
21
22def 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
34def 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
43def 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
55def 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
74def 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
108def 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
135def 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
170def 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
223if __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)