Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(493)

Side by Side Diff: owners_finder.py

Issue 12712002: An interactive tool to help find owners covering current change list. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Adding unit tests Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 #!/usr/bin/python
M-A Ruel 2013/04/24 01:32:20 Remove, this file is not executable.
Bei Zhang 2013/04/24 03:29:02 Done.
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """A tool to help picking owner_to_files for reviewing."""
M-A Ruel 2013/04/24 01:32:20 """Helps picking ..
Bei Zhang 2013/04/24 03:29:02 Done.
7
8 import os.path
9 import copy
10 import owners as owners_module
11
12
13 def first(iterable):
14 for element in iterable:
15 return element
16
17
18 class OwnersFinder(object):
19 COLOR_LINK = '\033[4m'
20 COLOR_BOLD = '\033[1;32m'
21 COLOR_GREY = '\033[0;37m'
22 COLOR_RESET = '\033[0m'
23
24 indentation = 0
25
26 def __init__(self, files, local_root,
27 fopen, os_path, glob,
28 email_postfix="@chromium.org",
29 disable_color=False):
30 self.email_postfix = email_postfix
31
32 if os.name == "nt" or disable_color:
33 self.COLOR_LINK = ''
34 self.COLOR_BOLD = ''
35 self.COLOR_GREY = ''
36 self.COLOR_RESET = ''
37
38 db = OwnersFinder._read_from_database(files, local_root, fopen, os_path,
39 glob)
40
41 self.file_to_owners = OwnersFinder._map_files_to_owners(files, db)
42 self.owner_to_files = OwnersFinder._map_owners_to_files(self.file_to_owners,
43 db)
44 self.original_files_to_owners = copy.deepcopy(self.file_to_owners)
45 self.comments = db.comments
46 self.owners_queue = []
47 self.unreviewed_files = set()
48 self.reviewed_by = {}
49 self.selected_owners = set()
50 self.deselected_owners = set()
51 self.reset()
52
53 @staticmethod
54 def _read_from_database(files, local_root, fopen, os_path, glob):
55 db = owners_module.Database(local_root, fopen, os_path, glob)
56 db.load_data_needed_for(files)
57 return db
58
59 @staticmethod
60 def _map_files_to_owners(files, db):
61 files_to_owners = {}
62 for file_name in files:
63 owners_set = set()
64 if file_name in db.owners_for:
65 owners_set = owners_set | db.owners_for[file_name]
66 else:
67 dir_name = file_name
68 while dir_name != '':
69 if dir_name in db.stop_looking:
70 break
71 dir_name = os.path.dirname(dir_name)
72 if dir_name in db.owners_for:
73 owners_set = owners_set | db.owners_for[dir_name]
74 if owners_module.EVERYONE in owners_set:
75 break
76
77 if len(owners_set) == 0:
78 raise Exception("File '%s' has no owner" % file_name)
79
80 # Eliminate files that EVERYONE can review
81 if owners_module.EVERYONE in owners_set:
82 continue
83 files_to_owners[file_name] = owners_set
84 return files_to_owners
85
86 @staticmethod
87 def _map_owners_to_files(files_to_owners, db):
88 owner_to_files = {}
89 for owner_name in db.owned_by:
90 if owner_name == owners_module.EVERYONE:
91 continue
92 files_set = set()
93 for file_name in files_to_owners:
94 if owner_name in files_to_owners[file_name]:
95 files_set.add(file_name)
96 if len(files_set) > 0:
97 owner_to_files[owner_name] = files_set
98 return owner_to_files
99
100 def bold(self, text):
101 return self.COLOR_BOLD + text + self.COLOR_RESET
102
103 def bold_name(self, name):
104 return (self.COLOR_BOLD +
105 name.replace(self.email_postfix, "") + self.COLOR_RESET)
106
107 def greyed(self, text):
108 return self.COLOR_GREY + text + self.COLOR_RESET
109
110 def indent(self):
111 self.indentation += 1
112
113 def unindent(self):
114 self.indentation -= 1
115
116 def print_indent(self):
117 return ' ' * self.indentation
118
119 def writeln(self, text=''):
120 print self.print_indent() + text
121
122 def reset(self):
123 self.file_to_owners = copy.deepcopy(self.original_files_to_owners)
124 self.unreviewed_files = set(self.file_to_owners.keys())
125 self.reviewed_by = {}
126 self.selected_owners = set()
127 self.deselected_owners = set()
128
129 # Initialize owners queue, sort it by the number of files
130 # each owns
131 self.owners_queue = list(sorted(self.owner_to_files.keys(),
132 key=lambda owner: len(
133 self.owner_to_files[owner]),
134 reverse=True))
135 self.find_mandatory_owners()
136
137 def select_owner(self, owner, findMandatoryOwners=True):
138 if owner in self.selected_owners:
139 return
140 if owner in self.deselected_owners:
141 return
142 if not (owner in self.owners_queue):
143 return
144 self.writeln("Selected: " + owner)
145 self.owners_queue.remove(owner)
146 self.selected_owners.add(owner)
147 for file_name in filter(
148 lambda file_name: file_name in self.unreviewed_files,
149 self.owner_to_files[owner]):
150 self.unreviewed_files.remove(file_name)
151 self.reviewed_by[file_name] = owner
152 if findMandatoryOwners:
153 self.find_mandatory_owners()
154
155 def deselect_owner(self, owner, findMandatoryOwners=True):
156 if owner in self.selected_owners:
157 return
158 if owner in self.deselected_owners:
159 return
160 if not (owner in self.owners_queue):
161 return
162 self.writeln("Deselected: " + owner)
163 self.owners_queue.remove(owner)
164 self.deselected_owners.add(owner)
165 for file_name in self.owner_to_files[owner] & self.unreviewed_files:
166 self.file_to_owners[file_name].remove(owner)
167 if findMandatoryOwners:
168 self.find_mandatory_owners()
169
170 def find_mandatory_owners(self):
171 continues = True
172 for owner in self.owners_queue:
173 if owner in self.selected_owners:
174 continue
175 if owner in self.deselected_owners:
176 continue
177 if len(self.owner_to_files[owner] & self.unreviewed_files) == 0:
178 self.deselect_owner(owner, False)
179
180 while continues:
181 continues = False
182 for file_name in filter(lambda file_name:
183 len(self.file_to_owners[file_name]) == 1,
184 self.unreviewed_files):
185 self.select_owner(first(self.file_to_owners[file_name]), False)
186 continues = True
187 break
188
189 def print_comments(self, owner):
190 if owner not in self.comments:
191 self.writeln(self.bold_name(owner))
192 else:
193 self.writeln(self.bold_name(owner) + " is commented as:")
194 self.indent()
195 for path in self.comments[owner]:
196 if len(self.comments[owner][path]) > 0:
197 self.writeln(self.greyed(self.comments[owner][path]) +
198 " (at " + self.bold(path or "<root>") + ")")
199 else:
200 self.writeln(self.greyed("[No comment] ") + " (at " +
201 self.bold(path or "<root>") + ")")
202 self.unindent()
203
204 def print_file_info(self, file_name, except_owner=''):
205 if file_name not in self.unreviewed_files:
206 self.writeln(self.greyed(file_name +
207 ' (by ' +
208 self.bold_name(self.reviewed_by[file_name]) +
209 ')'))
210 else:
211 self.indent()
212 if len(self.file_to_owners[file_name]) <= 3:
213 other_owners = []
214 for ow in self.file_to_owners[file_name]:
215 if ow != except_owner:
216 other_owners.append(self.bold_name(ow))
217 self.writeln(file_name +
218 " [" + (", ".join(other_owners)) + "]")
219 else:
220 self.writeln(file_name + " [" +
221 self.bold(str(len(self.file_to_owners[file_name]))) +
222 "]")
223 self.unindent()
224
225 def print_file_info_detailed(self, file_name):
226 self.writeln(file_name)
227 self.indent()
228 for ow in sorted(self.file_to_owners[file_name]):
229 if ow in self.deselected_owners:
230 self.writeln(self.bold_name(self.greyed(ow)))
231 elif ow in self.selected_owners:
232 self.writeln(self.bold_name(self.greyed(ow)))
233 else:
234 self.writeln(self.bold_name(ow))
235 self.unindent()
236
237 def print_owned_files_for(self, owner):
238 # Print owned files
239 self.print_comments(owner)
240 self.writeln(self.bold_name(owner) + " owns " +
241 str(len(self.owner_to_files[owner])) + " file(s):")
242 for file_name in sorted(self.owner_to_files[owner]):
243 self.print_file_info(file_name, owner)
244 self.writeln()
245
246 def list_owners(self, owners_queue):
247 if (len(self.owner_to_files) - len(self.deselected_owners) -
248 len(self.selected_owners)) > 3:
249 for ow in owners_queue:
250 if ow not in self.deselected_owners and ow not in self.selected_owners:
251 self.print_comments(ow)
252 else:
253 for ow in owners_queue:
254 if ow not in self.deselected_owners and ow not in self.selected_owners:
255 self.writeln()
256 self.print_owned_files_for(ow)
257
258 def list_files(self):
259 if len(self.unreviewed_files) > 5:
260 for file_name in sorted(self.unreviewed_files):
261 self.print_file_info(file_name)
262 else:
263 for file_name in self.unreviewed_files:
264 self.print_file_info_detailed(file_name)
265
266 def pick_owner(self, ow):
267 # Allowing to omit domain suffixes
268 if ow not in self.owner_to_files:
269 if ow + self.email_postfix in self.owner_to_files:
270 ow += self.email_postfix
271
272 if ow not in self.owner_to_files:
273 self.writeln("You cannot pick " + self.bold_name(ow) + " manually. " +
274 "It's an invalid name or not related to the change list.")
275 return False
276 elif ow in self.selected_owners:
277 self.writeln("You cannot pick " + self.bold_name(ow) + " manually. " +
278 "It's already selected.")
279 return False
280 elif ow in self.deselected_owners:
281 self.writeln("You cannot pick " + self.bold_name(ow) + " manually." +
282 "It's already unselected.")
283 return False
284
285 self.select_owner(ow)
286 return True
287
288 def print_result(self):
289 # Print results
290 self.writeln("** You selected these owners **")
291 self.writeln()
292 for owner in self.selected_owners:
293 self.writeln(self.bold_name(owner) + ":")
294 self.indent()
295 for file_name in sorted(self.owner_to_files[owner]):
296 self.writeln(file_name)
297 self.unindent()
298
299 def hr(self):
300 self.writeln("=====================")
301
302 def run(self):
303 self.reset()
304 while len(self.owners_queue) > 0 and len(self.unreviewed_files) > 0:
305 owner = self.owners_queue[0]
306 try:
307 if owner in self.selected_owners:
308 continue
309 if len(self.unreviewed_files) == 0:
310 self.writeln("Finished.\n\n")
311 break
312 if owner in self.deselected_owners:
313 # If this owner is already deseleted.
314 continue
315 if not any((file_name in self.unreviewed_files)
316 for file_name in self.owner_to_files[owner]):
317 self.deselect_owner(owner)
318 continue
319 self.hr()
320 self.writeln(
321 self.bold(str(len(self.unreviewed_files))) + " file(s) left.")
322 self.print_owned_files_for(owner)
323 while True:
324 self.writeln("Add " + self.bold_name(owner) + " as your reviewer? ")
325 inp = raw_input(
326 "[yes/no/Defer/pick/files/owners/quit/restart]: ").lower()
327 if inp == "y" or inp == "yes":
328 self.select_owner(owner)
329 break
330 elif inp == "n" or inp == "no":
331 self.deselect_owner(owner)
332 break
333 elif inp == "" or inp == "d" or inp == "defer":
334 self.owners_queue.append(owner)
335 break
336 elif inp == "f" or inp == "files":
337 self.list_files()
338 elif inp == "o" or inp == "owners":
339 self.list_owners(self.owners_queue)
340 elif inp == "p" or inp == "pick":
341 self.pick_owner(raw_input("Pick an owner:"))
342 self.owners_queue.insert(0, owner)
343 break
344 elif inp.startswith("p ") or inp.startswith("pick "):
345 self.pick_owner(inp.split(' ', 2)[1])
346 self.owners_queue.insert(0, owner)
347 break
348 elif inp == 'r' or inp == 'restart':
349 self.reset()
350 break
351 elif inp == "q" or inp == "quit":
352 # Exit with error
353 return 1
354 finally:
355 if len(self.owners_queue):
356 self.owners_queue.pop(0)
357
358 self.print_result()
359 return 0
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698