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

Side by Side Diff: find_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: Fixes and optimization Created 7 years, 9 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
« no previous file with comments | « no previous file | git_cl.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/python
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
Dirk Pranke 2013/04/03 00:54:16 2013.
Bei Zhang 2013/04/08 17:33:38 Done.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5 '''A tool to help picking owner_to_files for reviewing.'''
6
7 import git_cl
8 import os.path
9 import sys
10 import copy
11 import glob
12 import owners as owners_module
13
14
15 def first(iterable):
Dirk Pranke 2013/04/03 00:54:16 I wouldn't use this function, it's easier to just
Bei Zhang 2013/04/08 17:33:38 It's for unordered set though. Maybe I should use
16 for element in iterable:
17 return element
18
19
20 class FindOwners:
Dirk Pranke 2013/04/03 00:54:16 class FindOwners(object):
Bei Zhang 2013/04/08 17:33:38 Done.
21 COLOR_LINK = '\033[4m'
22 COLOR_BOLD = '\033[1;32m'
23 COLOR_GREY = '\033[0;37m'
24 COLOR_RESET = '\033[0m'
25 def __init__(self, files, local_root, disable_color=False):
Dirk Pranke 2013/04/03 00:54:16 generally speaking, this method is too long, and i
Bei Zhang 2013/04/08 17:33:38 Done.
26 # Files in a change list
27 self.owner_to_files = owner_to_files = {}
28
29 self.file_to_owners = file_to_owners = {}
30
31 if os.name == "nt" or disable_color:
32 self.COLOR_LINK = ''
33 self.COLOR_BOLD = ''
34 self.COLOR_GREY = ''
35 self.COLOR_RESET = ''
36
37 db = owners_module.Database(local_root,
38 fopen=file, os_path=os.path, glob=glob.glob)
39 db.reviewers_for(files, None)
40
41 for file_name in files:
42 owners_set = set()
43 if file_name in db.owners_for:
44 owners_set = owners_set | db.owners_for[file_name]
45 else:
46 dir_name = file_name
47 while dir_name != '':
48 if dir_name in db.stop_looking:
49 break
50 dir_name = os.path.dirname(dir_name)
51 if dir_name in db.owners_for:
52 owners_set = owners_set | db.owners_for[dir_name]
53 if owners_module.EVERYONE in owners_set:
54 break
Dirk Pranke 2013/04/03 00:54:16 can we just use db._all_possible_owners() here (yo
Bei Zhang 2013/04/08 17:33:38 I was a little bit scared of the leading underscor
55
56 if len(owners_set) == 0:
57 raise Exception("File '%s' has no owner" % file_name)
58
59 # Eliminate files that EVERYONE can review
60 if owners_module.EVERYONE in owners_set:
61 continue
62 file_to_owners[file_name] = owners_set
63
64 self.original_files_to_owners = copy.deepcopy(file_to_owners)
65
66 for owner_name in db.owned_by:
67 if owner_name == owners_module.EVERYONE:
68 continue
69 files_set = set()
70 for file_name in file_to_owners:
71 if owner_name in file_to_owners[file_name]:
72 files_set.add(file_name)
73 if len(files_set) > 0:
74 owner_to_files[owner_name] = files_set
Dirk Pranke 2013/04/03 00:54:16 it's unfortunate that we can't use just db.owned_b
Bei Zhang 2013/04/24 00:13:08 Done.
75
76 self.comments = db.comments
77 self.unreviewedFiles = set()
78 self.reviewedBy = {}
79 self.usedOwners = set()
80 self.unusedOwners = set()
81
82 def reset(self):
83 self.file_to_owners = copy.deepcopy(self.original_files_to_owners)
84 self.unreviewedFiles = set(self.file_to_owners.keys())
85 self.reviewedBy = {}
86 self.usedOwners = set()
87 self.unusedOwners = set()
88
89 def bold(self, text):
90 return (self.COLOR_BOLD + text + self.COLOR_RESET)
91
92 def bold_name(self, name):
93 return (self.COLOR_BOLD +
94 name.replace("@chromium.org", "") + self.COLOR_RESET)
95
96 def greyed(self, text):
97 return self.COLOR_GREY + text + self.COLOR_RESET
98
99 def unuseOwner(self, owner):
100 self.unusedOwners.add(owner)
101 for file_name in self.owner_to_files[owner]:
102 if file_name in self.unreviewedFiles:
103 self.file_to_owners[file_name].remove(owner)
104 self.findMustPickOwners()
105
106 def useOwner(self, owner):
107 self.usedOwners.add(owner)
108 for file_name in self.owner_to_files[owner]:
109 if file_name in self.unreviewedFiles:
110 self.unreviewedFiles.remove(file_name)
111 self.reviewedBy[file_name] = owner
112 self.findMustPickOwners()
113
114 def findMustPickOwners(self):
Dirk Pranke 2013/04/03 00:54:16 so "findMustPickOwners" selects all the people tha
Bei Zhang 2013/04/08 17:33:38 Done. Sorry for my bad English. On 2013/04/03 00:
115 continues = True
116 while continues:
117 continues = False
118 for file_name in self.unreviewedFiles:
119 if len(self.file_to_owners[file_name]) == 1:
120 self.useOwner(first(self.file_to_owners[file_name]))
121 continues = True
122 break
Dirk Pranke 2013/04/03 00:54:16 Can you rewrite this as a loop over a filtered lis
Bei Zhang 2013/04/08 17:33:38 Done.
123
124 def printComments(self, owner):
125 if owner not in self.comments:
126 print self.bold_name(owner)
127 else:
128 print self.bold_name(owner) + " is commented as:"
129 for path in self.comments[owner]:
130 if len(self.comments[owner][path]) > 0:
131 print (self.greyed(" " + self.comments[owner][path]) + " (at " +
132 self.bold(path or "<root>") + ")")
133 else:
134 print (self.greyed(" [No comment] ") + " (at " +
135 self.bold(path or "<root>") + ")")
136
137 def printOwnedFiles(self, owner):
138 # Print owned files
139 self.printComments(owner)
140 print
141 print (self.bold_name(owner) + " owns " +
142 str(len(self.owner_to_files[owner])) + " file(s):")
143 for file_name in sorted(self.owner_to_files[owner]):
144 if file_name not in self.unreviewedFiles:
145 print " " + self.greyed(file_name + ' (by ' +
146 self.bold_name(self.reviewedBy[file_name]) +
147 ')')
148 else:
149 if len(self.file_to_owners[file_name]) <= 3:
150 other_owners = []
151 for ow in self.file_to_owners[file_name]:
152 if ow != owner:
153 other_owners.append(self.bold_name(ow))
154 print (" " + file_name +
155 " [" + (", ".join(other_owners)) + "]")
156 else:
157 print (" " + file_name +
158 " [" + self.bold(str(len(self.file_to_owners[file_name]) - 1)) + "]")
159 print
160
161 def listOwners(self, owners_queue):
162 if (len(self.owner_to_files) - len(self.unusedOwners) -
163 len(self.usedOwners)) > 3:
164 for ow in owners_queue:
165 if ow not in self.unusedOwners and ow not in self.usedOwners:
166 self.printComments(ow)
167 else:
168 for ow in owners_queue:
169 if ow not in self.unusedOwners and ow not in self.usedOwners:
170 self.printOwnedFiles(ow)
171
172 def listFiles(self):
173 if len(self.unreviewedFiles) > 5:
174 for file_name in sorted(self.unreviewedFiles):
175 print (file_name + " [" +
176 self.bold(str(len(self.file_to_owners[file_name]))) + "]")
177 else:
178 for file_name in self.unreviewedFiles:
179 print file_name
180 for ow in sorted(self.file_to_owners[file_name]):
181 if ow in self.unusedOwners:
182 print " " + self.bold_name(self.greyed(ow))
183 elif ow in self.usedOwners:
184 print " " + self.bold_name(self.greyed(ow))
185 else:
186 print " " + self.bold_name(ow)
187
188 def pickOwner(self, ow = False):
189 if not ow:
190 ow = raw_input("Pick an owner:")
191
192 # Allowing to omit domain suffixes
193 if ow not in self.owner_to_files:
194 if ow + "@chromium.org" in self.owner_to_files:
195 ow += "@chromium.org"
196
197 if ow not in self.owner_to_files:
198 print ("You cannot pick " + self.bold_name(ow) + " manually. " +
199 "It's an invalid name or not related to the change list.")
200 return False
201 elif ow in self.usedOwners:
202 print ("You cannot pick " + self.bold_name(ow) + " manually. " +
203 "It's already selected.")
204 return False
205 elif ow in self.unusedOwners:
206 print ("You cannot pick " + self.bold_name(ow) + " manually." +
207 "It's already unselected.")
208 return False
209 elif not any(f in self.unreviewedFiles for f in self.owner_to_files[ow]):
210 while True:
211 inp = raw_input("Picking " + self.bold_name(ow) +
212 " does not cover any unreviewed file. "
213 "Are you sure? [yes/No]").lower()
214 if inp == "" or inp == "n" or inp == "no":
215 return False
216 elif inp == "y" or inp == "yes":
217 break
218
219 self.useOwner(ow)
220 return True
221
222 def runOnce(self):
Dirk Pranke 2013/04/03 00:54:16 Can you document what runOnce() and run() are doin
Bei Zhang 2013/04/08 17:33:38 Done. It was for a hack to restart the run(). Remo
223 self.reset()
224
225 # Initialize owners queue, sort it by the number of files
226 # each owns
227 owners_queue = list(sorted(self.owner_to_files.keys(),
228 key=lambda owner: len(self.owner_to_files[owner]),
229 reverse=True))
230
231 self.findMustPickOwners()
232 while len(owners_queue) > 0 and len(self.unreviewedFiles) > 0:
233 owner = owners_queue[0]
234 try:
235 if owner in self.usedOwners:
236 continue
237 if len(self.unreviewedFiles) == 0:
238 print "Finished.\n\n"
239 break
240 if owner in self.unusedOwners:
241 # If this owner is already deseleted.
Dirk Pranke 2013/04/03 00:54:16 deselected?
Bei Zhang 2013/04/08 17:33:38 That is, when you select "no" on this owner. So e
242 continue
243 if not any((file_name in self.unreviewedFiles)
244 for file_name in self.owner_to_files[owner]):
245 self.unuseOwner(owner)
246 continue
247 print "====================="
248 print self.bold(str(len(self.unreviewedFiles))) + " file(s) left."
249 self.printOwnedFiles(owner)
250 while True:
251 print "Add " + self.bold_name(owner) + " as your reviewer? "
252 inp = raw_input(
253 "[yes/no/Defer/pick/files/owners/quit/restart]: ").lower()
254 if inp == "y" or inp == "yes":
255 self.useOwner(owner)
256 break
257 elif inp == "n" or inp == "no":
258 self.unuseOwner(owner)
259 break
260 elif inp == "" or inp == "d" or inp == "defer":
261 owners_queue.append(owner)
262 break
263 elif inp == "f" or inp == "files":
264 self.listFiles()
265 elif inp == "o" or inp == "owners":
266 self.listOwners(owners_queue)
267 elif inp == "p" or inp == "pick":
268 self.pickOwner()
269 owners_queue.insert(0, owner)
270 break
271 elif inp.startswith("p ") or inp.startswith("pick "):
272 self.pickOwner(inp.split(' ', 2)[1])
273 owners_queue.insert(0, owner)
274 break
275 elif inp == 'r' or inp == 'restart':
276 return -1
277 elif inp == "q" or inp == "quit":
278 # Exit with error
279 return 1
280 finally:
281 if len(owners_queue):
282 owners_queue.pop(0)
283 # End of queue
284
285 # Print results
286 print "** You picked these owners **"
287 for owner in self.usedOwners:
288 print self.bold_name(owner) + ":"
289 for file_name in sorted(self.owner_to_files[owner]):
290 print " " + file_name
291 return 0
292
293 def run(self):
294 result = -1
295 while result == -1:
296 result = self.runOnce()
297 return result
298
299
300 def main():
Dirk Pranke 2013/04/03 00:54:16 Do you actually imagine this being used as a separ
Bei Zhang 2013/04/08 17:33:38 Done.
301 cl = git_cl.Changelist()
302 local_root = os.path.abspath(
303 git_cl.RunGit(['rev-parse', '--show-cdup']).strip() or '.')
304 base_branch = git_cl.RunGit(
305 ['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
306 changes = cl.GetChange(base_branch, None)
307 files = [f.LocalPath() for f in changes.AffectedFiles()]
308 return FindOwners(files, local_root).run()
309
310
311 if __name__ == "__main__":
312 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | git_cl.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698