pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 1 | # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 5 | """A database of OWNERS files. |
| 6 | |
| 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). |
| 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. |
| 11 | |
| 12 | The syntax of the OWNERS file is, roughly: |
| 13 | |
| 14 | lines := (\s* line? \s* "\n")* |
| 15 | |
| 16 | line := directive |
dpranke@chromium.org | d16e48b | 2012-12-03 21:53:49 +0000 | [diff] [blame] | 17 | | "per-file" \s+ glob \s* "=" \s* directive |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 18 | | comment |
| 19 | |
| 20 | directive := "set noparent" |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 21 | | "file:" glob |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 22 | | email_address |
| 23 | | "*" |
| 24 | |
| 25 | glob := [a-zA-Z0-9_-*?]+ |
| 26 | |
| 27 | comment := "#" [^"\n"]* |
| 28 | |
| 29 | Email addresses must follow the foo@bar.com short form (exact syntax given |
| 30 | in BASIC_EMAIL_REGEXP, below). Filename globs follow the simple unix |
| 31 | shell conventions, and relative and absolute paths are not allowed (i.e., |
| 32 | globs only refer to the files in the current directory). |
| 33 | |
| 34 | If a user's email is one of the email_addresses in the file, the user is |
| 35 | considered an "OWNER" for all files in the directory. |
| 36 | |
| 37 | If the "per-file" directive is used, the line only applies to files in that |
| 38 | directory that match the filename glob specified. |
| 39 | |
| 40 | If the "set noparent" directive used, then only entries in this OWNERS file |
| 41 | apply to files in this directory; if the "set noparent" directive is not |
| 42 | used, then entries in OWNERS files in enclosing (upper) directories also |
| 43 | apply (up until a "set noparent is encountered"). |
| 44 | |
| 45 | If "per-file glob=set noparent" is used, then global directives are ignored |
| 46 | for the glob, and only the "per-file" owners are used for files matching that |
| 47 | glob. |
| 48 | |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 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 relative to the root of the repository, otherwise it is relative to |
| 52 | the current file |
| 53 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 54 | Examples for all of these combinations can be found in tests/owners_unittest.py. |
| 55 | """ |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 56 | |
dpranke@chromium.org | fdecfb7 | 2011-03-16 23:27:23 +0000 | [diff] [blame] | 57 | import collections |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 58 | import fnmatch |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 59 | import random |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 60 | import re |
| 61 | |
| 62 | |
| 63 | # If this is present by itself on a line, this means that everyone can review. |
| 64 | EVERYONE = '*' |
| 65 | |
| 66 | |
| 67 | # Recognizes 'X@Y' email addresses. Very simplistic. |
| 68 | BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$' |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 69 | |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 70 | |
dpranke@chromium.org | 923950f | 2011-03-17 23:40:00 +0000 | [diff] [blame] | 71 | def _assert_is_collection(obj): |
dpranke@chromium.org | e6a4ab3 | 2011-03-31 01:23:08 +0000 | [diff] [blame] | 72 | assert not isinstance(obj, basestring) |
maruel@chromium.org | 725f1c3 | 2011-04-01 20:24:54 +0000 | [diff] [blame] | 73 | # Module 'collections' has no 'Iterable' member |
| 74 | # pylint: disable=E1101 |
dpranke@chromium.org | e6a4ab3 | 2011-03-31 01:23:08 +0000 | [diff] [blame] | 75 | if hasattr(collections, 'Iterable') and hasattr(collections, 'Sized'): |
| 76 | assert (isinstance(obj, collections.Iterable) and |
| 77 | isinstance(obj, collections.Sized)) |
dpranke@chromium.org | 923950f | 2011-03-17 23:40:00 +0000 | [diff] [blame] | 78 | |
| 79 | |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 80 | class SyntaxErrorInOwnersFile(Exception): |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 81 | def __init__(self, path, lineno, msg): |
| 82 | super(SyntaxErrorInOwnersFile, self).__init__((path, lineno, msg)) |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 83 | self.path = path |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 84 | self.lineno = lineno |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 85 | self.msg = msg |
| 86 | |
| 87 | def __str__(self): |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 88 | return '%s:%d syntax error: %s' % (self.path, self.lineno, self.msg) |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 89 | |
| 90 | |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 91 | class 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 | |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 98 | def __init__(self, root, fopen, os_path): |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 99 | """Args: |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 100 | root: the path to the root of the Repository |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 101 | open: function callback to open a text file for reading |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 102 | os_path: module/object callback with fields for 'abspath', 'dirname', |
mbjorge | f2d7352 | 2016-07-14 13:28:59 -0700 | [diff] [blame] | 103 | 'exists', 'join', and 'relpath' |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 104 | """ |
| 105 | self.root = root |
| 106 | self.fopen = fopen |
| 107 | self.os_path = os_path |
| 108 | |
dpranke@chromium.org | 627ea67 | 2011-03-11 23:29:03 +0000 | [diff] [blame] | 109 | # Pick a default email regexp to use; callers can override as desired. |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 110 | self.email_regexp = re.compile(BASIC_EMAIL_REGEXP) |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 111 | |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 112 | # Mapping of owners to the paths or globs they own. |
| 113 | self._owners_to_paths = {EVERYONE: set()} |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 114 | |
| 115 | # Mapping of paths to authorized owners. |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 116 | self._paths_to_owners = {} |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 117 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 118 | # Mapping reviewers to the preceding comment per file in the OWNERS files. |
| 119 | self.comments = {} |
| 120 | |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 121 | # Set of paths that stop us from looking above them for owners. |
| 122 | # (This is implicitly true for the root directory). |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 123 | self._stop_looking = set(['']) |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 124 | |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 125 | # Set of files which have already been read. |
| 126 | self.read_files = set() |
| 127 | |
dpranke@chromium.org | dbf8b4e | 2013-02-28 19:24:16 +0000 | [diff] [blame] | 128 | def reviewers_for(self, files, author): |
dpranke@chromium.org | fdecfb7 | 2011-03-16 23:27:23 +0000 | [diff] [blame] | 129 | """Returns a suggested set of reviewers that will cover the files. |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 130 | |
dpranke@chromium.org | dbf8b4e | 2013-02-28 19:24:16 +0000 | [diff] [blame] | 131 | files is a sequence of paths relative to (and under) self.root. |
| 132 | If author is nonempty, we ensure it is not included in the set returned |
| 133 | in order avoid suggesting the author as a reviewer for their own changes.""" |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 134 | self._check_paths(files) |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 135 | self.load_data_needed_for(files) |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 136 | |
dpranke@chromium.org | dbf8b4e | 2013-02-28 19:24:16 +0000 | [diff] [blame] | 137 | suggested_owners = self._covering_set_of_owners_for(files, author) |
dpranke@chromium.org | 9d66f48 | 2013-01-18 02:57:11 +0000 | [diff] [blame] | 138 | if EVERYONE in suggested_owners: |
| 139 | if len(suggested_owners) > 1: |
| 140 | suggested_owners.remove(EVERYONE) |
| 141 | else: |
| 142 | suggested_owners = set(['<anyone>']) |
| 143 | return suggested_owners |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 144 | |
dpranke@chromium.org | 6b1e3ee | 2013-02-23 00:06:38 +0000 | [diff] [blame] | 145 | def files_not_covered_by(self, files, reviewers): |
| 146 | """Returns the files not owned by one of the reviewers. |
dpranke@chromium.org | fdecfb7 | 2011-03-16 23:27:23 +0000 | [diff] [blame] | 147 | |
| 148 | Args: |
| 149 | files is a sequence of paths relative to (and under) self.root. |
pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 150 | reviewers is a sequence of strings matching self.email_regexp. |
| 151 | """ |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 152 | self._check_paths(files) |
| 153 | self._check_reviewers(reviewers) |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 154 | self.load_data_needed_for(files) |
pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 155 | |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 156 | return set(f for f in files if not self._is_obj_covered_by(f, reviewers)) |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 157 | |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 158 | def _check_paths(self, files): |
| 159 | def _is_under(f, pfx): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 160 | return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx) |
dpranke@chromium.org | 923950f | 2011-03-17 23:40:00 +0000 | [diff] [blame] | 161 | _assert_is_collection(files) |
dpranke@chromium.org | b54a78e | 2012-12-13 23:37:23 +0000 | [diff] [blame] | 162 | assert all(not self.os_path.isabs(f) and |
| 163 | _is_under(f, self.os_path.abspath(self.root)) for f in files) |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 164 | |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 165 | def _check_reviewers(self, reviewers): |
dpranke@chromium.org | 923950f | 2011-03-17 23:40:00 +0000 | [diff] [blame] | 166 | _assert_is_collection(reviewers) |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 167 | assert all(self.email_regexp.match(r) for r in reviewers) |
| 168 | |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 169 | def _is_obj_covered_by(self, objname, reviewers): |
| 170 | reviewers = list(reviewers) + [EVERYONE] |
| 171 | while True: |
| 172 | for reviewer in reviewers: |
| 173 | for owned_pattern in self._owners_to_paths.get(reviewer, set()): |
| 174 | if fnmatch.fnmatch(objname, owned_pattern): |
| 175 | return True |
| 176 | if self._should_stop_looking(objname): |
| 177 | break |
dpranke@chromium.org | 6b1e3ee | 2013-02-23 00:06:38 +0000 | [diff] [blame] | 178 | objname = self.os_path.dirname(objname) |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 179 | return False |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 180 | |
dpranke@chromium.org | 6b1e3ee | 2013-02-23 00:06:38 +0000 | [diff] [blame] | 181 | def _enclosing_dir_with_owners(self, objname): |
pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 182 | """Returns the innermost enclosing directory that has an OWNERS file.""" |
dpranke@chromium.org | 6b1e3ee | 2013-02-23 00:06:38 +0000 | [diff] [blame] | 183 | dirpath = objname |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 184 | while not self._owners_for(dirpath): |
| 185 | if self._should_stop_looking(dirpath): |
pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 186 | break |
| 187 | dirpath = self.os_path.dirname(dirpath) |
| 188 | return dirpath |
| 189 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 190 | def load_data_needed_for(self, files): |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 191 | for f in files: |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 192 | dirpath = self.os_path.dirname(f) |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 193 | while not self._owners_for(dirpath): |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 194 | self._read_owners(self.os_path.join(dirpath, 'OWNERS')) |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 195 | if self._should_stop_looking(dirpath): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 196 | break |
| 197 | dirpath = self.os_path.dirname(dirpath) |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 198 | |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 199 | def _should_stop_looking(self, objname): |
| 200 | return any(fnmatch.fnmatch(objname, stop_looking) |
| 201 | for stop_looking in self._stop_looking) |
| 202 | |
| 203 | def _owners_for(self, objname): |
| 204 | obj_owners = set() |
| 205 | for owned_path, path_owners in self._paths_to_owners.iteritems(): |
| 206 | if fnmatch.fnmatch(objname, owned_path): |
| 207 | obj_owners |= path_owners |
| 208 | return obj_owners |
| 209 | |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 210 | def _read_owners(self, path): |
| 211 | owners_path = self.os_path.join(self.root, path) |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 212 | if not self.os_path.exists(owners_path): |
| 213 | return |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 214 | |
| 215 | if owners_path in self.read_files: |
| 216 | return |
| 217 | |
| 218 | self.read_files.add(owners_path) |
| 219 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 220 | comment = [] |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 221 | dirpath = self.os_path.dirname(path) |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 222 | in_comment = False |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 223 | lineno = 0 |
| 224 | for line in self.fopen(owners_path): |
| 225 | lineno += 1 |
| 226 | line = line.strip() |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 227 | if line.startswith('#'): |
| 228 | if not in_comment: |
| 229 | comment = [] |
| 230 | comment.append(line[1:].strip()) |
| 231 | in_comment = True |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 232 | continue |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 233 | if line == '': |
| 234 | continue |
| 235 | in_comment = False |
| 236 | |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 237 | if line == 'set noparent': |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 238 | self._stop_looking.add(dirpath) |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 239 | continue |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 240 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 241 | m = re.match('per-file (.+)=(.+)', line) |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 242 | if m: |
dpranke@chromium.org | d16e48b | 2012-12-03 21:53:49 +0000 | [diff] [blame] | 243 | glob_string = m.group(1).strip() |
| 244 | directive = m.group(2).strip() |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 245 | full_glob_string = self.os_path.join(self.root, dirpath, glob_string) |
dpranke@chromium.org | 9e227d5 | 2012-10-20 23:47:42 +0000 | [diff] [blame] | 246 | if '/' in glob_string or '\\' in glob_string: |
dpranke@chromium.org | e3b1c3d | 2012-10-20 22:28:14 +0000 | [diff] [blame] | 247 | raise SyntaxErrorInOwnersFile(owners_path, lineno, |
dpranke@chromium.org | 9e227d5 | 2012-10-20 23:47:42 +0000 | [diff] [blame] | 248 | 'per-file globs cannot span directories or use escapes: "%s"' % |
| 249 | line) |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 250 | relative_glob_string = self.os_path.relpath(full_glob_string, self.root) |
| 251 | self._add_entry(relative_glob_string, directive, 'per-file line', |
| 252 | owners_path, lineno, '\n'.join(comment)) |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 253 | continue |
| 254 | |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 255 | if line.startswith('set '): |
| 256 | raise SyntaxErrorInOwnersFile(owners_path, lineno, |
| 257 | 'unknown option: "%s"' % line[4:].strip()) |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 258 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 259 | self._add_entry(dirpath, line, 'line', owners_path, lineno, |
| 260 | ' '.join(comment)) |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 261 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 262 | def _add_entry(self, path, directive, |
| 263 | line_type, owners_path, lineno, comment): |
| 264 | if directive == 'set noparent': |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 265 | self._stop_looking.add(path) |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 266 | elif directive.startswith('file:'): |
| 267 | owners_file = self._resolve_include(directive[5:], owners_path) |
| 268 | if not owners_file: |
| 269 | raise SyntaxErrorInOwnersFile(owners_path, lineno, |
| 270 | ('%s does not refer to an existing file.' % directive[5:])) |
| 271 | |
| 272 | self._read_owners(owners_file) |
| 273 | |
| 274 | dirpath = self.os_path.dirname(owners_file) |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 275 | for key in self._owners_to_paths: |
| 276 | if not dirpath in self._owners_to_paths[key]: |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 277 | continue |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 278 | self._owners_to_paths[key].add(path) |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 279 | |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 280 | if dirpath in self._paths_to_owners: |
| 281 | self._paths_to_owners.setdefault(path, set()).update( |
| 282 | self._paths_to_owners[dirpath]) |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 283 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 284 | elif self.email_regexp.match(directive) or directive == EVERYONE: |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 285 | self.comments.setdefault(directive, {}) |
| 286 | self.comments[directive][path] = comment |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 287 | self._owners_to_paths.setdefault(directive, set()).add(path) |
| 288 | self._paths_to_owners.setdefault(path, set()).add(directive) |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 289 | else: |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 290 | raise SyntaxErrorInOwnersFile(owners_path, lineno, |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 291 | ('%s is not a "set" directive, file include, "*", ' |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 292 | 'or an email address: "%s"' % (line_type, directive))) |
| 293 | |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 294 | def _resolve_include(self, path, start): |
| 295 | if path.startswith('//'): |
| 296 | include_path = path[2:] |
| 297 | else: |
| 298 | assert start.startswith(self.root) |
mbjorge | f2d7352 | 2016-07-14 13:28:59 -0700 | [diff] [blame] | 299 | start = self.os_path.dirname(self.os_path.relpath(start, self.root)) |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 300 | include_path = self.os_path.join(start, path) |
| 301 | |
| 302 | owners_path = self.os_path.join(self.root, include_path) |
| 303 | if not self.os_path.exists(owners_path): |
| 304 | return None |
| 305 | |
| 306 | return include_path |
| 307 | |
dpranke@chromium.org | dbf8b4e | 2013-02-28 19:24:16 +0000 | [diff] [blame] | 308 | def _covering_set_of_owners_for(self, files, author): |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 309 | dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files) |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 310 | all_possible_owners = self.all_possible_owners(dirs_remaining, author) |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 311 | suggested_owners = set() |
| 312 | while dirs_remaining: |
| 313 | owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining) |
| 314 | suggested_owners.add(owner) |
| 315 | dirs_to_remove = set(el[0] for el in all_possible_owners[owner]) |
| 316 | dirs_remaining -= dirs_to_remove |
| 317 | return suggested_owners |
dpranke@chromium.org | 5e5d37b | 2012-12-19 21:04:58 +0000 | [diff] [blame] | 318 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 319 | def all_possible_owners(self, dirs, author): |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 320 | """Returns a list of (potential owner, distance-from-dir) tuples; a |
| 321 | distance of 1 is the lowest/closest possible distance (which makes the |
| 322 | subsequent math easier).""" |
| 323 | all_possible_owners = {} |
zork@chromium.org | 046e175 | 2012-05-07 05:56:12 +0000 | [diff] [blame] | 324 | for current_dir in dirs: |
zork@chromium.org | 046e175 | 2012-05-07 05:56:12 +0000 | [diff] [blame] | 325 | dirname = current_dir |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 326 | distance = 1 |
| 327 | while True: |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 328 | for owner in self._owners_for(dirname): |
dpranke@chromium.org | dbf8b4e | 2013-02-28 19:24:16 +0000 | [diff] [blame] | 329 | if author and owner == author: |
| 330 | continue |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 331 | all_possible_owners.setdefault(owner, []) |
| 332 | # If the same person is in multiple OWNERS files above a given |
| 333 | # directory, only count the closest one. |
| 334 | if not any(current_dir == el[0] for el in all_possible_owners[owner]): |
| 335 | all_possible_owners[owner].append((current_dir, distance)) |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 336 | if self._should_stop_looking(dirname): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 337 | break |
| 338 | dirname = self.os_path.dirname(dirname) |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 339 | distance += 1 |
| 340 | return all_possible_owners |
zork@chromium.org | 046e175 | 2012-05-07 05:56:12 +0000 | [diff] [blame] | 341 | |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 342 | @staticmethod |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 343 | def total_costs_by_owner(all_possible_owners, dirs): |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 344 | # We want to minimize both the number of reviewers and the distance |
| 345 | # from the files/dirs needing reviews. The "pow(X, 1.75)" below is |
| 346 | # an arbitrarily-selected scaling factor that seems to work well - it |
| 347 | # will select one reviewer in the parent directory over three reviewers |
| 348 | # in subdirs, but not one reviewer over just two. |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 349 | result = {} |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 350 | for owner in all_possible_owners: |
| 351 | total_distance = 0 |
| 352 | num_directories_owned = 0 |
| 353 | for dirname, distance in all_possible_owners[owner]: |
| 354 | if dirname in dirs: |
| 355 | total_distance += distance |
| 356 | num_directories_owned += 1 |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 357 | if num_directories_owned: |
| 358 | result[owner] = (total_distance / |
| 359 | pow(num_directories_owned, 1.75)) |
| 360 | return result |
zork@chromium.org | 046e175 | 2012-05-07 05:56:12 +0000 | [diff] [blame] | 361 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 362 | @staticmethod |
| 363 | def lowest_cost_owner(all_possible_owners, dirs): |
| 364 | total_costs_by_owner = Database.total_costs_by_owner(all_possible_owners, |
| 365 | dirs) |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 366 | # Return the lowest cost owner. In the case of a tie, pick one randomly. |
| 367 | lowest_cost = min(total_costs_by_owner.itervalues()) |
| 368 | lowest_cost_owners = filter( |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 369 | lambda owner: total_costs_by_owner[owner] == lowest_cost, |
| 370 | total_costs_by_owner) |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 371 | return random.Random().choice(lowest_cost_owners) |