blob: 3d6096f43d3cf7db1dced4c277eb8d199f7f0945 [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
Raul Tambre80ee78e2019-05-06 22:41:05 +00006from __future__ import print_function
7
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00008import os
9import copy
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000010
Edward Lesmesae3586b2020-03-23 21:21:14 +000011import gclient_utils
12
13
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014def first(iterable):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000015 for element in iterable:
16 return element
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000017
18
19class OwnersFinder(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000020 COLOR_LINK = '\033[4m'
21 COLOR_BOLD = '\033[1;32m'
22 COLOR_GREY = '\033[0;37m'
23 COLOR_RESET = '\033[0m'
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000024
Mike Frysinger124bb8e2023-09-06 05:48:55 +000025 indentation = 0
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000026
Mike Frysinger124bb8e2023-09-06 05:48:55 +000027 def __init__(self,
28 files,
29 author,
30 reviewers,
31 owners_client,
32 email_postfix='@chromium.org',
33 disable_color=False,
34 ignore_author=False):
35 self.email_postfix = email_postfix
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000036
Mike Frysinger124bb8e2023-09-06 05:48:55 +000037 if os.name == 'nt' or disable_color:
38 self.COLOR_LINK = ''
39 self.COLOR_BOLD = ''
40 self.COLOR_GREY = ''
41 self.COLOR_RESET = ''
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000042
Mike Frysinger124bb8e2023-09-06 05:48:55 +000043 self.author = author
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000044
Mike Frysinger124bb8e2023-09-06 05:48:55 +000045 filtered_files = files
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000046
Mike Frysinger124bb8e2023-09-06 05:48:55 +000047 reviewers = list(reviewers)
48 if author and not ignore_author:
49 reviewers.append(author)
Edward Lemur707d70b2018-02-07 00:50:14 +010050
Mike Frysinger124bb8e2023-09-06 05:48:55 +000051 # Eliminate files that existing reviewers can review.
52 self.owners_client = owners_client
53 approval_status = self.owners_client.GetFilesApprovalStatus(
54 filtered_files, reviewers, [])
55 filtered_files = [
56 f for f in filtered_files
57 if approval_status[f] != self.owners_client.APPROVED
58 ]
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000059
Mike Frysinger124bb8e2023-09-06 05:48:55 +000060 # If some files are eliminated.
61 if len(filtered_files) != len(files):
62 files = filtered_files
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000063
Mike Frysinger124bb8e2023-09-06 05:48:55 +000064 self.files_to_owners = self.owners_client.BatchListOwners(files)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000065
Mike Frysinger124bb8e2023-09-06 05:48:55 +000066 self.owners_to_files = {}
67 self._map_owners_to_files()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000068
Mike Frysinger124bb8e2023-09-06 05:48:55 +000069 self.original_files_to_owners = copy.deepcopy(self.files_to_owners)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000070
Mike Frysinger124bb8e2023-09-06 05:48:55 +000071 # This is the queue that will be shown in the interactive questions.
72 # It is initially sorted by the score in descending order. In the
73 # interactive questions a user can choose to "defer" its decision, then
74 # the owner will be put to the end of the queue and shown later.
75 self.owners_queue = []
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000076
Mike Frysinger124bb8e2023-09-06 05:48:55 +000077 self.unreviewed_files = set()
78 self.reviewed_by = {}
79 self.selected_owners = set()
80 self.deselected_owners = set()
81 self.reset()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000082
Mike Frysinger124bb8e2023-09-06 05:48:55 +000083 def run(self):
84 self.reset()
85 while self.owners_queue and self.unreviewed_files:
86 owner = self.owners_queue[0]
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000087
Mike Frysinger124bb8e2023-09-06 05:48:55 +000088 if (owner in self.selected_owners) or (owner
89 in self.deselected_owners):
90 continue
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000091
Mike Frysinger124bb8e2023-09-06 05:48:55 +000092 if not any((file_name in self.unreviewed_files)
93 for file_name in self.owners_to_files[owner]):
94 self.deselect_owner(owner)
95 continue
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000096
Mike Frysinger124bb8e2023-09-06 05:48:55 +000097 self.print_info(owner)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000098
Mike Frysinger124bb8e2023-09-06 05:48:55 +000099 while True:
100 inp = self.input_command(owner)
101 if inp in ('y', 'yes'):
102 self.select_owner(owner)
103 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000104
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000105 if inp in ('n', 'no'):
106 self.deselect_owner(owner)
107 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000108
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000109 if inp in ('', 'd', 'defer'):
110 self.owners_queue.append(self.owners_queue.pop(0))
111 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000112
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000113 if inp in ('f', 'files'):
114 self.list_files()
115 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000116
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000117 if inp in ('o', 'owners'):
118 self.list_owners(self.owners_queue)
119 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000120
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000121 if inp in ('p', 'pick'):
122 self.pick_owner(gclient_utils.AskForData('Pick an owner: '))
123 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000124
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000125 if inp.startswith('p ') or inp.startswith('pick '):
126 self.pick_owner(inp.split(' ', 2)[1].strip())
127 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000128
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000129 if inp in ('r', 'restart'):
130 self.reset()
131 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000132
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000133 if inp in ('q', 'quit'):
134 # Exit with error
135 return 1
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000136
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000137 self.print_result()
138 return 0
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000139
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000140 def _map_owners_to_files(self):
141 for file_name in self.files_to_owners:
142 for owner in self.files_to_owners[file_name]:
143 self.owners_to_files.setdefault(owner, set())
144 self.owners_to_files[owner].add(file_name)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000145
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000146 def reset(self):
147 self.files_to_owners = copy.deepcopy(self.original_files_to_owners)
148 self.unreviewed_files = set(self.files_to_owners.keys())
149 self.reviewed_by = {}
150 self.selected_owners = set()
151 self.deselected_owners = set()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000152
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000153 # Randomize owners' names so that if many reviewers have identical
154 # scores they will be randomly ordered to avoid bias.
155 owners = list(
156 self.owners_client.ScoreOwners(self.files_to_owners.keys()))
157 if self.author and self.author in owners:
158 owners.remove(self.author)
159 self.owners_queue = owners
160 self.find_mandatory_owners()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000161
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000162 def select_owner(self, owner, findMandatoryOwners=True):
163 if owner in self.selected_owners or owner in self.deselected_owners\
164 or not (owner in self.owners_queue):
165 return
166 self.writeln('Selected: ' + owner)
167 self.owners_queue.remove(owner)
168 self.selected_owners.add(owner)
169 for file_name in filter(
170 lambda file_name: file_name in self.unreviewed_files,
171 self.owners_to_files[owner]):
172 self.unreviewed_files.remove(file_name)
173 self.reviewed_by[file_name] = owner
174 if findMandatoryOwners:
175 self.find_mandatory_owners()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000176
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000177 def deselect_owner(self, owner, findMandatoryOwners=True):
178 if owner in self.selected_owners or owner in self.deselected_owners\
179 or not (owner in self.owners_queue):
180 return
181 self.writeln('Deselected: ' + owner)
182 self.owners_queue.remove(owner)
183 self.deselected_owners.add(owner)
184 for file_name in self.owners_to_files[owner] & self.unreviewed_files:
185 self.files_to_owners[file_name].remove(owner)
186 if findMandatoryOwners:
187 self.find_mandatory_owners()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000188
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000189 def find_mandatory_owners(self):
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000190 continues = True
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000191 for owner in self.owners_queue:
192 if owner in self.selected_owners:
193 continue
194 if owner in self.deselected_owners:
195 continue
196 if len(self.owners_to_files[owner] & self.unreviewed_files) == 0:
197 self.deselect_owner(owner, False)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000198
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000199 while continues:
200 continues = False
201 for file_name in filter(
202 lambda file_name: len(self.files_to_owners[file_name]) == 1,
203 self.unreviewed_files):
204 owner = first(self.files_to_owners[file_name])
205 self.select_owner(owner, False)
206 continues = True
207 break
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000208
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000209 def print_file_info(self, file_name, except_owner=''):
210 if file_name not in self.unreviewed_files:
211 self.writeln(
212 self.greyed(file_name + ' (by ' +
213 self.bold_name(self.reviewed_by[file_name]) + ')'))
214 else:
215 if len(self.files_to_owners[file_name]) <= 3:
216 other_owners = []
217 for ow in self.files_to_owners[file_name]:
218 if ow != except_owner:
219 other_owners.append(self.bold_name(ow))
220 self.writeln(file_name + ' [' + (', '.join(other_owners)) + ']')
221 else:
222 self.writeln(
223 file_name + ' [' +
224 self.bold(str(len(self.files_to_owners[file_name]))) + ']')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000225
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000226 def print_file_info_detailed(self, file_name):
227 self.writeln(file_name)
Bruce Dawson9b4a0572020-05-06 17:05:01 +0000228 self.indent()
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000229 for ow in sorted(self.files_to_owners[file_name]):
230 if ow in self.deselected_owners:
231 self.writeln(self.bold_name(self.greyed(ow)))
232 elif ow in self.selected_owners:
233 self.writeln(self.bold_name(self.greyed(ow)))
234 else:
235 self.writeln(self.bold_name(ow))
Bruce Dawson9b4a0572020-05-06 17:05:01 +0000236 self.unindent()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000237
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000238 def print_owned_files_for(self, owner):
239 # Print owned files
240 self.writeln(self.bold_name(owner))
241 self.writeln(
242 self.bold_name(owner) + ' owns ' +
243 str(len(self.owners_to_files[owner])) + ' file(s):')
244 self.indent()
245 for file_name in sorted(self.owners_to_files[owner]):
246 self.print_file_info(file_name, owner)
247 self.unindent()
248 self.writeln()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000249
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000250 def list_owners(self, owners_queue):
251 if (len(self.owners_to_files) - len(self.deselected_owners) -
252 len(self.selected_owners)) > 3:
253 for ow in owners_queue:
254 if (ow not in self.deselected_owners
255 and ow not in self.selected_owners):
256 self.writeln(self.bold_name(ow))
257 else:
258 for ow in owners_queue:
259 if (ow not in self.deselected_owners
260 and ow not in self.selected_owners):
261 self.writeln()
262 self.print_owned_files_for(ow)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000263
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000264 def list_files(self):
265 self.indent()
266 if len(self.unreviewed_files) > 5:
267 for file_name in sorted(self.unreviewed_files):
268 self.print_file_info(file_name)
269 else:
270 for file_name in self.unreviewed_files:
271 self.print_file_info_detailed(file_name)
272 self.unindent()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000273
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000274 def pick_owner(self, ow):
275 # Allowing to omit domain suffixes
276 if ow not in self.owners_to_files:
277 if ow + self.email_postfix in self.owners_to_files:
278 ow += self.email_postfix
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000279
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000280 if ow not in self.owners_to_files:
281 self.writeln(
282 'You cannot pick ' + self.bold_name(ow) + ' manually. ' +
283 'It\'s an invalid name or not related to the change list.')
284 return False
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000285
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000286 if ow in self.selected_owners:
287 self.writeln('You cannot pick ' + self.bold_name(ow) +
288 ' manually. ' + 'It\'s already selected.')
289 return False
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000290
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000291 if ow in self.deselected_owners:
292 self.writeln('You cannot pick ' + self.bold_name(ow) +
293 ' manually.' + 'It\'s already unselected.')
294 return False
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000295
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000296 self.select_owner(ow)
297 return True
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000298
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000299 def print_result(self):
300 # Print results
301 self.writeln()
302 self.writeln()
303 if len(self.selected_owners) == 0:
304 self.writeln('This change list already has owner-reviewers for all '
305 'files.')
306 self.writeln('Use --ignore-current if you want to ignore them.')
307 else:
308 self.writeln('** You selected these owners **')
309 self.writeln()
310 for owner in self.selected_owners:
311 self.writeln(self.bold_name(owner) + ':')
312 self.indent()
313 for file_name in sorted(self.owners_to_files[owner]):
314 self.writeln(file_name)
315 self.unindent()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000316
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000317 def bold(self, text):
318 return self.COLOR_BOLD + text + self.COLOR_RESET
319
320 def bold_name(self, name):
321 return (self.COLOR_BOLD + name.replace(self.email_postfix, '') +
322 self.COLOR_RESET)
323
324 def greyed(self, text):
325 return self.COLOR_GREY + text + self.COLOR_RESET
326
327 def indent(self):
328 self.indentation += 1
329
330 def unindent(self):
331 self.indentation -= 1
332
333 def print_indent(self):
334 return ' ' * self.indentation
335
336 def writeln(self, text=''):
337 print(self.print_indent() + text)
338
339 def hr(self):
340 self.writeln('=====================')
341
342 def print_info(self, owner):
343 self.hr()
344 self.writeln(
345 self.bold(str(len(self.unreviewed_files))) + ' file(s) left.')
346 self.print_owned_files_for(owner)
347
348 def input_command(self, owner):
349 self.writeln('Add ' + self.bold_name(owner) + ' as your reviewer? ')
350 return gclient_utils.AskForData(
351 '[yes/no/Defer/pick/files/owners/quit/restart]: ').lower()