2 # Copyright (c) 2009, Google Inc. All rights reserved.
3 # Copyright (c) 2009 Apple Inc. All rights reserved.
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
9 # * Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 # * Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following disclaimer
13 # in the documentation and/or other materials provided with the
15 # * Neither the name of Google Inc. nor the names of its
16 # contributors may be used to endorse or promote products derived from
17 # this software without specific prior written permission.
19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 # A tool for automating dealing with bugzilla, posting patches, committing patches, etc.
35 import StringIO # for add_patch_to_bug file wrappers
40 from datetime import datetime, timedelta
41 from optparse import make_option
43 # Import WebKit-specific modules.
44 from modules.bugzilla import Bugzilla, parse_bug_id
45 from modules.buildbot import BuildBot
46 from modules.changelogs import ChangeLog
47 from modules.landingsequence import ConditionalLandingSequence
48 from modules.logging import error, log, tee
49 from modules.multicommandtool import MultiCommandTool, Command
50 from modules.patchcollection import PatchCollection
51 from modules.scm import CommitMessage, detect_scm_system, ScriptError, CheckoutNeedsUpdate
52 from modules.statusbot import StatusBot
53 from modules.webkitlandingscripts import WebKitLandingScripts, commit_message_for_this_commit
54 from modules.webkitport import WebKitPort
55 from modules.workqueue import WorkQueue, WorkQueueDelegate
58 # This is a dumb plural() implementation which was just enough for our uses.
59 if re.search("h$", noun):
64 def pluralize(noun, count):
67 return "%d %s" % (count, noun)
70 class BugsToCommit(Command):
72 Command.__init__(self, "Bugs in the commit queue")
74 def execute(self, options, args, tool):
75 bug_ids = tool.bugs.fetch_bug_ids_from_commit_queue()
76 for bug_id in bug_ids:
80 class PatchesToCommit(Command):
82 Command.__init__(self, "Patches in the commit queue")
84 def execute(self, options, args, tool):
85 patches = tool.bugs.fetch_patches_from_commit_queue()
86 log("Patches in commit queue:")
88 print "%s" % patch["url"]
91 class ReviewedPatches(Command):
93 Command.__init__(self, "r+\'d patches on a bug", "BUGID")
95 def execute(self, options, args, tool):
97 patches_to_land = tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
98 for patch in patches_to_land:
99 print "%s" % patch["url"]
102 class CheckStyle(Command):
104 options = WebKitLandingScripts.cleaning_options()
105 Command.__init__(self, "Runs check-webkit-style on the specified attachment", "ATTACHMENT_ID", options=options)
108 def check_style(cls, patch, options, tool):
109 tool.scm().update_webkit()
110 log("Checking style for patch %s from bug %s." % (patch["id"], patch["bug_id"]))
112 # FIXME: check-webkit-style shouldn't really have to apply the patch to check the style.
113 tool.scm().apply_patch(patch)
114 WebKitLandingScripts.run_webkit_script("check-webkit-style")
115 except ScriptError, e:
116 log("Patch %s from bug %s failed to apply and check style." % (patch["id"], patch["bug_id"]))
119 # This is safe because in order to get here the working directory had to be
120 # clean at the beginning. Clean it out again before we exit.
121 tool.scm().ensure_clean_working_directory(force_clean=True)
123 def execute(self, options, args, tool):
124 attachment_id = args[0]
125 attachment = tool.bugs.fetch_attachment(attachment_id)
127 WebKitLandingScripts.prepare_clean_working_directory(tool.scm(), options)
128 self.check_style(attachment, options, tool)
131 class ApplyAttachment(Command):
133 options = WebKitApplyingScripts.apply_options() + WebKitLandingScripts.cleaning_options()
134 Command.__init__(self, "Applies an attachment to the local working directory.", "ATTACHMENT_ID", options=options)
136 def execute(self, options, args, tool):
137 WebKitApplyingScripts.setup_for_patch_apply(tool.scm(), options)
138 attachment_id = args[0]
139 attachment = tool.bugs.fetch_attachment(attachment_id)
140 WebKitApplyingScripts.apply_patches_with_options(tool.scm(), [attachment], options)
143 class ApplyPatches(Command):
145 options = WebKitApplyingScripts.apply_options() + WebKitLandingScripts.cleaning_options()
146 Command.__init__(self, "Applies all patches on a bug to the local working directory.", "BUGID", options=options)
148 def execute(self, options, args, tool):
149 WebKitApplyingScripts.setup_for_patch_apply(tool.scm(), options)
151 patches = tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
152 WebKitApplyingScripts.apply_patches_with_options(tool.scm(), patches, options)
155 class WebKitApplyingScripts:
159 make_option("--no-update", action="store_false", dest="update", default=True, help="Don't update the working directory before applying patches"),
160 make_option("--local-commit", action="store_true", dest="local_commit", default=False, help="Make a local commit for each applied patch"),
164 def setup_for_patch_apply(scm, options):
165 WebKitLandingScripts.prepare_clean_working_directory(scm, options, allow_local_commits=True)
170 def apply_patches_with_options(scm, patches, options):
171 if options.local_commit and not scm.supports_local_commits():
172 error("--local-commit passed, but %s does not support local commits" % scm.display_name())
174 for patch in patches:
175 log("Applying attachment %s from bug %s" % (patch["id"], patch["bug_id"]))
176 scm.apply_patch(patch)
177 if options.local_commit:
178 commit_message = commit_message_for_this_commit(scm)
179 scm.commit_locally_with_message(commit_message.message() or patch["name"])
182 class LandDiffLandingSequence(ConditionalLandingSequence):
183 def __init__(self, patch, options, tool):
184 ConditionalLandingSequence.__init__(self, patch, options, tool)
189 def apply_patch(self):
192 def close_patch(self, commit_log):
193 self._comment_test = bug_comment_from_commit_text(self._tool.scm(), commit_log)
194 # There is no patch to close.
197 bug_id = self._patch["bug_id"]
199 log("Updating bug %s" % bug_id)
200 if self._options.close_bug:
201 self._tool.bugs.close_bug_as_fixed(bug_id, self._comment_test)
203 # FIXME: We should a smart way to figure out if the patch is attached
204 # to the bug, and if so obsolete it.
205 self._tool.bugs.post_comment_to_bug(bug_id, self._comment_test)
207 log(self._comment_test)
208 log("No bug id provided.")
211 class LandDiff(Command):
214 make_option("-r", "--reviewer", action="store", type="string", dest="reviewer", help="Update ChangeLogs to say Reviewed by REVIEWER."),
216 options += WebKitLandingScripts.land_options()
217 Command.__init__(self, "Lands the current working directory diff and updates the bug if provided.", "[BUGID]", options=options)
219 def guess_reviewer_from_bug(self, bugs, bug_id):
220 patches = bugs.fetch_reviewed_patches_from_bug(bug_id)
221 if len(patches) != 1:
222 log("%s on bug %s, cannot infer reviewer." % (pluralize("reviewed patch", len(patches)), bug_id))
225 reviewer = patch["reviewer"]
226 log("Guessing \"%s\" as reviewer from attachment %s on bug %s." % (reviewer, patch["id"], bug_id))
229 def update_changelogs_with_reviewer(self, reviewer, bug_id, tool):
232 log("No bug id provided and --reviewer= not provided. Not updating ChangeLogs with reviewer.")
234 reviewer = self.guess_reviewer_from_bug(tool.bugs, bug_id)
237 log("Failed to guess reviewer from bug %s and --reviewer= not provided. Not updating ChangeLogs with reviewer." % bug_id)
240 for changelog_path in tool.scm().modified_changelogs():
241 ChangeLog(changelog_path).set_reviewer(reviewer)
243 def execute(self, options, args, tool):
244 bug_id = (args and args[0]) or parse_bug_id(tool.scm().create_patch())
246 WebKitLandingScripts.ensure_builders_are_green(tool.buildbot, options)
248 os.chdir(tool.scm().checkout_root)
249 self.update_changelogs_with_reviewer(options.reviewer, bug_id, tool)
256 sequence = LandDiffLandingSequence(fake_patch, options, tool)
260 class AbstractPatchProcessingCommand(Command):
261 def __init__(self, description, args_description, options):
262 Command.__init__(self, description, args_description, options=options)
264 def _fetch_list_of_patches_to_process(self, options, args, tool):
265 raise NotImplementedError, "subclasses must implement"
267 def _prepare_to_process(self, options, args, tool):
268 raise NotImplementedError, "subclasses must implement"
271 def _collect_patches_by_bug(patches):
273 for patch in patches:
274 bug_id = patch["bug_id"]
275 bugs_to_patches[bug_id] = bugs_to_patches.get(bug_id, []).append(patch)
276 return bugs_to_patches
278 def execute(self, options, args, tool):
280 error("%s required" % self.argument_names)
282 self._prepare_to_process(options, args, tool)
283 patches = self._fetch_list_of_patches_to_process(options, args, tool)
285 # It's nice to print out total statistics.
286 bugs_to_patches = self._collect_patches_by_bug(patches)
287 log("Processing %s from %s." % (pluralize("patch", len(patches)), pluralize("bug", len(bugs_to_patches))))
289 for patch in patches:
290 self._process_patch(patch, options, args, tool)
293 class AbstractPatchLandingCommand(AbstractPatchProcessingCommand):
294 def __init__(self, description, args_description):
295 options = WebKitLandingScripts.cleaning_options() + WebKitLandingScripts.land_options()
296 AbstractPatchProcessingCommand.__init__(self, description, args_description, options)
298 def _prepare_to_process(self, options, args, tool):
299 # Check the tree status first so we can fail early.
300 WebKitLandingScripts.ensure_builders_are_green(tool.buildbot, options)
301 WebKitLandingScripts.prepare_clean_working_directory(tool.scm(), options)
303 def _process_patch(self, patch, options, args, tool):
304 sequence = ConditionalLandingSequence(patch, options, tool)
305 sequence.run_and_handle_errors()
307 class LandAttachment(AbstractPatchLandingCommand):
309 AbstractPatchLandingCommand.__init__(self, "Lands a patches from bugzilla, optionally building and testing them first", "ATTACHMENT_ID [ATTACHMENT_IDS]")
311 def _fetch_list_of_patches_to_process(self, options, args, tool):
312 return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args)
315 class LandPatches(AbstractPatchLandingCommand):
317 AbstractPatchLandingCommand.__init__(self, "Lands all patches on the given bugs, optionally building and testing them first", "BUGID [BUGIDS]")
319 def _fetch_list_of_patches_to_process(self, options, args, tool):
322 patches = tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
323 log("%s found on bug %s." % (pluralize("reviewed patch", len(patches)), bug_id))
324 all_patches += patches
328 class CommitMessageForCurrentDiff(Command):
330 Command.__init__(self, "Prints a commit message suitable for the uncommitted changes.")
332 def execute(self, options, args, tool):
333 os.chdir(tool.scm().checkout_root)
334 print "%s" % commit_message_for_this_commit(tool.scm()).message()
337 class ObsoleteAttachments(Command):
339 Command.__init__(self, "Marks all attachments on a bug as obsolete.", "BUGID")
341 def execute(self, options, args, tool):
343 attachments = tool.bugs.fetch_attachments_from_bug(bug_id)
344 for attachment in attachments:
345 if not attachment["is_obsolete"]:
346 tool.bugs.obsolete_attachment(attachment["id"])
349 class PostDiff(Command):
352 make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: \"patch\")"),
354 options += self.posting_options()
355 Command.__init__(self, "Attaches the current working directory diff to a bug as a patch file.", "[BUGID]", options=options)
358 def posting_options():
360 make_option("--no-obsolete", action="store_false", dest="obsolete_patches", default=True, help="Do not obsolete old patches before posting this one."),
361 make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
362 make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
366 def obsolete_patches_on_bug(bug_id, bugs):
367 patches = bugs.fetch_patches_from_bug(bug_id)
369 log("Obsoleting %s on bug %s" % (pluralize("old patch", len(patches)), bug_id))
370 for patch in patches:
371 bugs.obsolete_attachment(patch["id"])
373 def execute(self, options, args, tool):
374 # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs).
375 bug_id = (args and args[0]) or parse_bug_id(tool.scm().create_patch())
377 error("No bug id passed and no bug url found in diff, can't post.")
379 if options.obsolete_patches:
380 self.obsolete_patches_on_bug(bug_id, tool.bugs)
382 diff = tool.scm().create_patch()
383 diff_file = StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object
385 description = options.description or "Patch"
386 tool.bugs.add_patch_to_bug(bug_id, diff_file, description, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
389 class PostCommits(Command):
392 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."),
393 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."),
394 make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"),
396 options += PostDiff.posting_options()
397 Command.__init__(self, "Attaches a range of local commits to bugs as patch files.", "COMMITISH", options=options, requires_local_commits=True)
399 def _comment_text_for_commit(self, options, commit_message, tool, commit_id):
401 if (options.add_log_as_comment):
402 comment_text = commit_message.body(lstrip=True)
403 comment_text += "---\n"
404 comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
407 def _diff_file_for_commit(self, tool, commit_id):
408 diff = tool.scm().create_patch_from_local_commit(commit_id)
409 return StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object
411 def execute(self, options, args, tool):
413 error("%s argument is required" % self.argument_names)
415 commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
416 if len(commit_ids) > 10: # We could lower this limit, 10 is too many for one bug as-is.
417 error("bugzilla-tool does not support attaching %s at once. Are you sure you passed the right commit range?" % (pluralize("patch", len(commit_ids))))
419 have_obsoleted_patches = set()
420 for commit_id in commit_ids:
421 commit_message = tool.scm().commit_message_for_local_commit(commit_id)
423 # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs).
424 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))
426 log("Skipping %s: No bug id found in commit or specified with --bug-id." % commit_id)
429 if options.obsolete_patches and bug_id not in have_obsoleted_patches:
430 PostDiff.obsolete_patches_on_bug(bug_id, tool.bugs)
431 have_obsoleted_patches.add(bug_id)
433 diff_file = self._diff_file_for_commit(tool, commit_id)
434 description = options.description or commit_message.description(lstrip=True, strip_url=True)
435 comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id)
436 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)
439 class Rollout(Command):
441 options = WebKitLandingScripts.land_options()
442 options += WebKitLandingScripts.cleaning_options()
443 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."))
444 Command.__init__(self, "Reverts the given revision and commits the revert and re-opens the original bug.", "REVISION [BUGID]", options=options)
447 def _create_changelogs_for_revert(scm, revision):
448 # First, discard the ChangeLog changes from the rollout.
449 changelog_paths = scm.modified_changelogs()
450 scm.revert_files(changelog_paths)
452 # Second, make new ChangeLog entries for this rollout.
453 # This could move to prepare-ChangeLog by adding a --revert= option.
454 WebKitLandingScripts.run_webkit_script("prepare-ChangeLog")
455 for changelog_path in changelog_paths:
456 ChangeLog(changelog_path).update_for_revert(revision)
459 def _parse_bug_id_from_revision_diff(tool, revision):
460 original_diff = tool.scm().diff_for_revision(revision)
461 return parse_bug_id(original_diff)
464 def _reopen_bug_after_rollout(tool, bug_id, comment_text):
466 tool.bugs.reopen_bug(bug_id, comment_text)
469 log("No bugs were updated or re-opened to reflect this rollout.")
471 def execute(self, options, args, tool):
473 error("REVISION is required, see --help.")
475 bug_id = self._parse_bug_id_from_revision_diff(tool, revision)
476 if options.complete_rollout:
478 log("Will re-open bug %s after rollout." % bug_id)
480 log("Failed to parse bug number from diff. No bugs will be updated/reopened after the rollout.")
482 WebKitLandingScripts.prepare_clean_working_directory(tool.scm(), options)
483 tool.scm().update_webkit()
484 tool.scm().apply_reverse_diff(revision)
485 self._create_changelogs_for_revert(tool.scm(), revision)
487 # FIXME: Fully automated rollout is not 100% idiot-proof yet, so for now just log with instructions on how to complete the rollout.
488 # Once we trust rollout we will remove this option.
489 if not options.complete_rollout:
490 log("\nNOTE: Rollout support is experimental.\nPlease verify the rollout diff and use \"bugzilla-tool land-diff %s\" to commit the rollout." % bug_id)
492 comment_text = WebKitLandingScripts.build_and_commit(tool.scm(), options)
493 self._reopen_bug_after_rollout(tool, bug_id, comment_text)
496 class CreateBug(Command):
499 make_option("--cc", action="store", type="string", dest="cc", help="Comma-separated list of email addresses to carbon-copy."),
500 make_option("--component", action="store", type="string", dest="component", help="Component for the new bug."),
501 make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."),
502 make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
503 make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
505 Command.__init__(self, "Create a bug from local changes or local commits.", "[COMMITISH]", options=options)
507 def create_bug_from_commit(self, options, args, tool):
508 commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
509 if len(commit_ids) > 3:
510 error("Are you sure you want to create one bug with %s patches?" % len(commit_ids))
512 commit_id = commit_ids[0]
517 (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
519 commit_message = tool.scm().commit_message_for_local_commit(commit_id)
520 bug_title = commit_message.description(lstrip=True, strip_url=True)
521 comment_text = commit_message.body(lstrip=True)
522 comment_text += "---\n"
523 comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
525 diff = tool.scm().create_patch_from_local_commit(commit_id)
526 diff_file = StringIO.StringIO(diff) # create_bug_with_patch expects a file-like object
527 bug_id = tool.bugs.create_bug_with_patch(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
529 if bug_id and len(commit_ids) > 1:
530 options.bug_id = bug_id
531 options.obsolete_patches = False
532 # FIXME: We should pass through --no-comment switch as well.
533 PostCommits.execute(self, options, commit_ids[1:], tool)
535 def create_bug_from_patch(self, options, args, tool):
539 (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
541 commit_message = commit_message_for_this_commit(tool.scm())
542 bug_title = commit_message.description(lstrip=True, strip_url=True)
543 comment_text = commit_message.body(lstrip=True)
545 diff = tool.scm().create_patch()
546 diff_file = StringIO.StringIO(diff) # create_bug_with_patch expects a file-like object
547 bug_id = tool.bugs.create_bug_with_patch(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
549 def prompt_for_bug_title_and_comment(self):
550 bug_title = raw_input("Bug title: ")
551 print "Bug comment (hit ^D on blank line to end):"
552 lines = sys.stdin.readlines()
554 sys.stdin.seek(0, os.SEEK_END)
556 # Cygwin raises an Illegal Seek (errno 29) exception when the above
557 # seek() call is made. Ignoring it seems to cause no harm.
558 # FIXME: Figure out a way to get avoid the exception in the first
561 comment_text = "".join(lines)
562 return (bug_title, comment_text)
564 def execute(self, options, args, tool):
566 if (not tool.scm().supports_local_commits()):
567 error("Extra arguments not supported; patch is taken from working directory.")
568 self.create_bug_from_commit(options, args, tool)
570 self.create_bug_from_patch(options, args, tool)
573 class TreeStatus(Command):
575 Command.__init__(self, "Print out the status of the webkit builders.")
577 def execute(self, options, args, tool):
578 for builder in tool.buildbot.builder_statuses():
579 status_string = "ok" if builder["is_green"] else "FAIL"
580 print "%s : %s" % (status_string.ljust(4), builder["name"])
583 class AbstractQueue(Command, WorkQueueDelegate):
584 def __init__(self, name):
587 make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue. Dangerous!"),
588 make_option("--status-host", action="store", type="string", dest="status_host", default=StatusBot.default_host, help="Hostname (e.g. localhost or commit.webkit.org) where status updates should be posted."),
590 Command.__init__(self, "Run the %s." % self._name, options=options)
592 def queue_log_path(self):
593 return "%s.log" % self._name
595 def work_logs_directory(self):
596 return "%s-logs" % self._name
598 def status_host(self):
599 return self.options.status_host
601 def begin_work_queue(self):
602 log("CAUTION: %s will discard all local changes in %s" % (self._name, self.tool.scm().checkout_root))
603 if self.options.confirm:
604 response = raw_input("Are you sure? Type \"yes\" to continue: ")
605 if (response != "yes"):
606 error("User declined.")
607 log("Running WebKit %s. %s" % (self._name, datetime.now().strftime(WorkQueue.log_date_format)))
609 def should_continue_work_queue(self):
612 def next_work_item(self):
613 raise NotImplementedError, "subclasses must implement"
615 def should_proceed_with_work_item(self, work_item):
616 raise NotImplementedError, "subclasses must implement"
618 def process_work_item(self, work_item):
619 raise NotImplementedError, "subclasses must implement"
621 def handle_unexpected_error(self, work_item, message):
622 raise NotImplementedError, "subclasses must implement"
625 def run_bugzilla_tool(args):
626 bugzilla_tool_path = __file__ # re-execute this script
627 bugzilla_tool_args = [bugzilla_tool_path] + args
628 WebKitLandingScripts.run_and_throw_if_fail(bugzilla_tool_args)
630 def log_progress(self, patch_ids):
631 log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self._name, ", ".join(patch_ids)))
633 def execute(self, options, args, tool):
634 self.options = options
636 work_queue = WorkQueue(self)
640 class CommitQueue(AbstractQueue):
642 AbstractQueue.__init__(self, "commit-queue")
644 def begin_work_queue(self):
645 AbstractQueue.begin_work_queue(self)
647 def next_work_item(self):
648 patches = self.tool.bugs.fetch_patches_from_commit_queue(reject_invalid_patches=True)
651 # Only bother logging if we have patches in the queue.
652 self.log_progress([patch['id'] for patch in patches])
655 def should_proceed_with_work_item(self, patch):
656 red_builders_names = self.tool.buildbot.red_core_builders_names()
657 if red_builders_names:
658 red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
659 return (False, "Builders [%s] are red. See http://build.webkit.org." % ", ".join(red_builders_names), None)
660 return (True, "Landing patch %s from bug %s." % (patch["id"], patch["bug_id"]), patch["bug_id"])
662 def process_work_item(self, patch):
663 self.run_bugzilla_tool(["land-attachment", "--force-clean", "--non-interactive", "--quiet", patch["id"]])
665 def handle_unexpected_error(self, patch, message):
666 self.tool.bugs.reject_patch_from_commit_queue(patch["id"], message)
669 class StyleQueue(AbstractQueue):
671 AbstractQueue.__init__(self, "style-queue")
673 def status_host(self):
674 return None # FIXME: A hack until we come up with a more generic status page.
676 def begin_work_queue(self):
677 AbstractQueue.begin_work_queue(self)
678 self._patches = PatchCollection(self.tool.bugs)
679 self._patches.add_patches(self.tool.bugs.fetch_patches_from_review_queue(limit=10))
681 def next_work_item(self):
682 self.log_progress(self._patches.patch_ids())
683 return self._patches.next()
685 def should_proceed_with_work_item(self, patch):
686 return (True, "Checking style for patch %s on bug %s." % (patch["id"], patch["bug_id"]), patch["bug_id"])
688 def process_work_item(self, patch):
689 self.run_bugzilla_tool(["check-style", "--force-clean", patch["id"]])
691 def handle_unexpected_error(self, patch, message):
695 class BugzillaTool(MultiCommandTool):
697 # HACK: Set self.cached_scm before calling MultiCommandTool.__init__ because
698 # MultiCommandTool._commands_usage() will call self.should_show_command_help which uses scm().
699 # This hack can be removed by overriding usage() printing in HelpPrintingOptionParser
700 # so that we don't need to create 'epilog' before constructing HelpPrintingOptionParser.
701 self.cached_scm = None
703 # FIXME: Commands should know their own name and register themselves with the BugzillaTool instead of having a manual list.
704 MultiCommandTool.__init__(self, commands=[
705 { "name" : "bugs-to-commit", "object" : BugsToCommit() },
706 { "name" : "patches-to-commit", "object" : PatchesToCommit() },
707 { "name" : "reviewed-patches", "object" : ReviewedPatches() },
708 { "name" : "create-bug", "object" : CreateBug() },
709 { "name" : "apply-attachment", "object" : ApplyAttachment() },
710 { "name" : "apply-patches", "object" : ApplyPatches() },
711 { "name" : "land-diff", "object" : LandDiff() },
712 { "name" : "land-attachment", "object" : LandAttachment() },
713 { "name" : "land-patches", "object" : LandPatches() },
714 { "name" : "check-style", "object" : CheckStyle() },
715 { "name" : "commit-message", "object" : CommitMessageForCurrentDiff() },
716 { "name" : "obsolete-attachments", "object" : ObsoleteAttachments() },
717 { "name" : "post-diff", "object" : PostDiff() },
718 { "name" : "post-commits", "object" : PostCommits() },
719 { "name" : "tree-status", "object" : TreeStatus() },
720 { "name" : "commit-queue", "object" : CommitQueue() },
721 { "name" : "style-queue", "object" : StyleQueue() },
722 { "name" : "rollout", "object" : Rollout() },
724 self.global_option_parser.add_option("--dry-run", action="callback", help="do not touch remote servers", callback=self.dry_run_callback)
726 self.bugs = Bugzilla()
727 self.buildbot = BuildBot()
729 def dry_run_callback(self, option, opt, value, parser):
730 self.scm().dryrun = True
731 self.bugs.dryrun = True
734 # Lazily initialize SCM to not error-out before command line parsing (or when running non-scm commands).
735 original_cwd = os.path.abspath(".")
736 if not self.cached_scm:
737 self.cached_scm = detect_scm_system(original_cwd)
739 if not self.cached_scm:
740 script_directory = os.path.abspath(sys.path[0])
741 webkit_directory = os.path.abspath(os.path.join(script_directory, "../.."))
742 self.cached_scm = detect_scm_system(webkit_directory)
744 log("The current directory (%s) is not a WebKit checkout, using %s" % (original_cwd, webkit_directory))
746 error("FATAL: Failed to determine the SCM system for either %s or %s" % (original_cwd, webkit_directory))
748 return self.cached_scm
750 def should_show_command_help(self, command):
751 if command["object"].requires_local_commits:
752 return self.scm().supports_local_commits()
755 def should_execute_command(self, command):
756 if command["object"].requires_local_commits and not self.scm().supports_local_commits():
757 failure_reason = "%s requires local commits using %s in %s." % (command["name"], self.scm().display_name(), self.scm().checkout_root)
758 return (False, failure_reason)
762 if __name__ == "__main__":
763 BugzillaTool().main()