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. |
6 | 6 |
7 OWNERS files indicate who is allowed to approve changes in a specific directory | 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). | 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, | 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. | 10 so you may need approval from both an OWNER and a reviewer in many cases. |
(...skipping 93 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
104 | 104 |
105 # Pick a default email regexp to use; callers can override as desired. | 105 # Pick a default email regexp to use; callers can override as desired. |
106 self.email_regexp = re.compile(BASIC_EMAIL_REGEXP) | 106 self.email_regexp = re.compile(BASIC_EMAIL_REGEXP) |
107 | 107 |
108 # Mapping of owners to the paths they own. | 108 # Mapping of owners to the paths they own. |
109 self.owned_by = {EVERYONE: set()} | 109 self.owned_by = {EVERYONE: set()} |
110 | 110 |
111 # Mapping of paths to authorized owners. | 111 # Mapping of paths to authorized owners. |
112 self.owners_for = {} | 112 self.owners_for = {} |
113 | 113 |
114 # Mapping reviewers to the most recent comment in the OWNERS files. | |
115 self.comments = {} | |
116 | |
114 # Set of paths that stop us from looking above them for owners. | 117 # Set of paths that stop us from looking above them for owners. |
115 # (This is implicitly true for the root directory). | 118 # (This is implicitly true for the root directory). |
116 self.stop_looking = set(['']) | 119 self.stop_looking = set(['']) |
117 | 120 |
118 def reviewers_for(self, files, author): | 121 def reviewers_for(self, files, author): |
119 """Returns a suggested set of reviewers that will cover the files. | 122 """Returns a suggested set of reviewers that will cover the files. |
120 | 123 |
121 files is a sequence of paths relative to (and under) self.root. | 124 files is a sequence of paths relative to (and under) self.root. |
122 If author is nonempty, we ensure it is not included in the set returned | 125 If author is nonempty, we ensure it is not included in the set returned |
123 in order avoid suggesting the author as a reviewer for their own changes.""" | 126 in order avoid suggesting the author as a reviewer for their own changes.""" |
124 self._check_paths(files) | 127 self._check_paths(files) |
125 self._load_data_needed_for(files) | 128 self.load_data_needed_for(files) |
126 suggested_owners = self._covering_set_of_owners_for(files, author) | 129 suggested_owners = self._covering_set_of_owners_for(files, author) |
127 if EVERYONE in suggested_owners: | 130 if EVERYONE in suggested_owners: |
128 if len(suggested_owners) > 1: | 131 if len(suggested_owners) > 1: |
129 suggested_owners.remove(EVERYONE) | 132 suggested_owners.remove(EVERYONE) |
130 else: | 133 else: |
131 suggested_owners = set(['<anyone>']) | 134 suggested_owners = set(['<anyone>']) |
132 return suggested_owners | 135 return suggested_owners |
133 | 136 |
134 def files_not_covered_by(self, files, reviewers): | 137 def files_not_covered_by(self, files, reviewers): |
135 """Returns the files not owned by one of the reviewers. | 138 """Returns the files not owned by one of the reviewers. |
136 | 139 |
137 Args: | 140 Args: |
138 files is a sequence of paths relative to (and under) self.root. | 141 files is a sequence of paths relative to (and under) self.root. |
139 reviewers is a sequence of strings matching self.email_regexp. | 142 reviewers is a sequence of strings matching self.email_regexp. |
140 """ | 143 """ |
141 self._check_paths(files) | 144 self._check_paths(files) |
142 self._check_reviewers(reviewers) | 145 self._check_reviewers(reviewers) |
143 self._load_data_needed_for(files) | 146 self.load_data_needed_for(files) |
144 | 147 |
145 covered_objs = self._objs_covered_by(reviewers) | 148 covered_objs = self._objs_covered_by(reviewers) |
146 uncovered_files = [f for f in files | 149 uncovered_files = [f for f in files |
147 if not self._is_obj_covered_by(f, covered_objs)] | 150 if not self._is_obj_covered_by(f, covered_objs)] |
148 | 151 |
149 return set(uncovered_files) | 152 return set(uncovered_files) |
150 | 153 |
151 def _check_paths(self, files): | 154 def _check_paths(self, files): |
152 def _is_under(f, pfx): | 155 def _is_under(f, pfx): |
153 return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx) | 156 return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx) |
(...skipping 21 matching lines...) Expand all Loading... | |
175 | 178 |
176 def _enclosing_dir_with_owners(self, objname): | 179 def _enclosing_dir_with_owners(self, objname): |
177 """Returns the innermost enclosing directory that has an OWNERS file.""" | 180 """Returns the innermost enclosing directory that has an OWNERS file.""" |
178 dirpath = objname | 181 dirpath = objname |
179 while not dirpath in self.owners_for: | 182 while not dirpath in self.owners_for: |
180 if self._stop_looking(dirpath): | 183 if self._stop_looking(dirpath): |
181 break | 184 break |
182 dirpath = self.os_path.dirname(dirpath) | 185 dirpath = self.os_path.dirname(dirpath) |
183 return dirpath | 186 return dirpath |
184 | 187 |
185 def _load_data_needed_for(self, files): | 188 def load_data_needed_for(self, files): |
186 for f in files: | 189 for f in files: |
187 dirpath = self.os_path.dirname(f) | 190 dirpath = self.os_path.dirname(f) |
188 while not dirpath in self.owners_for: | 191 while not dirpath in self.owners_for: |
189 self._read_owners_in_dir(dirpath) | 192 self._read_owners_in_dir(dirpath) |
190 if self._stop_looking(dirpath): | 193 if self._stop_looking(dirpath): |
191 break | 194 break |
192 dirpath = self.os_path.dirname(dirpath) | 195 dirpath = self.os_path.dirname(dirpath) |
193 | 196 |
194 def _read_owners_in_dir(self, dirpath): | 197 def _read_owners_in_dir(self, dirpath): |
195 owners_path = self.os_path.join(self.root, dirpath, 'OWNERS') | 198 owners_path = self.os_path.join(self.root, dirpath, 'OWNERS') |
196 if not self.os_path.exists(owners_path): | 199 if not self.os_path.exists(owners_path): |
197 return | 200 return |
198 | 201 comment = "" |
M-A Ruel
2013/04/24 13:04:45
'' everywhere
Bei Zhang
2013/04/24 16:42:01
Done.
| |
202 last_line_is_comment = False | |
M-A Ruel
2013/04/24 13:04:45
This is not needed.
comments = []
Then accumulate
Bei Zhang
2013/04/24 16:42:01
That case I will have to introduce a last_comment
| |
199 lineno = 0 | 203 lineno = 0 |
200 for line in self.fopen(owners_path): | 204 for line in self.fopen(owners_path): |
201 lineno += 1 | 205 lineno += 1 |
202 line = line.strip() | 206 line = line.strip() |
203 if line.startswith('#') or line == '': | 207 if line.startswith('#'): |
208 if last_line_is_comment: | |
209 comment += " " + line[1:].strip() | |
M-A Ruel
2013/04/24 13:04:45
why not keep the LF as-is?
Bei Zhang
2013/04/24 16:42:01
I think the comment could be wrapped. Do you think
| |
210 else: | |
211 comment = line[1:].strip() | |
212 last_line_is_comment = True | |
204 continue | 213 continue |
214 if line == '': | |
215 continue | |
216 last_line_is_comment = False | |
217 | |
205 if line == 'set noparent': | 218 if line == 'set noparent': |
206 self.stop_looking.add(dirpath) | 219 self.stop_looking.add(dirpath) |
207 continue | 220 continue |
208 | 221 |
209 m = re.match("per-file (.+)=(.+)", line) | 222 m = re.match("per-file (.+)=(.+)", line) |
210 if m: | 223 if m: |
211 glob_string = m.group(1).strip() | 224 glob_string = m.group(1).strip() |
212 directive = m.group(2).strip() | 225 directive = m.group(2).strip() |
213 full_glob_string = self.os_path.join(self.root, dirpath, glob_string) | 226 full_glob_string = self.os_path.join(self.root, dirpath, glob_string) |
214 if '/' in glob_string or '\\' in glob_string: | 227 if '/' in glob_string or '\\' in glob_string: |
215 raise SyntaxErrorInOwnersFile(owners_path, lineno, | 228 raise SyntaxErrorInOwnersFile(owners_path, lineno, |
216 'per-file globs cannot span directories or use escapes: "%s"' % | 229 'per-file globs cannot span directories or use escapes: "%s"' % |
217 line) | 230 line) |
218 baselines = self.glob(full_glob_string) | 231 baselines = self.glob(full_glob_string) |
219 for baseline in (self.os_path.relpath(b, self.root) for b in baselines): | 232 for baseline in (self.os_path.relpath(b, self.root) for b in baselines): |
220 self._add_entry(baseline, directive, "per-file line", | 233 self._add_entry(baseline, directive, "per-file line", |
221 owners_path, lineno) | 234 owners_path, lineno, comment) |
222 continue | 235 continue |
223 | 236 |
224 if line.startswith('set '): | 237 if line.startswith('set '): |
225 raise SyntaxErrorInOwnersFile(owners_path, lineno, | 238 raise SyntaxErrorInOwnersFile(owners_path, lineno, |
226 'unknown option: "%s"' % line[4:].strip()) | 239 'unknown option: "%s"' % line[4:].strip()) |
227 | 240 |
228 self._add_entry(dirpath, line, "line", owners_path, lineno) | 241 self._add_entry(dirpath, line, "line", owners_path, lineno, comment) |
229 | 242 |
230 def _add_entry(self, path, directive, line_type, owners_path, lineno): | 243 def _add_entry(self, path, directive, |
244 line_type, owners_path, lineno, comment): | |
231 if directive == "set noparent": | 245 if directive == "set noparent": |
232 self.stop_looking.add(path) | 246 self.stop_looking.add(path) |
233 elif self.email_regexp.match(directive) or directive == EVERYONE: | 247 elif self.email_regexp.match(directive) or directive == EVERYONE: |
248 if directive not in self.comments: | |
249 self.comments[directive] = {} | |
250 self.comments[directive][path] = comment | |
234 self.owned_by.setdefault(directive, set()).add(path) | 251 self.owned_by.setdefault(directive, set()).add(path) |
235 self.owners_for.setdefault(path, set()).add(directive) | 252 self.owners_for.setdefault(path, set()).add(directive) |
236 else: | 253 else: |
237 raise SyntaxErrorInOwnersFile(owners_path, lineno, | 254 raise SyntaxErrorInOwnersFile(owners_path, lineno, |
238 ('%s is not a "set" directive, "*", ' | 255 ('%s is not a "set" directive, "*", ' |
239 'or an email address: "%s"' % (line_type, directive))) | 256 'or an email address: "%s"' % (line_type, directive))) |
240 | 257 |
241 def _covering_set_of_owners_for(self, files, author): | 258 def _covering_set_of_owners_for(self, files, author): |
242 dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files) | 259 dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files) |
243 all_possible_owners = self._all_possible_owners(dirs_remaining, author) | 260 all_possible_owners = self._all_possible_owners(dirs_remaining, author) |
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
290 if num_directories_owned: | 307 if num_directories_owned: |
291 total_costs_by_owner[owner] = (total_distance / | 308 total_costs_by_owner[owner] = (total_distance / |
292 pow(num_directories_owned, 1.75)) | 309 pow(num_directories_owned, 1.75)) |
293 | 310 |
294 # Return the lowest cost owner. In the case of a tie, pick one randomly. | 311 # Return the lowest cost owner. In the case of a tie, pick one randomly. |
295 lowest_cost = min(total_costs_by_owner.itervalues()) | 312 lowest_cost = min(total_costs_by_owner.itervalues()) |
296 lowest_cost_owners = filter( | 313 lowest_cost_owners = filter( |
297 lambda owner: total_costs_by_owner[owner] == lowest_cost, | 314 lambda owner: total_costs_by_owner[owner] == lowest_cost, |
298 total_costs_by_owner) | 315 total_costs_by_owner) |
299 return random.Random().choice(lowest_cost_owners) | 316 return random.Random().choice(lowest_cost_owners) |
OLD | NEW |