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