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

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: More docs and minor improvements 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.
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 import readline # pylint: disable=W0611
Bei Zhang 2013/03/12 17:00:10 Not working on Windows
14
15 def first(iterable):
16 for element in iterable:
17 return element
18
19 class FindOwners:
20 COLOR_LINK = '\033[4m'
21 COLOR_BOLD = '\033[1;32m'
22 COLOR_GREY = '\033[0;37m'
23 COLOR_RESET = '\033[0m'
24 def __init__(self, files, local_root, disable_color=False):
25 # Files in a change list
26 self.files = files
27
28 self.owner_to_files = owner_to_files = {}
29
30 self.file_to_owners = file_to_owners = {}
31
32 if os.name == "nt" or disable_color:
33 self.COLOR_LINK = ''
34 self.COLOR_BOLD = ''
35 self.COLOR_GREY = ''
36 self.COLOR_RESET = ''
37
38 db = owners_module.Database(local_root,
39 fopen=file, os_path=os.path, glob=glob.glob)
40 db.reviewers_for(files, None)
41
42 for file_name in self.files:
43 owners_set = set()
44 if file_name in db.owners_for:
45 owners_set = owners_set | db.owners_for[file_name]
46 else:
47 dir_name = file_name
48 while dir_name != '':
49 if dir_name in db.stop_looking:
50 break
51 dir_name = os.path.dirname(dir_name)
52 if dir_name in db.owners_for:
53 if owners_module.EVERYONE in db.owners_for[dir_name]:
54 break
55 owners_set = owners_set | db.owners_for[dir_name]
56
57 # Eliminate files that EVERYONE can review
58 if owners_module.EVERYONE in owners_set:
59 continue
60 file_to_owners[file_name] = owners_set
61
62 self.original_files_to_owners = copy.deepcopy(file_to_owners)
63
64 for owner_name in db.owned_by:
65 if owner_name == owners_module.EVERYONE:
66 continue
67 files_set = set()
68 for file_name in self.files:
69 if owner_name in file_to_owners[file_name]:
70 files_set.add(file_name)
71 if len(files_set) > 0:
72 owner_to_files[owner_name] = files_set
73
74 self.comments = db.comments
75 self.unreviewedFiles = set(self.files)
76 self.usedOwners = set()
77 self.unusedOwners = set()
78
79 def reset(self):
80 self.file_to_owners = copy.deepcopy(self.original_files_to_owners)
81 self.unreviewedFiles = set(self.files)
82 self.usedOwners = set()
83 self.unusedOwners = set()
84
85 def bold(self, text):
86 return (self.COLOR_BOLD +
87 text + self.COLOR_RESET)
88
89 def bold_name(self, name):
90 return (self.COLOR_BOLD +
91 name.replace("@chromium.org", "") + self.COLOR_RESET)
92
93 def greyed(self, text):
94 return self.COLOR_GREY + text + self.COLOR_RESET
95
96 def unuseOwner(self, owner):
97 self.unusedOwners.add(owner)
98 for file_name in self.owner_to_files[owner]:
99 self.file_to_owners[file_name].remove(owner)
100 self.findMustPickOwners()
101
102 def useOwner(self, owner):
103 self.usedOwners.add(owner)
104 for file_name in self.owner_to_files[owner]:
105 if file_name in self.unreviewedFiles:
106 self.unreviewedFiles.remove(file_name)
107 self.findMustPickOwners()
108
109 def findMustPickOwners(self):
110 continues = True
111 while continues:
112 continues = False
113 for file_name in self.unreviewedFiles:
114 if len(self.file_to_owners[file_name]) == 1:
115 self.useOwner(first(self.file_to_owners[file_name]))
116 continues = True
117 break
118
119 def printComments(self, owner):
120 if owner not in self.comments:
121 print self.bold_name(owner)
122 else:
123 print self.bold_name(owner) + " is commented as:"
124 for path in self.comments[owner]:
125 if len(self.comments[owner][path]) > 0:
126 print (self.greyed(" " + self.comments[owner][path]) + " (at " +
127 self.bold(path or "<root>") + ")")
128 else:
129 print (self.greyed(" [No comment] ") + " (at " +
130 self.bold(path or "<root>") + ")")
131
132 def printOwnedFiles(self, owner):
133 # Print owned files
134 self.printComments(owner)
135 print
136 print (self.bold_name(owner) + " owns " +
137 str(len(self.owner_to_files[owner])) + " file(s):")
138 for file_name in sorted(self.owner_to_files[owner]):
139 if file_name not in self.unreviewedFiles:
140 print " " + self.greyed(file_name)
141 else:
142 if len(self.file_to_owners[file_name]) <= 3:
143 other_owners = []
144 for ow in self.file_to_owners[file_name]:
145 if ow != owner:
146 other_owners.append(self.bold_name(ow))
147 print (" " + file_name +
148 " [" + (", ".join(other_owners)) + "]")
149 else:
150 print (" " + file_name +
151 " [" + self.bold(str(len(self.file_to_owners[file_name]) - 1)) + "]")
152 print
153
154 def listOwners(self, owners_queue):
155 if (len(self.owner_to_files) - len(self.unusedOwners) -
156 len(self.usedOwners)) > 3:
157 for ow in owners_queue:
158 if ow not in self.unusedOwners and ow not in self.usedOwners:
159 self.printComments(ow)
160 else:
161 for ow in owners_queue:
162 if ow not in self.unusedOwners and ow not in self.usedOwners:
163 self.printOwnedFiles(ow)
164
165 def listFiles(self):
166 if len(self.unreviewedFiles) > 5:
167 for file_name in sorted(self.unreviewedFiles):
168 print (file_name + " [" +
169 self.bold(str(len(self.file_to_owners[file_name]))) + "]")
170 else:
171 for file_name in self.unreviewedFiles:
172 print file_name
173 for ow in sorted(self.file_to_owners[file_name]):
174 if ow in self.unusedOwners:
175 print " " + self.bold_name(self.greyed(ow))
176 elif ow in self.usedOwners:
177 print " " + self.bold_name(self.greyed(ow))
178 else:
179 print " " + self.bold_name(ow)
180
181 def pickOwner(self):
182 ow = raw_input("Pick an owner:")
183
184 # Allowing to omit domain suffixes
185 if ow not in self.owner_to_files:
186 if ow + "@chromium.org" in self.owner_to_files:
187 ow += "@chromium.org"
188
189 if ow not in self.owner_to_files:
190 print ("You cannot pick " + self.bold_name(ow) + " manually." +
191 "It's an invalid name or not related to the change list.")
192 return False
193 elif ow in self.usedOwners:
194 print ("You cannot pick " + self.bold_name(ow) + " manually. " +
195 "It's already selected.")
196 return False
197 elif ow in self.unusedOwners:
198 print ("You cannot pick " + self.bold_name(ow) + " manually." +
199 "It's already unselected.")
200 return False
201 elif not any(f in self.unreviewedFiles for f in self.owner_to_files[ow]):
202 while True:
203 inp = raw_input("Picking " + self.bold_name(ow) +
204 " does not cover any unreviewed file. "
205 "Are you sure? [yes/No]").lower()
206 if inp == "" or inp == "n" or inp == "no":
207 return False
208 elif inp == "y" or inp == "yes":
209 break
210
211 self.useOwner(ow)
212 return True
213
214 def run(self):
215 self.reset()
216
217 # Initialize owners queue, sort it by the number of files
218 # each owns
219 owners_queue = list(sorted(self.owner_to_files.keys(),
220 key=lambda owner: len(self.owner_to_files[owner]),
221 reverse=True))
222
223 self.findMustPickOwners()
224 while len(owners_queue) > 0 and len(self.unreviewedFiles) > 0:
225 if len(self.unreviewedFiles) == 0:
226 print "Finished.\n\n"
227 break
228 owner = owners_queue[0]
229 try:
230 if owner in self.usedOwners:
231 # If this owner is already picked automatically
232 continue
233 if not any((file in self.unreviewedFiles)
234 for file in self.owner_to_files[owner]):
235 self.unuseOwner(owner)
236 continue
237 print "====================="
238 print self.bold(str(len(self.unreviewedFiles))) + " file(s) left."
239 self.printOwnedFiles(owner)
240 while True:
241 print "Add " + self.bold_name(owner) + " as your reviewer? "
242 inp = raw_input("[yes/no/Defer/pick/files/owners/quit]").lower()
scheib 2013/03/11 20:06:36 It's awkward to respond to the prompt with my inse
Bei Zhang 2013/03/11 20:14:26 Done.
243 if inp == "y" or inp == "yes":
244 self.useOwner(owner)
245 break
246 elif inp == "n" or inp == "no":
247 self.unuseOwner(owner)
248 break
249 elif inp == "" or inp == "d" or inp == "defer":
250 owners_queue.append(owner)
251 break
252 elif inp == "f" or inp == "files":
253 self.listFiles()
254 continue
255 elif inp == "o" or inp == "owners":
256 self.listOwners(owners_queue)
257 continue
258 elif inp == "p" or inp == "pick":
259 if self.pickOwner():
260 if owner in self.usedOwners:
261 break
262 elif inp == "q" or inp == "quit":
263 # Exit with error
264 return 1
265 finally:
266 owners_queue.pop(0)
267 # End of queue
268
269 # Print results
270 print "** You picked these owners **"
271 for owner in self.usedOwners:
272 print self.bold_name(owner) + ":"
273 for file_name in sorted(self.owner_to_files[owner]):
274 print " " + file_name
275 return 0
276
277 def main():
278 cl = git_cl.Changelist()
279 local_root = os.path.abspath(
280 git_cl.RunGit(['rev-parse', '--show-cdup']).strip() or '.')
281 base_branch = git_cl.RunGit(
282 ['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
283 changes = cl.GetChange(base_branch, None)
284 files = [f.LocalPath() for f in changes.AffectedFiles()]
285 return FindOwners(files, local_root).run()
286
287 if __name__ == "__main__":
288 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