Index: find_owners.py |
diff --git a/find_owners.py b/find_owners.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..01192304ece998ae5b63b69643949419e17214b5 |
--- /dev/null |
+++ b/find_owners.py |
@@ -0,0 +1,288 @@ |
+#!/usr/bin/python |
+# Copyright (c) 2011 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+'''A tool to help picking owner_to_files for reviewing.''' |
+ |
+import git_cl |
+import os.path |
+import sys |
+import copy |
+import glob |
+import owners as owners_module |
+import readline # pylint: disable=W0611 |
Bei Zhang
2013/03/12 17:00:10
Not working on Windows
|
+ |
+def first(iterable): |
+ for element in iterable: |
+ return element |
+ |
+class FindOwners: |
+ COLOR_LINK = '\033[4m' |
+ COLOR_BOLD = '\033[1;32m' |
+ COLOR_GREY = '\033[0;37m' |
+ COLOR_RESET = '\033[0m' |
+ def __init__(self, files, local_root, disable_color=False): |
+ # Files in a change list |
+ self.files = files |
+ |
+ self.owner_to_files = owner_to_files = {} |
+ |
+ self.file_to_owners = file_to_owners = {} |
+ |
+ if os.name == "nt" or disable_color: |
+ self.COLOR_LINK = '' |
+ self.COLOR_BOLD = '' |
+ self.COLOR_GREY = '' |
+ self.COLOR_RESET = '' |
+ |
+ db = owners_module.Database(local_root, |
+ fopen=file, os_path=os.path, glob=glob.glob) |
+ db.reviewers_for(files, None) |
+ |
+ for file_name in self.files: |
+ owners_set = set() |
+ if file_name in db.owners_for: |
+ owners_set = owners_set | db.owners_for[file_name] |
+ else: |
+ dir_name = file_name |
+ while dir_name != '': |
+ if dir_name in db.stop_looking: |
+ break |
+ dir_name = os.path.dirname(dir_name) |
+ if dir_name in db.owners_for: |
+ if owners_module.EVERYONE in db.owners_for[dir_name]: |
+ break |
+ owners_set = owners_set | db.owners_for[dir_name] |
+ |
+ # Eliminate files that EVERYONE can review |
+ if owners_module.EVERYONE in owners_set: |
+ continue |
+ file_to_owners[file_name] = owners_set |
+ |
+ self.original_files_to_owners = copy.deepcopy(file_to_owners) |
+ |
+ for owner_name in db.owned_by: |
+ if owner_name == owners_module.EVERYONE: |
+ continue |
+ files_set = set() |
+ for file_name in self.files: |
+ if owner_name in file_to_owners[file_name]: |
+ files_set.add(file_name) |
+ if len(files_set) > 0: |
+ owner_to_files[owner_name] = files_set |
+ |
+ self.comments = db.comments |
+ self.unreviewedFiles = set(self.files) |
+ self.usedOwners = set() |
+ self.unusedOwners = set() |
+ |
+ def reset(self): |
+ self.file_to_owners = copy.deepcopy(self.original_files_to_owners) |
+ self.unreviewedFiles = set(self.files) |
+ self.usedOwners = set() |
+ self.unusedOwners = set() |
+ |
+ def bold(self, text): |
+ return (self.COLOR_BOLD + |
+ text + self.COLOR_RESET) |
+ |
+ def bold_name(self, name): |
+ return (self.COLOR_BOLD + |
+ name.replace("@chromium.org", "") + self.COLOR_RESET) |
+ |
+ def greyed(self, text): |
+ return self.COLOR_GREY + text + self.COLOR_RESET |
+ |
+ def unuseOwner(self, owner): |
+ self.unusedOwners.add(owner) |
+ for file_name in self.owner_to_files[owner]: |
+ self.file_to_owners[file_name].remove(owner) |
+ self.findMustPickOwners() |
+ |
+ def useOwner(self, owner): |
+ self.usedOwners.add(owner) |
+ for file_name in self.owner_to_files[owner]: |
+ if file_name in self.unreviewedFiles: |
+ self.unreviewedFiles.remove(file_name) |
+ self.findMustPickOwners() |
+ |
+ def findMustPickOwners(self): |
+ continues = True |
+ while continues: |
+ continues = False |
+ for file_name in self.unreviewedFiles: |
+ if len(self.file_to_owners[file_name]) == 1: |
+ self.useOwner(first(self.file_to_owners[file_name])) |
+ continues = True |
+ break |
+ |
+ def printComments(self, owner): |
+ if owner not in self.comments: |
+ print self.bold_name(owner) |
+ else: |
+ print self.bold_name(owner) + " is commented as:" |
+ for path in self.comments[owner]: |
+ if len(self.comments[owner][path]) > 0: |
+ print (self.greyed(" " + self.comments[owner][path]) + " (at " + |
+ self.bold(path or "<root>") + ")") |
+ else: |
+ print (self.greyed(" [No comment] ") + " (at " + |
+ self.bold(path or "<root>") + ")") |
+ |
+ def printOwnedFiles(self, owner): |
+ # Print owned files |
+ self.printComments(owner) |
+ print (self.bold_name(owner) + " owns " + |
+ str(len(self.owner_to_files[owner])) + " file(s):") |
+ for file_name in sorted(self.owner_to_files[owner]): |
+ if file_name not in self.unreviewedFiles: |
+ print " " + self.greyed(file_name) |
+ else: |
+ if len(self.file_to_owners[file_name]) <= 3: |
+ other_owners = [] |
+ for ow in self.file_to_owners[file_name]: |
+ if ow != owner: |
+ other_owners.append(self.bold_name(ow)) |
+ print (" " + file_name + |
+ " [" + (", ".join(other_owners)) + "]") |
+ else: |
+ print (" " + file_name + |
+ " [" + self.bold(str(len(self.file_to_owners[file_name]) - 1)) + "]") |
+ |
+ def listOwners(self, owners_queue): |
+ if (len(self.owner_to_files) - len(self.unusedOwners) - |
+ len(self.usedOwners)) > 3: |
+ for ow in owners_queue: |
+ if ow not in self.unusedOwners and ow not in self.usedOwners: |
+ self.printComments(ow) |
+ else: |
+ for ow in owners_queue: |
+ if ow not in self.unusedOwners and ow not in self.usedOwners: |
+ self.printOwnedFiles(ow) |
+ |
+ def listFiles(self): |
+ if len(self.unreviewedFiles) > 5: |
+ for file_name in sorted(self.unreviewedFiles): |
+ print (file_name + " [" + |
+ self.bold(str(len(self.file_to_owners[file_name]))) + "]") |
+ else: |
+ for file_name in self.unreviewedFiles: |
+ print file_name |
+ for ow in sorted(self.file_to_owners[file_name]): |
+ if ow in self.unusedOwners: |
+ print " " + self.bold_name(self.greyed(ow)) |
+ elif ow in self.usedOwners: |
+ print " " + self.bold_name(self.greyed(ow)) |
+ else: |
+ print " " + self.bold_name(ow) |
+ |
+ def pickOwner(self): |
+ ow = raw_input("Pick an owner:") |
+ |
+ # Allowing to omit domain suffixes |
+ if ow not in self.owner_to_files: |
+ if ow + "@chromium.org" in self.owner_to_files: |
+ ow += "@chromium.org" |
+ |
+ if ow not in self.owner_to_files: |
+ print ("You cannot pick " + self.bold_name(ow) + " manually." + |
+ "It's an invalid name or not related to the change list.") |
+ return False |
+ elif ow in self.usedOwners: |
+ print ("You cannot pick " + self.bold_name(ow) + " manually. " + |
+ "It's already selected.") |
+ return False |
+ elif ow in self.unusedOwners: |
+ print ("You cannot pick " + self.bold_name(ow) + " manually." + |
+ "It's already unselected.") |
+ return False |
+ elif not any(f in self.unreviewedFiles for f in self.owner_to_files[ow]): |
+ while True: |
+ inp = raw_input("Picking " + self.bold_name(ow) + |
+ " does not cover any unreviewed file. " |
+ "Are you sure? [yes/No]").lower() |
+ if inp == "" or inp == "n" or inp == "no": |
+ return False |
+ elif inp == "y" or inp == "yes": |
+ break |
+ |
+ self.useOwner(ow) |
+ return True |
+ |
+ def run(self): |
+ self.reset() |
+ |
+ # Initialize owners queue, sort it by the number of files |
+ # each owns |
+ owners_queue = list(sorted(self.owner_to_files.keys(), |
+ key=lambda owner: len(self.owner_to_files[owner]), |
+ reverse=True)) |
+ |
+ self.findMustPickOwners() |
+ while len(owners_queue) > 0 and len(self.unreviewedFiles) > 0: |
+ if len(self.unreviewedFiles) == 0: |
+ print "Finished.\n\n" |
+ break |
+ owner = owners_queue[0] |
+ try: |
+ if owner in self.usedOwners: |
+ # If this owner is already picked automatically |
+ continue |
+ if not any((file in self.unreviewedFiles) |
+ for file in self.owner_to_files[owner]): |
+ self.unuseOwner(owner) |
+ continue |
+ print "=====================" |
+ print self.bold(str(len(self.unreviewedFiles))) + " file(s) left." |
+ self.printOwnedFiles(owner) |
+ while True: |
+ print "Add " + self.bold_name(owner) + " as your reviewer? " |
+ 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.
|
+ if inp == "y" or inp == "yes": |
+ self.useOwner(owner) |
+ break |
+ elif inp == "n" or inp == "no": |
+ self.unuseOwner(owner) |
+ break |
+ elif inp == "" or inp == "d" or inp == "defer": |
+ owners_queue.append(owner) |
+ break |
+ elif inp == "f" or inp == "files": |
+ self.listFiles() |
+ continue |
+ elif inp == "o" or inp == "owners": |
+ self.listOwners(owners_queue) |
+ continue |
+ elif inp == "p" or inp == "pick": |
+ if self.pickOwner(): |
+ if owner in self.usedOwners: |
+ break |
+ elif inp == "q" or inp == "quit": |
+ # Exit with error |
+ return 1 |
+ finally: |
+ owners_queue.pop(0) |
+ # End of queue |
+ |
+ # Print results |
+ print "** You picked these owners **" |
+ for owner in self.usedOwners: |
+ print self.bold_name(owner) + ":" |
+ for file_name in sorted(self.owner_to_files[owner]): |
+ print " " + file_name |
+ return 0 |
+ |
+def main(): |
+ cl = git_cl.Changelist() |
+ local_root = os.path.abspath( |
+ git_cl.RunGit(['rev-parse', '--show-cdup']).strip() or '.') |
+ base_branch = git_cl.RunGit( |
+ ['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip() |
+ changes = cl.GetChange(base_branch, None) |
+ files = [f.LocalPath() for f in changes.AffectedFiles()] |
+ return FindOwners(files, local_root).run() |
+ |
+if __name__ == "__main__": |
+ sys.exit(main()) |