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 |