| OLD | NEW |
| 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """A database of OWNERS files. | 5 """A database of OWNERS files. |
| 6 | 6 |
| 7 OWNERS files indicate who is allowed to approve changes in a specific directory | 7 OWNERS files indicate who is allowed to approve changes in a specific directory |
| 8 (or who is allowed to make changes without needing approval of another OWNER). | 8 (or who is allowed to make changes without needing approval of another OWNER). |
| 9 Note that all changes must still be reviewed by someone familiar with the code, | 9 Note that all changes must still be reviewed by someone familiar with the code, |
| 10 so you may need approval from both an OWNER and a reviewer in many cases. | 10 so you may need approval from both an OWNER and a reviewer in many cases. |
| (...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 42 apply (up until a "set noparent is encountered"). | 42 apply (up until a "set noparent is encountered"). |
| 43 | 43 |
| 44 If "per-file glob=set noparent" is used, then global directives are ignored | 44 If "per-file glob=set noparent" is used, then global directives are ignored |
| 45 for the glob, and only the "per-file" owners are used for files matching that | 45 for the glob, and only the "per-file" owners are used for files matching that |
| 46 glob. | 46 glob. |
| 47 | 47 |
| 48 Examples for all of these combinations can be found in tests/owners_unittest.py. | 48 Examples for all of these combinations can be found in tests/owners_unittest.py. |
| 49 """ | 49 """ |
| 50 | 50 |
| 51 import collections | 51 import collections |
| 52 import random |
| 52 import re | 53 import re |
| 53 | 54 |
| 54 | 55 |
| 55 # If this is present by itself on a line, this means that everyone can review. | 56 # If this is present by itself on a line, this means that everyone can review. |
| 56 EVERYONE = '*' | 57 EVERYONE = '*' |
| 57 | 58 |
| 58 | 59 |
| 59 # Recognizes 'X@Y' email addresses. Very simplistic. | 60 # Recognizes 'X@Y' email addresses. Very simplistic. |
| 60 BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$' | 61 BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$' |
| 61 | 62 |
| (...skipping 183 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 245 if directive == "set noparent": | 246 if directive == "set noparent": |
| 246 self.stop_looking.add(path) | 247 self.stop_looking.add(path) |
| 247 elif self.email_regexp.match(directive) or directive == EVERYONE: | 248 elif self.email_regexp.match(directive) or directive == EVERYONE: |
| 248 self.owned_by.setdefault(directive, set()).add(path) | 249 self.owned_by.setdefault(directive, set()).add(path) |
| 249 self.owners_for.setdefault(path, set()).add(directive) | 250 self.owners_for.setdefault(path, set()).add(directive) |
| 250 else: | 251 else: |
| 251 raise SyntaxErrorInOwnersFile(owners_path, lineno, | 252 raise SyntaxErrorInOwnersFile(owners_path, lineno, |
| 252 ('%s is not a "set" directive, "*", ' | 253 ('%s is not a "set" directive, "*", ' |
| 253 'or an email address: "%s"' % (line_type, directive))) | 254 'or an email address: "%s"' % (line_type, directive))) |
| 254 | 255 |
| 256 def _covering_set_of_owners_for(self, files): |
| 257 dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files) |
| 258 all_possible_owners = self._all_possible_owners(dirs_remaining) |
| 259 suggested_owners = set() |
| 260 while dirs_remaining: |
| 261 owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining) |
| 262 suggested_owners.add(owner) |
| 263 for dirname, _ in all_possible_owners[owner]: |
| 264 dirs_remaining.remove(dirname) |
| 265 return suggested_owners |
| 255 | 266 |
| 256 def _covering_set_of_owners_for(self, files): | 267 def _all_possible_owners(self, dirs): |
| 257 # Get the set of directories from the files. | 268 """Returns a list of (potential owner, distance-from-dir) tuples; a |
| 258 dirs = set() | 269 distance of 1 is the lowest/closest possible distance (which makes the |
| 259 for f in files: | 270 subsequent math easier).""" |
| 260 dirs.add(self._enclosing_dir_with_owners(f)) | 271 all_possible_owners = {} |
| 261 | |
| 262 | |
| 263 owned_dirs = {} | |
| 264 dir_owners = {} | |
| 265 | |
| 266 for current_dir in dirs: | 272 for current_dir in dirs: |
| 267 # Get the list of owners for each directory. | |
| 268 current_owners = set() | |
| 269 dirname = current_dir | 273 dirname = current_dir |
| 270 while dirname in self.owners_for: | 274 distance = 1 |
| 271 current_owners |= self.owners_for[dirname] | 275 while True: |
| 276 for owner in self.owners_for.get(dirname, []): |
| 277 all_possible_owners.setdefault(owner, []) |
| 278 # It's possible the same owner might match a directory from |
| 279 # multiple files, and we only want the closest entry. |
| 280 if not any(current_dir == el[0] for el in all_possible_owners[owner]): |
| 281 all_possible_owners[owner].append((current_dir, distance)) |
| 272 if self._stop_looking(dirname): | 282 if self._stop_looking(dirname): |
| 273 break | 283 break |
| 274 prev_parent = dirname | |
| 275 dirname = self.os_path.dirname(dirname) | 284 dirname = self.os_path.dirname(dirname) |
| 276 if prev_parent == dirname: | 285 distance += 1 |
| 277 break | 286 return all_possible_owners |
| 278 | 287 |
| 279 # Map each directory to a list of its owners. | 288 @staticmethod |
| 280 dir_owners[current_dir] = current_owners | 289 def lowest_cost_owner(all_possible_owners, dirs): |
| 290 # We want to minimize both the number of reviewers and the distance |
| 291 # from the files/dirs needing reviews. The "pow(X, 1.75)" below is |
| 292 # an arbitrarily-selected scaling factor that seems to work well - it |
| 293 # will select one reviewer in the parent directory over three reviewers |
| 294 # in subdirs, but not one reviewer over just two. |
| 295 total_costs_by_owner = {} |
| 296 for owner in all_possible_owners: |
| 297 total_distance = 0 |
| 298 num_directories_owned = 0 |
| 299 for dirname, distance in all_possible_owners[owner]: |
| 300 if dirname in dirs: |
| 301 total_distance += distance |
| 302 num_directories_owned += 1 |
| 303 if num_directories_owned: |
| 304 total_costs_by_owner[owner] = (total_distance / |
| 305 pow(num_directories_owned, 1.75)) |
| 281 | 306 |
| 282 # Add the directory to the list of each owner. | 307 # Return the lowest cost owner. In the case of a tie, pick one randomly. |
| 283 for owner in current_owners: | 308 lowest_cost = min(total_costs_by_owner.itervalues()) |
| 284 owned_dirs.setdefault(owner, set()).add(current_dir) | 309 lowest_cost_owners = filter( |
| 285 | 310 lambda owner: total_costs_by_owner[owner] == lowest_cost, |
| 286 final_owners = set() | 311 total_costs_by_owner) |
| 287 while dirs: | 312 return random.Random().choice(lowest_cost_owners) |
| 288 # Find the owner that has the most directories. | |
| 289 max_count = 0 | |
| 290 max_owner = None | |
| 291 owner_count = {} | |
| 292 for dirname in dirs: | |
| 293 for owner in dir_owners[dirname]: | |
| 294 count = owner_count.get(owner, 0) + 1 | |
| 295 owner_count[owner] = count | |
| 296 if count >= max_count: | |
| 297 max_owner = owner | |
| 298 max_count = count | |
| 299 | |
| 300 # If no more directories have OWNERS, we're done. | |
| 301 if not max_owner: | |
| 302 break | |
| 303 | |
| 304 final_owners.add(max_owner) | |
| 305 | |
| 306 # Remove all directories owned by the current owner from the remaining | |
| 307 # list. | |
| 308 for dirname in owned_dirs[max_owner]: | |
| 309 dirs.discard(dirname) | |
| 310 | |
| 311 return final_owners | |
| OLD | NEW |