Index: find_owners.py |
diff --git a/find_owners.py b/find_owners.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..5ba8af2c40d064477f1d6ac9b69bbeb9ac9223fe |
--- /dev/null |
+++ b/find_owners.py |
@@ -0,0 +1,312 @@ |
+#!/usr/bin/python |
+# 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.
|
+# 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 |
+ |
+ |
+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
|
+ for element in iterable: |
+ return element |
+ |
+ |
+class FindOwners: |
Dirk Pranke
2013/04/03 00:54:16
class FindOwners(object):
Bei Zhang
2013/04/08 17:33:38
Done.
|
+ 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): |
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.
|
+ # Files in a change list |
+ 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 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: |
+ owners_set = owners_set | db.owners_for[dir_name] |
+ if owners_module.EVERYONE in owners_set: |
+ 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
|
+ |
+ if len(owners_set) == 0: |
+ raise Exception("File '%s' has no owner" % file_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 file_to_owners: |
+ 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 |
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.
|
+ |
+ self.comments = db.comments |
+ self.unreviewedFiles = set() |
+ self.reviewedBy = {} |
+ self.usedOwners = set() |
+ self.unusedOwners = set() |
+ |
+ def reset(self): |
+ self.file_to_owners = copy.deepcopy(self.original_files_to_owners) |
+ self.unreviewedFiles = set(self.file_to_owners.keys()) |
+ self.reviewedBy = {} |
+ 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]: |
+ if file_name in self.unreviewedFiles: |
+ 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.reviewedBy[file_name] = owner |
+ self.findMustPickOwners() |
+ |
+ 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:
|
+ 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 |
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.
|
+ |
+ 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 + ' (by ' + |
+ self.bold_name(self.reviewedBy[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 = False): |
+ if not ow: |
+ 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 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
|
+ 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: |
+ owner = owners_queue[0] |
+ try: |
+ if owner in self.usedOwners: |
+ continue |
+ if len(self.unreviewedFiles) == 0: |
+ print "Finished.\n\n" |
+ break |
+ if owner in self.unusedOwners: |
+ # 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
|
+ continue |
+ if not any((file_name in self.unreviewedFiles) |
+ for file_name 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/restart]: ").lower() |
+ 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() |
+ elif inp == "o" or inp == "owners": |
+ self.listOwners(owners_queue) |
+ elif inp == "p" or inp == "pick": |
+ self.pickOwner() |
+ owners_queue.insert(0, owner) |
+ break |
+ elif inp.startswith("p ") or inp.startswith("pick "): |
+ self.pickOwner(inp.split(' ', 2)[1]) |
+ owners_queue.insert(0, owner) |
+ break |
+ elif inp == 'r' or inp == 'restart': |
+ return -1 |
+ elif inp == "q" or inp == "quit": |
+ # Exit with error |
+ return 1 |
+ finally: |
+ if len(owners_queue): |
+ 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 run(self): |
+ result = -1 |
+ while result == -1: |
+ result = self.runOnce() |
+ return result |
+ |
+ |
+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.
|
+ 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()) |