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 |