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 |
| 17 | | "per-file" \s+ glob "=" directive |
| 18 | | comment |
| 19 | |
| 20 | directive := "set noparent" |
| 21 | | email_address |
| 22 | | "*" |
| 23 | |
| 24 | glob := [a-zA-Z0-9_-*?]+ |
| 25 | |
| 26 | comment := "#" [^"\n"]* |
| 27 | |
| 28 | Email addresses must follow the foo@bar.com short form (exact syntax given |
| 29 | in BASIC_EMAIL_REGEXP, below). Filename globs follow the simple unix |
| 30 | shell conventions, and relative and absolute paths are not allowed (i.e., |
| 31 | globs only refer to the files in the current directory). |
| 32 | |
| 33 | If a user's email is one of the email_addresses in the file, the user is |
| 34 | considered an "OWNER" for all files in the directory. |
| 35 | |
| 36 | If the "per-file" directive is used, the line only applies to files in that |
| 37 | directory that match the filename glob specified. |
| 38 | |
| 39 | If the "set noparent" directive used, then only entries in this OWNERS file |
| 40 | apply to files in this directory; if the "set noparent" directive is not |
| 41 | used, then entries in OWNERS files in enclosing (upper) directories also |
| 42 | apply (up until a "set noparent is encountered"). |
| 43 | |
| 44 | If "per-file glob=set noparent" is used, then global directives are ignored |
| 45 | for the glob, and only the "per-file" owners are used for files matching that |
| 46 | glob. |
| 47 | |
| 48 | Examples for all of these combinations can be found in tests/owners_unittest.py. |
| 49 | """ |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 50 | |
dpranke@chromium.org | fdecfb7 | 2011-03-16 23:27:23 +0000 | [diff] [blame] | 51 | import collections |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 52 | import re |
| 53 | |
| 54 | |
| 55 | # If this is present by itself on a line, this means that everyone can review. |
| 56 | EVERYONE = '*' |
| 57 | |
| 58 | |
| 59 | # Recognizes 'X@Y' email addresses. Very simplistic. |
| 60 | BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$' |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 61 | |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 62 | |
dpranke@chromium.org | 923950f | 2011-03-17 23:40:00 +0000 | [diff] [blame] | 63 | def _assert_is_collection(obj): |
dpranke@chromium.org | e6a4ab3 | 2011-03-31 01:23:08 +0000 | [diff] [blame] | 64 | assert not isinstance(obj, basestring) |
maruel@chromium.org | 725f1c3 | 2011-04-01 20:24:54 +0000 | [diff] [blame] | 65 | # Module 'collections' has no 'Iterable' member |
| 66 | # pylint: disable=E1101 |
dpranke@chromium.org | e6a4ab3 | 2011-03-31 01:23:08 +0000 | [diff] [blame] | 67 | if hasattr(collections, 'Iterable') and hasattr(collections, 'Sized'): |
| 68 | assert (isinstance(obj, collections.Iterable) and |
| 69 | isinstance(obj, collections.Sized)) |
dpranke@chromium.org | 923950f | 2011-03-17 23:40:00 +0000 | [diff] [blame] | 70 | |
| 71 | |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 72 | class SyntaxErrorInOwnersFile(Exception): |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 73 | def __init__(self, path, lineno, msg): |
| 74 | super(SyntaxErrorInOwnersFile, self).__init__((path, lineno, msg)) |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 75 | self.path = path |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 76 | self.lineno = lineno |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 77 | self.msg = msg |
| 78 | |
| 79 | def __str__(self): |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 80 | 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] | 81 | |
| 82 | |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 83 | class Database(object): |
| 84 | """A database of OWNERS files for a repository. |
| 85 | |
| 86 | This class allows you to find a suggested set of reviewers for a list |
| 87 | of changed files, and see if a list of changed files is covered by a |
| 88 | list of reviewers.""" |
| 89 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 90 | def __init__(self, root, fopen, os_path, glob): |
dpranke@chromium.org | 898a10e | 2011-03-04 21:54:43 +0000 | [diff] [blame] | 91 | """Args: |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 92 | root: the path to the root of the Repository |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 93 | open: function callback to open a text file for reading |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 94 | os_path: module/object callback with fields for 'abspath', 'dirname', |
| 95 | 'exists', and 'join' |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 96 | glob: function callback to list entries in a directory match a glob |
| 97 | (i.e., glob.glob) |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 98 | """ |
| 99 | self.root = root |
| 100 | self.fopen = fopen |
| 101 | self.os_path = os_path |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 102 | self.glob = glob |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 103 | |
dpranke@chromium.org | 627ea67 | 2011-03-11 23:29:03 +0000 | [diff] [blame] | 104 | # 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] | 105 | self.email_regexp = re.compile(BASIC_EMAIL_REGEXP) |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 106 | |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 107 | # Mapping of owners to the paths they own. |
| 108 | self.owned_by = {EVERYONE: set()} |
| 109 | |
| 110 | # Mapping of paths to authorized owners. |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 111 | self.owners_for = {} |
| 112 | |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 113 | # Set of paths that stop us from looking above them for owners. |
| 114 | # (This is implicitly true for the root directory). |
| 115 | self.stop_looking = set(['']) |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 116 | |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 117 | def reviewers_for(self, files): |
dpranke@chromium.org | fdecfb7 | 2011-03-16 23:27:23 +0000 | [diff] [blame] | 118 | """Returns a suggested set of reviewers that will cover the files. |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 119 | |
dpranke@chromium.org | fdecfb7 | 2011-03-16 23:27:23 +0000 | [diff] [blame] | 120 | files is a sequence of paths relative to (and under) self.root.""" |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 121 | self._check_paths(files) |
| 122 | self._load_data_needed_for(files) |
| 123 | return self._covering_set_of_owners_for(files) |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 124 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 125 | # TODO(dpranke): rename to objects_not_covered_by |
pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 126 | def directories_not_covered_by(self, files, reviewers): |
| 127 | """Returns the set of directories that are not owned by a reviewer. |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 128 | |
pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 129 | Determines which of the given files are not owned by at least one of the |
| 130 | reviewers, then returns a set containing the applicable enclosing |
| 131 | directories, i.e. the ones upward from the files that have OWNERS files. |
dpranke@chromium.org | fdecfb7 | 2011-03-16 23:27:23 +0000 | [diff] [blame] | 132 | |
| 133 | Args: |
| 134 | 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] | 135 | reviewers is a sequence of strings matching self.email_regexp. |
| 136 | """ |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 137 | self._check_paths(files) |
| 138 | self._check_reviewers(reviewers) |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 139 | self._load_data_needed_for(files) |
pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 140 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 141 | objs = set() |
| 142 | for f in files: |
| 143 | if f in self.owners_for: |
| 144 | objs.add(f) |
| 145 | else: |
| 146 | objs.add(self.os_path.dirname(f)) |
pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 147 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 148 | covered_objs = self._objs_covered_by(reviewers) |
| 149 | uncovered_objs = [self._enclosing_obj_with_owners(o) for o in objs |
| 150 | if not self._is_obj_covered_by(o, covered_objs)] |
| 151 | |
| 152 | return set(uncovered_objs) |
| 153 | |
| 154 | objects_not_covered_by = directories_not_covered_by |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 155 | |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 156 | def _check_paths(self, files): |
| 157 | def _is_under(f, pfx): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 158 | 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] | 159 | _assert_is_collection(files) |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 160 | assert all(_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] | 161 | |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 162 | def _check_reviewers(self, reviewers): |
dpranke@chromium.org | 923950f | 2011-03-17 23:40:00 +0000 | [diff] [blame] | 163 | _assert_is_collection(reviewers) |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 164 | assert all(self.email_regexp.match(r) for r in reviewers) |
| 165 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 166 | # TODO(dpranke): Rename to _objs_covered_by and update_callers |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 167 | def _dirs_covered_by(self, reviewers): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 168 | dirs = self.owned_by[EVERYONE] |
| 169 | for r in reviewers: |
| 170 | dirs = dirs | self.owned_by.get(r, set()) |
| 171 | return dirs |
| 172 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 173 | _objs_covered_by = _dirs_covered_by |
| 174 | |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 175 | def _stop_looking(self, dirname): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 176 | return dirname in self.stop_looking |
| 177 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 178 | # TODO(dpranke): Rename to _is_dir_covered_by and update callers. |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 179 | def _is_dir_covered_by(self, dirname, covered_dirs): |
| 180 | while not dirname in covered_dirs and not self._stop_looking(dirname): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 181 | dirname = self.os_path.dirname(dirname) |
| 182 | return dirname in covered_dirs |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 183 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 184 | _is_obj_covered_by = _is_dir_covered_by |
| 185 | |
| 186 | # TODO(dpranke): Rename to _enclosing_obj_with_owners and update callers. |
pam@chromium.org | f46aed9 | 2012-03-08 09:18:17 +0000 | [diff] [blame] | 187 | def _enclosing_dir_with_owners(self, directory): |
| 188 | """Returns the innermost enclosing directory that has an OWNERS file.""" |
| 189 | dirpath = directory |
| 190 | while not dirpath in self.owners_for: |
| 191 | if self._stop_looking(dirpath): |
| 192 | break |
| 193 | dirpath = self.os_path.dirname(dirpath) |
| 194 | return dirpath |
| 195 | |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 196 | _enclosing_obj_with_owners = _enclosing_dir_with_owners |
| 197 | |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 198 | def _load_data_needed_for(self, files): |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 199 | for f in files: |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 200 | dirpath = self.os_path.dirname(f) |
| 201 | while not dirpath in self.owners_for: |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 202 | self._read_owners_in_dir(dirpath) |
| 203 | if self._stop_looking(dirpath): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 204 | break |
| 205 | dirpath = self.os_path.dirname(dirpath) |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 206 | |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 207 | def _read_owners_in_dir(self, dirpath): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 208 | owners_path = self.os_path.join(self.root, dirpath, 'OWNERS') |
| 209 | if not self.os_path.exists(owners_path): |
| 210 | return |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 211 | |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 212 | lineno = 0 |
| 213 | for line in self.fopen(owners_path): |
| 214 | lineno += 1 |
| 215 | line = line.strip() |
bauerb@chromium.org | 20d1943 | 2011-06-08 16:34:18 +0000 | [diff] [blame] | 216 | if line.startswith('#') or line == '': |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 217 | continue |
| 218 | if line == 'set noparent': |
| 219 | self.stop_looking.add(dirpath) |
| 220 | continue |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 221 | |
| 222 | m = re.match("per-file (.+)=(.+)", line) |
| 223 | if m: |
| 224 | glob_string = m.group(1) |
| 225 | directive = m.group(2) |
| 226 | 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^] | 227 | if '/' in glob_string or '\\' in glob_string: |
dpranke@chromium.org | e3b1c3d | 2012-10-20 22:28:14 +0000 | [diff] [blame] | 228 | raise SyntaxErrorInOwnersFile(owners_path, lineno, |
dpranke@chromium.org | 9e227d5 | 2012-10-20 23:47:42 +0000 | [diff] [blame^] | 229 | 'per-file globs cannot span directories or use escapes: "%s"' % |
| 230 | line) |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 231 | baselines = self.glob(full_glob_string) |
dpranke@chromium.org | e3b1c3d | 2012-10-20 22:28:14 +0000 | [diff] [blame] | 232 | for baseline in (self.os_path.relpath(b, self.root) for b in baselines): |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 233 | self._add_entry(baseline, directive, "per-file line", |
| 234 | owners_path, lineno) |
| 235 | continue |
| 236 | |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 237 | if line.startswith('set '): |
| 238 | raise SyntaxErrorInOwnersFile(owners_path, lineno, |
| 239 | 'unknown option: "%s"' % line[4:].strip()) |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 240 | |
| 241 | self._add_entry(dirpath, line, "line", owners_path, lineno) |
| 242 | |
| 243 | def _add_entry(self, path, directive, line_type, owners_path, lineno): |
| 244 | if directive == "set noparent": |
| 245 | self.stop_looking.add(path) |
| 246 | elif self.email_regexp.match(directive) or directive == EVERYONE: |
| 247 | self.owned_by.setdefault(directive, set()).add(path) |
| 248 | self.owners_for.setdefault(path, set()).add(directive) |
| 249 | else: |
dpranke@chromium.org | 86bbf19 | 2011-03-09 21:37:06 +0000 | [diff] [blame] | 250 | raise SyntaxErrorInOwnersFile(owners_path, lineno, |
dpranke@chromium.org | 17cc244 | 2012-10-17 21:12:09 +0000 | [diff] [blame] | 251 | ('%s is not a "set" directive, "*", ' |
| 252 | 'or an email address: "%s"' % (line_type, directive))) |
| 253 | |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 254 | |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 255 | def _covering_set_of_owners_for(self, files): |
zork@chromium.org | 046e175 | 2012-05-07 05:56:12 +0000 | [diff] [blame] | 256 | # Get the set of directories from the files. |
| 257 | dirs = set() |
dpranke@chromium.org | 2a00962 | 2011-03-01 02:43:31 +0000 | [diff] [blame] | 258 | for f in files: |
zork@chromium.org | 046e175 | 2012-05-07 05:56:12 +0000 | [diff] [blame] | 259 | dirs.add(self.os_path.dirname(f)) |
| 260 | |
| 261 | owned_dirs = {} |
| 262 | dir_owners = {} |
| 263 | |
| 264 | for current_dir in dirs: |
| 265 | # Get the list of owners for each directory. |
| 266 | current_owners = set() |
| 267 | dirname = current_dir |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 268 | while dirname in self.owners_for: |
zork@chromium.org | 04704f7 | 2012-05-15 01:15:30 +0000 | [diff] [blame] | 269 | current_owners |= self.owners_for[dirname] |
dpranke@chromium.org | 7eea259 | 2011-03-09 21:35:46 +0000 | [diff] [blame] | 270 | if self._stop_looking(dirname): |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 271 | break |
zork@chromium.org | 04704f7 | 2012-05-15 01:15:30 +0000 | [diff] [blame] | 272 | prev_parent = dirname |
dpranke@chromium.org | 6dada4e | 2011-03-08 22:32:40 +0000 | [diff] [blame] | 273 | dirname = self.os_path.dirname(dirname) |
zork@chromium.org | 04704f7 | 2012-05-15 01:15:30 +0000 | [diff] [blame] | 274 | if prev_parent == dirname: |
| 275 | break |
zork@chromium.org | 046e175 | 2012-05-07 05:56:12 +0000 | [diff] [blame] | 276 | |
| 277 | # Map each directory to a list of its owners. |
| 278 | dir_owners[current_dir] = current_owners |
| 279 | |
| 280 | # Add the directory to the list of each owner. |
| 281 | for owner in current_owners: |
zork@chromium.org | 04704f7 | 2012-05-15 01:15:30 +0000 | [diff] [blame] | 282 | owned_dirs.setdefault(owner, set()).add(current_dir) |
zork@chromium.org | 046e175 | 2012-05-07 05:56:12 +0000 | [diff] [blame] | 283 | |
| 284 | final_owners = set() |
| 285 | while dirs: |
| 286 | # Find the owner that has the most directories. |
| 287 | max_count = 0 |
| 288 | max_owner = None |
| 289 | owner_count = {} |
| 290 | for dirname in dirs: |
| 291 | for owner in dir_owners[dirname]: |
| 292 | count = owner_count.get(owner, 0) + 1 |
| 293 | owner_count[owner] = count |
| 294 | if count >= max_count: |
| 295 | max_owner = owner |
zork@chromium.org | 04704f7 | 2012-05-15 01:15:30 +0000 | [diff] [blame] | 296 | max_count = count |
zork@chromium.org | 046e175 | 2012-05-07 05:56:12 +0000 | [diff] [blame] | 297 | |
| 298 | # If no more directories have OWNERS, we're done. |
| 299 | if not max_owner: |
| 300 | break |
| 301 | |
| 302 | final_owners.add(max_owner) |
| 303 | |
| 304 | # Remove all directories owned by the current owner from the remaining |
| 305 | # list. |
| 306 | for dirname in owned_dirs[max_owner]: |
zork@chromium.org | 04704f7 | 2012-05-15 01:15:30 +0000 | [diff] [blame] | 307 | dirs.discard(dirname) |
zork@chromium.org | 046e175 | 2012-05-07 05:56:12 +0000 | [diff] [blame] | 308 | |
| 309 | return final_owners |