Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(537)

Side by Side Diff: owners.py

Issue 1085993004: Implement support for file: includes in OWNERS files. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: add a noparent tset Created 5 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | testing_support/filesystem_mock.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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)
OLDNEW
« no previous file with comments | « no previous file | testing_support/filesystem_mock.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698