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