Chromium Code Reviews| 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. |
| 11 | 11 |
| 12 The syntax of the OWNERS file is, roughly: | 12 The syntax of the OWNERS file is, roughly: |
| 13 | 13 |
| 14 lines := (\s* line? \s* "\n")* | 14 lines := (\s* line? \s* "\n")* |
| 15 | 15 |
| 16 line := directive | 16 line := directive |
| 17 | "per-file" \s+ glob \s* "=" \s* directive | 17 | "per-file" \s+ glob \s* "=" \s* directive |
| 18 | comment | 18 | comment |
| 19 | 19 |
| 20 directive := "set noparent" | 20 directive := "set noparent" |
| 21 | "file:" glob | |
| 21 | email_address | 22 | email_address |
| 22 | "*" | 23 | "*" |
| 23 | 24 |
| 24 glob := [a-zA-Z0-9_-*?]+ | 25 glob := [a-zA-Z0-9_-*?]+ |
| 25 | 26 |
| 26 comment := "#" [^"\n"]* | 27 comment := "#" [^"\n"]* |
| 27 | 28 |
| 28 Email addresses must follow the foo@bar.com short form (exact syntax given | 29 Email addresses must follow the foo@bar.com short form (exact syntax given |
| 29 in BASIC_EMAIL_REGEXP, below). Filename globs follow the simple unix | 30 in BASIC_EMAIL_REGEXP, below). Filename globs follow the simple unix |
| 30 shell conventions, and relative and absolute paths are not allowed (i.e., | 31 shell conventions, and relative and absolute paths are not allowed (i.e., |
| 31 globs only refer to the files in the current directory). | 32 globs only refer to the files in the current directory). |
| 32 | 33 |
| 33 If a user's email is one of the email_addresses in the file, the user is | 34 If a user's email is one of the email_addresses in the file, the user is |
| 34 considered an "OWNER" for all files in the directory. | 35 considered an "OWNER" for all files in the directory. |
| 35 | 36 |
| 36 If the "per-file" directive is used, the line only applies to files in that | 37 If the "per-file" directive is used, the line only applies to files in that |
| 37 directory that match the filename glob specified. | 38 directory that match the filename glob specified. |
| 38 | 39 |
| 39 If the "set noparent" directive used, then only entries in this OWNERS file | 40 If the "set noparent" directive used, then only entries in this OWNERS file |
| 40 apply to files in this directory; if the "set noparent" directive is not | 41 apply to files in this directory; if the "set noparent" directive is not |
| 41 used, then entries in OWNERS files in enclosing (upper) directories also | 42 used, then entries in OWNERS files in enclosing (upper) directories also |
| 42 apply (up until a "set noparent is encountered"). | 43 apply (up until a "set noparent is encountered"). |
| 43 | 44 |
| 44 If "per-file glob=set noparent" is used, then global directives are ignored | 45 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 | 46 for the glob, and only the "per-file" owners are used for files matching that |
| 46 glob. | 47 glob. |
| 47 | 48 |
| 49 If the "file:" directive is used, the referred to OWNERS file will be parsed and | |
| 50 considered when determining the valid set of OWNERS. If the filename starts with | |
| 51 "//" it is considered to be an absolute path, otherwise a relative path. | |
|
Dirk Pranke
2015/04/15 19:42:37
"absolute path" might be confusing. I might call t
Peter Beverloo
2015/04/15 20:04:31
Ah yes, thanks. I'll adopt your comment with s/top
Peter Beverloo
2015/04/16 09:54:05
Done.
| |
| 52 | |
| 48 Examples for all of these combinations can be found in tests/owners_unittest.py. | 53 Examples for all of these combinations can be found in tests/owners_unittest.py. |
| 49 """ | 54 """ |
| 50 | 55 |
| 51 import collections | 56 import collections |
| 52 import random | 57 import random |
| 53 import re | 58 import re |
| 54 | 59 |
| 60 # Name of the file in a directory containing the OWNERS contents. | |
| 61 OWNERS_FILE = 'OWNERS' | |
| 62 | |
| 55 | 63 |
| 56 # If this is present by itself on a line, this means that everyone can review. | 64 # If this is present by itself on a line, this means that everyone can review. |
| 57 EVERYONE = '*' | 65 EVERYONE = '*' |
| 58 | 66 |
| 59 | 67 |
| 60 # Recognizes 'X@Y' email addresses. Very simplistic. | 68 # Recognizes 'X@Y' email addresses. Very simplistic. |
| 61 BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$' | 69 BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$' |
| 62 | 70 |
| 63 | 71 |
| 64 def _assert_is_collection(obj): | 72 def _assert_is_collection(obj): |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 111 # Mapping of paths to authorized owners. | 119 # Mapping of paths to authorized owners. |
| 112 self.owners_for = {} | 120 self.owners_for = {} |
| 113 | 121 |
| 114 # Mapping reviewers to the preceding comment per file in the OWNERS files. | 122 # Mapping reviewers to the preceding comment per file in the OWNERS files. |
| 115 self.comments = {} | 123 self.comments = {} |
| 116 | 124 |
| 117 # Set of paths that stop us from looking above them for owners. | 125 # Set of paths that stop us from looking above them for owners. |
| 118 # (This is implicitly true for the root directory). | 126 # (This is implicitly true for the root directory). |
| 119 self.stop_looking = set(['']) | 127 self.stop_looking = set(['']) |
| 120 | 128 |
| 129 # Set of files which have already been read. | |
| 130 self.read_files = set() | |
| 131 | |
| 121 def reviewers_for(self, files, author): | 132 def reviewers_for(self, files, author): |
| 122 """Returns a suggested set of reviewers that will cover the files. | 133 """Returns a suggested set of reviewers that will cover the files. |
| 123 | 134 |
| 124 files is a sequence of paths relative to (and under) self.root. | 135 files is a sequence of paths relative to (and under) self.root. |
| 125 If author is nonempty, we ensure it is not included in the set returned | 136 If author is nonempty, we ensure it is not included in the set returned |
| 126 in order avoid suggesting the author as a reviewer for their own changes.""" | 137 in order avoid suggesting the author as a reviewer for their own changes.""" |
| 127 self._check_paths(files) | 138 self._check_paths(files) |
| 128 self.load_data_needed_for(files) | 139 self.load_data_needed_for(files) |
| 129 suggested_owners = self._covering_set_of_owners_for(files, author) | 140 suggested_owners = self._covering_set_of_owners_for(files, author) |
| 130 if EVERYONE in suggested_owners: | 141 if EVERYONE in suggested_owners: |
| (...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 182 while not dirpath in self.owners_for: | 193 while not dirpath in self.owners_for: |
| 183 if self._stop_looking(dirpath): | 194 if self._stop_looking(dirpath): |
| 184 break | 195 break |
| 185 dirpath = self.os_path.dirname(dirpath) | 196 dirpath = self.os_path.dirname(dirpath) |
| 186 return dirpath | 197 return dirpath |
| 187 | 198 |
| 188 def load_data_needed_for(self, files): | 199 def load_data_needed_for(self, files): |
| 189 for f in files: | 200 for f in files: |
| 190 dirpath = self.os_path.dirname(f) | 201 dirpath = self.os_path.dirname(f) |
| 191 while not dirpath in self.owners_for: | 202 while not dirpath in self.owners_for: |
| 192 self._read_owners_in_dir(dirpath) | 203 self._read_owners_in_dir(dirpath, OWNERS_FILE) |
| 193 if self._stop_looking(dirpath): | 204 if self._stop_looking(dirpath): |
| 194 break | 205 break |
| 195 dirpath = self.os_path.dirname(dirpath) | 206 dirpath = self.os_path.dirname(dirpath) |
| 196 | 207 |
| 197 def _read_owners_in_dir(self, dirpath): | 208 def _read_owners_in_dir(self, dirpath, owners_file): |
| 198 owners_path = self.os_path.join(self.root, dirpath, 'OWNERS') | 209 owners_path = self.os_path.join(self.root, dirpath, owners_file) |
| 199 if not self.os_path.exists(owners_path): | 210 if not self.os_path.exists(owners_path): |
| 200 return | 211 return |
| 212 | |
| 213 if owners_path in self.read_files: | |
| 214 return | |
|
Dirk Pranke
2015/04/15 19:42:37
Since read_files is a set, calling add() multiple
Peter Beverloo
2015/04/15 20:04:31
I may be misunderstanding you, but both the set an
Dirk Pranke
2015/04/15 20:52:03
Oh, I was misreading the code, and thought the fun
| |
| 215 | |
| 216 self.read_files.add(owners_path) | |
| 217 | |
| 201 comment = [] | 218 comment = [] |
| 202 in_comment = False | 219 in_comment = False |
| 203 lineno = 0 | 220 lineno = 0 |
| 204 for line in self.fopen(owners_path): | 221 for line in self.fopen(owners_path): |
| 205 lineno += 1 | 222 lineno += 1 |
| 206 line = line.strip() | 223 line = line.strip() |
| 207 if line.startswith('#'): | 224 if line.startswith('#'): |
| 208 if not in_comment: | 225 if not in_comment: |
| 209 comment = [] | 226 comment = [] |
| 210 comment.append(line[1:].strip()) | 227 comment.append(line[1:].strip()) |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 237 raise SyntaxErrorInOwnersFile(owners_path, lineno, | 254 raise SyntaxErrorInOwnersFile(owners_path, lineno, |
| 238 'unknown option: "%s"' % line[4:].strip()) | 255 'unknown option: "%s"' % line[4:].strip()) |
| 239 | 256 |
| 240 self._add_entry(dirpath, line, 'line', owners_path, lineno, | 257 self._add_entry(dirpath, line, 'line', owners_path, lineno, |
| 241 ' '.join(comment)) | 258 ' '.join(comment)) |
| 242 | 259 |
| 243 def _add_entry(self, path, directive, | 260 def _add_entry(self, path, directive, |
| 244 line_type, owners_path, lineno, comment): | 261 line_type, owners_path, lineno, comment): |
| 245 if directive == 'set noparent': | 262 if directive == 'set noparent': |
| 246 self.stop_looking.add(path) | 263 self.stop_looking.add(path) |
| 264 elif directive.startswith('file:'): | |
| 265 dirpath, owners_file = self._resolve_include(directive[5:], owners_path) | |
| 266 if not dirpath: | |
| 267 raise SyntaxErrorInOwnersFile(owners_path, lineno, | |
| 268 ('%s does not refer to an existing file.' % directive[5:])) | |
| 269 | |
| 270 self._read_owners_in_dir(dirpath, owners_file) | |
| 271 for key in self.owned_by: | |
| 272 if not dirpath in self.owned_by[key]: | |
| 273 continue | |
| 274 self.owned_by[key].add(path) | |
| 275 | |
| 276 if dirpath in self.owners_for: | |
| 277 self.owners_for.setdefault(path, set()).update(self.owners_for[dirpath]) | |
| 278 | |
| 247 elif self.email_regexp.match(directive) or directive == EVERYONE: | 279 elif self.email_regexp.match(directive) or directive == EVERYONE: |
| 248 self.comments.setdefault(directive, {}) | 280 self.comments.setdefault(directive, {}) |
| 249 self.comments[directive][path] = comment | 281 self.comments[directive][path] = comment |
| 250 self.owned_by.setdefault(directive, set()).add(path) | 282 self.owned_by.setdefault(directive, set()).add(path) |
| 251 self.owners_for.setdefault(path, set()).add(directive) | 283 self.owners_for.setdefault(path, set()).add(directive) |
| 252 else: | 284 else: |
| 253 raise SyntaxErrorInOwnersFile(owners_path, lineno, | 285 raise SyntaxErrorInOwnersFile(owners_path, lineno, |
| 254 ('%s is not a "set" directive, "*", ' | 286 ('%s is not a "set" directive, include, "*", ' |
| 255 'or an email address: "%s"' % (line_type, directive))) | 287 'or an email address: "%s"' % (line_type, directive))) |
| 256 | 288 |
| 289 def _resolve_include(self, include_path, base_path): | |
| 290 if include_path.startswith('//'): | |
| 291 dirpath = self.os_path.dirname(include_path[2:]) | |
| 292 else: | |
| 293 assert base_path.startswith(self.root) | |
| 294 base_path = self.os_path.dirname(base_path[len(self.root):]) | |
| 295 dirpath = self.os_path.join(base_path, self.os_path.dirname(include_path)) | |
| 296 | |
| 297 owners_file = self.os_path.basename(include_path) | |
| 298 | |
| 299 owners_path = self.os_path.join(self.root, dirpath, owners_file) | |
| 300 if not self.os_path.exists(owners_path): | |
| 301 return None, None | |
| 302 | |
| 303 return dirpath, owners_path | |
|
Dirk Pranke
2015/04/15 19:42:37
see comment above; it's not clear to me that retur
Peter Beverloo
2015/04/15 20:04:31
I named them to be consistent with the arguments o
Dirk Pranke
2015/04/15 20:52:03
I'm not fully sure I understood your comments, but
Peter Beverloo
2015/04/15 20:57:09
Yes, it does. Thanks! I'll apply the changes tomor
Peter Beverloo
2015/04/16 09:54:06
Done.
| |
| 304 | |
| 257 def _covering_set_of_owners_for(self, files, author): | 305 def _covering_set_of_owners_for(self, files, author): |
| 258 dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files) | 306 dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files) |
| 259 all_possible_owners = self.all_possible_owners(dirs_remaining, author) | 307 all_possible_owners = self.all_possible_owners(dirs_remaining, author) |
| 260 suggested_owners = set() | 308 suggested_owners = set() |
| 261 while dirs_remaining: | 309 while dirs_remaining: |
| 262 owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining) | 310 owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining) |
| 263 suggested_owners.add(owner) | 311 suggested_owners.add(owner) |
| 264 dirs_to_remove = set(el[0] for el in all_possible_owners[owner]) | 312 dirs_to_remove = set(el[0] for el in all_possible_owners[owner]) |
| 265 dirs_remaining -= dirs_to_remove | 313 dirs_remaining -= dirs_to_remove |
| 266 return suggested_owners | 314 return suggested_owners |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 311 @staticmethod | 359 @staticmethod |
| 312 def lowest_cost_owner(all_possible_owners, dirs): | 360 def lowest_cost_owner(all_possible_owners, dirs): |
| 313 total_costs_by_owner = Database.total_costs_by_owner(all_possible_owners, | 361 total_costs_by_owner = Database.total_costs_by_owner(all_possible_owners, |
| 314 dirs) | 362 dirs) |
| 315 # Return the lowest cost owner. In the case of a tie, pick one randomly. | 363 # Return the lowest cost owner. In the case of a tie, pick one randomly. |
| 316 lowest_cost = min(total_costs_by_owner.itervalues()) | 364 lowest_cost = min(total_costs_by_owner.itervalues()) |
| 317 lowest_cost_owners = filter( | 365 lowest_cost_owners = filter( |
| 318 lambda owner: total_costs_by_owner[owner] == lowest_cost, | 366 lambda owner: total_costs_by_owner[owner] == lowest_cost, |
| 319 total_costs_by_owner) | 367 total_costs_by_owner) |
| 320 return random.Random().choice(lowest_cost_owners) | 368 return random.Random().choice(lowest_cost_owners) |
| OLD | NEW |