 Chromium Code Reviews
 Chromium Code Reviews Issue 11114005:
  implement per-file OWNERS support  (Closed) 
  Base URL: https://git.chromium.org/chromium/tools/depot_tools.git@master
    
  
    Issue 11114005:
  implement per-file OWNERS support  (Closed) 
  Base URL: https://git.chromium.org/chromium/tools/depot_tools.git@master| OLD | NEW | 
|---|---|
| 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be | 
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. | 
| 4 | 4 | 
| 5 """A database of OWNERS files.""" | 5 """A database of OWNERS files.""" | 
| 
M-A Ruel
2012/10/12 15:19:04
One thing I'd really like is a formal description
 
Dirk Pranke
2012/10/16 22:42:41
Okay, I'll write up something and see what you thi
 | |
| 6 | 6 | 
| 7 import collections | 7 import collections | 
| 8 import re | 8 import re | 
| 9 | 9 | 
| 10 | 10 | 
| 11 # If this is present by itself on a line, this means that everyone can review. | 11 # If this is present by itself on a line, this means that everyone can review. | 
| 12 EVERYONE = '*' | 12 EVERYONE = '*' | 
| 13 | 13 | 
| 14 | 14 | 
| 15 # Recognizes 'X@Y' email addresses. Very simplistic. | 15 # Recognizes 'X@Y' email addresses. Very simplistic. | 
| (...skipping 13 matching lines...) Expand all Loading... | |
| 29 def __init__(self, path, lineno, msg): | 29 def __init__(self, path, lineno, msg): | 
| 30 super(SyntaxErrorInOwnersFile, self).__init__((path, lineno, msg)) | 30 super(SyntaxErrorInOwnersFile, self).__init__((path, lineno, msg)) | 
| 31 self.path = path | 31 self.path = path | 
| 32 self.lineno = lineno | 32 self.lineno = lineno | 
| 33 self.msg = msg | 33 self.msg = msg | 
| 34 | 34 | 
| 35 def __str__(self): | 35 def __str__(self): | 
| 36 return "%s:%d syntax error: %s" % (self.path, self.lineno, self.msg) | 36 return "%s:%d syntax error: %s" % (self.path, self.lineno, self.msg) | 
| 37 | 37 | 
| 38 | 38 | 
| 39 class Database(object): | 39 class Database(object): | 
| 
M-A Ruel
2012/10/12 15:19:04
I think the code would be easier to read if it was
 
Dirk Pranke
2012/10/16 22:42:41
I'll think about this, but definitely not do it in
 | |
| 40 """A database of OWNERS files for a repository. | 40 """A database of OWNERS files for a repository. | 
| 41 | 41 | 
| 42 This class allows you to find a suggested set of reviewers for a list | 42 This class allows you to find a suggested set of reviewers for a list | 
| 43 of changed files, and see if a list of changed files is covered by a | 43 of changed files, and see if a list of changed files is covered by a | 
| 44 list of reviewers.""" | 44 list of reviewers.""" | 
| 45 | 45 | 
| 46 def __init__(self, root, fopen, os_path): | 46 def __init__(self, root, fopen, os_path, glob): | 
| 47 """Args: | 47 """Args: | 
| 48 root: the path to the root of the Repository | 48 root: the path to the root of the Repository | 
| 49 open: function callback to open a text file for reading | 49 open: function callback to open a text file for reading | 
| 50 os_path: module/object callback with fields for 'abspath', 'dirname', | 50 os_path: module/object callback with fields for 'abspath', 'dirname', | 
| 51 'exists', and 'join' | 51 'exists', and 'join' | 
| 52 glob: function callback to list entries in a directory match a glob (i.e., glob.glob) | |
| 
M-A Ruel
2012/10/12 15:19:04
80 cols
 
Dirk Pranke
2012/10/16 22:42:41
Done.
 | |
