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 100 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
111 | 111 |
112 # Mapping of owners to the paths or globs they own. | 112 # Mapping of owners to the paths or globs they own. |
113 self._owners_to_paths = {EVERYONE: set()} | 113 self._owners_to_paths = {EVERYONE: set()} |
114 | 114 |
115 # Mapping of paths to authorized owners. | 115 # Mapping of paths to authorized owners. |
116 self._paths_to_owners = {} | 116 self._paths_to_owners = {} |
117 | 117 |
118 # Mapping reviewers to the preceding comment per file in the OWNERS files. | 118 # Mapping reviewers to the preceding comment per file in the OWNERS files. |
119 self.comments = {} | 119 self.comments = {} |
120 | 120 |
| 121 # Cache of compiled regexes for _fnmatch() |
| 122 self._fnmatch_cache = {} |
| 123 |
121 # Set of paths that stop us from looking above them for owners. | 124 # Set of paths that stop us from looking above them for owners. |
122 # (This is implicitly true for the root directory). | 125 # (This is implicitly true for the root directory). |
123 self._stop_looking = set(['']) | 126 self._stop_looking = set(['']) |
124 | 127 |
125 # Set of files which have already been read. | 128 # Set of files which have already been read. |
126 self.read_files = set() | 129 self.read_files = set() |
127 | 130 |
128 def reviewers_for(self, files, author): | 131 def reviewers_for(self, files, author): |
129 """Returns a suggested set of reviewers that will cover the files. | 132 """Returns a suggested set of reviewers that will cover the files. |
130 | 133 |
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
190 def load_data_needed_for(self, files): | 193 def load_data_needed_for(self, files): |
191 for f in files: | 194 for f in files: |
192 dirpath = self.os_path.dirname(f) | 195 dirpath = self.os_path.dirname(f) |
193 while not self._owners_for(dirpath): | 196 while not self._owners_for(dirpath): |
194 self._read_owners(self.os_path.join(dirpath, 'OWNERS')) | 197 self._read_owners(self.os_path.join(dirpath, 'OWNERS')) |
195 if self._should_stop_looking(dirpath): | 198 if self._should_stop_looking(dirpath): |
196 break | 199 break |
197 dirpath = self.os_path.dirname(dirpath) | 200 dirpath = self.os_path.dirname(dirpath) |
198 | 201 |
199 def _should_stop_looking(self, objname): | 202 def _should_stop_looking(self, objname): |
200 return any(fnmatch.fnmatch(objname, stop_looking) | 203 return any(self._fnmatch(objname, stop_looking) |
201 for stop_looking in self._stop_looking) | 204 for stop_looking in self._stop_looking) |
202 | 205 |
203 def _owners_for(self, objname): | 206 def _owners_for(self, objname): |
204 obj_owners = set() | 207 obj_owners = set() |
205 for owned_path, path_owners in self._paths_to_owners.iteritems(): | 208 for owned_path, path_owners in self._paths_to_owners.iteritems(): |
206 if fnmatch.fnmatch(objname, owned_path): | 209 if self._fnmatch(objname, owned_path): |
207 obj_owners |= path_owners | 210 obj_owners |= path_owners |
208 return obj_owners | 211 return obj_owners |
209 | 212 |
210 def _read_owners(self, path): | 213 def _read_owners(self, path): |
211 owners_path = self.os_path.join(self.root, path) | 214 owners_path = self.os_path.join(self.root, path) |
212 if not self.os_path.exists(owners_path): | 215 if not self.os_path.exists(owners_path): |
213 return | 216 return |
214 | 217 |
215 if owners_path in self.read_files: | 218 if owners_path in self.read_files: |
216 return | 219 return |
(...skipping 115 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
332 # If the same person is in multiple OWNERS files above a given | 335 # If the same person is in multiple OWNERS files above a given |
333 # directory, only count the closest one. | 336 # directory, only count the closest one. |
334 if not any(current_dir == el[0] for el in all_possible_owners[owner]): | 337 if not any(current_dir == el[0] for el in all_possible_owners[owner]): |
335 all_possible_owners[owner].append((current_dir, distance)) | 338 all_possible_owners[owner].append((current_dir, distance)) |
336 if self._should_stop_looking(dirname): | 339 if self._should_stop_looking(dirname): |
337 break | 340 break |
338 dirname = self.os_path.dirname(dirname) | 341 dirname = self.os_path.dirname(dirname) |
339 distance += 1 | 342 distance += 1 |
340 return all_possible_owners | 343 return all_possible_owners |
341 | 344 |
| 345 def _fnmatch(self, filename, pattern): |
| 346 """Same as fnmatch.fnmatch(), but interally caches the compiled regexes.""" |
| 347 matcher = self._fnmatch_cache.get(pattern) |
| 348 if matcher is None: |
| 349 matcher = re.compile(fnmatch.translate(pattern)).match |
| 350 self._fnmatch_cache[pattern] = matcher |
| 351 return matcher(filename) |
| 352 |
342 @staticmethod | 353 @staticmethod |
343 def total_costs_by_owner(all_possible_owners, dirs): | 354 def total_costs_by_owner(all_possible_owners, dirs): |
344 # We want to minimize both the number of reviewers and the distance | 355 # We want to minimize both the number of reviewers and the distance |
345 # from the files/dirs needing reviews. The "pow(X, 1.75)" below is | 356 # from the files/dirs needing reviews. The "pow(X, 1.75)" below is |
346 # an arbitrarily-selected scaling factor that seems to work well - it | 357 # an arbitrarily-selected scaling factor that seems to work well - it |
347 # will select one reviewer in the parent directory over three reviewers | 358 # will select one reviewer in the parent directory over three reviewers |
348 # in subdirs, but not one reviewer over just two. | 359 # in subdirs, but not one reviewer over just two. |
349 result = {} | 360 result = {} |
350 for owner in all_possible_owners: | 361 for owner in all_possible_owners: |
351 total_distance = 0 | 362 total_distance = 0 |
(...skipping 10 matching lines...) Expand all Loading... |
362 @staticmethod | 373 @staticmethod |
363 def lowest_cost_owner(all_possible_owners, dirs): | 374 def lowest_cost_owner(all_possible_owners, dirs): |
364 total_costs_by_owner = Database.total_costs_by_owner(all_possible_owners, | 375 total_costs_by_owner = Database.total_costs_by_owner(all_possible_owners, |
365 dirs) | 376 dirs) |
366 # Return the lowest cost owner. In the case of a tie, pick one randomly. | 377 # Return the lowest cost owner. In the case of a tie, pick one randomly. |
367 lowest_cost = min(total_costs_by_owner.itervalues()) | 378 lowest_cost = min(total_costs_by_owner.itervalues()) |
368 lowest_cost_owners = filter( | 379 lowest_cost_owners = filter( |
369 lambda owner: total_costs_by_owner[owner] == lowest_cost, | 380 lambda owner: total_costs_by_owner[owner] == lowest_cost, |
370 total_costs_by_owner) | 381 total_costs_by_owner) |
371 return random.Random().choice(lowest_cost_owners) | 382 return random.Random().choice(lowest_cost_owners) |
OLD | NEW |