blob: 068e35b4c79b3d9b92241700695ab687a7102230 [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
dpranke@chromium.org923950f2011-03-17 23:40:00 +000071def _assert_is_collection(obj):
dpranke@chromium.orge6a4ab32011-03-31 01:23:08 +000072 assert not isinstance(obj, basestring)
maruel@chromium.org725f1c32011-04-01 20:24:54 +000073 # Module 'collections' has no 'Iterable' member
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080074 # pylint: disable=no-member
dpranke@chromium.orge6a4ab32011-03-31 01:23:08 +000075 if hasattr(collections, 'Iterable') and hasattr(collections, 'Sized'):
76 assert (isinstance(obj, collections.Iterable) and
77 isinstance(obj, collections.Sized))
dpranke@chromium.org923950f2011-03-17 23:40:00 +000078
79
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000080class SyntaxErrorInOwnersFile(Exception):
dpranke@chromium.org86bbf192011-03-09 21:37:06 +000081 def __init__(self, path, lineno, msg):
82 super(SyntaxErrorInOwnersFile, self).__init__((path, lineno, msg))
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000083 self.path = path
dpranke@chromium.org86bbf192011-03-09 21:37:06 +000084 self.lineno = lineno
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000085 self.msg = msg
86
87 def __str__(self):
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000088 return '%s:%d syntax error: %s' % (self.path, self.lineno, self.msg)
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000089
90
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000091class Database(object):
92 """A database of OWNERS files for a repository.
93
94 This class allows you to find a suggested set of reviewers for a list
95 of changed files, and see if a list of changed files is covered by a
96 list of reviewers."""
97
dtu944b6052016-07-14 14:48:21 -070098 def __init__(self, root, fopen, os_path):
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000099 """Args:
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000100 root: the path to the root of the Repository
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000101 open: function callback to open a text file for reading
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000102 os_path: module/object callback with fields for 'abspath', 'dirname',
mbjorgef2d73522016-07-14 13:28:59 -0700103 'exists', 'join', and 'relpath'
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000104 """
105 self.root = root
106 self.fopen = fopen
107 self.os_path = os_path
108
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000109 # Pick a default email regexp to use; callers can override as desired.
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000110 self.email_regexp = re.compile(BASIC_EMAIL_REGEXP)
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000111
dtu944b6052016-07-14 14:48:21 -0700112 # Mapping of owners to the paths or globs they own.
113 self._owners_to_paths = {EVERYONE: set()}
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000114
115 # Mapping of paths to authorized owners.
dtu944b6052016-07-14 14:48:21 -0700116 self._paths_to_owners = {}
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000117
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000118 # Mapping reviewers to the preceding comment per file in the OWNERS files.
119 self.comments = {}
120
nick7e16cf32016-09-16 16:05:05 -0700121 # Cache of compiled regexes for _fnmatch()
122 self._fnmatch_cache = {}
123
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000124 # Set of paths that stop us from looking above them for owners.
125 # (This is implicitly true for the root directory).
dtu944b6052016-07-14 14:48:21 -0700126 self._stop_looking = set([''])
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000127
peter@chromium.org2ce13132015-04-16 16:42:08 +0000128 # Set of files which have already been read.
129 self.read_files = set()
130
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800131 # Set of files which were included from other files. Files are processed
132 # differently depending on whether they are regular owners files or
133 # being included from another file.
134 self._included_files = {}
135
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000136 def reviewers_for(self, files, author):
dpranke@chromium.orgfdecfb72011-03-16 23:27:23 +0000137 """Returns a suggested set of reviewers that will cover the files.
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000138
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000139 files is a sequence of paths relative to (and under) self.root.
140 If author is nonempty, we ensure it is not included in the set returned
141 in order avoid suggesting the author as a reviewer for their own changes."""
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000142 self._check_paths(files)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000143 self.load_data_needed_for(files)
dtu944b6052016-07-14 14:48:21 -0700144
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000145 suggested_owners = self._covering_set_of_owners_for(files, author)
dpranke@chromium.org9d66f482013-01-18 02:57:11 +0000146 if EVERYONE in suggested_owners:
147 if len(suggested_owners) > 1:
148 suggested_owners.remove(EVERYONE)
149 else:
150 suggested_owners = set(['<anyone>'])
151 return suggested_owners
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000152
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000153 def files_not_covered_by(self, files, reviewers):
154 """Returns the files not owned by one of the reviewers.
dpranke@chromium.orgfdecfb72011-03-16 23:27:23 +0000155
156 Args:
157 files is a sequence of paths relative to (and under) self.root.
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000158 reviewers is a sequence of strings matching self.email_regexp.
159 """
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000160 self._check_paths(files)
161 self._check_reviewers(reviewers)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000162 self.load_data_needed_for(files)
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000163
dtu944b6052016-07-14 14:48:21 -0700164 return set(f for f in files if not self._is_obj_covered_by(f, reviewers))
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000165
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000166 def _check_paths(self, files):
167 def _is_under(f, pfx):
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000168 return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx)
dpranke@chromium.org923950f2011-03-17 23:40:00 +0000169 _assert_is_collection(files)
dpranke@chromium.orgb54a78e2012-12-13 23:37:23 +0000170 assert all(not self.os_path.isabs(f) and
171 _is_under(f, self.os_path.abspath(self.root)) for f in files)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000172
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000173 def _check_reviewers(self, reviewers):
dpranke@chromium.org923950f2011-03-17 23:40:00 +0000174 _assert_is_collection(reviewers)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000175 assert all(self.email_regexp.match(r) for r in reviewers)
176
dtu944b6052016-07-14 14:48:21 -0700177 def _is_obj_covered_by(self, objname, reviewers):
178 reviewers = list(reviewers) + [EVERYONE]
179 while True:
180 for reviewer in reviewers:
181 for owned_pattern in self._owners_to_paths.get(reviewer, set()):
182 if fnmatch.fnmatch(objname, owned_pattern):
183 return True
184 if self._should_stop_looking(objname):
185 break
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000186 objname = self.os_path.dirname(objname)
dtu944b6052016-07-14 14:48:21 -0700187 return False
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000188
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000189 def _enclosing_dir_with_owners(self, objname):
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000190 """Returns the innermost enclosing directory that has an OWNERS file."""
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000191 dirpath = objname
dtu944b6052016-07-14 14:48:21 -0700192 while not self._owners_for(dirpath):
193 if self._should_stop_looking(dirpath):
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000194 break
195 dirpath = self.os_path.dirname(dirpath)
196 return dirpath
197
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000198 def load_data_needed_for(self, files):
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000199 for f in files:
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000200 dirpath = self.os_path.dirname(f)
dtu944b6052016-07-14 14:48:21 -0700201 while not self._owners_for(dirpath):
peter@chromium.org2ce13132015-04-16 16:42:08 +0000202 self._read_owners(self.os_path.join(dirpath, 'OWNERS'))
dtu944b6052016-07-14 14:48:21 -0700203 if self._should_stop_looking(dirpath):
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000204 break
205 dirpath = self.os_path.dirname(dirpath)
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000206
dtu944b6052016-07-14 14:48:21 -0700207 def _should_stop_looking(self, objname):
nick7e16cf32016-09-16 16:05:05 -0700208 return any(self._fnmatch(objname, stop_looking)
dtu944b6052016-07-14 14:48:21 -0700209 for stop_looking in self._stop_looking)
210
211 def _owners_for(self, objname):
212 obj_owners = set()
213 for owned_path, path_owners in self._paths_to_owners.iteritems():
nick7e16cf32016-09-16 16:05:05 -0700214 if self._fnmatch(objname, owned_path):
dtu944b6052016-07-14 14:48:21 -0700215 obj_owners |= path_owners
216 return obj_owners
217
peter@chromium.org2ce13132015-04-16 16:42:08 +0000218 def _read_owners(self, path):
219 owners_path = self.os_path.join(self.root, path)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000220 if not self.os_path.exists(owners_path):
221 return
peter@chromium.org2ce13132015-04-16 16:42:08 +0000222
223 if owners_path in self.read_files:
224 return
225
226 self.read_files.add(owners_path)
227
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000228 comment = []
peter@chromium.org2ce13132015-04-16 16:42:08 +0000229 dirpath = self.os_path.dirname(path)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000230 in_comment = False
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000231 lineno = 0
232 for line in self.fopen(owners_path):
233 lineno += 1
234 line = line.strip()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000235 if line.startswith('#'):
236 if not in_comment:
237 comment = []
238 comment.append(line[1:].strip())
239 in_comment = True
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000240 continue
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000241 if line == '':
242 continue
243 in_comment = False
244
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000245 if line == 'set noparent':
dtu944b6052016-07-14 14:48:21 -0700246 self._stop_looking.add(dirpath)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000247 continue
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000248
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000249 m = re.match('per-file (.+)=(.+)', line)
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000250 if m:
dpranke@chromium.orgd16e48b2012-12-03 21:53:49 +0000251 glob_string = m.group(1).strip()
252 directive = m.group(2).strip()
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000253 full_glob_string = self.os_path.join(self.root, dirpath, glob_string)
dpranke@chromium.org9e227d52012-10-20 23:47:42 +0000254 if '/' in glob_string or '\\' in glob_string:
dpranke@chromium.orge3b1c3d2012-10-20 22:28:14 +0000255 raise SyntaxErrorInOwnersFile(owners_path, lineno,
dpranke@chromium.org9e227d52012-10-20 23:47:42 +0000256 'per-file globs cannot span directories or use escapes: "%s"' %
257 line)
dtu944b6052016-07-14 14:48:21 -0700258 relative_glob_string = self.os_path.relpath(full_glob_string, self.root)
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800259 self._add_entry(relative_glob_string, directive, owners_path,
260 lineno, '\n'.join(comment))
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000261 continue
262
dpranke@chromium.org86bbf192011-03-09 21:37:06 +0000263 if line.startswith('set '):
264 raise SyntaxErrorInOwnersFile(owners_path, lineno,
265 'unknown option: "%s"' % line[4:].strip())
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000266
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800267 self._add_entry(dirpath, line, owners_path, lineno,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000268 ' '.join(comment))
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000269
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800270 def _add_entry(self, owned_paths, directive, owners_path, lineno, comment):
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000271 if directive == 'set noparent':
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800272 self._stop_looking.add(owned_paths)
peter@chromium.org2ce13132015-04-16 16:42:08 +0000273 elif directive.startswith('file:'):
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800274 include_file = self._resolve_include(directive[5:], owners_path)
275 if not include_file:
peter@chromium.org2ce13132015-04-16 16:42:08 +0000276 raise SyntaxErrorInOwnersFile(owners_path, lineno,
277 ('%s does not refer to an existing file.' % directive[5:]))
278
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800279 included_owners = self._read_just_the_owners(include_file)
280 for owner in included_owners:
281 self._owners_to_paths.setdefault(owner, set()).add(owned_paths)
282 self._paths_to_owners.setdefault(owned_paths, set()).add(owner)
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000283 elif self.email_regexp.match(directive) or directive == EVERYONE:
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000284 self.comments.setdefault(directive, {})
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800285 self.comments[directive][owned_paths] = comment
286 self._owners_to_paths.setdefault(directive, set()).add(owned_paths)
287 self._paths_to_owners.setdefault(owned_paths, set()).add(directive)
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000288 else:
dpranke@chromium.org86bbf192011-03-09 21:37:06 +0000289 raise SyntaxErrorInOwnersFile(owners_path, lineno,
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800290 ('"%s" is not a "set noparent", file include, "*", '
291 'or an email address.' % (directive,)))
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000292
peter@chromium.org2ce13132015-04-16 16:42:08 +0000293 def _resolve_include(self, path, start):
294 if path.startswith('//'):
295 include_path = path[2:]
296 else:
297 assert start.startswith(self.root)
mbjorgef2d73522016-07-14 13:28:59 -0700298 start = self.os_path.dirname(self.os_path.relpath(start, self.root))
peter@chromium.org2ce13132015-04-16 16:42:08 +0000299 include_path = self.os_path.join(start, path)
300
301 owners_path = self.os_path.join(self.root, include_path)
302 if not self.os_path.exists(owners_path):
303 return None
304
305 return include_path
306
Dirk Pranke4dc849f2017-02-28 15:31:19 -0800307 def _read_just_the_owners(self, include_file):
308 if include_file in self._included_files:
309 return self._included_files[include_file]
310
311 owners = set()
312 self._included_files[include_file] = owners
313 lineno = 0
314 for line in self.fopen(self.os_path.join(self.root, include_file)):
315 lineno += 1
316 line = line.strip()
317 if (line.startswith('#') or line == '' or
318 line.startswith('set noparent') or
319 line.startswith('per-file')):
320 continue
321
322 if self.email_regexp.match(line) or line == EVERYONE:
323 owners.add(line)
324 continue
325 if line.startswith('file:'):
326 sub_include_file = self._resolve_include(line[5:], include_file)
327 sub_owners = self._read_just_the_owners(sub_include_file)
328 owners.update(sub_owners)
329 continue
330
331 raise SyntaxErrorInOwnersFile(include_file, lineno,
332 ('"%s" is not a "set noparent", file include, "*", '
333 'or an email address.' % (line,)))
334 return owners
335
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000336 def _covering_set_of_owners_for(self, files, author):
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000337 dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000338 all_possible_owners = self.all_possible_owners(dirs_remaining, author)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000339 suggested_owners = set()
340 while dirs_remaining:
341 owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining)
342 suggested_owners.add(owner)
343 dirs_to_remove = set(el[0] for el in all_possible_owners[owner])
344 dirs_remaining -= dirs_to_remove
345 return suggested_owners
dpranke@chromium.org5e5d37b2012-12-19 21:04:58 +0000346
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000347 def all_possible_owners(self, dirs, author):
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000348 """Returns a list of (potential owner, distance-from-dir) tuples; a
349 distance of 1 is the lowest/closest possible distance (which makes the
350 subsequent math easier)."""
351 all_possible_owners = {}
zork@chromium.org046e1752012-05-07 05:56:12 +0000352 for current_dir in dirs:
zork@chromium.org046e1752012-05-07 05:56:12 +0000353 dirname = current_dir
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000354 distance = 1
355 while True:
dtu944b6052016-07-14 14:48:21 -0700356 for owner in self._owners_for(dirname):
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000357 if author and owner == author:
358 continue
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000359 all_possible_owners.setdefault(owner, [])
360 # If the same person is in multiple OWNERS files above a given
361 # directory, only count the closest one.
362 if not any(current_dir == el[0] for el in all_possible_owners[owner]):
363 all_possible_owners[owner].append((current_dir, distance))
dtu944b6052016-07-14 14:48:21 -0700364 if self._should_stop_looking(dirname):
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000365 break
366 dirname = self.os_path.dirname(dirname)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000367 distance += 1
368 return all_possible_owners
zork@chromium.org046e1752012-05-07 05:56:12 +0000369
nick7e16cf32016-09-16 16:05:05 -0700370 def _fnmatch(self, filename, pattern):
371 """Same as fnmatch.fnmatch(), but interally caches the compiled regexes."""
372 matcher = self._fnmatch_cache.get(pattern)
373 if matcher is None:
374 matcher = re.compile(fnmatch.translate(pattern)).match
375 self._fnmatch_cache[pattern] = matcher
376 return matcher(filename)
377
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000378 @staticmethod
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000379 def total_costs_by_owner(all_possible_owners, dirs):
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000380 # We want to minimize both the number of reviewers and the distance
381 # from the files/dirs needing reviews. The "pow(X, 1.75)" below is
382 # an arbitrarily-selected scaling factor that seems to work well - it
383 # will select one reviewer in the parent directory over three reviewers
384 # in subdirs, but not one reviewer over just two.
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000385 result = {}
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000386 for owner in all_possible_owners:
387 total_distance = 0
388 num_directories_owned = 0
389 for dirname, distance in all_possible_owners[owner]:
390 if dirname in dirs:
391 total_distance += distance
392 num_directories_owned += 1
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000393 if num_directories_owned:
394 result[owner] = (total_distance /
395 pow(num_directories_owned, 1.75))
396 return result
zork@chromium.org046e1752012-05-07 05:56:12 +0000397
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000398 @staticmethod
399 def lowest_cost_owner(all_possible_owners, dirs):
400 total_costs_by_owner = Database.total_costs_by_owner(all_possible_owners,
401 dirs)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000402 # Return the lowest cost owner. In the case of a tie, pick one randomly.
403 lowest_cost = min(total_costs_by_owner.itervalues())
404 lowest_cost_owners = filter(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000405 lambda owner: total_costs_by_owner[owner] == lowest_cost,
406 total_costs_by_owner)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000407 return random.Random().choice(lowest_cost_owners)