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 97 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
108 # Mapping of owners to the paths they own. | 108 # Mapping of owners to the paths they own. |
109 self.owned_by = {EVERYONE: set()} | 109 self.owned_by = {EVERYONE: set()} |
110 | 110 |
111 # Mapping of paths to authorized owners. | 111 # Mapping of paths to authorized owners. |
112 self.owners_for = {} | 112 self.owners_for = {} |
113 | 113 |
114 # Set of paths that stop us from looking above them for owners. | 114 # Set of paths that stop us from looking above them for owners. |
115 # (This is implicitly true for the root directory). | 115 # (This is implicitly true for the root directory). |
116 self.stop_looking = set(['']) | 116 self.stop_looking = set(['']) |
117 | 117 |
118 def reviewers_for(self, files): | 118 def reviewers_for(self, files, author): |
119 """Returns a suggested set of reviewers that will cover the files. | 119 """Returns a suggested set of reviewers that will cover the files. |
120 | 120 |
121 files is a sequence of paths relative to (and under) self.root.""" | 121 files is a sequence of paths relative to (and under) self.root. |
| 122 If author is nonempty, we ensure it is not included in the set returned |
| 123 in order avoid suggesting the author as a reviewer for their own changes.""" |
122 self._check_paths(files) | 124 self._check_paths(files) |
123 self._load_data_needed_for(files) | 125 self._load_data_needed_for(files) |
124 suggested_owners = self._covering_set_of_owners_for(files) | 126 suggested_owners = self._covering_set_of_owners_for(files, author) |
125 if EVERYONE in suggested_owners: | 127 if EVERYONE in suggested_owners: |
126 if len(suggested_owners) > 1: | 128 if len(suggested_owners) > 1: |
127 suggested_owners.remove(EVERYONE) | 129 suggested_owners.remove(EVERYONE) |
128 else: | 130 else: |
129 suggested_owners = set(['<anyone>']) | 131 suggested_owners = set(['<anyone>']) |
130 return suggested_owners | 132 return suggested_owners |
131 | 133 |
132 def files_not_covered_by(self, files, reviewers): | 134 def files_not_covered_by(self, files, reviewers): |
133 """Returns the files not owned by one of the reviewers. | 135 """Returns the files not owned by one of the reviewers. |
134 | 136 |
(...skipping 94 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
229 if directive == "set noparent": | 231 if directive == "set noparent": |
230 self.stop_looking.add(path) | 232 self.stop_looking.add(path) |
231 elif self.email_regexp.match(directive) or directive == EVERYONE: | 233 elif self.email_regexp.match(directive) or directive == EVERYONE: |
232 self.owned_by.setdefault(directive, set()).add(path) | 234 self.owned_by.setdefault(directive, set()).add(path) |
233 self.owners_for.setdefault(path, set()).add(directive) | 235 self.owners_for.setdefault(path, set()).add(directive) |
234 else: | 236 else: |
235 raise SyntaxErrorInOwnersFile(owners_path, lineno, | 237 raise SyntaxErrorInOwnersFile(owners_path, lineno, |
236 ('%s is not a "set" directive, "*", ' | 238 ('%s is not a "set" directive, "*", ' |
237 'or an email address: "%s"' % (line_type, directive))) | 239 'or an email address: "%s"' % (line_type, directive))) |
238 | 240 |
239 def _covering_set_of_owners_for(self, files): | 241 def _covering_set_of_owners_for(self, files, author): |
240 dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files) | 242 dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files) |
241 all_possible_owners = self._all_possible_owners(dirs_remaining) | 243 all_possible_owners = self._all_possible_owners(dirs_remaining, author) |
242 suggested_owners = set() | 244 suggested_owners = set() |
243 while dirs_remaining: | 245 while dirs_remaining: |
244 owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining) | 246 owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining) |
245 suggested_owners.add(owner) | 247 suggested_owners.add(owner) |
246 dirs_to_remove = set(el[0] for el in all_possible_owners[owner]) | 248 dirs_to_remove = set(el[0] for el in all_possible_owners[owner]) |
247 dirs_remaining -= dirs_to_remove | 249 dirs_remaining -= dirs_to_remove |
248 return suggested_owners | 250 return suggested_owners |
249 | 251 |
250 def _all_possible_owners(self, dirs): | 252 def _all_possible_owners(self, dirs, author): |
251 """Returns a list of (potential owner, distance-from-dir) tuples; a | 253 """Returns a list of (potential owner, distance-from-dir) tuples; a |
252 distance of 1 is the lowest/closest possible distance (which makes the | 254 distance of 1 is the lowest/closest possible distance (which makes the |
253 subsequent math easier).""" | 255 subsequent math easier).""" |
254 all_possible_owners = {} | 256 all_possible_owners = {} |
255 for current_dir in dirs: | 257 for current_dir in dirs: |
256 dirname = current_dir | 258 dirname = current_dir |
257 distance = 1 | 259 distance = 1 |
258 while True: | 260 while True: |
259 for owner in self.owners_for.get(dirname, []): | 261 for owner in self.owners_for.get(dirname, []): |
| 262 if author and owner == author: |
| 263 continue |
260 all_possible_owners.setdefault(owner, []) | 264 all_possible_owners.setdefault(owner, []) |
261 # If the same person is in multiple OWNERS files above a given | 265 # If the same person is in multiple OWNERS files above a given |
262 # directory, only count the closest one. | 266 # directory, only count the closest one. |
263 if not any(current_dir == el[0] for el in all_possible_owners[owner]): | 267 if not any(current_dir == el[0] for el in all_possible_owners[owner]): |
264 all_possible_owners[owner].append((current_dir, distance)) | 268 all_possible_owners[owner].append((current_dir, distance)) |
265 if self._stop_looking(dirname): | 269 if self._stop_looking(dirname): |
266 break | 270 break |
267 dirname = self.os_path.dirname(dirname) | 271 dirname = self.os_path.dirname(dirname) |
268 distance += 1 | 272 distance += 1 |
269 return all_possible_owners | 273 return all_possible_owners |
(...skipping 16 matching lines...) Expand all Loading... |
286 if num_directories_owned: | 290 if num_directories_owned: |
287 total_costs_by_owner[owner] = (total_distance / | 291 total_costs_by_owner[owner] = (total_distance / |
288 pow(num_directories_owned, 1.75)) | 292 pow(num_directories_owned, 1.75)) |
289 | 293 |
290 # Return the lowest cost owner. In the case of a tie, pick one randomly. | 294 # Return the lowest cost owner. In the case of a tie, pick one randomly. |
291 lowest_cost = min(total_costs_by_owner.itervalues()) | 295 lowest_cost = min(total_costs_by_owner.itervalues()) |
292 lowest_cost_owners = filter( | 296 lowest_cost_owners = filter( |
293 lambda owner: total_costs_by_owner[owner] == lowest_cost, | 297 lambda owner: total_costs_by_owner[owner] == lowest_cost, |
294 total_costs_by_owner) | 298 total_costs_by_owner) |
295 return random.Random().choice(lowest_cost_owners) | 299 return random.Random().choice(lowest_cost_owners) |
OLD | NEW |