blob: 89ce219b3c774fef5cedff8589f20b8a4deded6b [file] [log] [blame]
pam@chromium.orgf46aed92012-03-08 09:18:17 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
dpranke@chromium.org2a009622011-03-01 02:43:31 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
dpranke@chromium.org17cc2442012-10-17 21:12:09 +00005"""A database of OWNERS files.
6
7OWNERS 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).
9Note that all changes must still be reviewed by someone familiar with the code,
10so you may need approval from both an OWNER and a reviewer in many cases.
11
12The syntax of the OWNERS file is, roughly:
13
14lines := (\s* line? \s* "\n")*
15
16line := directive
dpranke@chromium.orgd16e48b2012-12-03 21:53:49 +000017 | "per-file" \s+ glob \s* "=" \s* directive
dpranke@chromium.org17cc2442012-10-17 21:12:09 +000018 | comment
19
20directive := "set noparent"
peter@chromium.org2ce13132015-04-16 16:42:08 +000021 | "file:" glob
dpranke@chromium.org17cc2442012-10-17 21:12:09 +000022 | email_address
23 | "*"
24
25glob := [a-zA-Z0-9_-*?]+
26
27comment := "#" [^"\n"]*
28
29Email addresses must follow the foo@bar.com short form (exact syntax given
30in BASIC_EMAIL_REGEXP, below). Filename globs follow the simple unix
31shell conventions, and relative and absolute paths are not allowed (i.e.,
32globs only refer to the files in the current directory).
33
34If a user's email is one of the email_addresses in the file, the user is
35considered an "OWNER" for all files in the directory.
36
37If the "per-file" directive is used, the line only applies to files in that
38directory that match the filename glob specified.
39
40If the "set noparent" directive used, then only entries in this OWNERS file
41apply to files in this directory; if the "set noparent" directive is not
42used, then entries in OWNERS files in enclosing (upper) directories also
43apply (up until a "set noparent is encountered").
44
45If "per-file glob=set noparent" is used, then global directives are ignored
46for the glob, and only the "per-file" owners are used for files matching that
47glob.
48
peter@chromium.org2ce13132015-04-16 16:42:08 +000049If the "file:" directive is used, the referred to OWNERS file will be parsed and
50considered when determining the valid set of OWNERS. If the filename starts with
51"//" it is relative to the root of the repository, otherwise it is relative to
52the current file
53
dpranke@chromium.org17cc2442012-10-17 21:12:09 +000054Examples for all of these combinations can be found in tests/owners_unittest.py.
55"""
dpranke@chromium.org2a009622011-03-01 02:43:31 +000056
dpranke@chromium.orgfdecfb72011-03-16 23:27:23 +000057import collections
dtu944b6052016-07-14 14:48:21 -070058import fnmatch
dpranke@chromium.orgc591a702012-12-20 20:14:58 +000059import random
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +000060import re
61
62
63# If this is present by itself on a line, this means that everyone can review.
64EVERYONE = '*'
65
66
67# Recognizes 'X@Y' email addresses. Very simplistic.
68BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$'
dpranke@chromium.org2a009622011-03-01 02:43:31 +000069
dpranke@chromium.org2a009622011-03-01 02:43:31 +000070
Jochen Eisinger72606f82017-04-04 10:44:18 +020071# Key for global comments per email address. Should be unlikely to be a
72# pathname.
73GLOBAL_STATUS = '*'
74
75
dpranke@chromium.org923950f2011-03-17 23:40:00 +000076def _assert_is_collection(obj):
dpranke@chromium.orge6a4ab32011-03-31 01:23:08 +000077 assert not isinstance(obj, basestring)
maruel@chromium.org725f1c32011-04-01 20:24:54 +000078 # Module 'collections' has no 'Iterable' member
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080079 # pylint: disable=no-member
dpranke@chromium.orge6a4ab32011-03-31 01:23:08 +000080 if hasattr(collections, 'Iterable') and hasattr(collections, 'Sized'):
81 assert (isinstance(obj, collections.Iterable) and
82 isinstance(obj, collections.Sized))
dpranke@chromium.org923950f2011-03-17 23:40:00 +000083
84
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000085class SyntaxErrorInOwnersFile(Exception):
dpranke@chromium.org86bbf192011-03-09 21:37:06 +000086 def __init__(self, path, lineno, msg):
87 super(SyntaxErrorInOwnersFile, self).__init__((path, lineno, msg))
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000088 self.path = path
dpranke@chromium.org86bbf192011-03-09 21:37:06 +000089 self.lineno = lineno
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000090 self.msg = msg
91
92 def __str__(self):
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000093 return '%s:%d syntax error: %s' % (self.path, self.lineno, self.msg)
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000094
95
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000096class Database(object):
97 """A database of OWNERS files for a repository.
98
99 This class allows you to find a suggested set of reviewers for a list
100 of changed files, and see if a list of changed files is covered by a
101 list of reviewers."""
102
Jochen Eisingereb744762017-04-05 11:00:05 +0200103 def __init__(self, root, fopen, os_path):
dpranke@chromium.org898a10e2011-03-04 21:54:43 +0000104 """Args:
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000105 root: the path to the root of the Repository
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000106 open: function callback to open a text file for reading
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000107 os_path: module/object callback with fields for 'abspath', 'dirname',
mbjorgef2d73522016-07-14 13:28:59 -0700108 'exists', 'join', and 'relpath'
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000109 """
110 self.root = root
111 self.fopen = fopen
112 self.os_path = os_path
113
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000114 # Pick a default email regexp to use; callers can override as desired.
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000115 self.email_regexp = re.compile(BASIC_EMAIL_REGEXP)
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000116
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200117 # Replacement contents for the given files. Maps the file name of an
118 # OWNERS file (relative to root) to an iterator returning the replacement
119 # file contents.
120 self.override_files = {}
121
dtu944b6052016-07-14 14:48:21 -0700122 # Mapping of owners to the paths or globs they own.
123 self._owners_to_paths = {EVERYONE: set()}
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000124
125 # Mapping of paths to authorized owners.
dtu944b6052016-07-14 14:48:21 -0700126 self._paths_to_owners = {}
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000127
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000128 # Mapping reviewers to the preceding comment per file in the OWNERS files.
129 self.comments = {}
130
nick7e16cf32016-09-16 16:05:05 -0700131 # Cache of compiled regexes for _fnmatch()
132 self._fnmatch_cache = {}
133
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000134 # Set of paths that stop us from looking above them for owners.
135 # (This is implicitly true for the root directory).
dtu944b6052016-07-14 14:48:21 -0700136 self._stop_looking = set([''])
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000137
peter@chromium.org2ce13132015-04-16 16:42:08 +0000138 # Set of files which have already been read.
139 self.read_files = set()
140
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800141 # Set of files which were included from other files. Files are processed
142 # differently depending on whether they are regular owners files or
143 # being included from another file.
144 self._included_files = {}
145
Jochen Eisingereb744762017-04-05 11:00:05 +0200146 # File with global status lines for owners.
147 self._status_file = None
148
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000149 def reviewers_for(self, files, author):
dpranke@chromium.orgfdecfb72011-03-16 23:27:23 +0000150 """Returns a suggested set of reviewers that will cover the files.
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000151
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000152 files is a sequence of paths relative to (and under) self.root.
153 If author is nonempty, we ensure it is not included in the set returned
154 in order avoid suggesting the author as a reviewer for their own changes."""
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000155 self._check_paths(files)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000156 self.load_data_needed_for(files)
dtu944b6052016-07-14 14:48:21 -0700157
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000158 suggested_owners = self._covering_set_of_owners_for(files, author)
dpranke@chromium.org9d66f482013-01-18 02:57:11 +0000159 if EVERYONE in suggested_owners:
160 if len(suggested_owners) > 1:
161 suggested_owners.remove(EVERYONE)
162 else:
163 suggested_owners = set(['<anyone>'])
164 return suggested_owners
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000165
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000166 def files_not_covered_by(self, files, reviewers):
167 """Returns the files not owned by one of the reviewers.
dpranke@chromium.orgfdecfb72011-03-16 23:27:23 +0000168
169 Args:
170 files is a sequence of paths relative to (and under) self.root.
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000171 reviewers is a sequence of strings matching self.email_regexp.
172 """
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000173 self._check_paths(files)
174 self._check_reviewers(reviewers)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000175 self.load_data_needed_for(files)
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000176
dtu944b6052016-07-14 14:48:21 -0700177 return set(f for f in files if not self._is_obj_covered_by(f, reviewers))
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000178
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000179 def _check_paths(self, files):
180 def _is_under(f, pfx):
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000181 return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx)
dpranke@chromium.org923950f2011-03-17 23:40:00 +0000182 _assert_is_collection(files)
dpranke@chromium.orgb54a78e2012-12-13 23:37:23 +0000183 assert all(not self.os_path.isabs(f) and
184 _is_under(f, self.os_path.abspath(self.root)) for f in files)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000185
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000186 def _check_reviewers(self, reviewers):
dpranke@chromium.org923950f2011-03-17 23:40:00 +0000187 _assert_is_collection(reviewers)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000188 assert all(self.email_regexp.match(r) for r in reviewers)
189
dtu944b6052016-07-14 14:48:21 -0700190 def _is_obj_covered_by(self, objname, reviewers):
191 reviewers = list(reviewers) + [EVERYONE]
192 while True:
193 for reviewer in reviewers:
194 for owned_pattern in self._owners_to_paths.get(reviewer, set()):
195 if fnmatch.fnmatch(objname, owned_pattern):
196 return True
197 if self._should_stop_looking(objname):
198 break
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000199 objname = self.os_path.dirname(objname)
dtu944b6052016-07-14 14:48:21 -0700200 return False
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000201
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000202 def _enclosing_dir_with_owners(self, objname):
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000203 """Returns the innermost enclosing directory that has an OWNERS file."""
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000204 dirpath = objname
dtu944b6052016-07-14 14:48:21 -0700205 while not self._owners_for(dirpath):
206 if self._should_stop_looking(dirpath):
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000207 break
208 dirpath = self.os_path.dirname(dirpath)
209 return dirpath
210
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000211 def load_data_needed_for(self, files):
Jochen Eisinger72606f82017-04-04 10:44:18 +0200212 self._read_global_comments()
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000213 for f in files:
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000214 dirpath = self.os_path.dirname(f)
dtu944b6052016-07-14 14:48:21 -0700215 while not self._owners_for(dirpath):
peter@chromium.org2ce13132015-04-16 16:42:08 +0000216 self._read_owners(self.os_path.join(dirpath, 'OWNERS'))
dtu944b6052016-07-14 14:48:21 -0700217 if self._should_stop_looking(dirpath):
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000218 break
219 dirpath = self.os_path.dirname(dirpath)
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000220
dtu944b6052016-07-14 14:48:21 -0700221 def _should_stop_looking(self, objname):
nick7e16cf32016-09-16 16:05:05 -0700222 return any(self._fnmatch(objname, stop_looking)
dtu944b6052016-07-14 14:48:21 -0700223 for stop_looking in self._stop_looking)
224
225 def _owners_for(self, objname):
226 obj_owners = set()
227 for owned_path, path_owners in self._paths_to_owners.iteritems():
nick7e16cf32016-09-16 16:05:05 -0700228 if self._fnmatch(objname, owned_path):
dtu944b6052016-07-14 14:48:21 -0700229 obj_owners |= path_owners
230 return obj_owners
231
peter@chromium.org2ce13132015-04-16 16:42:08 +0000232 def _read_owners(self, path):
233 owners_path = self.os_path.join(self.root, path)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000234 if not self.os_path.exists(owners_path):
235 return
peter@chromium.org2ce13132015-04-16 16:42:08 +0000236
237 if owners_path in self.read_files:
238 return
239
240 self.read_files.add(owners_path)
241
Jochen Eisingereb744762017-04-05 11:00:05 +0200242 is_toplevel = path == 'OWNERS'
243
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000244 comment = []
peter@chromium.org2ce13132015-04-16 16:42:08 +0000245 dirpath = self.os_path.dirname(path)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000246 in_comment = False
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000247 lineno = 0
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200248
249 if path in self.override_files:
250 file_iter = self.override_files[path]
251 else:
252 file_iter = self.fopen(owners_path)
253
254 for line in file_iter:
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000255 lineno += 1
256 line = line.strip()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000257 if line.startswith('#'):
Jochen Eisingereb744762017-04-05 11:00:05 +0200258 if is_toplevel:
259 m = re.match('#\s*OWNERS_STATUS\s+=\s+(.+)$', line)
260 if m:
261 self._status_file = m.group(1).strip()
262 continue
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000263 if not in_comment:
264 comment = []
265 comment.append(line[1:].strip())
266 in_comment = True
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000267 continue
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000268 if line == '':
269 continue
270 in_comment = False
271
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000272 if line == 'set noparent':
dtu944b6052016-07-14 14:48:21 -0700273 self._stop_looking.add(dirpath)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000274 continue
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000275
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000276 m = re.match('per-file (.+)=(.+)', line)
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000277 if m:
dpranke@chromium.orgd16e48b2012-12-03 21:53:49 +0000278 glob_string = m.group(1).strip()
279 directive = m.group(2).strip()
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000280 full_glob_string = self.os_path.join(self.root, dirpath, glob_string)
dpranke@chromium.org9e227d52012-10-20 23:47:42 +0000281 if '/' in glob_string or '\\' in glob_string:
dpranke@chromium.orge3b1c3d2012-10-20 22:28:14 +0000282 raise SyntaxErrorInOwnersFile(owners_path, lineno,
dpranke@chromium.org9e227d52012-10-20 23:47:42 +0000283 'per-file globs cannot span directories or use escapes: "%s"' %
284 line)
dtu944b6052016-07-14 14:48:21 -0700285 relative_glob_string = self.os_path.relpath(full_glob_string, self.root)
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800286 self._add_entry(relative_glob_string, directive, owners_path,
287 lineno, '\n'.join(comment))
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000288 continue
289
dpranke@chromium.org86bbf192011-03-09 21:37:06 +0000290 if line.startswith('set '):
291 raise SyntaxErrorInOwnersFile(owners_path, lineno,
292 'unknown option: "%s"' % line[4:].strip())
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000293
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800294 self._add_entry(dirpath, line, owners_path, lineno,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000295 ' '.join(comment))
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000296
Jochen Eisinger72606f82017-04-04 10:44:18 +0200297 def _read_global_comments(self):
Jochen Eisingereb744762017-04-05 11:00:05 +0200298 if not self._status_file:
299 if not 'OWNERS' in self.read_files:
300 self._read_owners('OWNERS')
301 if not self._status_file:
302 return
Jochen Eisinger72606f82017-04-04 10:44:18 +0200303
Jochen Eisingereb744762017-04-05 11:00:05 +0200304 owners_status_path = self.os_path.join(self.root, self._status_file)
Jochen Eisinger72606f82017-04-04 10:44:18 +0200305 if not self.os_path.exists(owners_status_path):
Jochen Eisingereb744762017-04-05 11:00:05 +0200306 raise IOError('Could not find global status file "%s"' %
Jochen Eisinger72606f82017-04-04 10:44:18 +0200307 owners_status_path)
308
309 if owners_status_path in self.read_files:
310 return
311
312 self.read_files.add(owners_status_path)
313
314 lineno = 0
315 for line in self.fopen(owners_status_path):
316 lineno += 1
317 line = line.strip()
318 if line.startswith('#'):
319 continue
320 if line == '':
321 continue
322
323 m = re.match('(.+?):(.+)', line)
324 if m:
325 owner = m.group(1).strip()
326 comment = m.group(2).strip()
327 if not self.email_regexp.match(owner):
328 raise SyntaxErrorInOwnersFile(owners_status_path, lineno,
329 'invalid email address: "%s"' % owner)
330
331 self.comments.setdefault(owner, {})
332 self.comments[owner][GLOBAL_STATUS] = comment
333 continue
334
335 raise SyntaxErrorInOwnersFile(owners_status_path, lineno,
336 'cannot parse status entry: "%s"' % line.strip())
337
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800338 def _add_entry(self, owned_paths, directive, owners_path, lineno, comment):
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000339 if directive == 'set noparent':
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800340 self._stop_looking.add(owned_paths)
peter@chromium.org2ce13132015-04-16 16:42:08 +0000341 elif directive.startswith('file:'):
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800342 include_file = self._resolve_include(directive[5:], owners_path)
343 if not include_file:
peter@chromium.org2ce13132015-04-16 16:42:08 +0000344 raise SyntaxErrorInOwnersFile(owners_path, lineno,
345 ('%s does not refer to an existing file.' % directive[5:]))
346
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800347 included_owners = self._read_just_the_owners(include_file)
348 for owner in included_owners:
349 self._owners_to_paths.setdefault(owner, set()).add(owned_paths)
350 self._paths_to_owners.setdefault(owned_paths, set()).add(owner)
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000351 elif self.email_regexp.match(directive) or directive == EVERYONE:
Jochen Eisinger72606f82017-04-04 10:44:18 +0200352 if comment:
353 self.comments.setdefault(directive, {})
354 self.comments[directive][owned_paths] = comment
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800355 self._owners_to_paths.setdefault(directive, set()).add(owned_paths)
356 self._paths_to_owners.setdefault(owned_paths, set()).add(directive)
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000357 else:
dpranke@chromium.org86bbf192011-03-09 21:37:06 +0000358 raise SyntaxErrorInOwnersFile(owners_path, lineno,
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800359 ('"%s" is not a "set noparent", file include, "*", '
360 'or an email address.' % (directive,)))
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000361
peter@chromium.org2ce13132015-04-16 16:42:08 +0000362 def _resolve_include(self, path, start):
363 if path.startswith('//'):
364 include_path = path[2:]
365 else:
366 assert start.startswith(self.root)
mbjorgef2d73522016-07-14 13:28:59 -0700367 start = self.os_path.dirname(self.os_path.relpath(start, self.root))
peter@chromium.org2ce13132015-04-16 16:42:08 +0000368 include_path = self.os_path.join(start, path)
369
370 owners_path = self.os_path.join(self.root, include_path)
371 if not self.os_path.exists(owners_path):
372 return None
373
374 return include_path
375
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800376 def _read_just_the_owners(self, include_file):
377 if include_file in self._included_files:
378 return self._included_files[include_file]
379
380 owners = set()
381 self._included_files[include_file] = owners
382 lineno = 0
383 for line in self.fopen(self.os_path.join(self.root, include_file)):
384 lineno += 1
385 line = line.strip()
386 if (line.startswith('#') or line == '' or
387 line.startswith('set noparent') or
388 line.startswith('per-file')):
389 continue
390
391 if self.email_regexp.match(line) or line == EVERYONE:
392 owners.add(line)
393 continue
394 if line.startswith('file:'):
395 sub_include_file = self._resolve_include(line[5:], include_file)
396 sub_owners = self._read_just_the_owners(sub_include_file)
397 owners.update(sub_owners)
398 continue
399
400 raise SyntaxErrorInOwnersFile(include_file, lineno,
401 ('"%s" is not a "set noparent", file include, "*", '
402 'or an email address.' % (line,)))
403 return owners
404
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000405 def _covering_set_of_owners_for(self, files, author):
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000406 dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000407 all_possible_owners = self.all_possible_owners(dirs_remaining, author)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000408 suggested_owners = set()
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200409 if len(all_possible_owners) == 0:
410 return suggested_owners
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000411 while dirs_remaining:
412 owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining)
413 suggested_owners.add(owner)
414 dirs_to_remove = set(el[0] for el in all_possible_owners[owner])
415 dirs_remaining -= dirs_to_remove
416 return suggested_owners
dpranke@chromium.org5e5d37b2012-12-19 21:04:58 +0000417
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000418 def all_possible_owners(self, dirs, author):
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000419 """Returns a list of (potential owner, distance-from-dir) tuples; a
420 distance of 1 is the lowest/closest possible distance (which makes the
421 subsequent math easier)."""
422 all_possible_owners = {}
zork@chromium.org046e1752012-05-07 05:56:12 +0000423 for current_dir in dirs:
zork@chromium.org046e1752012-05-07 05:56:12 +0000424 dirname = current_dir
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000425 distance = 1
426 while True:
dtu944b6052016-07-14 14:48:21 -0700427 for owner in self._owners_for(dirname):
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000428 if author and owner == author:
429 continue
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000430 all_possible_owners.setdefault(owner, [])
431 # If the same person is in multiple OWNERS files above a given
432 # directory, only count the closest one.
433 if not any(current_dir == el[0] for el in all_possible_owners[owner]):
434 all_possible_owners[owner].append((current_dir, distance))
dtu944b6052016-07-14 14:48:21 -0700435 if self._should_stop_looking(dirname):
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000436 break
437 dirname = self.os_path.dirname(dirname)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000438 distance += 1
439 return all_possible_owners
zork@chromium.org046e1752012-05-07 05:56:12 +0000440
nick7e16cf32016-09-16 16:05:05 -0700441 def _fnmatch(self, filename, pattern):
442 """Same as fnmatch.fnmatch(), but interally caches the compiled regexes."""
443 matcher = self._fnmatch_cache.get(pattern)
444 if matcher is None:
445 matcher = re.compile(fnmatch.translate(pattern)).match
446 self._fnmatch_cache[pattern] = matcher
447 return matcher(filename)
448
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000449 @staticmethod
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000450 def total_costs_by_owner(all_possible_owners, dirs):
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000451 # We want to minimize both the number of reviewers and the distance
452 # from the files/dirs needing reviews. The "pow(X, 1.75)" below is
453 # an arbitrarily-selected scaling factor that seems to work well - it
454 # will select one reviewer in the parent directory over three reviewers
455 # in subdirs, but not one reviewer over just two.
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000456 result = {}
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000457 for owner in all_possible_owners:
458 total_distance = 0
459 num_directories_owned = 0
460 for dirname, distance in all_possible_owners[owner]:
461 if dirname in dirs:
462 total_distance += distance
463 num_directories_owned += 1
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000464 if num_directories_owned:
465 result[owner] = (total_distance /
466 pow(num_directories_owned, 1.75))
467 return result
zork@chromium.org046e1752012-05-07 05:56:12 +0000468
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000469 @staticmethod
470 def lowest_cost_owner(all_possible_owners, dirs):
471 total_costs_by_owner = Database.total_costs_by_owner(all_possible_owners,
472 dirs)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000473 # Return the lowest cost owner. In the case of a tie, pick one randomly.
474 lowest_cost = min(total_costs_by_owner.itervalues())
475 lowest_cost_owners = filter(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000476 lambda owner: total_costs_by_owner[owner] == lowest_cost,
477 total_costs_by_owner)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000478 return random.Random().choice(lowest_cost_owners)