OLD | NEW |
---|---|
(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 | |
OLD | NEW |