diff options
Diffstat (limited to 'WebKitTools/Scripts/bugzilla-tool')
-rwxr-xr-x | WebKitTools/Scripts/bugzilla-tool | 918 |
1 files changed, 49 insertions, 869 deletions
diff --git a/WebKitTools/Scripts/bugzilla-tool b/WebKitTools/Scripts/bugzilla-tool index 8e899b5..fdbb740 100755 --- a/WebKitTools/Scripts/bugzilla-tool +++ b/WebKitTools/Scripts/bugzilla-tool @@ -31,894 +31,74 @@ # A tool for automating dealing with bugzilla, posting patches, committing patches, etc. import os -import re -import StringIO # for add_patch_to_bug file wrappers -import subprocess -import sys -import time -from datetime import datetime, timedelta -from optparse import OptionParser, IndentedHelpFormatter, SUPPRESS_USAGE, make_option - -# Import WebKit-specific modules. -from modules.bugzilla import Bugzilla, parse_bug_id -from modules.changelogs import ChangeLog -from modules.comments import bug_comment_from_commit_text -from modules.logging import error, log, tee -from modules.scm import CommitMessage, detect_scm_system, ScriptError, CheckoutNeedsUpdate +from modules.bugzilla import Bugzilla from modules.buildbot import BuildBot -from modules.statusbot import StatusBot - -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) - -def commit_message_for_this_commit(scm): - changelog_paths = scm.modified_changelogs() - if not len(changelog_paths): - raise ScriptError(message="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 changelog_path in changelog_paths: - log("Parsing ChangeLog: %s" % changelog_path) - changelog_entry = ChangeLog(changelog_path).latest_entry() - if not changelog_entry: - raise ScriptError(message="Failed to parse ChangeLog: " + os.path.abspath(changelog_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("--local-commit", action="store_true", dest="local_commit", default=False, help="Make a local commit for each applied patch"), - ] - options += WebKitLandingScripts.cleaning_options() - 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) - - -class WebKitLandingScripts: - @staticmethod - def cleaning_options(): - return [ - 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"), - ] - - @staticmethod - def land_options(): - return [ - make_option("--ignore-builders", action="store_false", dest="check_builders", default=True, help="Don't check to see if the build.webkit.org builders are green before landing."), - 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)."), - ] - - @staticmethod - def run_command_with_teed_output(args, teed_output): - child_process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - - # Use our own custom wait loop because Popen ignores a tee'd stderr/stdout. - # FIXME: This could be improved not to flatten output to stdout. - while True: - output_line = child_process.stdout.readline() - if output_line == '' and child_process.poll() != None: - return child_process.poll() - teed_output.write(output_line) - - @staticmethod - def run_and_throw_if_fail(args, quiet=False): - # Cache the child's output locally so it can be used for error reports. - child_out_file = StringIO.StringIO() - if quiet: - dev_null = open(os.devnull, "w") - child_stdout = tee(child_out_file, dev_null if quiet else sys.stdout) - exit_code = WebKitLandingScripts.run_command_with_teed_output(args, child_stdout) - if quiet: - dev_null.close() - - child_output = child_out_file.getvalue() - child_out_file.close() - - if exit_code: - raise ScriptError(script_args=args, exit_code=exit_code, output=child_output) - - # 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): - log("Running %s" % 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) - - @staticmethod - def ensure_builders_are_green(buildbot, options): - if not options.check_builders or buildbot.core_builders_are_green(): - return - error("Builders at %s are red, please do not commit. Pass --ignore-builders to bypass this check." % (buildbot.buildbot_host)) - - @classmethod - def run_webkit_tests(cls, launch_safari, fail_fast=False, quiet=False): - args = [cls.webkit_script_path("run-webkit-tests")] - if not launch_safari: - args.append("--no-launch-safari") - if quiet: - args.append("--quiet") - if fail_fast: - args.append("--exit-after-n-failures=1") - 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: - # When running the commit-queue we don't want to launch Safari and we want to exit after the first failure. - cls.run_webkit_tests(launch_safari=not options.commit_queue, fail_fast=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) - - -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."), - ] - options += WebKitLandingScripts.land_options() - 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 - - for changelog_path in tool.scm().modified_changelogs(): - ChangeLog(changelog_path).set_reviewer(reviewer) - - def execute(self, options, args, tool): - bug_id = args[0] if len(args) else None - os.chdir(tool.scm().checkout_root) - - WebKitLandingScripts.ensure_builders_are_green(tool.buildbot, options) - - self.update_changelogs_with_reviewer(options.reviewer, bug_id, tool) - - comment_text = WebKitLandingScripts.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 = WebKitLandingScripts.cleaning_options() - options += WebKitLandingScripts.land_options() - Command.__init__(self, 'Lands all patches on a bug optionally testing them first', 'BUGID', options=options) - - @staticmethod - def handled_error(error): - log(error) - exit(2) # Exit 2 insted of 1 to indicate to the commit-queue to indicate we handled the error, and that the queue should keep looping. - - @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 - log("Applying %s from bug %s." % (patch['id'], bug_id)) - tool.scm().apply_patch(patch, force=options.commit_queue) - # Make sure the tree is still green after updating, before building this patch. - # The first patch ends up checking tree status twice, but that's OK. - WebKitLandingScripts.ensure_builders_are_green(tool.buildbot, options) - comment_text = WebKitLandingScripts.build_and_commit(tool.scm(), options) - tool.bugs.clear_attachment_flags(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 CheckoutNeedsUpdate, e: - log("Commit was rejected because the checkout is out of date. Please update and try again.") - log("You can pass --no-build to skip building/testing after update if you believe the new commits did not affect the results.") - cls.handled_error(e) - except ScriptError, e: - # Mark the patch as commit-queue- and comment in the bug. - tool.bugs.reject_patch_from_commit_queue(patch['id'], e.message_with_output()) - cls.handled_error(e) - - @staticmethod - def _fetch_list_of_patches_to_land(options, args, tool): - 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, reject_invalid_patches=True) - else: - patches = tool.bugs.fetch_reviewed_patches_from_bug(bug_id) - - patches_found = len(patches) - log("%s found on bug %s." % (pluralize("reviewed patch", patches_found), bug_id)) - - patch_count += patches_found - if patches_found: - bugs_to_patches[bug_id] = patches - - log("Landing %s from %s." % (pluralize("patch", patch_count), pluralize("bug", len(args)))) - return bugs_to_patches - - def execute(self, options, args, tool): - if not len(args): - error("bug-id(s) required") - - # Check the tree status here so we can fail early - WebKitLandingScripts.ensure_builders_are_green(tool.buildbot, options) - - bugs_to_patches = self._fetch_list_of_patches_to_land(options, args, tool) - - WebKitLandingScripts.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']) - +from modules.buildsteps import BuildSteps +from modules.commands.download import * +from modules.commands.early_warning_system import * +from modules.commands.queries import * +from modules.commands.queues import * +from modules.commands.upload import * +from modules.executive import Executive +from modules.logging import log +from modules.multicommandtool import MultiCommandTool +from modules.scm import detect_scm_system + +class BugzillaTool(MultiCommandTool): + def __init__(self): + MultiCommandTool.__init__(self) + self.global_option_parser.add_option("--dry-run", action="callback", help="do not touch remote servers", callback=self.dry_run_callback) -class PostDiffAsPatchToBug(Command): - def __init__(self): - options = [ - make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: 'patch')"), - ] - options += self.posting_options() - Command.__init__(self, 'Attaches the current working directory diff to a bug as a patch file.', '[BUGID]', options=options) - - @staticmethod - def posting_options(): - return [ - 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("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."), - ] - - @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): - # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs). - bug_id = (args and args[0]) or parse_bug_id(tool.scm().create_patch()) - if not bug_id: - error("No bug id passed and no bug url found in diff, can't post.") - - 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, mark_for_commit_queue=options.request_commit) - - -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("--add-log-as-comment", action="store_true", dest="add_log_as_comment", default=False, help="Add commit log message as a comment when uploading the patch."), - make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"), - ] - options += PostDiffAsPatchToBug.posting_options() - Command.__init__(self, 'Attaches a range of local commits to bugs as patch files.', 'COMMITISH', options=options, requires_local_commits=True) - - def _comment_text_for_commit(self, options, commit_message, tool, commit_id): - comment_text = None - if (options.add_log_as_comment): - comment_text = commit_message.body(lstrip=True) - comment_text += "---\n" - comment_text += tool.scm().files_changed_summary_for_commit(commit_id) - return comment_text - - def _diff_file_for_commit(self, tool, commit_id): - diff = tool.scm().create_patch_from_local_commit(commit_id) - return StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object - - 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: # We could lower this limit, 10 is too many for one bug as-is. - error("bugzilla-tool does not support attaching %s at once. Are you sure you passed the right commit range?" % (pluralize('patch', len(commit_ids)))) - - have_obsoleted_patches = set() - for commit_id in commit_ids: - commit_message = tool.scm().commit_message_for_local_commit(commit_id) - - # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs). - bug_id = options.bug_id or parse_bug_id(commit_message.message()) or parse_bug_id(tool.scm().create_patch_from_local_commit(commit_id)) - if not bug_id: - log("Skipping %s: No bug id found in commit 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) - - diff_file = self._diff_file_for_commit(tool, commit_id) - description = options.description or commit_message.description(lstrip=True, strip_url=True) - comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id) - tool.bugs.add_patch_to_bug(bug_id, diff_file, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit) - - -class RolloutCommit(Command): - def __init__(self): - options = WebKitLandingScripts.land_options() - options += WebKitLandingScripts.cleaning_options() - options.append(make_option("--complete-rollout", action="store_true", dest="complete_rollout", help="Experimental support for complete unsupervised rollouts, including re-opening the bug. Not recommended.")) - Command.__init__(self, 'Reverts the given revision and commits the revert and re-opens the original bug.', 'REVISION [BUGID]', options=options) - - @staticmethod - def _create_changelogs_for_revert(scm, revision): - # First, discard the ChangeLog changes from the rollout. - changelog_paths = scm.modified_changelogs() - scm.revert_files(changelog_paths) - - # Second, make new ChangeLog entries for this rollout. - # This could move to prepare-ChangeLog by adding a --revert= option. - WebKitLandingScripts.run_webkit_script("prepare-ChangeLog") - for changelog_path in changelog_paths: - ChangeLog(changelog_path).update_for_revert(revision) - - @staticmethod - def _parse_bug_id_from_revision_diff(tool, revision): - original_diff = tool.scm().diff_for_revision(revision) - return parse_bug_id(original_diff) - - @staticmethod - def _reopen_bug_after_rollout(tool, bug_id, comment_text): - if bug_id: - tool.bugs.reopen_bug(bug_id, comment_text) - else: - log(comment_text) - log("No bugs were updated or re-opened to reflect this rollout.") - - def execute(self, options, args, tool): - if not args: - error("REVISION is required, see --help.") - revision = args[0] - bug_id = self._parse_bug_id_from_revision_diff(tool, revision) - if options.complete_rollout: - if bug_id: - log("Will re-open bug %s after rollout." % bug_id) - else: - log("Failed to parse bug number from diff. No bugs will be updated/reopened after the rollout.") - - WebKitLandingScripts.setup_for_landing(tool.scm(), options) - tool.scm().update_webkit() - tool.scm().apply_reverse_diff(revision) - self._create_changelogs_for_revert(tool.scm(), revision) - - # FIXME: Fully automated rollout is not 100% idiot-proof yet, so for now just log with instructions on how to complete the rollout. - # Once we trust rollout we will remove this option. - if not options.complete_rollout: - log("\nNOTE: Rollout support is experimental.\nPlease verify the rollout diff and use 'bugzilla-tool land-diff %s' to commit the rollout." % bug_id) - else: - comment_text = WebKitLandingScripts.build_and_commit(tool.scm(), options) - self._reopen_bug_after_rollout(tool, bug_id, comment_text) - - -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."), - make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after 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, mark_for_commit_queue=options.request_commit) - - 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, mark_for_commit_queue=options.request_commit) - - 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() - try: - sys.stdin.seek(0, os.SEEK_END) - except IOError: - # Cygwin raises an Illegal Seek (errno 29) exception when the above - # seek() call is made. Ignoring it seems to cause no harm. - # FIXME: Figure out a way to get avoid the exception in the first - # place. - pass - else: - raise - 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 CheckTreeStatus(Command): - def __init__(self): - Command.__init__(self, 'Print out the status of the webkit builders.') - - def execute(self, options, args, tool): - for builder in tool.buildbot.builder_statuses(): - status_string = "ok" if builder['is_green'] else 'FAIL' - print "%s : %s" % (status_string.ljust(4), builder['name']) - - -class LandPatchesFromCommitQueue(Command): - def __init__(self): - options = [ - make_option("--is-relaunch", action="store_true", dest="is_relaunch", default=False, help="Internal: Used by the queue to indicate that it's relaunching itself."), - make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue. Dangerous!"), - make_option("--status-host", action="store", type="string", dest="status_host", default=StatusBot.default_host, help="Do not ask the user for confirmation before running the queue. Dangerous!"), - ] - Command.__init__(self, 'Run the commit queue.', options=options) - self._original_stdout = None - self._original_stderr = None - self._files_for_output = [] - - queue_log_path = 'commit_queue.log' - bug_logs_directory = 'commit_queue_logs' - - log_date_format = "%Y-%m-%d %H:%M:%S" - sleep_duration_text = "5 mins" - seconds_to_sleep = 300 - - def _tee_outputs_to_files(self, files): - if not self._original_stdout: - self._original_stdout = sys.stdout - self._original_stderr = sys.stderr - if files and len(files): - sys.stdout = tee(self._original_stdout, *files) - sys.stderr = tee(self._original_stderr, *files) - else: - sys.stdout = self._original_stdout - sys.stderr = self._original_stderr - - @classmethod - def _sleep_message(cls, message): - wake_time = datetime.now() + timedelta(seconds=cls.seconds_to_sleep) - return "%s Sleeping until %s (%s)." % (message, wake_time.strftime(cls.log_date_format), cls.sleep_duration_text) - - def _sleep(self, message): - log(self._sleep_message(message)) - time.sleep(self.seconds_to_sleep) - self._next_patch() - - def _update_status_and_sleep(self, message): - status_message = self._sleep_message(message) - self.status_bot.update_status(status_message) - log(status_message) - time.sleep(self.seconds_to_sleep) - self._next_patch() - - def _next_patch(self): - # Re-exec this script to catch any updates to the script. - # Make sure that the re-execed commit-queue does not wait for the user. - args = sys.argv[:] - if args.count("--is-relaunch") == 0: - args.append("--is-relaunch") - os.execvp(sys.argv[0], args) - - @staticmethod - def _open_log_file(log_path): - (log_directory, log_name) = os.path.split(log_path) - if log_directory and not os.path.exists(log_directory): - os.makedirs(log_directory) - return open(log_path, 'a+') - - def _add_log_to_output_tee(self, path): - log_file = self._open_log_file(path) - self._files_for_output.append(log_file) - self._tee_outputs_to_files(self._files_for_output) - return log_file - - def _remove_log_from_output_tee(self, log_file): - self._files_for_output.remove(log_file) - self._tee_outputs_to_files(self._files_for_output) - log_file.close() - - def execute(self, options, args, tool): - if not options.is_relaunch: - log("CAUTION: commit-queue will discard all local changes in %s" % tool.scm().checkout_root) - if options.confirm: - response = raw_input("Are you sure? Type 'yes' to continue: ") - if (response != 'yes'): - error("User declined.") - - queue_log = self._add_log_to_output_tee(self.queue_log_path) - if not options.is_relaunch: - log("Running WebKit Commit Queue. %s" % datetime.now().strftime(self.log_date_format)) - - self.status_bot = StatusBot(host=options.status_host) - - # Either of these calls could throw URLError which shouldn't stop the queue. - # We catch all exceptions just in case. - try: - # Fetch patches instead of just bug ids to that we validate reviewer/committer flags on every patch. - patches = tool.bugs.fetch_patches_from_commit_queue(reject_invalid_patches=True) - if not len(patches): - self._update_status_and_sleep("Empty queue.") - patch_ids = map(lambda patch: patch['id'], patches) - first_bug_id = patches[0]['bug_id'] - log("%s in commit queue [%s]" % (pluralize('patch', len(patches)), ", ".join(patch_ids))) - - red_builders_names = tool.buildbot.red_core_builders_names() - if red_builders_names: - red_builders_names = map(lambda name: '"%s"' % name, red_builders_names) # Add quotes around the names. - self._update_status_and_sleep("Builders [%s] are red. See http://build.webkit.org." % ", ".join(red_builders_names)) - - self.status_bot.update_status("Landing patches from bug %s." % first_bug_id, bug_id=first_bug_id) - except Exception, e: - # Don't try tell the status bot, in case telling it causes an exception. - self._sleep("Exception while checking queue and bots: %s." % e) - - # Try to land patches on the first bug in the queue before looping - bug_log_path = os.path.join(self.bug_logs_directory, "%s.log" % first_bug_id) - bug_log = self._add_log_to_output_tee(bug_log_path) - bugzilla_tool_path = __file__ # re-execute this script - bugzilla_tool_args = [bugzilla_tool_path, 'land-patches', '--force-clean', '--commit-queue', '--quiet', first_bug_id] - try: - WebKitLandingScripts.run_and_throw_if_fail(bugzilla_tool_args) - except ScriptError, e: - # Unexpected failure! Mark the patch as commit-queue- and comment in the bug. - # exit(2) is a special exit code we use to indicate that the error was already handled by land-patches and we should keep looping anyway. - if e.exit_code != 2: - tool.bugs.reject_patch_from_commit_queue(patch['id'], "Unexpected failure when landing patch! Please file a bug against bugzilla-tool.\n%s" % e.message_with_output()) - self._remove_log_from_output_tee(bug_log) - # self._remove_log_from_output_tee(queue_log) # implicit in the exec() - self._next_patch() - - -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.buildbot = BuildBot() - 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() }, - { 'name' : 'tree-status', 'object' : CheckTreeStatus() }, - { 'name' : 'commit-queue', 'object' : LandPatchesFromCommitQueue() }, - { 'name' : 'rollout', 'object' : RolloutCommit() }, - ] + self.executive = Executive() + self._scm = None + self._status = None + self.steps = BuildSteps() - 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 dry_run_callback(self, option, opt, value, parser): + self.scm().dryrun = True + self.bugs.dryrun = True 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: + original_cwd = os.path.abspath(".") + if not self._scm: + self._scm = detect_scm_system(original_cwd) + + if not self._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: + self._scm = detect_scm_system(webkit_directory) + if self._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") + return self._scm - command_object = command['object'] + def status(self): + if not self._status: + self._status = StatusBot() + return self._status - 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)) + def path(self): + return __file__ - (command_options, command_args) = command_object.parse_args(args_after_command_name) - return command_object.execute(command_options, command_args, self) + def should_show_in_main_help(self, command): + if not command.show_in_main_help: + return False + if command.requires_local_commits: + return self.scm().supports_local_commits() + return True + def should_execute_command(self, command): + if command.requires_local_commits and not self.scm().supports_local_commits(): + failure_reason = "%s requires local commits using %s in %s." % (command.name, self.scm().display_name(), self.scm().checkout_root) + return (False, failure_reason) + return (True, None) -def main(): - tool = BugzillaTool() - return tool.main() if __name__ == "__main__": - main() + BugzillaTool().main() |