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

Side by Side Diff: owners.py

Issue 12712002: An interactive tool to help find owners covering current change list. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Fix Deselect Created 7 years, 8 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 | Annotate | Revision Log
« git_cl.py ('K') | « git_cl.py ('k') | owners_finder.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 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
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
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
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)
OLDNEW
« git_cl.py ('K') | « git_cl.py ('k') | owners_finder.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698