| 52 """ | 53 """ | 
| 53 self.root = root | 54 self.root = root | 
| 54 self.fopen = fopen | 55 self.fopen = fopen | 
| 55 self.os_path = os_path | 56 self.os_path = os_path | 
| 57 self.glob = glob | |
| 56 | 58 | 
| 57 # Pick a default email regexp to use; callers can override as desired. | 59 # Pick a default email regexp to use; callers can override as desired. | 
| 58 self.email_regexp = re.compile(BASIC_EMAIL_REGEXP) | 60 self.email_regexp = re.compile(BASIC_EMAIL_REGEXP) | 
| 59 | 61 | 
| 60 # Mapping of owners to the paths they own. | 62 # Mapping of owners to the paths they own. | 
| 61 self.owned_by = {EVERYONE: set()} | 63 self.owned_by = {EVERYONE: set()} | 
| 62 | 64 | 
| 63 # Mapping of paths to authorized owners. | 65 # Mapping of paths to authorized owners. | 
| 64 self.owners_for = {} | 66 self.owners_for = {} | 
| 65 | 67 | 
| 66 # Set of paths that stop us from looking above them for owners. | 68 # Set of paths that stop us from looking above them for owners. | 
| 67 # (This is implicitly true for the root directory). | 69 # (This is implicitly true for the root directory). | 
| 68 self.stop_looking = set(['']) | 70 self.stop_looking = set(['']) | 
| 69 | 71 | 
| 70 def reviewers_for(self, files): | 72 def reviewers_for(self, files): | 
| 71 """Returns a suggested set of reviewers that will cover the files. | 73 """Returns a suggested set of reviewers that will cover the files. | 
| 72 | 74 | 
| 73 files is a sequence of paths relative to (and under) self.root.""" | 75 files is a sequence of paths relative to (and under) self.root.""" | 
| 74 self._check_paths(files) | 76 self._check_paths(files) | 
| 75 self._load_data_needed_for(files) | 77 self._load_data_needed_for(files) | 
| 76 return self._covering_set_of_owners_for(files) | 78 return self._covering_set_of_owners_for(files) | 
| 77 | 79 | 
| 80 # TODO(dpranke): rename to objects_not_covered_by | |
| 78 def directories_not_covered_by(self, files, reviewers): | 81 def directories_not_covered_by(self, files, reviewers): | 
| 79 """Returns the set of directories that are not owned by a reviewer. | 82 """Returns the set of directories that are not owned by a reviewer. | 
| 80 | 83 | 
| 81 Determines which of the given files are not owned by at least one of the | 84 Determines which of the given files are not owned by at least one of the | 
| 82 reviewers, then returns a set containing the applicable enclosing | 85 reviewers, then returns a set containing the applicable enclosing | 
| 83 directories, i.e. the ones upward from the files that have OWNERS files. | 86 directories, i.e. the ones upward from the files that have OWNERS files. | 
| 84 | 87 | 
| 85 Args: | 88 Args: | 
| 86 files is a sequence of paths relative to (and under) self.root. | 89 files is a sequence of paths relative to (and under) self.root. | 
| 87 reviewers is a sequence of strings matching self.email_regexp. | 90 reviewers is a sequence of strings matching self.email_regexp. | 
| 88 """ | 91 """ | 
| 89 self._check_paths(files) | 92 self._check_paths(files) | 
| 90 self._check_reviewers(reviewers) | 93 self._check_reviewers(reviewers) | 
| 91 self._load_data_needed_for(files) | 94 self._load_data_needed_for(files) | 
| 92 | 95 | 
| 93 dirs = set([self.os_path.dirname(f) for f in files]) | 96 objs = set() | 
| 94 covered_dirs = self._dirs_covered_by(reviewers) | 97 for f in files: | 
| 95 uncovered_dirs = [self._enclosing_dir_with_owners(d) for d in dirs | 98 if f in self.owners_for: | 
| 96 if not self._is_dir_covered_by(d, covered_dirs)] | 99 objs.add(f) | 
| 100 else: | |
| 101 objs.add(self.os_path.dirname(f)) | |
| 97 | 102 | 
| 98 return set(uncovered_dirs) | 103 covered_objs = self._objs_covered_by(reviewers) | 
| 104 uncovered_objs = [self._enclosing_obj_with_owners(o) for o in objs | |
| 105 if not self._is_obj_covered_by(o, covered_objs)] | |
| 106 | |
| 107 return set(uncovered_objs) | |
| 108 | |
| 109 objects_not_covered_by = directories_not_covered_by | |
| 
M-A Ruel
2012/10/12 15:19:04
Why not rename right away? I don't see any advanta
 
Dirk Pranke
2012/10/16 22:42:41
I thought it would make the patch easier to review
 | |
