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 | |
53 import re | 52 import re |
54 | 53 |
55 | 54 |
56 # If this is present by itself on a line, this means that everyone can review. | 55 # If this is present by itself on a line, this means that everyone can review. |
57 EVERYONE = '*' | 56 EVERYONE = '*' |
58 | 57 |
59 | 58 |
60 # Recognizes 'X@Y' email addresses. Very simplistic. | 59 # Recognizes 'X@Y' email addresses. Very simplistic. |
61 BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$' | 60 BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$' |
62 | 61 |
(...skipping 183 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
246 if directive == "set noparent": | 245 if directive == "set noparent": |
247 self.stop_looking.add(path) | 246 self.stop_looking.add(path) |
248 elif self.email_regexp.match(directive) or directive == EVERYONE: | 247 elif self.email_regexp.match(directive) or directive == EVERYONE: |
249 self.owned_by.setdefault(directive, set()).add(path) | 248 self.owned_by.setdefault(directive, set()).add(path) |
250 self.owners_for.setdefault(path, set()).add(directive) | 249 self.owners_for.setdefault(path, set()).add(directive) |
251 else: | 250 else: |
252 raise SyntaxErrorInOwnersFile(owners_path, lineno, | 251 raise SyntaxErrorInOwnersFile(owners_path, lineno, |
253 ('%s is not a "set" directive, "*", ' | 252 ('%s is not a "set" directive, "*", ' |
254 'or an email address: "%s"' % (line_type, directive))) | 253 'or an email address: "%s"' % (line_type, directive))) |
255 | 254 |
| 255 |
256 def _covering_set_of_owners_for(self, files): | 256 def _covering_set_of_owners_for(self, files): |
257 dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files) | 257 # Get the set of directories from the files. |
258 all_possible_owners = self._all_possible_owners(dirs_remaining) | 258 dirs = set() |
259 suggested_owners = set() | 259 for f in files: |
260 while dirs_remaining: | 260 dirs.add(self._enclosing_dir_with_owners(f)) |
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 | |
266 | 261 |
267 def _all_possible_owners(self, dirs): | 262 |
268 """Returns a list of (potential owner, distance-from-dir) tuples; a | 263 owned_dirs = {} |
269 distance of 1 is the lowest/closest possible distance (which makes the | 264 dir_owners = {} |
270 subsequent math easier).""" | 265 |
271 all_possible_owners = {} | |
272 for current_dir in dirs: | 266 for current_dir in dirs: |
| 267 # Get the list of owners for each directory. |
| 268 current_owners = set() |
273 dirname = current_dir | 269 dirname = current_dir |
274 distance = 1 | 270 while dirname in self.owners_for: |
275 while True: | 271 current_owners |= self.owners_for[dirname] |
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)) | |
282 if self._stop_looking(dirname): | 272 if self._stop_looking(dirname): |
283 break | 273 break |
| 274 prev_parent = dirname |
284 dirname = self.os_path.dirname(dirname) | 275 dirname = self.os_path.dirname(dirname) |
285 distance += 1 | 276 if prev_parent == dirname: |
286 return all_possible_owners | 277 break |
287 | 278 |
288 @staticmethod | 279 # Map each directory to a list of its owners. |
289 def lowest_cost_owner(all_possible_owners, dirs): | 280 dir_owners[current_dir] = current_owners |
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)) | |
306 | 281 |
307 # Return the lowest cost owner. In the case of a tie, pick one randomly. | 282 # Add the directory to the list of each owner. |
308 lowest_cost = min(total_costs_by_owner.itervalues()) | 283 for owner in current_owners: |
309 lowest_cost_owners = filter( | 284 owned_dirs.setdefault(owner, set()).add(current_dir) |
310 lambda owner: total_costs_by_owner[owner] == lowest_cost, | 285 |
311 total_costs_by_owner) | 286 final_owners = set() |
312 return random.Random().choice(lowest_cost_owners) | 287 while dirs: |
| 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 |