blob: b258b4320bde52f31bab0e109f0663cff566ba86 [file] [log] [blame]
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00001# Copyright 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004"""Interactive tool for finding reviewers/owners for a change."""
5
6import os
7import copy
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00008
Edward Lesmesae3586b2020-03-23 21:21:14 +00009import gclient_utils
10
11
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000012def first(iterable):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000013 for element in iterable:
14 return element
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000015
16
17class OwnersFinder(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000018 COLOR_LINK = '\033[4m'
19 COLOR_BOLD = '\033[1;32m'
20 COLOR_GREY = '\033[0;37m'
21 COLOR_RESET = '\033[0m'
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000022
Mike Frysinger124bb8e2023-09-06 05:48:55 +000023 indentation = 0
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000024
Mike Frysinger124bb8e2023-09-06 05:48:55 +000025 def __init__(self,
26 files,
27 author,
28 reviewers,
29 owners_client,
30 email_postfix='@chromium.org',
31 disable_color=False,
32 ignore_author=False):
33 self.email_postfix = email_postfix
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000034
Mike Frysinger124bb8e2023-09-06 05:48:55 +000035 if os.name == 'nt' or disable_color:
36 self.COLOR_LINK = ''
37 self.COLOR_BOLD = ''
38 self.COLOR_GREY = ''
39 self.COLOR_RESET = ''
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000040
Mike Frysinger124bb8e2023-09-06 05:48:55 +000041 self.author = author
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000042
Mike Frysinger124bb8e2023-09-06 05:48:55 +000043 filtered_files = files
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000044
Mike Frysinger124bb8e2023-09-06 05:48:55 +000045 reviewers = list(reviewers)
46 if author and not ignore_author:
47 reviewers.append(author)
Edward Lemur707d70b2018-02-07 00:50:14 +010048
Mike Frysinger124bb8e2023-09-06 05:48:55 +000049 # Eliminate files that existing reviewers can review.
50 self.owners_client = owners_client
51 approval_status = self.owners_client.GetFilesApprovalStatus(
52 filtered_files, reviewers, [])
53 filtered_files = [
54 f for f in filtered_files
55 if approval_status[f] != self.owners_client.APPROVED
56 ]
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000057
Mike Frysinger124bb8e2023-09-06 05:48:55 +000058 # If some files are eliminated.
59 if len(filtered_files) != len(files):
60 files = filtered_files
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000061
Mike Frysinger124bb8e2023-09-06 05:48:55 +000062 self.files_to_owners = self.owners_client.BatchListOwners(files)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000063
Mike Frysinger124bb8e2023-09-06 05:48:55 +000064 self.owners_to_files = {}
65 self._map_owners_to_files()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000066
Mike Frysinger124bb8e2023-09-06 05:48:55 +000067 self.original_files_to_owners = copy.deepcopy(self.files_to_owners)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000068
Mike Frysinger124bb8e2023-09-06 05:48:55 +000069 # This is the queue that will be shown in the interactive questions.
70 # It is initially sorted by the score in descending order. In the
71 # interactive questions a user can choose to "defer" its decision, then
72 # the owner will be put to the end of the queue and shown later.
73 self.owners_queue = []
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000074
Mike Frysinger124bb8e2023-09-06 05:48:55 +000075 self.unreviewed_files = set()
76 self.reviewed_by = {}
77 self.selected_owners = set()
78 self.deselected_owners = set()
79 self.reset()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000080
Mike Frysinger124bb8e2023-09-06 05:48:55 +000081 def run(self):
82 self.reset()
83 while self.owners_queue and self.unreviewed_files:
84 owner = self.owners_queue[0]
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000085
Mike Frysinger124bb8e2023-09-06 05:48:55 +000086 if (owner in self.selected_owners) or (owner
87 in self.deselected_owners):
88 continue
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000089
Mike Frysinger124bb8e2023-09-06 05:48:55 +000090 if not any((file_name in self.unreviewed_files)
91 for file_name in self.owners_to_files[owner]):
92 self.deselect_owner(owner)
93 continue
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000094
Mike Frysinger124bb8e2023-09-06 05:48:55 +000095 self.print_info(owner)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000096
Mike Frysinger124bb8e2023-09-06 05:48:55 +000097 while True:
98 inp = self.input_command(owner)
99 if inp in ('y', 'yes'):
100 self.select_owner(owner)
101 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000102
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000103 if inp in ('n', 'no'):
104 self.deselect_owner(owner)
105 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000106
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000107 if inp in ('', 'd', 'defer'):
108 self.owners_queue.append(self.owners_queue.pop(0))
109 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000110
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000111 if inp in ('f', 'files'):
112 self.list_files()
113 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000114
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000115 if inp in ('o', 'owners'):
116 self.list_owners(self.owners_queue)
117 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000118
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000119 if inp in ('p', 'pick'):
120 self.pick_owner(gclient_utils.AskForData('Pick an owner: '))
121 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000122
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000123 if inp.startswith('p ') or inp.startswith('pick '):
124 self.pick_owner(inp.split(' ', 2)[1].strip())
125 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000126
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000127 if inp in ('r', 'restart'):
128 self.reset()
129 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000130
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000131 if inp in ('q', 'quit'):
132 # Exit with error
133 return 1
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000134
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000135 self.print_result()
136 return 0
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000137
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000138 def _map_owners_to_files(self):
139 for file_name in self.files_to_owners:
140 for owner in self.files_to_owners[file_name]:
141 self.owners_to_files.setdefault(owner, set())
142 self.owners_to_files[owner].add(file_name)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000143
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000144 def reset(self):
145 self.files_to_owners = copy.deepcopy(self.original_files_to_owners)
146 self.unreviewed_files = set(self.files_to_owners.keys())
147 self.reviewed_by = {}
148 self.selected_owners = set()
149 self.deselected_owners = set()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000150
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000151 # Randomize owners' names so that if many reviewers have identical
152 # scores they will be randomly ordered to avoid bias.
153 owners = list(
154 self.owners_client.ScoreOwners(self.files_to_owners.keys()))
155 if self.author and self.author in owners:
156 owners.remove(self.author)
157 self.owners_queue = owners
158 self.find_mandatory_owners()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000159
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000160 def select_owner(self, owner, findMandatoryOwners=True):
161 if owner in self.selected_owners or owner in self.deselected_owners\
162 or not (owner in self.owners_queue):
163 return
164 self.writeln('Selected: ' + owner)
165 self.owners_queue.remove(owner)
166 self.selected_owners.add(owner)
167 for file_name in filter(
168 lambda file_name: file_name in self.unreviewed_files,
169 self.owners_to_files[owner]):
170 self.unreviewed_files.remove(file_name)
171 self.reviewed_by[file_name] = owner
172 if findMandatoryOwners:
173 self.find_mandatory_owners()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000174
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000175 def deselect_owner(self, owner, findMandatoryOwners=True):
176 if owner in self.selected_owners or owner in self.deselected_owners\
177 or not (owner in self.owners_queue):
178 return
179 self.writeln('Deselected: ' + owner)
180 self.owners_queue.remove(owner)
181 self.deselected_owners.add(owner)
182 for file_name in self.owners_to_files[owner] & self.unreviewed_files:
183 self.files_to_owners[file_name].remove(owner)
184 if findMandatoryOwners:
185 self.find_mandatory_owners()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000186
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000187 def find_mandatory_owners(self):
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000188 continues = True
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000189 for owner in self.owners_queue:
190 if owner in self.selected_owners:
191 continue
192 if owner in self.deselected_owners:
193 continue
194 if len(self.owners_to_files[owner] & self.unreviewed_files) == 0:
195 self.deselect_owner(owner, False)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000196
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000197 while continues:
198 continues = False
199 for file_name in filter(
200 lambda file_name: len(self.files_to_owners[file_name]) == 1,
201 self.unreviewed_files):
202 owner = first(self.files_to_owners[file_name])
203 self.select_owner(owner, False)
204 continues = True
205 break
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000206
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000207 def print_file_info(self, file_name, except_owner=''):
208 if file_name not in self.unreviewed_files:
209 self.writeln(
210 self.greyed(file_name + ' (by ' +
211 self.bold_name(self.reviewed_by[file_name]) + ')'))
212 else:
213 if len(self.files_to_owners[file_name]) <= 3:
214 other_owners = []
215 for ow in self.files_to_owners[file_name]:
216 if ow != except_owner:
217 other_owners.append(self.bold_name(ow))
218 self.writeln(file_name + ' [' + (', '.join(other_owners)) + ']')
219 else:
220 self.writeln(
221 file_name + ' [' +
222 self.bold(str(len(self.files_to_owners[file_name]))) + ']')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000223
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000224 def print_file_info_detailed(self, file_name):
225 self.writeln(file_name)
Bruce Dawson9b4a0572020-05-06 17:05:01 +0000226 self.indent()
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000227 for ow in sorted(self.files_to_owners[file_name]):
228 if ow in self.deselected_owners:
229 self.writeln(self.bold_name(self.greyed(ow)))
230 elif ow in self.selected_owners:
231 self.writeln(self.bold_name(self.greyed(ow)))
232 else:
233 self.writeln(self.bold_name(ow))
Bruce Dawson9b4a0572020-05-06 17:05:01 +0000234 self.unindent()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000235
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000236 def print_owned_files_for(self, owner):
237 # Print owned files
238 self.writeln(self.bold_name(owner))
239 self.writeln(
240 self.bold_name(owner) + ' owns ' +
241 str(len(self.owners_to_files[owner])) + ' file(s):')
242 self.indent()
243 for file_name in sorted(self.owners_to_files[owner]):
244 self.print_file_info(file_name, owner)
245 self.unindent()
246 self.writeln()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000247
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000248 def list_owners(self, owners_queue):
249 if (len(self.owners_to_files) - len(self.deselected_owners) -
250 len(self.selected_owners)) > 3:
251 for ow in owners_queue:
252 if (ow not in self.deselected_owners
253 and ow not in self.selected_owners):
254 self.writeln(self.bold_name(ow))
255 else:
256 for ow in owners_queue:
257 if (ow not in self.deselected_owners
258 and ow not in self.selected_owners):
259 self.writeln()
260 self.print_owned_files_for(ow)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000261
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000262 def list_files(self):
263 self.indent()
264 if len(self.unreviewed_files) > 5:
265 for file_name in sorted(self.unreviewed_files):
266 self.print_file_info(file_name)
267 else:
268 for file_name in self.unreviewed_files:
269 self.print_file_info_detailed(file_name)
270 self.unindent()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000271
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000272 def pick_owner(self, ow):
273 # Allowing to omit domain suffixes
274 if ow not in self.owners_to_files:
275 if ow + self.email_postfix in self.owners_to_files:
276 ow += self.email_postfix
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000277
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000278 if ow not in self.owners_to_files:
279 self.writeln(
280 'You cannot pick ' + self.bold_name(ow) + ' manually. ' +
281 'It\'s an invalid name or not related to the change list.')
282 return False
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000283
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000284 if ow in self.selected_owners:
285 self.writeln('You cannot pick ' + self.bold_name(ow) +
286 ' manually. ' + 'It\'s already selected.')
287 return False
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000288
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000289 if ow in self.deselected_owners:
290 self.writeln('You cannot pick ' + self.bold_name(ow) +
291 ' manually.' + 'It\'s already unselected.')
292 return False
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000293
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000294 self.select_owner(ow)
295 return True
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000296
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000297 def print_result(self):
298 # Print results
299 self.writeln()
300 self.writeln()
301 if len(self.selected_owners) == 0:
302 self.writeln('This change list already has owner-reviewers for all '
303 'files.')
304 self.writeln('Use --ignore-current if you want to ignore them.')
305 else:
306 self.writeln('** You selected these owners **')
307 self.writeln()
308 for owner in self.selected_owners:
309 self.writeln(self.bold_name(owner) + ':')
310 self.indent()
311 for file_name in sorted(self.owners_to_files[owner]):
312 self.writeln(file_name)
313 self.unindent()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000314
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000315 def bold(self, text):
316 return self.COLOR_BOLD + text + self.COLOR_RESET
317
318 def bold_name(self, name):
319 return (self.COLOR_BOLD + name.replace(self.email_postfix, '') +
320 self.COLOR_RESET)
321
322 def greyed(self, text):
323 return self.COLOR_GREY + text + self.COLOR_RESET
324
325 def indent(self):
326 self.indentation += 1
327
328 def unindent(self):
329 self.indentation -= 1
330
331 def print_indent(self):
332 return ' ' * self.indentation
333
334 def writeln(self, text=''):
335 print(self.print_indent() + text)
336
337 def hr(self):
338 self.writeln('=====================')
339
340 def print_info(self, owner):
341 self.hr()
342 self.writeln(
343 self.bold(str(len(self.unreviewed_files))) + ' file(s) left.')
344 self.print_owned_files_for(owner)
345
346 def input_command(self, owner):
347 self.writeln('Add ' + self.bold_name(owner) + ' as your reviewer? ')
348 return gclient_utils.AskForData(
349 '[yes/no/Defer/pick/files/owners/quit/restart]: ').lower()