| 99 | 110 | 
| 100 def _check_paths(self, files): | 111 def _check_paths(self, files): | 
| 101 def _is_under(f, pfx): | 112 def _is_under(f, pfx): | 
| 102 return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx) | 113 return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx) | 
| 103 _assert_is_collection(files) | 114 _assert_is_collection(files) | 
| 104 assert all(_is_under(f, self.os_path.abspath(self.root)) for f in files) | 115 assert all(_is_under(f, self.os_path.abspath(self.root)) for f in files) | 
| 105 | 116 | 
| 106 def _check_reviewers(self, reviewers): | 117 def _check_reviewers(self, reviewers): | 
| 107 _assert_is_collection(reviewers) | 118 _assert_is_collection(reviewers) | 
| 108 assert all(self.email_regexp.match(r) for r in reviewers) | 119 assert all(self.email_regexp.match(r) for r in reviewers) | 
| 109 | 120 | 
| 121 # TODO(dpranke): Rename to _objs_covered_by and update_callers | |
| 110 def _dirs_covered_by(self, reviewers): | 122 def _dirs_covered_by(self, reviewers): | 
| 111 dirs = self.owned_by[EVERYONE] | 123 dirs = self.owned_by[EVERYONE] | 
| 112 for r in reviewers: | 124 for r in reviewers: | 
| 113 dirs = dirs | self.owned_by.get(r, set()) | 125 dirs = dirs | self.owned_by.get(r, set()) | 
| 114 return dirs | 126 return dirs | 
| 115 | 127 | 
| 128 _objs_covered_by = _dirs_covered_by | |
| 129 | |
| 116 def _stop_looking(self, dirname): | 130 def _stop_looking(self, dirname): | 
| 117 return dirname in self.stop_looking | 131 return dirname in self.stop_looking | 
| 118 | 132 | 
| 133 # TODO(dpranke): Rename to _is_dir_covered_by and update callers. | |
| 119 def _is_dir_covered_by(self, dirname, covered_dirs): | 134 def _is_dir_covered_by(self, dirname, covered_dirs): | 
| 120 while not dirname in covered_dirs and not self._stop_looking(dirname): | 135 while not dirname in covered_dirs and not self._stop_looking(dirname): | 
| 121 dirname = self.os_path.dirname(dirname) | 136 dirname = self.os_path.dirname(dirname) | 
| 122 return dirname in covered_dirs | 137 return dirname in covered_dirs | 
| 123 | 138 | 
| 139 _is_obj_covered_by = _is_dir_covered_by | |
| 140 | |
| 141 # TODO(dpranke): Rename to _enclosing_obj_with_owners and update callers. | |
| 124 def _enclosing_dir_with_owners(self, directory): | 142 def _enclosing_dir_with_owners(self, directory): | 
| 125 """Returns the innermost enclosing directory that has an OWNERS file.""" | 143 """Returns the innermost enclosing directory that has an OWNERS file.""" | 
| 126 dirpath = directory | 144 dirpath = directory | 
| 127 while not dirpath in self.owners_for: | 145 while not dirpath in self.owners_for: | 
| 128 if self._stop_looking(dirpath): | 146 if self._stop_looking(dirpath): | 
| 129 break | 147 break | 
| 130 dirpath = self.os_path.dirname(dirpath) | 148 dirpath = self.os_path.dirname(dirpath) | 
| 131 return dirpath | 149 return dirpath | 
| 132 | 150 | 
| 151 _enclosing_obj_with_owners = _enclosing_dir_with_owners | |
| 152 | |
| 133 def _load_data_needed_for(self, files): | 153 def _load_data_needed_for(self, files): | 
| 134 for f in files: | 154 for f in files: | 
| 135 dirpath = self.os_path.dirname(f) | 155 dirpath = self.os_path.dirname(f) | 
| 136 while not dirpath in self.owners_for: | 156 while not dirpath in self.owners_for: | 
| 137 self._read_owners_in_dir(dirpath) | 157 self._read_owners_in_dir(dirpath) | 
| 138 if self._stop_looking(dirpath): | 158 if self._stop_looking(dirpath): | 
| 139 break | 159 break | 
| 140 dirpath = self.os_path.dirname(dirpath) | 160 dirpath = self.os_path.dirname(dirpath) | 
| 141 | 161 | 
| 142 def _read_owners_in_dir(self, dirpath): | 162 def _read_owners_in_dir(self, dirpath): | 
| 143 owners_path = self.os_path.join(self.root, dirpath, 'OWNERS') | 163 owners_path = self.os_path.join(self.root, dirpath, 'OWNERS') | 
| 144 if not self.os_path.exists(owners_path): | 164 if not self.os_path.exists(owners_path): | 
| 145 return | 165 return | 
| 146 | 166 | 
| 147 lineno = 0 | 167 lineno = 0 | 
| 148 for line in self.fopen(owners_path): | 168 for line in self.fopen(owners_path): | 
| 149 lineno += 1 | 169 lineno += 1 | 
| 150 line = line.strip() | 170 line = line.strip() | 
| 151 if line.startswith('#') or line == '': | 171 if line.startswith('#') or line == '': | 
| 152 continue | 172 continue | 
| 153 if line == 'set noparent': | 173 if line == 'set noparent': | 
| 154 self.stop_looking.add(dirpath) | 174 self.stop_looking.add(dirpath) | 
| 155 continue | 175 continue | 
| 176 | |
| 177 m = re.match("per-file (.+)=(.+)", line) | |
| 178 if m: | |
| 179 glob_string = m.group(1) | |
| 180 directive = m.group(2) | |
| 181 full_glob_string = self.os_path.join(self.root, dirpath, glob_string) | |
| 
M-A Ruel
2012/10/12 15:19:04
You should assert not self.os_path.isabs(glob_stri
 
Dirk Pranke
2012/10/16 22:42:41
Good point. Actually it's probably better to ensur
 | |
| 182 baselines = self.glob(full_glob_string) | |
| 183 for baseline in [self.os_path.relpath(self.root, b) for b in baselines]: | |
| 
M-A Ruel
2012/10/12 15:19:04
for baseline in (self.os_path.relpath(self.root, b
 
Dirk Pranke
2012/10/16 22:42:41
I tend to prefer the comprehension notation to the
 | |
| 184 self._add_entry(baseline, directive, "per-file line", | |
| 185 owners_path, lineno) | |
| 186 continue | |
| 187 | |
| 156 if line.startswith('set '): | 188 if line.startswith('set '): | 
| 157 raise SyntaxErrorInOwnersFile(owners_path, lineno, | 189 raise SyntaxErrorInOwnersFile(owners_path, lineno, | 
| 158 'unknown option: "%s"' % line[4:].strip()) | 190 'unknown option: "%s"' % line[4:].strip()) | 
| 159 if self.email_regexp.match(line) or line == EVERYONE: | 191 | 
| 160 self.owned_by.setdefault(line, set()).add(dirpath) | 192 self._add_entry(dirpath, line, "line", owners_path, lineno) | 
| 161 self.owners_for.setdefault(dirpath, set()).add(line) | 193 | 
| 162 continue | 194 def _add_entry(self, path, directive, line_type, owners_path, lineno): | 
| 195 if directive == "set noparent": | |
| 196 self.stop_looking.add(path) | |
| 197 elif self.email_regexp.match(directive) or directive == EVERYONE: | |
| 198 self.owned_by.setdefault(directive, set()).add(path) | |
| 199 self.owners_for.setdefault(path, set()).add(directive) | |
| 200 else: | |
| 163 raise SyntaxErrorInOwnersFile(owners_path, lineno, | 201 raise SyntaxErrorInOwnersFile(owners_path, lineno, | 
| 164 ('line is not a comment, a "set" directive, ' | 202 ('%s is not a "set" directive, ' | 
| 
M-A Ruel
2012/10/12 15:19:04
Technically, you'd also want to say it's not an EV
 
Dirk Pranke
2012/10/16 22:42:41
Done.
 | |
| 165 'or an email address: "%s"' % line)) | 203 'or an email address: "%s"' % (line_type, directive))) | 
| 204 | |
| 166 | 205 | 
| 167 def _covering_set_of_owners_for(self, files): | 206 def _covering_set_of_owners_for(self, files): | 
| 168 # Get the set of directories from the files. | 207 # Get the set of directories from the files. | 
| 169 dirs = set() | 208 dirs = set() | 
| 170 for f in files: | 209 for f in files: | 
| 171 dirs.add(self.os_path.dirname(f)) | 210 dirs.add(self.os_path.dirname(f)) | 
| 172 | 211 | 
| 173 owned_dirs = {} | 212 owned_dirs = {} | 
| 174 dir_owners = {} | 213 dir_owners = {} | 
| 175 | 214 | 
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 212 break | 251 break | 
| 213 | 252 | 
| 214 final_owners.add(max_owner) | 253 final_owners.add(max_owner) | 
| 215 | 254 | 
| 216 # Remove all directories owned by the current owner from the remaining | 255 # Remove all directories owned by the current owner from the remaining | 
| 217 # list. | 256 # list. | 
| 218 for dirname in owned_dirs[max_owner]: | 257 for dirname in owned_dirs[max_owner]: | 
| 219 dirs.discard(dirname) | 258 dirs.discard(dirname) | 
| 220 | 259 | 
| 221 return final_owners | 260 return final_owners | 
| OLD | NEW |