#!/usr/bin/python # Copyright (c) 2009, Google Inc. All rights reserved. # Copyright (c) 2009 Apple Inc. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # A tool for automating dealing with bugzilla, posting patches, committing patches, etc. import fileinput # inplace file editing for set_reviewer_in_changelog import os import re import StringIO # for add_patch_to_bug file wrappers import subprocess import sys from optparse import OptionParser, IndentedHelpFormatter, SUPPRESS_USAGE, make_option # Import WebKit-specific modules. from modules.bugzilla import Bugzilla from modules.logging import error, log from modules.scm import CommitMessage, detect_scm_system, ScriptError def plural(noun): # This is a dumb plural() implementation which was just enough for our uses. if re.search('h$', noun): return noun + 'es' else: return noun + 's' def pluralize(noun, count): if count != 1: noun = plural(noun) return "%d %s" % (count, noun) # These could be put in some sort of changelogs.py. def latest_changelog_entry(changelog_path): # e.g. 2009-06-03 Eric Seidel changelog_date_line_regexp = re.compile('^(\d{4}-\d{2}-\d{2})' # Consume the date. + '\s+(.+)\s+' # Consume the name. + '<([^<>]+)>$') # And finally the email address. entry_lines = [] changelog = open(changelog_path) try: log("Parsing ChangeLog: " + changelog_path) # The first line should be a date line. first_line = changelog.readline() if not changelog_date_line_regexp.match(first_line): return None entry_lines.append(first_line) for line in changelog: # If we've hit the next entry, return. if changelog_date_line_regexp.match(line): return ''.join(entry_lines) entry_lines.append(line) finally: changelog.close() # We never found a date line! return None def set_reviewer_in_changelog(changelog_path, reviewer): # inplace=1 creates a backup file and re-directs stdout to the file for line in fileinput.FileInput(changelog_path, inplace=1): print line.replace("NOBODY (OOPS!)", reviewer.encode("utf-8")), # Trailing comma suppresses printing newline def modified_changelogs(scm): changelog_paths = [] paths = scm.changed_files() for path in paths: if os.path.basename(path) == "ChangeLog": changelog_paths.append(path) return changelog_paths def parse_bug_id(commit_message): message = commit_message.message() match = re.search("http\://webkit\.org/b/(?P\d+)", message) if match: return match.group('bug_id') match = re.search(Bugzilla.bug_server_regex + "show_bug\.cgi\?id=(?P\d+)", message) if match: return match.group('bug_id') return None def commit_message_for_this_commit(scm): changelog_paths = modified_changelogs(scm) if not len(changelog_paths): raise ScriptError("Found no modified ChangeLogs, cannot create a commit message.\n" "All changes require a ChangeLog. See:\n" "http://webkit.org/coding/contributing.html") changelog_messages = [] for path in changelog_paths: changelog_entry = latest_changelog_entry(path) if not changelog_entry: error("Failed to parse ChangeLog: " + os.path.abspath(path)) changelog_messages.append(changelog_entry) # FIXME: We should sort and label the ChangeLog messages like commit-log-editor does. return CommitMessage(''.join(changelog_messages).splitlines()) class Command: def __init__(self, help_text, argument_names="", options=[], requires_local_commits=False): self.help_text = help_text self.argument_names = argument_names self.options = options self.option_parser = HelpPrintingOptionParser(usage=SUPPRESS_USAGE, add_help_option=False, option_list=self.options) self.requires_local_commits = requires_local_commits def name_with_arguments(self, command_name): usage_string = command_name if len(self.options) > 0: usage_string += " [options]" if self.argument_names: usage_string += " " + self.argument_names return usage_string def parse_args(self, args): return self.option_parser.parse_args(args) def execute(self, options, args, tool): raise NotImplementedError, "subclasses must implement" class BugsInCommitQueue(Command): def __init__(self): Command.__init__(self, 'Bugs in the commit queue') def execute(self, options, args, tool): bug_ids = tool.bugs.fetch_bug_ids_from_commit_queue() for bug_id in bug_ids: print "%s" % bug_id class PatchesInCommitQueue(Command): def __init__(self): Command.__init__(self, 'Patches in the commit queue') def execute(self, options, args, tool): patches = tool.bugs.fetch_patches_from_commit_queue() log("Patches in commit queue:") for patch in patches: print "%s" % patch['url'] class ReviewedPatchesOnBug(Command): def __init__(self): Command.__init__(self, 'r+\'d patches on a bug', 'BUGID') def execute(self, options, args, tool): bug_id = args[0] patches_to_land = tool.bugs.fetch_reviewed_patches_from_bug(bug_id) for patch in patches_to_land: print "%s" % patch['url'] class ApplyPatchesFromBug(Command): def __init__(self): options = [ make_option("--no-update", action="store_false", dest="update", default=True, help="Don't update the working directory before applying patches"), make_option("--force-clean", action="store_true", dest="force_clean", default=False, help="Clean working directory before applying patches (removes local changes and commits)"), make_option("--no-clean", action="store_false", dest="clean", default=True, help="Don't check if the working directory is clean before applying patches"), make_option("--local-commit", action="store_true", dest="local_commit", default=False, help="Make a local commit for each applied patch"), ] Command.__init__(self, 'Applies all patches on a bug to the local working directory without committing.', 'BUGID', options=options) @staticmethod def apply_patches(patches, scm, commit_each): for patch in patches: scm.apply_patch(patch) if commit_each: commit_message = commit_message_for_this_commit(scm) scm.commit_locally_with_message(commit_message.message() or patch['name']) def execute(self, options, args, tool): bug_id = args[0] patches = tool.bugs.fetch_reviewed_patches_from_bug(bug_id) os.chdir(tool.scm().checkout_root) if options.clean: tool.scm().ensure_clean_working_directory(options.force_clean) if options.update: tool.scm().update_webkit() if options.local_commit and not tool.scm().supports_local_commits(): error("--local-commit passed, but %s does not support local commits" % tool.scm().display_name()) self.apply_patches(patches, tool.scm(), options.local_commit) def bug_comment_from_commit_text(scm, commit_text): match = re.search(scm.commit_success_regexp(), commit_text, re.MULTILINE) svn_revision = match.group('svn_revision') commit_text += ("\nhttp://trac.webkit.org/changeset/%s" % svn_revision) return commit_text class LandAndUpdateBug(Command): def __init__(self): options = [ make_option("-r", "--reviewer", action="store", type="string", dest="reviewer", help="Update ChangeLogs to say Reviewed by REVIEWER."), make_option("--no-close", action="store_false", dest="close_bug", default=True, help="Leave bug open after landing."), make_option("--no-build", action="store_false", dest="build", default=True, help="Commit without building first, implies --no-test."), make_option("--no-test", action="store_false", dest="test", default=True, help="Commit without running run-webkit-tests."), make_option("--quiet", action="store_true", dest="quiet", default=False, help="Produce less console output."), make_option("--commit-queue", action="store_true", dest="commit_queue", default=False, help="Run in commit queue mode (no user interaction)."), ] Command.__init__(self, 'Lands the current working directory diff and updates the bug if provided.', '[BUGID]', options=options) def guess_reviewer_from_bug(self, bugs, bug_id): patches = bugs.fetch_reviewed_patches_from_bug(bug_id) if len(patches) != 1: log("%s on bug %s, cannot infer reviewer." % (pluralize("reviewed patch", len(patches)), bug_id)) return None patch = patches[0] reviewer = patch['reviewer'] log('Guessing "%s" as reviewer from attachment %s on bug %s.' % (reviewer, patch['id'], bug_id)) return reviewer def update_changelogs_with_reviewer(self, reviewer, bug_id, tool): if not reviewer: if not bug_id: log("No bug id provided and --reviewer= not provided. Not updating ChangeLogs with reviewer.") return reviewer = self.guess_reviewer_from_bug(tool.bugs, bug_id) if not reviewer: log("Failed to guess reviewer from bug %s and --reviewer= not provided. Not updating ChangeLogs with reviewer." % bug_id) return changelogs = modified_changelogs(tool.scm()) for changelog in changelogs: set_reviewer_in_changelog(changelog, reviewer) def execute(self, options, args, tool): bug_id = args[0] if len(args) else None os.chdir(tool.scm().checkout_root) self.update_changelogs_with_reviewer(options.reviewer, bug_id, tool) comment_text = LandPatchesFromBugs.build_and_commit(tool.scm(), options) if bug_id: log("Updating bug %s" % bug_id) if options.close_bug: tool.bugs.close_bug_as_fixed(bug_id, comment_text) else: # FIXME: We should a smart way to figure out if the patch is attached # to the bug, and if so obsolete it. tool.bugs.post_comment_to_bug(bug_id, comment_text) else: log(comment_text) log("No bug id provided.") class LandPatchesFromBugs(Command): def __init__(self): options = [ make_option("--force-clean", action="store_true", dest="force_clean", default=False, help="Clean working directory before applying patches (removes local changes and commits)"), make_option("--no-clean", action="store_false", dest="clean", default=True, help="Don't check if the working directory is clean before applying patches"), make_option("--no-close", action="store_false", dest="close_bug", default=True, help="Leave bug open after landing."), make_option("--no-build", action="store_false", dest="build", default=True, help="Commit without building first, implies --no-test."), make_option("--no-test", action="store_false", dest="test", default=True, help="Commit without running run-webkit-tests."), make_option("--quiet", action="store_true", dest="quiet", default=False, help="Produce less console output."), make_option("--commit-queue", action="store_true", dest="commit_queue", default=False, help="Run in commit queue mode (no user interaction)."), ] Command.__init__(self, 'Lands all patches on a bug optionally testing them first', 'BUGID', options=options) @staticmethod def run_and_throw_if_fail(args, quiet=False): child_stdout = subprocess.PIPE if quiet else None child_process = subprocess.Popen(args, stdout=child_stdout) if child_process.stdout: child_process.communicate() return_code = child_process.wait() if return_code: raise ScriptError("%s failed with exit code %d" % (" ".join(args), return_code)) # We might need to pass scm into this function for scm.checkout_root @staticmethod def webkit_script_path(script_name): return os.path.join("WebKitTools", "Scripts", script_name) @classmethod def run_webkit_script(cls, script_name, quiet=False): print "Running WebKit Script " + script_name cls.run_and_throw_if_fail(cls.webkit_script_path(script_name), quiet) @classmethod def build_webkit(cls, quiet=False): cls.run_webkit_script("build-webkit", quiet) @classmethod def run_webkit_tests(cls, launch_safari, quiet=False): args = [cls.webkit_script_path("run-webkit-tests")] if not launch_safari: args.append("--no-launch-safari") if quiet: args.append("--quiet") cls.run_and_throw_if_fail(args) @staticmethod def setup_for_landing(scm, options): os.chdir(scm.checkout_root) scm.ensure_no_local_commits(options.force_clean) if options.clean: scm.ensure_clean_working_directory(options.force_clean) @classmethod def build_and_commit(cls, scm, options): if options.build: cls.build_webkit(quiet=options.quiet) if options.test: cls.run_webkit_tests(launch_safari=not options.commit_queue, quiet=options.quiet) commit_message = commit_message_for_this_commit(scm) commit_log = scm.commit_with_message(commit_message.message()) return bug_comment_from_commit_text(scm, commit_log) @classmethod def land_patches(cls, bug_id, patches, options, tool): try: comment_text = "" for patch in patches: tool.scm().update_webkit() # Update before every patch in case the tree has changed tool.scm().apply_patch(patch, force=options.commit_queue) comment_text = cls.build_and_commit(tool.scm(), options) tool.bugs.clear_attachment_review_flag(patch['id'], comment_text) if options.close_bug: tool.bugs.close_bug_as_fixed(bug_id, "All reviewed patches have been landed. Closing bug.") except ScriptError, e: # We should add a comment to the bug, and r- the patch on failure error(e) def execute(self, options, args, tool): if not len(args): error("bug-id(s) required") bugs_to_patches = {} patch_count = 0 for bug_id in args: patches = [] if options.commit_queue: patches = tool.bugs.fetch_commit_queue_patches_from_bug(bug_id) else: patches = tool.bugs.fetch_reviewed_patches_from_bug(bug_id) if not len(patches): log("No reviewed patches found on %s." % bug_id) continue patch_count += len(patches) bugs_to_patches[bug_id] = patches log("Landing %s from %s." % (pluralize("patch", patch_count), pluralize("bug", len(args)))) self.setup_for_landing(tool.scm(), options) for bug_id in bugs_to_patches.keys(): self.land_patches(bug_id, bugs_to_patches[bug_id], options, tool) class CommitMessageForCurrentDiff(Command): def __init__(self): Command.__init__(self, 'Prints a commit message suitable for the uncommitted changes.') def execute(self, options, args, tool): os.chdir(tool.scm().checkout_root) print "%s" % commit_message_for_this_commit(tool.scm()).message() class ObsoleteAttachmentsOnBug(Command): def __init__(self): Command.__init__(self, 'Marks all attachments on a bug as obsolete.', 'BUGID') def execute(self, options, args, tool): bug_id = args[0] attachments = tool.bugs.fetch_attachments_from_bug(bug_id) for attachment in attachments: if not attachment['is_obsolete']: tool.bugs.obsolete_attachment(attachment['id']) class PostDiffAsPatchToBug(Command): def __init__(self): options = [ make_option("--no-obsolete", action="store_false", dest="obsolete_patches", default=True, help="Do not obsolete old patches before posting this one."), make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."), make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: 'patch')"), ] Command.__init__(self, 'Attaches the current working directory diff to a bug as a patch file.', 'BUGID', options=options) @staticmethod def obsolete_patches_on_bug(bug_id, bugs): patches = bugs.fetch_patches_from_bug(bug_id) if len(patches): log("Obsoleting %s on bug %s" % (pluralize('old patch', len(patches)), bug_id)) for patch in patches: bugs.obsolete_attachment(patch['id']) def execute(self, options, args, tool): bug_id = args[0] if options.obsolete_patches: self.obsolete_patches_on_bug(bug_id, tool.bugs) diff = tool.scm().create_patch() diff_file = StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object description = options.description or "Patch v1" tool.bugs.add_patch_to_bug(bug_id, diff_file, description, mark_for_review=options.review) class PostCommitsAsPatchesToBug(Command): def __init__(self): options = [ make_option("-b", "--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."), make_option("--no-comment", action="store_false", dest="comment", default=True, help="Do not use commit log message as a comment for the patch."), make_option("--no-obsolete", action="store_false", dest="obsolete_patches", default=True, help="Do not obsolete old patches before posting new ones."), make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."), make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: 'patch')"), ] Command.__init__(self, 'Attaches a range of local commits to bugs as patch files.', 'COMMITISH', options=options, requires_local_commits=True) def execute(self, options, args, tool): if not args: error("%s argument is required" % self.argument_names) commit_ids = tool.scm().commit_ids_from_commitish_arguments(args) if len(commit_ids) > 10: error("Are you sure you want to attach %s patches?" % (pluralize('patch', len(commit_ids)))) # Could add a --patches-limit option. have_obsoleted_patches = set() for commit_id in commit_ids: # FIXME: commit_message is the wrong place to look for the bug_id # the ChangeLogs should have the bug id, but the local commit message might not. commit_message = tool.scm().commit_message_for_local_commit(commit_id) bug_id = options.bug_id or parse_bug_id(commit_message) if not bug_id: log("Skipping %s: No bug id found in commit log or specified with --bug-id." % commit_id) continue if options.obsolete_patches and bug_id not in have_obsoleted_patches: PostDiffAsPatchToBug.obsolete_patches_on_bug(bug_id, tool.bugs) have_obsoleted_patches.add(bug_id) description = options.description or commit_message.description(lstrip=True, strip_url=True) comment_text = None if (options.comment): comment_text = commit_message.body(lstrip=True) comment_text += "---\n" comment_text += tool.scm().files_changed_summary_for_commit(commit_id) diff = tool.scm().create_patch_from_local_commit(commit_id) diff_file = StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object tool.bugs.add_patch_to_bug(bug_id, diff_file, description, comment_text, mark_for_review=options.review) class CreateBug(Command): def __init__(self): options = [ make_option("--cc", action="store", type="string", dest="cc", help="Comma-separated list of email addresses to carbon-copy."), make_option("--component", action="store", type="string", dest="component", help="Component for the new bug."), make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."), make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."), ] Command.__init__(self, 'Create a bug from local changes or local commits.', '[COMMITISH]', options=options) def create_bug_from_commit(self, options, args, tool): commit_ids = tool.scm().commit_ids_from_commitish_arguments(args) if len(commit_ids) > 3: error("Are you sure you want to create one bug with %s patches?" % len(commit_ids)) commit_id = commit_ids[0] bug_title = "" comment_text = "" if options.prompt: (bug_title, comment_text) = self.prompt_for_bug_title_and_comment() else: commit_message = tool.scm().commit_message_for_local_commit(commit_id) bug_title = commit_message.description(lstrip=True, strip_url=True) comment_text = commit_message.body(lstrip=True) comment_text += "---\n" comment_text += tool.scm().files_changed_summary_for_commit(commit_id) diff = tool.scm().create_patch_from_local_commit(commit_id) diff_file = StringIO.StringIO(diff) # create_bug_with_patch expects a file-like object bug_id = tool.bugs.create_bug_with_patch(bug_title, comment_text, options.component, diff_file, "Patch v1", cc=options.cc, mark_for_review=options.review) if bug_id and len(commit_ids) > 1: options.bug_id = bug_id options.obsolete_patches = False # FIXME: We should pass through --no-comment switch as well. PostCommitsAsPatchesToBug.execute(self, options, commit_ids[1:], tool) def create_bug_from_patch(self, options, args, tool): bug_title = "" comment_text = "" if options.prompt: (bug_title, comment_text) = self.prompt_for_bug_title_and_comment() else: commit_message = commit_message_for_this_commit(tool.scm()) bug_title = commit_message.description(lstrip=True, strip_url=True) comment_text = commit_message.body(lstrip=True) diff = tool.scm().create_patch() diff_file = StringIO.StringIO(diff) # create_bug_with_patch expects a file-like object bug_id = tool.bugs.create_bug_with_patch(bug_title, comment_text, options.component, diff_file, "Patch v1", cc=options.cc, mark_for_review=options.review) def prompt_for_bug_title_and_comment(self): bug_title = raw_input("Bug title: ") print("Bug comment (hit ^D on blank line to end):") lines = sys.stdin.readlines() sys.stdin.seek(0, os.SEEK_END) comment_text = ''.join(lines) return (bug_title, comment_text) def execute(self, options, args, tool): if len(args): if (not tool.scm().supports_local_commits()): error("Extra arguments not supported; patch is taken from working directory.") self.create_bug_from_commit(options, args, tool) else: self.create_bug_from_patch(options, args, tool) class NonWrappingEpilogIndentedHelpFormatter(IndentedHelpFormatter): def __init__(self): IndentedHelpFormatter.__init__(self) # The standard IndentedHelpFormatter paragraph-wraps the epilog, killing our custom formatting. def format_epilog(self, epilog): if epilog: return "\n" + epilog + "\n" return "" class HelpPrintingOptionParser(OptionParser): def error(self, msg): self.print_usage(sys.stderr) error_message = "%s: error: %s\n" % (self.get_prog_name(), msg) error_message += "\nType '" + self.get_prog_name() + " --help' to see usage.\n" self.exit(2, error_message) class BugzillaTool: def __init__(self): self.cached_scm = None self.bugs = Bugzilla() self.commands = [ { 'name' : 'bugs-to-commit', 'object' : BugsInCommitQueue() }, { 'name' : 'patches-to-commit', 'object' : PatchesInCommitQueue() }, { 'name' : 'reviewed-patches', 'object' : ReviewedPatchesOnBug() }, { 'name' : 'create-bug', 'object' : CreateBug() }, { 'name' : 'apply-patches', 'object' : ApplyPatchesFromBug() }, { 'name' : 'land-diff', 'object' : LandAndUpdateBug() }, { 'name' : 'land-patches', 'object' : LandPatchesFromBugs() }, { 'name' : 'commit-message', 'object' : CommitMessageForCurrentDiff() }, { 'name' : 'obsolete-attachments', 'object' : ObsoleteAttachmentsOnBug() }, { 'name' : 'post-diff', 'object' : PostDiffAsPatchToBug() }, { 'name' : 'post-commits', 'object' : PostCommitsAsPatchesToBug() }, ] self.global_option_parser = HelpPrintingOptionParser(usage=self.usage_line(), formatter=NonWrappingEpilogIndentedHelpFormatter(), epilog=self.commands_usage()) self.global_option_parser.add_option("--dry-run", action="store_true", dest="dryrun", help="do not touch remote servers", default=False) def scm(self): # Lazily initialize SCM to not error-out before command line parsing (or when running non-scm commands). original_cwd = os.path.abspath('.') if not self.cached_scm: self.cached_scm = detect_scm_system(original_cwd) if not self.cached_scm: script_directory = os.path.abspath(sys.path[0]) webkit_directory = os.path.abspath(os.path.join(script_directory, "../..")) self.cached_scm = detect_scm_system(webkit_directory) if self.cached_scm: log("The current directory (%s) is not a WebKit checkout, using %s" % (original_cwd, webkit_directory)) else: error("FATAL: Failed to determine the SCM system for either %s or %s" % (original_cwd, webkit_directory)) return self.cached_scm @staticmethod def usage_line(): return "Usage: %prog [options] command [command-options] [command-arguments]" def commands_usage(self): commands_text = "Commands:\n" longest_name_length = 0 command_rows = [] scm_supports_local_commits = self.scm().supports_local_commits() for command in self.commands: command_object = command['object'] if command_object.requires_local_commits and not scm_supports_local_commits: continue command_name_and_args = command_object.name_with_arguments(command['name']) command_rows.append({ 'name-and-args': command_name_and_args, 'object': command_object }) longest_name_length = max([longest_name_length, len(command_name_and_args)]) # Use our own help formatter so as to indent enough. formatter = IndentedHelpFormatter() formatter.indent() formatter.indent() for row in command_rows: command_object = row['object'] commands_text += " " + row['name-and-args'].ljust(longest_name_length + 3) + command_object.help_text + "\n" commands_text += command_object.option_parser.format_option_help(formatter) return commands_text def handle_global_args(self, args): (options, args) = self.global_option_parser.parse_args(args) if len(args): # We'll never hit this because split_args splits at the first arg without a leading '-' self.global_option_parser.error("Extra arguments before command: " + args) if options.dryrun: self.scm().dryrun = True self.bugs.dryrun = True @staticmethod def split_args(args): # Assume the first argument which doesn't start with '-' is the command name. command_index = 0 for arg in args: if arg[0] != '-': break command_index += 1 else: return (args[:], None, []) global_args = args[:command_index] command = args[command_index] command_args = args[command_index + 1:] return (global_args, command, command_args) def command_by_name(self, command_name): for command in self.commands: if command_name == command['name']: return command return None def main(self): (global_args, command_name, args_after_command_name) = self.split_args(sys.argv[1:]) # Handle --help, etc: self.handle_global_args(global_args) if not command_name: self.global_option_parser.error("No command specified") command = self.command_by_name(command_name) if not command: self.global_option_parser.error(command_name + " is not a recognized command") command_object = command['object'] if command_object.requires_local_commits and not self.scm().supports_local_commits(): error(command_name + " requires local commits using %s in %s." % (self.scm().display_name(), self.scm().checkout_root)) (command_options, command_args) = command_object.parse_args(args_after_command_name) return command_object.execute(command_options, command_args, self) def main(): tool = BugzillaTool() return tool.main() if __name__ == "__main__": main()