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 | |
Bruce Dawson | 37740e2 | 2019-11-14 00:27:44 +0000 | [diff] [blame^] | 5 | r"""A database of OWNERS files. |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 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 | |
Daniel Cheng | 74fda71 | 2018-09-05 03:56:39 +0000 | [diff] [blame] | 14 | lines := (\s* line? \s* comment? \s* "\n")* |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 15 | |
Daniel Cheng | 74fda71 | 2018-09-05 03:56:39 +0000 | [diff] [blame] | 16 | line := directive |
| 17 | | "per-file" \s+ glob \s* "=" \s* directive |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 18 | |
Daniel Cheng | 74fda71 | 2018-09-05 03:56:39 +0000 | [diff] [blame] | 19 | directive := "set noparent" |
| 20 | | "file:" owner_file |
| 21 | | email_address |
| 22 | | "*" |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 23 | |
Daniel Cheng | 74fda71 | 2018-09-05 03:56:39 +0000 | [diff] [blame] | 24 | glob := [a-zA-Z0-9_-*?]+ |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 25 | |
Daniel Cheng | 74fda71 | 2018-09-05 03:56:39 +0000 | [diff] [blame] | 26 | comment := "#" [^"\n"]* |
| 27 | |
| 28 | owner_file := "OWNERS" |
| 29 | | [^"\n"]* "_OWNERS" |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 30 | |
| 31 | Email addresses must follow the foo@bar.com short form (exact syntax given |
| 32 | in BASIC_EMAIL_REGEXP, below). Filename globs follow the simple unix |
| 33 | shell conventions, and relative and absolute paths are not allowed (i.e., |
| 34 | globs only refer to the files in the current directory). |
| 35 | |
| 36 | If a user's email is one of the email_addresses in the file, the user is |
| 37 | considered an "OWNER" for all files in the directory. |
| 38 | |
| 39 | If the "per-file" directive is used, the line only applies to files in that |
| 40 | directory that match the filename glob specified. |
| 41 | |
| 42 | If the "set noparent" directive used, then only entries in this OWNERS file |
| 43 | apply to files in this directory; if the "set noparent" directive is not |
| 44 | used, then entries in OWNERS files in enclosing (upper) directories also |
| 45 | apply (up until a "set noparent is encountered"). |
| 46 | |
| 47 | If "per-file glob=set noparent" is used, then global directives are ignored |
| 48 | for the glob, and only the "per-file" owners are used for files matching that |
| 49 | glob. |
| 50 | |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 51 | If the "file:" directive is used, the referred to OWNERS file will be parsed and |
| 52 | considered when determining the valid set of OWNERS. If the filename starts with |
| 53 | "//" it is relative to the root of the repository, otherwise it is relative to |
Daniel Cheng | 74fda71 | 2018-09-05 03:56:39 +0000 | [diff] [blame] | 54 | the current file. The referred to file *must* be named OWNERS or end in a suffix |
| 55 | of _OWNERS. |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 56 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 57 | Examples for all of these combinations can be found in tests/owners_unittest.py. |
| 58 | """ |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 59 | |
dpranke@chromium.org | fdecfb7 | 2011-03-16 23:27:23 +0000 | [diff] [blame] | 60 | import collections |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 61 | import fnmatch |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 62 | import random |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 63 | import re |
| 64 | |
Bruce Dawson | 37740e2 | 2019-11-14 00:27:44 +0000 | [diff] [blame^] | 65 | try: |
| 66 | # This fallback applies for all versions of Python before 3.3 |
| 67 | import collections.abc as collections_abc |
| 68 | except ImportError: |
| 69 | import collections as collections_abc |
| 70 | |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 71 | |
| 72 | # If this is present by itself on a line, this means that everyone can review. |
| 73 | EVERYONE = '*' |
| 74 | |
| 75 | |
| 76 | # Recognizes 'X@Y' email addresses. Very simplistic. |
| 77 | BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$' |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 78 | |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 79 | |
Jochen Eisinger | 72606f8 | 2017-04-04 10:44:18 +0200 | [diff] [blame] | 80 | # Key for global comments per email address. Should be unlikely to be a |
| 81 | # pathname. |
| 82 | GLOBAL_STATUS = '*' |
| 83 | |
| 84 | |
dpranke@chromium.org | 923950f | 2011-03-17 23:40:00 +0000 | [diff] [blame] | 85 | def _assert_is_collection(obj): |
Edward Lemur | 14705d8 | 2019-10-30 22:17:10 +0000 | [diff] [blame] | 86 | assert not isinstance(obj, str) |
maruel@chromium.org | 725f1c3 | 2011-04-01 20:24:54 +0000 | [diff] [blame] | 87 | # Module 'collections' has no 'Iterable' member |
Quinten Yearsley | b2cc4a9 | 2016-12-15 13:53:26 -0800 | [diff] [blame] | 88 | # pylint: disable=no-member |
Bruce Dawson | 37740e2 | 2019-11-14 00:27:44 +0000 | [diff] [blame^] | 89 | if hasattr(collections_abc, 'Iterable') and hasattr(collections_abc, 'Sized'): |
| 90 | assert (isinstance(obj, collections_abc.Iterable) and |
| 91 | isinstance(obj, collections_abc.Sized)) |
dpranke@chromium.org | 923950f | 2011-03-17 23:40:00 +0000 | [diff] [blame] | 92 | |
| 93 | |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 94 | class SyntaxErrorInOwnersFile(Exception): |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 95 | def __init__(self, path, lineno, msg): |
| 96 | super(SyntaxErrorInOwnersFile, self).__init__((path, lineno, msg)) |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 97 | self.path = path |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 98 | self.lineno = lineno |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 99 | self.msg = msg |
| 100 | |
| 101 | def __str__(self): |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 102 | 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] | 103 | |
| 104 | |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 105 | class Database(object): |
| 106 | """A database of OWNERS files for a repository. |
| 107 | |
| 108 | This class allows you to find a suggested set of reviewers for a list |
| 109 | of changed files, and see if a list of changed files is covered by a |
| 110 | list of reviewers.""" |
| 111 | |
Jochen Eisinger | eb74476 | 2017-04-05 11:00:05 +0200 | [diff] [blame] | 112 | def __init__(self, root, fopen, os_path): |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 113 | """Args: |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 114 | root: the path to the root of the Repository |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 115 | open: function callback to open a text file for reading |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 116 | os_path: module/object callback with fields for 'abspath', 'dirname', |
mbjorge | f2d7352 | 2016-07-14 13:28:59 -0700 | [diff] [blame] | 117 | 'exists', 'join', and 'relpath' |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 118 | """ |
| 119 | self.root = root |
| 120 | self.fopen = fopen |
| 121 | self.os_path = os_path |
| 122 | |
dpranke@chromium.org | 627ea67 | 2011-03-11 23:29:03 +0000 | [diff] [blame] | 123 | # 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] | 124 | self.email_regexp = re.compile(BASIC_EMAIL_REGEXP) |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 125 | |
Jochen Eisinger | d0573ec | 2017-04-13 10:55:06 +0200 | [diff] [blame] | 126 | # Replacement contents for the given files. Maps the file name of an |
| 127 | # OWNERS file (relative to root) to an iterator returning the replacement |
| 128 | # file contents. |
| 129 | self.override_files = {} |
| 130 | |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 131 | # Mapping of owners to the paths or globs they own. |
| 132 | self._owners_to_paths = {EVERYONE: set()} |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 133 | |
Daniel Bratell | d6bf517 | 2019-05-21 07:20:12 +0000 | [diff] [blame] | 134 | # Mappings of directories -> globs in the directory -> owners |
| 135 | # Example: "chrome/browser" -> "chrome/browser/*.h" -> ("john", "maria") |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 136 | self._paths_to_owners = {} |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 137 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 138 | # Mapping reviewers to the preceding comment per file in the OWNERS files. |
| 139 | self.comments = {} |
| 140 | |
nick | 7e16cf3 | 2016-09-16 16:05:05 -0700 | [diff] [blame] | 141 | # Cache of compiled regexes for _fnmatch() |
| 142 | self._fnmatch_cache = {} |
| 143 | |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 144 | # Sets of paths that stop us from looking above them for owners. |
Daniel Bratell | d6bf517 | 2019-05-21 07:20:12 +0000 | [diff] [blame] | 145 | # (This is implicitly true for the root directory). |
| 146 | # |
| 147 | # The implementation is a mapping: |
| 148 | # Directory -> globs in the directory, |
| 149 | # |
| 150 | # Example: |
| 151 | # 'ui/events/devices/mojo' -> 'ui/events/devices/mojo/*_struct_traits*.*' |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 152 | self._stop_looking = {'': set([''])} |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 153 | |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 154 | # Set of files which have already been read. |
| 155 | self.read_files = set() |
| 156 | |
Dirk Pranke | 4dc849f | 2017-02-28 15:31:19 -0800 | [diff] [blame] | 157 | # Set of files which were included from other files. Files are processed |
| 158 | # differently depending on whether they are regular owners files or |
| 159 | # being included from another file. |
| 160 | self._included_files = {} |
| 161 | |
Jochen Eisinger | eb74476 | 2017-04-05 11:00:05 +0200 | [diff] [blame] | 162 | # File with global status lines for owners. |
| 163 | self._status_file = None |
| 164 | |
Daniel Cheng | 24bca4e | 2018-11-01 04:11:41 +0000 | [diff] [blame] | 165 | def _file_affects_ownership(self, path): |
| 166 | """Returns true if the path refers to a file that could affect ownership.""" |
| 167 | filename = self.os_path.split(path)[-1] |
| 168 | return filename == 'OWNERS' or filename.endswith('_OWNERS') |
| 169 | |
| 170 | |
dpranke@chromium.org | dbf8b4e | 2013-02-28 19:24:16 +0000 | [diff] [blame] | 171 | def reviewers_for(self, files, author): |
dpranke@chromium.org | fdecfb7 | 2011-03-16 23:27:23 +0000 | [diff] [blame] | 172 | """Returns a suggested set of reviewers that will cover the files. |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 173 | |
dpranke@chromium.org | dbf8b4e | 2013-02-28 19:24:16 +0000 | [diff] [blame] | 174 | files is a sequence of paths relative to (and under) self.root. |
| 175 | If author is nonempty, we ensure it is not included in the set returned |
| 176 | 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] | 177 | self._check_paths(files) |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 178 | self.load_data_needed_for(files) |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 179 | |
dpranke@chromium.org | dbf8b4e | 2013-02-28 19:24:16 +0000 | [diff] [blame] | 180 | suggested_owners = self._covering_set_of_owners_for(files, author) |
dpranke@chromium.org | 9d66f48 | 2013-01-18 02:57:11 +0000 | [diff] [blame] | 181 | if EVERYONE in suggested_owners: |
| 182 | if len(suggested_owners) > 1: |
| 183 | suggested_owners.remove(EVERYONE) |
| 184 | else: |
| 185 | suggested_owners = set(['<anyone>']) |
| 186 | return suggested_owners |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 187 | |
dpranke@chromium.org | 6b1e3ee | 2013-02-23 00:06:38 +0000 | [diff] [blame] | 188 | def files_not_covered_by(self, files, reviewers): |
| 189 | """Returns the files not owned by one of the reviewers. |
dpranke@chromium.org | fdecfb7 | 2011-03-16 23:27:23 +0000 | [diff] [blame] | 190 | |
| 191 | Args: |
| 192 | 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] | 193 | reviewers is a sequence of strings matching self.email_regexp. |
| 194 | """ |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 195 | self._check_paths(files) |
| 196 | self._check_reviewers(reviewers) |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 197 | self.load_data_needed_for(files) |
pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 198 | |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 199 | 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] | 200 | |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 201 | def _check_paths(self, files): |
| 202 | def _is_under(f, pfx): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 203 | 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] | 204 | _assert_is_collection(files) |
dpranke@chromium.org | b54a78e | 2012-12-13 23:37:23 +0000 | [diff] [blame] | 205 | assert all(not self.os_path.isabs(f) and |
| 206 | _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] | 207 | |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 208 | def _check_reviewers(self, reviewers): |
dpranke@chromium.org | 923950f | 2011-03-17 23:40:00 +0000 | [diff] [blame] | 209 | _assert_is_collection(reviewers) |
Gabriel Charette | 9df9e9f | 2017-06-14 15:44:50 -0400 | [diff] [blame] | 210 | assert all(self.email_regexp.match(r) for r in reviewers), reviewers |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 211 | |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 212 | def _is_obj_covered_by(self, objname, reviewers): |
| 213 | reviewers = list(reviewers) + [EVERYONE] |
| 214 | while True: |
| 215 | for reviewer in reviewers: |
| 216 | for owned_pattern in self._owners_to_paths.get(reviewer, set()): |
| 217 | if fnmatch.fnmatch(objname, owned_pattern): |
| 218 | return True |
| 219 | if self._should_stop_looking(objname): |
| 220 | break |
dpranke@chromium.org | 6b1e3ee | 2013-02-23 00:06:38 +0000 | [diff] [blame] | 221 | objname = self.os_path.dirname(objname) |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 222 | return False |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 223 | |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 224 | def enclosing_dir_with_owners(self, objname): |
pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 225 | """Returns the innermost enclosing directory that has an OWNERS file.""" |
dpranke@chromium.org | 6b1e3ee | 2013-02-23 00:06:38 +0000 | [diff] [blame] | 226 | dirpath = objname |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 227 | while not self._owners_for(dirpath): |
| 228 | if self._should_stop_looking(dirpath): |
pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 229 | break |
| 230 | dirpath = self.os_path.dirname(dirpath) |
| 231 | return dirpath |
| 232 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 233 | def load_data_needed_for(self, files): |
Jochen Eisinger | 72606f8 | 2017-04-04 10:44:18 +0200 | [diff] [blame] | 234 | self._read_global_comments() |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 235 | visited_dirs = set() |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 236 | for f in files: |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 237 | dirpath = self.os_path.dirname(f) |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 238 | while dirpath not in visited_dirs: |
| 239 | visited_dirs.add(dirpath) |
| 240 | |
| 241 | obj_owners = self._owners_for(dirpath) |
| 242 | if obj_owners: |
| 243 | break |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 244 | self._read_owners(self.os_path.join(dirpath, 'OWNERS')) |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 245 | if self._should_stop_looking(dirpath): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 246 | break |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 247 | |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 248 | dirpath = self.os_path.dirname(dirpath) |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 249 | |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 250 | def _should_stop_looking(self, objname): |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 251 | dirname = objname |
| 252 | while True: |
| 253 | if dirname in self._stop_looking: |
| 254 | if any(self._fnmatch(objname, stop_looking) |
| 255 | for stop_looking in self._stop_looking[dirname]): |
| 256 | return True |
| 257 | up_dirname = self.os_path.dirname(dirname) |
| 258 | if up_dirname == dirname: |
| 259 | break |
| 260 | dirname = up_dirname |
| 261 | return False |
| 262 | |
| 263 | def _get_root_affected_dir(self, obj_name): |
| 264 | """Returns the deepest directory/path that is affected by a file pattern |
| 265 | |obj_name|.""" |
| 266 | root_affected_dir = obj_name |
| 267 | while '*' in root_affected_dir: |
| 268 | root_affected_dir = self.os_path.dirname(root_affected_dir) |
| 269 | return root_affected_dir |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 270 | |
| 271 | def _owners_for(self, objname): |
| 272 | obj_owners = set() |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 273 | |
| 274 | # Possibly relevant rules can be found stored at every directory |
| 275 | # level so iterate upwards, looking for them. |
| 276 | dirname = objname |
| 277 | while True: |
| 278 | dir_owner_rules = self._paths_to_owners.get(dirname) |
| 279 | if dir_owner_rules: |
Marc-Antoine Ruel | 8e57b4b | 2019-10-11 01:01:36 +0000 | [diff] [blame] | 280 | for owned_path, path_owners in dir_owner_rules.items(): |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 281 | if self._fnmatch(objname, owned_path): |
| 282 | obj_owners |= path_owners |
| 283 | up_dirname = self.os_path.dirname(dirname) |
| 284 | if up_dirname == dirname: |
| 285 | break |
| 286 | dirname = up_dirname |
| 287 | |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 288 | return obj_owners |
| 289 | |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 290 | def _read_owners(self, path): |
| 291 | owners_path = self.os_path.join(self.root, path) |
Jochen Eisinger | e3991bc | 2017-11-05 13:18:58 -0800 | [diff] [blame] | 292 | if not (self.os_path.exists(owners_path) or (path in self.override_files)): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 293 | return |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 294 | |
| 295 | if owners_path in self.read_files: |
| 296 | return |
| 297 | |
| 298 | self.read_files.add(owners_path) |
| 299 | |
Jochen Eisinger | eb74476 | 2017-04-05 11:00:05 +0200 | [diff] [blame] | 300 | is_toplevel = path == 'OWNERS' |
| 301 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 302 | comment = [] |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 303 | dirpath = self.os_path.dirname(path) |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 304 | in_comment = False |
Jochen Eisinger | b624bfe | 2017-04-19 14:55:34 +0200 | [diff] [blame] | 305 | # We treat the beginning of the file as an blank line. |
| 306 | previous_line_was_blank = True |
| 307 | reset_comment_after_use = False |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 308 | lineno = 0 |
Jochen Eisinger | d0573ec | 2017-04-13 10:55:06 +0200 | [diff] [blame] | 309 | |
| 310 | if path in self.override_files: |
| 311 | file_iter = self.override_files[path] |
| 312 | else: |
| 313 | file_iter = self.fopen(owners_path) |
| 314 | |
| 315 | for line in file_iter: |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 316 | lineno += 1 |
| 317 | line = line.strip() |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 318 | if line.startswith('#'): |
Jochen Eisinger | eb74476 | 2017-04-05 11:00:05 +0200 | [diff] [blame] | 319 | if is_toplevel: |
Bruce Dawson | 9c06201 | 2019-05-02 19:20:28 +0000 | [diff] [blame] | 320 | m = re.match(r'#\s*OWNERS_STATUS\s+=\s+(.+)$', line) |
Jochen Eisinger | eb74476 | 2017-04-05 11:00:05 +0200 | [diff] [blame] | 321 | if m: |
| 322 | self._status_file = m.group(1).strip() |
| 323 | continue |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 324 | if not in_comment: |
| 325 | comment = [] |
Jochen Eisinger | b624bfe | 2017-04-19 14:55:34 +0200 | [diff] [blame] | 326 | reset_comment_after_use = not previous_line_was_blank |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 327 | comment.append(line[1:].strip()) |
| 328 | in_comment = True |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 329 | continue |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 330 | in_comment = False |
| 331 | |
Jochen Eisinger | b624bfe | 2017-04-19 14:55:34 +0200 | [diff] [blame] | 332 | if line == '': |
| 333 | comment = [] |
| 334 | previous_line_was_blank = True |
| 335 | continue |
| 336 | |
Edward Lesmes | 5c62ed5 | 2018-04-19 16:47:15 -0400 | [diff] [blame] | 337 | # If the line ends with a comment, strip the comment and store it for this |
| 338 | # line only. |
| 339 | line, _, line_comment = line.partition('#') |
| 340 | line = line.strip() |
| 341 | line_comment = [line_comment.strip()] if line_comment else [] |
| 342 | |
Jochen Eisinger | b624bfe | 2017-04-19 14:55:34 +0200 | [diff] [blame] | 343 | previous_line_was_blank = False |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 344 | if line == 'set noparent': |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 345 | self._stop_looking.setdefault( |
| 346 | self._get_root_affected_dir(dirpath), set()).add(dirpath) |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 347 | continue |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 348 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 349 | m = re.match('per-file (.+)=(.+)', line) |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 350 | if m: |
dpranke@chromium.org | d16e48b | 2012-12-03 21:53:49 +0000 | [diff] [blame] | 351 | glob_string = m.group(1).strip() |
| 352 | directive = m.group(2).strip() |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 353 | 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] | 354 | if '/' in glob_string or '\\' in glob_string: |
dpranke@chromium.org | e3b1c3d | 2012-10-20 22:28:14 +0000 | [diff] [blame] | 355 | raise SyntaxErrorInOwnersFile(owners_path, lineno, |
dpranke@chromium.org | 9e227d5 | 2012-10-20 23:47:42 +0000 | [diff] [blame] | 356 | 'per-file globs cannot span directories or use escapes: "%s"' % |
| 357 | line) |
dtu | 944b605 | 2016-07-14 14:48:21 -0700 | [diff] [blame] | 358 | relative_glob_string = self.os_path.relpath(full_glob_string, self.root) |
Dirk Pranke | 4dc849f | 2017-02-28 15:31:19 -0800 | [diff] [blame] | 359 | self._add_entry(relative_glob_string, directive, owners_path, |
Edward Lesmes | 5c62ed5 | 2018-04-19 16:47:15 -0400 | [diff] [blame] | 360 | lineno, '\n'.join(comment + line_comment)) |
Jochen Eisinger | b624bfe | 2017-04-19 14:55:34 +0200 | [diff] [blame] | 361 | if reset_comment_after_use: |
| 362 | comment = [] |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 363 | continue |
| 364 | |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 365 | if line.startswith('set '): |
| 366 | raise SyntaxErrorInOwnersFile(owners_path, lineno, |
| 367 | 'unknown option: "%s"' % line[4:].strip()) |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 368 | |
Dirk Pranke | 4dc849f | 2017-02-28 15:31:19 -0800 | [diff] [blame] | 369 | self._add_entry(dirpath, line, owners_path, lineno, |
Edward Lesmes | 5c62ed5 | 2018-04-19 16:47:15 -0400 | [diff] [blame] | 370 | ' '.join(comment + line_comment)) |
Jochen Eisinger | b624bfe | 2017-04-19 14:55:34 +0200 | [diff] [blame] | 371 | if reset_comment_after_use: |
| 372 | comment = [] |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 373 | |
Jochen Eisinger | 72606f8 | 2017-04-04 10:44:18 +0200 | [diff] [blame] | 374 | def _read_global_comments(self): |
Jochen Eisinger | eb74476 | 2017-04-05 11:00:05 +0200 | [diff] [blame] | 375 | if not self._status_file: |
| 376 | if not 'OWNERS' in self.read_files: |
| 377 | self._read_owners('OWNERS') |
| 378 | if not self._status_file: |
| 379 | return |
Jochen Eisinger | 72606f8 | 2017-04-04 10:44:18 +0200 | [diff] [blame] | 380 | |
Jochen Eisinger | eb74476 | 2017-04-05 11:00:05 +0200 | [diff] [blame] | 381 | owners_status_path = self.os_path.join(self.root, self._status_file) |
Jochen Eisinger | 72606f8 | 2017-04-04 10:44:18 +0200 | [diff] [blame] | 382 | if not self.os_path.exists(owners_status_path): |
Jochen Eisinger | eb74476 | 2017-04-05 11:00:05 +0200 | [diff] [blame] | 383 | raise IOError('Could not find global status file "%s"' % |
Jochen Eisinger | 72606f8 | 2017-04-04 10:44:18 +0200 | [diff] [blame] | 384 | owners_status_path) |
| 385 | |
| 386 | if owners_status_path in self.read_files: |
| 387 | return |
| 388 | |
| 389 | self.read_files.add(owners_status_path) |
| 390 | |
| 391 | lineno = 0 |
| 392 | for line in self.fopen(owners_status_path): |
| 393 | lineno += 1 |
| 394 | line = line.strip() |
| 395 | if line.startswith('#'): |
| 396 | continue |
| 397 | if line == '': |
| 398 | continue |
| 399 | |
| 400 | m = re.match('(.+?):(.+)', line) |
| 401 | if m: |
| 402 | owner = m.group(1).strip() |
| 403 | comment = m.group(2).strip() |
| 404 | if not self.email_regexp.match(owner): |
| 405 | raise SyntaxErrorInOwnersFile(owners_status_path, lineno, |
| 406 | 'invalid email address: "%s"' % owner) |
| 407 | |
| 408 | self.comments.setdefault(owner, {}) |
| 409 | self.comments[owner][GLOBAL_STATUS] = comment |
| 410 | continue |
| 411 | |
| 412 | raise SyntaxErrorInOwnersFile(owners_status_path, lineno, |
| 413 | 'cannot parse status entry: "%s"' % line.strip()) |
| 414 | |
Dirk Pranke | 4dc849f | 2017-02-28 15:31:19 -0800 | [diff] [blame] | 415 | def _add_entry(self, owned_paths, directive, owners_path, lineno, comment): |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 416 | if directive == 'set noparent': |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 417 | self._stop_looking.setdefault( |
| 418 | self._get_root_affected_dir(owned_paths), set()).add(owned_paths) |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 419 | elif directive.startswith('file:'): |
Daniel Cheng | 74fda71 | 2018-09-05 03:56:39 +0000 | [diff] [blame] | 420 | include_file = self._resolve_include(directive[5:], owners_path, lineno) |
Dirk Pranke | 4dc849f | 2017-02-28 15:31:19 -0800 | [diff] [blame] | 421 | if not include_file: |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 422 | raise SyntaxErrorInOwnersFile(owners_path, lineno, |
| 423 | ('%s does not refer to an existing file.' % directive[5:])) |
| 424 | |
Dirk Pranke | 4dc849f | 2017-02-28 15:31:19 -0800 | [diff] [blame] | 425 | included_owners = self._read_just_the_owners(include_file) |
| 426 | for owner in included_owners: |
| 427 | self._owners_to_paths.setdefault(owner, set()).add(owned_paths) |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 428 | self._paths_to_owners.setdefault( |
| 429 | self._get_root_affected_dir(owned_paths), {}).setdefault( |
| 430 | owned_paths, set()).add(owner) |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 431 | elif self.email_regexp.match(directive) or directive == EVERYONE: |
Jochen Eisinger | 72606f8 | 2017-04-04 10:44:18 +0200 | [diff] [blame] | 432 | if comment: |
| 433 | self.comments.setdefault(directive, {}) |
| 434 | self.comments[directive][owned_paths] = comment |
Dirk Pranke | 4dc849f | 2017-02-28 15:31:19 -0800 | [diff] [blame] | 435 | self._owners_to_paths.setdefault(directive, set()).add(owned_paths) |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 436 | self._paths_to_owners.setdefault( |
| 437 | self._get_root_affected_dir(owned_paths), {}).setdefault( |
| 438 | owned_paths, set()).add(directive) |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 439 | else: |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 440 | raise SyntaxErrorInOwnersFile(owners_path, lineno, |
Dirk Pranke | 4dc849f | 2017-02-28 15:31:19 -0800 | [diff] [blame] | 441 | ('"%s" is not a "set noparent", file include, "*", ' |
| 442 | 'or an email address.' % (directive,))) |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 443 | |
Daniel Cheng | 74fda71 | 2018-09-05 03:56:39 +0000 | [diff] [blame] | 444 | def _resolve_include(self, path, start, lineno): |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 445 | if path.startswith('//'): |
| 446 | include_path = path[2:] |
| 447 | else: |
| 448 | assert start.startswith(self.root) |
mbjorge | f2d7352 | 2016-07-14 13:28:59 -0700 | [diff] [blame] | 449 | start = self.os_path.dirname(self.os_path.relpath(start, self.root)) |
Michael Achenbach | ff46da8 | 2019-10-21 19:40:10 +0000 | [diff] [blame] | 450 | include_path = self.os_path.normpath(self.os_path.join(start, path)) |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 451 | |
Jochen Eisinger | e3991bc | 2017-11-05 13:18:58 -0800 | [diff] [blame] | 452 | if include_path in self.override_files: |
| 453 | return include_path |
| 454 | |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 455 | owners_path = self.os_path.join(self.root, include_path) |
Daniel Cheng | 74fda71 | 2018-09-05 03:56:39 +0000 | [diff] [blame] | 456 | # Paths included via "file:" must end in OWNERS or _OWNERS. Files that can |
| 457 | # affect ownership have a different set of ownership rules, so that users |
| 458 | # cannot self-approve changes adding themselves to an OWNERS file. |
Daniel Cheng | 24bca4e | 2018-11-01 04:11:41 +0000 | [diff] [blame] | 459 | if not self._file_affects_ownership(owners_path): |
Daniel Cheng | 74fda71 | 2018-09-05 03:56:39 +0000 | [diff] [blame] | 460 | raise SyntaxErrorInOwnersFile(start, lineno, 'file: include must specify ' |
| 461 | 'a file named OWNERS or ending in _OWNERS') |
| 462 | |
peter@chromium.org | 2ce1313 | 2015-04-16 16:42:08 +0000 | [diff] [blame] | 463 | if not self.os_path.exists(owners_path): |
| 464 | return None |
| 465 | |
| 466 | return include_path |
| 467 | |
Dirk Pranke | 4dc849f | 2017-02-28 15:31:19 -0800 | [diff] [blame] | 468 | def _read_just_the_owners(self, include_file): |
| 469 | if include_file in self._included_files: |
| 470 | return self._included_files[include_file] |
| 471 | |
| 472 | owners = set() |
| 473 | self._included_files[include_file] = owners |
| 474 | lineno = 0 |
Jochen Eisinger | e3991bc | 2017-11-05 13:18:58 -0800 | [diff] [blame] | 475 | if include_file in self.override_files: |
| 476 | file_iter = self.override_files[include_file] |
| 477 | else: |
| 478 | file_iter = self.fopen(self.os_path.join(self.root, include_file)) |
| 479 | for line in file_iter: |
Dirk Pranke | 4dc849f | 2017-02-28 15:31:19 -0800 | [diff] [blame] | 480 | lineno += 1 |
| 481 | line = line.strip() |
| 482 | if (line.startswith('#') or line == '' or |
| 483 | line.startswith('set noparent') or |
| 484 | line.startswith('per-file')): |
| 485 | continue |
| 486 | |
John Budorick | 7f75c0e | 2019-08-23 22:51:00 +0000 | [diff] [blame] | 487 | # If the line ends with a comment, strip the comment. |
| 488 | line, _delim, _comment = line.partition('#') |
| 489 | line = line.strip() |
| 490 | |
Dirk Pranke | 4dc849f | 2017-02-28 15:31:19 -0800 | [diff] [blame] | 491 | if self.email_regexp.match(line) or line == EVERYONE: |
| 492 | owners.add(line) |
| 493 | continue |
| 494 | if line.startswith('file:'): |
Daniel Cheng | 74fda71 | 2018-09-05 03:56:39 +0000 | [diff] [blame] | 495 | sub_include_file = self._resolve_include(line[5:], include_file, lineno) |
Dirk Pranke | 4dc849f | 2017-02-28 15:31:19 -0800 | [diff] [blame] | 496 | sub_owners = self._read_just_the_owners(sub_include_file) |
| 497 | owners.update(sub_owners) |
| 498 | continue |
| 499 | |
| 500 | raise SyntaxErrorInOwnersFile(include_file, lineno, |
| 501 | ('"%s" is not a "set noparent", file include, "*", ' |
| 502 | 'or an email address.' % (line,))) |
| 503 | return owners |
| 504 | |
dpranke@chromium.org | dbf8b4e | 2013-02-28 19:24:16 +0000 | [diff] [blame] | 505 | def _covering_set_of_owners_for(self, files, author): |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 506 | 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] | 507 | all_possible_owners = self.all_possible_owners(dirs_remaining, author) |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 508 | suggested_owners = set() |
Aaron Gable | 93248c5 | 2017-05-15 11:23:02 -0700 | [diff] [blame] | 509 | while dirs_remaining and all_possible_owners: |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 510 | owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining) |
| 511 | suggested_owners.add(owner) |
| 512 | dirs_to_remove = set(el[0] for el in all_possible_owners[owner]) |
| 513 | dirs_remaining -= dirs_to_remove |
Aaron Gable | 93248c5 | 2017-05-15 11:23:02 -0700 | [diff] [blame] | 514 | # Now that we've used `owner` and covered all their dirs, remove them |
| 515 | # from consideration. |
| 516 | del all_possible_owners[owner] |
Edward Lemur | 14705d8 | 2019-10-30 22:17:10 +0000 | [diff] [blame] | 517 | for o, dirs in list(all_possible_owners.items()): |
Aaron Gable | 93248c5 | 2017-05-15 11:23:02 -0700 | [diff] [blame] | 518 | new_dirs = [(d, dist) for (d, dist) in dirs if d not in dirs_to_remove] |
| 519 | if not new_dirs: |
| 520 | del all_possible_owners[o] |
| 521 | else: |
| 522 | all_possible_owners[o] = new_dirs |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 523 | return suggested_owners |
dpranke@chromium.org | 5e5d37b | 2012-12-19 21:04:58 +0000 | [diff] [blame] | 524 | |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 525 | def _all_possible_owners_for_dir_or_file(self, dir_or_file, author, |
| 526 | cache): |
| 527 | """Returns a dict of {potential owner: (dir_or_file, distance)} mappings. |
| 528 | """ |
| 529 | assert not dir_or_file.startswith("/") |
| 530 | res = cache.get(dir_or_file) |
| 531 | if res is None: |
| 532 | res = {} |
| 533 | dirname = dir_or_file |
| 534 | for owner in self._owners_for(dirname): |
| 535 | if author and owner == author: |
| 536 | continue |
| 537 | res.setdefault(owner, []) |
| 538 | res[owner] = (dir_or_file, 1) |
| 539 | if not self._should_stop_looking(dirname): |
| 540 | dirname = self.os_path.dirname(dirname) |
| 541 | |
| 542 | parent_res = self._all_possible_owners_for_dir_or_file(dirname, |
| 543 | author, cache) |
| 544 | |
| 545 | # Merge the parent information with our information, adjusting |
| 546 | # distances as necessary, and replacing the parent directory |
| 547 | # names with our names. |
Marc-Antoine Ruel | 8e57b4b | 2019-10-11 01:01:36 +0000 | [diff] [blame] | 548 | for owner, par_dir_and_distances in parent_res.items(): |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 549 | if owner in res: |
| 550 | # If the same person is in multiple OWNERS files above a given |
| 551 | # directory, only count the closest one. |
| 552 | continue |
| 553 | parent_distance = par_dir_and_distances[1] |
| 554 | res[owner] = (dir_or_file, parent_distance + 1) |
| 555 | |
| 556 | cache[dir_or_file] = res |
| 557 | |
| 558 | return res |
| 559 | |
| 560 | def all_possible_owners(self, dirs_and_files, author): |
Aaron Gable | 93248c5 | 2017-05-15 11:23:02 -0700 | [diff] [blame] | 561 | """Returns a dict of {potential owner: (dir, distance)} mappings. |
| 562 | |
| 563 | A distance of 1 is the lowest/closest possible distance (which makes the |
| 564 | subsequent math easier). |
| 565 | """ |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 566 | |
| 567 | all_possible_owners_for_dir_or_file_cache = {} |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 568 | all_possible_owners = {} |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 569 | for current_dir in dirs_and_files: |
| 570 | dir_owners = self._all_possible_owners_for_dir_or_file( |
| 571 | current_dir, author, |
| 572 | all_possible_owners_for_dir_or_file_cache) |
Marc-Antoine Ruel | 8e57b4b | 2019-10-11 01:01:36 +0000 | [diff] [blame] | 573 | for owner, dir_and_distance in dir_owners.items(): |
Daniel Bratell | b2b6699 | 2019-04-25 15:19:33 +0000 | [diff] [blame] | 574 | if owner in all_possible_owners: |
| 575 | all_possible_owners[owner].append(dir_and_distance) |
| 576 | else: |
| 577 | all_possible_owners[owner] = [dir_and_distance] |
| 578 | |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 579 | return all_possible_owners |
zork@chromium.org | 046e175 | 2012-05-07 05:56:12 +0000 | [diff] [blame] | 580 | |
nick | 7e16cf3 | 2016-09-16 16:05:05 -0700 | [diff] [blame] | 581 | def _fnmatch(self, filename, pattern): |
| 582 | """Same as fnmatch.fnmatch(), but interally caches the compiled regexes.""" |
| 583 | matcher = self._fnmatch_cache.get(pattern) |
| 584 | if matcher is None: |
| 585 | matcher = re.compile(fnmatch.translate(pattern)).match |
| 586 | self._fnmatch_cache[pattern] = matcher |
| 587 | return matcher(filename) |
| 588 | |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 589 | @staticmethod |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 590 | def total_costs_by_owner(all_possible_owners, dirs): |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 591 | # We want to minimize both the number of reviewers and the distance |
| 592 | # from the files/dirs needing reviews. The "pow(X, 1.75)" below is |
| 593 | # an arbitrarily-selected scaling factor that seems to work well - it |
| 594 | # will select one reviewer in the parent directory over three reviewers |
| 595 | # in subdirs, but not one reviewer over just two. |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 596 | result = {} |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 597 | for owner in all_possible_owners: |
| 598 | total_distance = 0 |
| 599 | num_directories_owned = 0 |
| 600 | for dirname, distance in all_possible_owners[owner]: |
| 601 | if dirname in dirs: |
| 602 | total_distance += distance |
| 603 | num_directories_owned += 1 |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 604 | if num_directories_owned: |
| 605 | result[owner] = (total_distance / |
| 606 | pow(num_directories_owned, 1.75)) |
| 607 | return result |
zork@chromium.org | 046e175 | 2012-05-07 05:56:12 +0000 | [diff] [blame] | 608 | |
ikarienator@chromium.org | faf3fdf | 2013-09-20 02:11:48 +0000 | [diff] [blame] | 609 | @staticmethod |
| 610 | def lowest_cost_owner(all_possible_owners, dirs): |
| 611 | total_costs_by_owner = Database.total_costs_by_owner(all_possible_owners, |
| 612 | dirs) |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 613 | # Return the lowest cost owner. In the case of a tie, pick one randomly. |
Marc-Antoine Ruel | 8e57b4b | 2019-10-11 01:01:36 +0000 | [diff] [blame] | 614 | lowest_cost = min(total_costs_by_owner.values()) |
Edward Lemur | 14705d8 | 2019-10-30 22:17:10 +0000 | [diff] [blame] | 615 | lowest_cost_owners = [ |
| 616 | owner for owner, cost in total_costs_by_owner.items() |
| 617 | if cost == lowest_cost] |
dpranke@chromium.org | c591a70 | 2012-12-20 20:14:58 +0000 | [diff] [blame] | 618 | return random.Random().choice(lowest_cost_owners) |
Elly Fong-Jones | ee8d9ce | 2019-08-28 20:32:21 +0000 | [diff] [blame] | 619 | |
| 620 | def owners_rooted_at_file(self, filename): |
| 621 | """Returns a set of all owners transitively listed in filename. |
| 622 | |
| 623 | This function returns a set of all the owners either listed in filename, or |
| 624 | in a file transitively included by filename. Lines that are not plain owners |
| 625 | (i.e. per-file owners) are ignored. |
| 626 | """ |
| 627 | return self._read_just_the_owners(filename) |