Chromium Code Reviews| Index: find_owners.py |
| diff --git a/find_owners.py b/find_owners.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..848e34c96b12d6fe64d856439abf96433512e6ce |
| --- /dev/null |
| +++ b/find_owners.py |
| @@ -0,0 +1,274 @@ |
| +#!/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 owners for reviewing.''' |
| + |
| +import git_cl |
| +import os.path |
| +import sys |
| +import copy |
| +import glob |
| +import owners as owners_module |
| + |
| +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): |
| + self.files = files |
| + 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.
|
| + self.ownership = ownership = {} |
| + |
| + 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 |
| + ownership[file_name] = owners_set |
| + |
| + self.original_ownership = copy.deepcopy(ownership) |
| + |
| + for owner_name in db.owned_by: |
| + if owner_name == owners_module.EVERYONE: |
| + continue |
| + 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.
|
| + for file_name in self.files: |
| + if owner_name in ownership[file_name]: |
| + owned_by.add(file_name) |
| + if len(owned_by) > 0: |
| + owners[owner_name] = owned_by |
| + |
| + self.comments = db.comments |
| + self.ownership = copy.deepcopy(self.original_ownership) |
| + self.unreviewedFiles = set(self.files) |
| + self.usedOwners = set() |
| + self.unusedOwners = set() |
| + |
| + def reset(self): |
| + self.ownership = copy.deepcopy(self.original_ownership) |
| + 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.owners[owner]: |
| + self.ownership[file_name].remove(owner) |
| + self.findMustPickOwners() |
| + |
| + def useOwner(self, owner): |
| + self.usedOwners.add(owner) |
| + for file_name in self.owners[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.ownership[file_name]) == 1: |
| + self.useOwner(first(self.ownership[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.owners[owner])) + " file(s):") |
| + for file_name in sorted(self.owners[owner]): |
| + if file_name not in self.unreviewedFiles: |
| + print " " + self.greyed(file_name) |
| + else: |
| + if len(self.ownership[file_name]) <= 3: |
| + other_owners = [] |
| + for ow in self.ownership[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.ownership[file_name]) - 1)) + "]") |
| + |
| + def run(self): |
| + self.reset() |
| + owners_queue = sorted(self.owners.keys(), |
| + key=lambda owner: len(self.owners[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 not any((file in self.unreviewedFiles) |
| + for file in self.owners[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("[" + |
| + "/".join(["yes" , |
| + "no", |
| + "Defer", |
| + "pick", |
| + "files" , |
| + "owners" , |
| + "quit"]) + "]") |
| + inp = inp.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": |
|
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.
|
| + if len(self.unreviewedFiles) > 5: |
| + for file_name in sorted(self.unreviewedFiles): |
| + print (file_name + " [" + |
| + self.bold(str(len(self.ownership[file_name]))) + "]") |
| + else: |
| + for file_name in self.unreviewedFiles: |
| + print file_name |
| + for ow in sorted(self.ownership[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) |
| + elif inp == "o" or inp == "owners": |
| + if (len(self.owners) - 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) |
| + elif inp == "p" or inp == "pick": |
| + ow = raw_input("Pick an owner:") |
| + if ow not in self.owners: |
| + if ow + "@chromium.org" in self.owners: |
| + ow += "@chromium.org" |
| + |
| + do_pick = False |
| + if ow in self.usedOwners or ow in self.unusedOwners: |
| + print "You cannot pick " + self.bold_name(ow) + " manually." |
| + elif ow in self.owners: |
| + if not any(f in self.unreviewedFiles for f in self.owners[ow]): |
| + while True: |
| + inp = raw_input("Picking " + self.bold_name(ow) + |
| + " does not cover any unreviewed file. " |
| + "Are you sure? [y/N]").lower() |
| + if inp == "y" or inp == "yes": |
| + do_pick = True |
| + break |
| + elif inp == "" or inp == "n" or inp == "no": |
| + break |
| + else: |
| + do_pick = True |
| + else: |
| + print "Invalid owner" |
| + |
| + if do_pick: |
| + self.useOwner(ow) |
| + if owner in self.usedOwners or len(self.unreviewedFiles) == 0: |
| + break |
| + |
| + elif inp == "q" or inp == "quit": |
| + # Exit with error |
| + return 1 |
| + |
| + if len(self.unreviewedFiles) == 0: |
| + print "Finished.\n\n" |
| + break |
| + 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.owners[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()) |