Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(315)

Side by Side Diff: owners.py

Issue 11114005: implement per-file OWNERS support (Closed) Base URL: https://git.chromium.org/chromium/tools/depot_tools.git@master
Patch Set: update w/ more review feedback Created 8 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | presubmit_support.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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.
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 """
6 50
7 import collections 51 import collections
8 import re 52 import re
9 53
10 54
11 # If this is present by itself on a line, this means that everyone can review. 55 # If this is present by itself on a line, this means that everyone can review.
12 EVERYONE = '*' 56 EVERYONE = '*'
13 57
14 58
15 # Recognizes 'X@Y' email addresses. Very simplistic. 59 # Recognizes 'X@Y' email addresses. Very simplistic.
(...skipping 20 matching lines...) Expand all
36 return "%s:%d syntax error: %s" % (self.path, self.lineno, self.msg) 80 return "%s:%d syntax error: %s" % (self.path, self.lineno, self.msg)
37 81
38 82
39 class Database(object): 83 class Database(object):
40 """A database of OWNERS files for a repository. 84 """A database of OWNERS files for a repository.
41 85
42 This class allows you to find a suggested set of reviewers for a list 86 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 87 of changed files, and see if a list of changed files is covered by a
44 list of reviewers.""" 88 list of reviewers."""
45 89
46 def __init__(self, root, fopen, os_path): 90 def __init__(self, root, fopen, os_path, glob):
47 """Args: 91 """Args:
48 root: the path to the root of the Repository 92 root: the path to the root of the Repository
49 open: function callback to open a text file for reading 93 open: function callback to open a text file for reading
50 os_path: module/object callback with fields for 'abspath', 'dirname', 94 os_path: module/object callback with fields for 'abspath', 'dirname',
51 'exists', and 'join' 95 'exists', and 'join'
96 glob: function callback to list entries in a directory match a glob
97 (i.e., glob.glob)
52 """ 98 """
53 self.root = root 99 self.root = root
54 self.fopen = fopen 100 self.fopen = fopen
55 self.os_path = os_path 101 self.os_path = os_path
102 self.glob = glob
56 103
57 # Pick a default email regexp to use; callers can override as desired. 104 # Pick a default email regexp to use; callers can override as desired.
58 self.email_regexp = re.compile(BASIC_EMAIL_REGEXP) 105 self.email_regexp = re.compile(BASIC_EMAIL_REGEXP)
59 106
60 # Mapping of owners to the paths they own. 107 # Mapping of owners to the paths they own.
61 self.owned_by = {EVERYONE: set()} 108 self.owned_by = {EVERYONE: set()}
62 109
63 # Mapping of paths to authorized owners. 110 # Mapping of paths to authorized owners.
64 self.owners_for = {} 111 self.owners_for = {}
65 112
66 # Set of paths that stop us from looking above them for owners. 113 # Set of paths that stop us from looking above them for owners.
67 # (This is implicitly true for the root directory). 114 # (This is implicitly true for the root directory).
68 self.stop_looking = set(['']) 115 self.stop_looking = set([''])
69 116
70 def reviewers_for(self, files): 117 def reviewers_for(self, files):
71 """Returns a suggested set of reviewers that will cover the files. 118 """Returns a suggested set of reviewers that will cover the files.
72 119
73 files is a sequence of paths relative to (and under) self.root.""" 120 files is a sequence of paths relative to (and under) self.root."""
74 self._check_paths(files) 121 self._check_paths(files)
75 self._load_data_needed_for(files) 122 self._load_data_needed_for(files)
76 return self._covering_set_of_owners_for(files) 123 return self._covering_set_of_owners_for(files)
77 124
125 # TODO(dpranke): rename to objects_not_covered_by
78 def directories_not_covered_by(self, files, reviewers): 126 def directories_not_covered_by(self, files, reviewers):
79 """Returns the set of directories that are not owned by a reviewer. 127 """Returns the set of directories that are not owned by a reviewer.
80 128
81 Determines which of the given files are not owned by at least one of the 129 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 130 reviewers, then returns a set containing the applicable enclosing
83 directories, i.e. the ones upward from the files that have OWNERS files. 131 directories, i.e. the ones upward from the files that have OWNERS files.
84 132
85 Args: 133 Args:
86 files is a sequence of paths relative to (and under) self.root. 134 files is a sequence of paths relative to (and under) self.root.
87 reviewers is a sequence of strings matching self.email_regexp. 135 reviewers is a sequence of strings matching self.email_regexp.
88 """ 136 """
89 self._check_paths(files) 137 self._check_paths(files)
90 self._check_reviewers(reviewers) 138 self._check_reviewers(reviewers)
91 self._load_data_needed_for(files) 139 self._load_data_needed_for(files)
92 140
93 dirs = set([self.os_path.dirname(f) for f in files]) 141 objs = set()
94 covered_dirs = self._dirs_covered_by(reviewers) 142 for f in files:
95 uncovered_dirs = [self._enclosing_dir_with_owners(d) for d in dirs 143 if f in self.owners_for:
96 if not self._is_dir_covered_by(d, covered_dirs)] 144 objs.add(f)
145 else:
146 objs.add(self.os_path.dirname(f))
97 147
98 return set(uncovered_dirs) 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
99 155
100 def _check_paths(self, files): 156 def _check_paths(self, files):
101 def _is_under(f, pfx): 157 def _is_under(f, pfx):
102 return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx) 158 return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx)
103 _assert_is_collection(files) 159 _assert_is_collection(files)
104 assert all(_is_under(f, self.os_path.abspath(self.root)) for f in files) 160 assert all(_is_under(f, self.os_path.abspath(self.root)) for f in files)
105 161
106 def _check_reviewers(self, reviewers): 162 def _check_reviewers(self, reviewers):
107 _assert_is_collection(reviewers) 163 _assert_is_collection(reviewers)
108 assert all(self.email_regexp.match(r) for r in reviewers) 164 assert all(self.email_regexp.match(r) for r in reviewers)
109 165
166 # TODO(dpranke): Rename to _objs_covered_by and update_callers
110 def _dirs_covered_by(self, reviewers): 167 def _dirs_covered_by(self, reviewers):
111 dirs = self.owned_by[EVERYONE] 168 dirs = self.owned_by[EVERYONE]
112 for r in reviewers: 169 for r in reviewers:
113 dirs = dirs | self.owned_by.get(r, set()) 170 dirs = dirs | self.owned_by.get(r, set())
114 return dirs 171 return dirs
115 172
173 _objs_covered_by = _dirs_covered_by
174
116 def _stop_looking(self, dirname): 175 def _stop_looking(self, dirname):
117 return dirname in self.stop_looking 176 return dirname in self.stop_looking
118 177
178 # TODO(dpranke): Rename to _is_dir_covered_by and update callers.
119 def _is_dir_covered_by(self, dirname, covered_dirs): 179 def _is_dir_covered_by(self, dirname, covered_dirs):
120 while not dirname in covered_dirs and not self._stop_looking(dirname): 180 while not dirname in covered_dirs and not self._stop_looking(dirname):
121 dirname = self.os_path.dirname(dirname) 181 dirname = self.os_path.dirname(dirname)
122 return dirname in covered_dirs 182 return dirname in covered_dirs
123 183
184 _is_obj_covered_by = _is_dir_covered_by
185
186 # TODO(dpranke): Rename to _enclosing_obj_with_owners and update callers.
124 def _enclosing_dir_with_owners(self, directory): 187 def _enclosing_dir_with_owners(self, directory):
125 """Returns the innermost enclosing directory that has an OWNERS file.""" 188 """Returns the innermost enclosing directory that has an OWNERS file."""
126 dirpath = directory 189 dirpath = directory
127 while not dirpath in self.owners_for: 190 while not dirpath in self.owners_for:
128 if self._stop_looking(dirpath): 191 if self._stop_looking(dirpath):
129 break 192 break
130 dirpath = self.os_path.dirname(dirpath) 193 dirpath = self.os_path.dirname(dirpath)
131 return dirpath 194 return dirpath
132 195
196 _enclosing_obj_with_owners = _enclosing_dir_with_owners
197
133 def _load_data_needed_for(self, files): 198 def _load_data_needed_for(self, files):
134 for f in files: 199 for f in files:
135 dirpath = self.os_path.dirname(f) 200 dirpath = self.os_path.dirname(f)
136 while not dirpath in self.owners_for: 201 while not dirpath in self.owners_for:
137 self._read_owners_in_dir(dirpath) 202 self._read_owners_in_dir(dirpath)
138 if self._stop_looking(dirpath): 203 if self._stop_looking(dirpath):
139 break 204 break
140 dirpath = self.os_path.dirname(dirpath) 205 dirpath = self.os_path.dirname(dirpath)
141 206
142 def _read_owners_in_dir(self, dirpath): 207 def _read_owners_in_dir(self, dirpath):
143 owners_path = self.os_path.join(self.root, dirpath, 'OWNERS') 208 owners_path = self.os_path.join(self.root, dirpath, 'OWNERS')
144 if not self.os_path.exists(owners_path): 209 if not self.os_path.exists(owners_path):
145 return 210 return
146 211
147 lineno = 0 212 lineno = 0
148 for line in self.fopen(owners_path): 213 for line in self.fopen(owners_path):
149 lineno += 1 214 lineno += 1
150 line = line.strip() 215 line = line.strip()
151 if line.startswith('#') or line == '': 216 if line.startswith('#') or line == '':
152 continue 217 continue
153 if line == 'set noparent': 218 if line == 'set noparent':
154 self.stop_looking.add(dirpath) 219 self.stop_looking.add(dirpath)
155 continue 220 continue
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)
227 baselines = self.glob(full_glob_string)
228 for baseline in (self.os_path.relpath(self.root, b) for b in baselines):
229 self._add_entry(baseline, directive, "per-file line",
230 owners_path, lineno)
231 continue
232
156 if line.startswith('set '): 233 if line.startswith('set '):
157 raise SyntaxErrorInOwnersFile(owners_path, lineno, 234 raise SyntaxErrorInOwnersFile(owners_path, lineno,
158 'unknown option: "%s"' % line[4:].strip()) 235 'unknown option: "%s"' % line[4:].strip())
159 if self.email_regexp.match(line) or line == EVERYONE: 236
160 self.owned_by.setdefault(line, set()).add(dirpath) 237 self._add_entry(dirpath, line, "line", owners_path, lineno)
161 self.owners_for.setdefault(dirpath, set()).add(line) 238
162 continue 239 def _add_entry(self, path, directive, line_type, owners_path, lineno):
240 if directive == "set noparent":
241 self.stop_looking.add(path)
242 elif self.email_regexp.match(directive) or directive == EVERYONE:
243 self.owned_by.setdefault(directive, set()).add(path)
244 self.owners_for.setdefault(path, set()).add(directive)
245 else:
163 raise SyntaxErrorInOwnersFile(owners_path, lineno, 246 raise SyntaxErrorInOwnersFile(owners_path, lineno,
164 ('line is not a comment, a "set" directive, ' 247 ('%s is not a "set" directive, "*", '
165 'or an email address: "%s"' % line)) 248 'or an email address: "%s"' % (line_type, directive)))
249
166 250
167 def _covering_set_of_owners_for(self, files): 251 def _covering_set_of_owners_for(self, files):
168 # Get the set of directories from the files. 252 # Get the set of directories from the files.
169 dirs = set() 253 dirs = set()
170 for f in files: 254 for f in files:
171 dirs.add(self.os_path.dirname(f)) 255 dirs.add(self.os_path.dirname(f))
172 256
173 owned_dirs = {} 257 owned_dirs = {}
174 dir_owners = {} 258 dir_owners = {}
175 259
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
212 break 296 break
213 297
214 final_owners.add(max_owner) 298 final_owners.add(max_owner)
215 299
216 # Remove all directories owned by the current owner from the remaining 300 # Remove all directories owned by the current owner from the remaining
217 # list. 301 # list.
218 for dirname in owned_dirs[max_owner]: 302 for dirname in owned_dirs[max_owner]:
219 dirs.discard(dirname) 303 dirs.discard(dirname)
220 304
221 return final_owners 305 return final_owners
OLDNEW
« no previous file with comments | « no previous file | presubmit_support.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698