Chromium Code Reviews| 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 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()) | |
| OLD | NEW |