1 # Copyright (c) 2009 Google Inc. All rights reserved.
2 # Copyright (c) 2009 Apple Inc. All rights reserved.
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
14 # * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 from webkitpy.tool import steps
34 from webkitpy.common.checkout.changelog import ChangeLog
35 from webkitpy.common.config import urls
36 from webkitpy.common.system.executive import ScriptError
37 from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
38 from webkitpy.tool.commands.stepsequence import StepSequence
39 from webkitpy.tool.comments import bug_comment_from_commit_text
40 from webkitpy.tool.grammar import pluralize
41 from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
42 from webkitpy.common.system.deprecated_logging import error, log
45 class Clean(AbstractSequencedCommand):
47 help_text = "Clean the working copy"
49 steps.CleanWorkingDirectory,
52 def _prepare_state(self, options, args, tool):
53 options.force_clean = True
56 class Update(AbstractSequencedCommand):
58 help_text = "Update working copy (used internally)"
60 steps.CleanWorkingDirectory,
65 class Build(AbstractSequencedCommand):
67 help_text = "Update working copy and build"
69 steps.CleanWorkingDirectory,
74 def _prepare_state(self, options, args, tool):
78 class BuildAndTest(AbstractSequencedCommand):
79 name = "build-and-test"
80 help_text = "Update working copy, build, and run the tests"
82 steps.CleanWorkingDirectory,
89 class Land(AbstractSequencedCommand):
91 help_text = "Land the current working directory diff and updates the associated bug if any"
92 argument_names = "[BUGID]"
93 show_in_main_help = True
95 steps.UpdateChangeLogsWithReviewer,
96 steps.ValidateReviewer,
97 steps.ValidateChangeLogs, # We do this after UpdateChangeLogsWithReviewer to avoid not having to cache the diff twice.
101 steps.CloseBugForLandDiff,
103 long_help = """land commits the current working copy diff (just as svn or git commit would).
104 land will NOT build and run the tests before committing, but you can use the --build option for that.
105 If a bug id is provided, or one can be found in the ChangeLog land will update the bug after committing."""
107 def _prepare_state(self, options, args, tool):
108 changed_files = self._tool.scm().changed_files(options.git_commit)
110 "changed_files": changed_files,
111 "bug_id": (args and args[0]) or tool.checkout().bug_id_for_this_commit(options.git_commit, changed_files),
115 class LandCowboy(AbstractSequencedCommand):
117 help_text = "Prepares a ChangeLog and lands the current working directory diff."
119 steps.PrepareChangeLog,
125 steps.CloseBugForLandDiff,
129 class AbstractPatchProcessingCommand(AbstractDeclarativeCommand):
130 # Subclasses must implement the methods below. We don't declare them here
131 # because we want to be able to implement them with mix-ins.
133 # def _fetch_list_of_patches_to_process(self, options, args, tool):
134 # def _prepare_to_process(self, options, args, tool):
137 def _collect_patches_by_bug(patches):
139 for patch in patches:
140 bugs_to_patches[patch.bug_id()] = bugs_to_patches.get(patch.bug_id(), []) + [patch]
141 return bugs_to_patches
143 def execute(self, options, args, tool):
144 self._prepare_to_process(options, args, tool)
145 patches = self._fetch_list_of_patches_to_process(options, args, tool)
147 # It's nice to print out total statistics.
148 bugs_to_patches = self._collect_patches_by_bug(patches)
149 log("Processing %s from %s." % (pluralize("patch", len(patches)), pluralize("bug", len(bugs_to_patches))))
151 for patch in patches:
152 self._process_patch(patch, options, args, tool)
155 class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand):
161 self._prepare_sequence = StepSequence(self.prepare_steps)
162 self._main_sequence = StepSequence(self.main_steps)
163 options = sorted(set(self._prepare_sequence.options() + self._main_sequence.options()))
164 AbstractPatchProcessingCommand.__init__(self, options)
166 def _prepare_to_process(self, options, args, tool):
167 self._prepare_sequence.run_and_handle_errors(tool, options)
169 def _process_patch(self, patch, options, args, tool):
170 state = { "patch" : patch }
171 self._main_sequence.run_and_handle_errors(tool, options, state)
174 class ProcessAttachmentsMixin(object):
175 def _fetch_list_of_patches_to_process(self, options, args, tool):
176 return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args)
179 class ProcessBugsMixin(object):
180 def _fetch_list_of_patches_to_process(self, options, args, tool):
183 patches = tool.bugs.fetch_bug(bug_id).reviewed_patches()
184 log("%s found on bug %s." % (pluralize("reviewed patch", len(patches)), bug_id))
185 all_patches += patches
189 class CheckStyle(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
191 help_text = "Run check-webkit-style on the specified attachments"
192 argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
194 steps.CleanWorkingDirectory,
201 class BuildAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
202 name = "build-attachment"
203 help_text = "Apply and build patches from bugzilla"
204 argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
206 steps.CleanWorkingDirectory,
213 class BuildAndTestAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
214 name = "build-and-test-attachment"
215 help_text = "Apply, build, and test patches from bugzilla"
216 argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
218 steps.CleanWorkingDirectory,
226 class AbstractPatchApplyingCommand(AbstractPatchSequencingCommand):
228 steps.EnsureLocalCommitIfNeeded,
229 steps.CleanWorkingDirectoryWithLocalCommits,
233 steps.ApplyPatchWithLocalCommit,
235 long_help = """Updates the working copy.
236 Downloads and applies the patches, creating local commits if necessary."""
239 class ApplyAttachment(AbstractPatchApplyingCommand, ProcessAttachmentsMixin):
240 name = "apply-attachment"
241 help_text = "Apply an attachment to the local working directory"
242 argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
243 show_in_main_help = True
246 class ApplyFromBug(AbstractPatchApplyingCommand, ProcessBugsMixin):
247 name = "apply-from-bug"
248 help_text = "Apply reviewed patches from provided bugs to the local working directory"
249 argument_names = "BUGID [BUGIDS]"
250 show_in_main_help = True
253 class AbstractPatchLandingCommand(AbstractPatchSequencingCommand):
255 steps.CleanWorkingDirectory,
258 steps.ValidateChangeLogs,
259 steps.ValidateReviewer,
266 long_help = """Checks to make sure builders are green.
267 Updates the working copy.
270 Runs the layout tests.
272 Clears the flags on the patch.
273 Closes the bug if no patches are marked for review."""
276 class LandAttachment(AbstractPatchLandingCommand, ProcessAttachmentsMixin):
277 name = "land-attachment"
278 help_text = "Land patches from bugzilla, optionally building and testing them first"
279 argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
280 show_in_main_help = True
283 class LandFromBug(AbstractPatchLandingCommand, ProcessBugsMixin):
284 name = "land-from-bug"
285 help_text = "Land all patches on the given bugs, optionally building and testing them first"
286 argument_names = "BUGID [BUGIDS]"
287 show_in_main_help = True
290 class AbstractRolloutPrepCommand(AbstractSequencedCommand):
291 argument_names = "REVISION [REVISIONS] REASON"
293 def _commit_info(self, revision):
294 commit_info = self._tool.checkout().commit_info_for_revision(revision)
295 if commit_info and commit_info.bug_id():
296 # Note: Don't print a bug URL here because it will confuse the
297 # SheriffBot because the SheriffBot just greps the output
298 # of create-rollout for bug URLs. It should do better
300 log("Preparing rollout for bug %s." % commit_info.bug_id())
302 log("Unable to parse bug number from diff.")
305 def _prepare_state(self, options, args, tool):
307 for revision in str(args[0]).split():
308 if revision.isdigit():
309 revision_list.append(int(revision))
311 raise ScriptError(message="Invalid svn revision number: " + revision)
314 # We use the earliest revision for the bug info
315 earliest_revision = revision_list[0]
316 commit_info = self._commit_info(earliest_revision)
317 cc_list = sorted([party.bugzilla_email()
318 for party in commit_info.responsible_parties()
319 if party.bugzilla_email()])
321 "revision": earliest_revision,
322 "revision_list": revision_list,
323 "bug_id": commit_info.bug_id(),
324 # FIXME: We should used the list as the canonical representation.
325 "bug_cc": ",".join(cc_list),
330 class PrepareRollout(AbstractRolloutPrepCommand):
331 name = "prepare-rollout"
332 help_text = "Revert the given revision(s) in the working copy and prepare ChangeLogs with revert reason"
333 long_help = """Updates the working copy.
334 Applies the inverse diff for the provided revision(s).
335 Creates an appropriate rollout ChangeLog, including a trac link and bug link.
338 steps.CleanWorkingDirectory,
340 steps.RevertRevision,
341 steps.PrepareChangeLogForRevert,
345 class CreateRollout(AbstractRolloutPrepCommand):
346 name = "create-rollout"
347 help_text = "Creates a bug to track the broken SVN revision(s) and uploads a rollout patch."
349 steps.CleanWorkingDirectory,
351 steps.RevertRevision,
353 steps.PrepareChangeLogForRevert,
354 steps.PostDiffForRevert,
357 def _prepare_state(self, options, args, tool):
358 state = AbstractRolloutPrepCommand._prepare_state(self, options, args, tool)
359 # Currently, state["bug_id"] points to the bug that caused the
360 # regression. We want to create a new bug that blocks the old bug
361 # so we move state["bug_id"] to state["bug_blocked"] and delete the
362 # old state["bug_id"] so that steps.CreateBug will actually create
363 # the new bug that we want (and subsequently store its bug id into
365 state["bug_blocked"] = state["bug_id"]
367 state["bug_title"] = "REGRESSION(r%s): %s" % (state["revision"], state["reason"])
368 state["bug_description"] = "%s broke the build:\n%s" % (urls.view_revision_url(state["revision"]), state["reason"])
369 # FIXME: If we had more context here, we could link to other open bugs
370 # that mention the test that regressed.
371 if options.parent_command == "sheriff-bot":
372 state["bug_description"] += """
374 This is an automatic bug report generated by the sheriff-bot. If this bug
375 report was created because of a flaky test, please file a bug for the flaky
376 test (if we don't already have one on file) and dup this bug against that bug
377 so that we can track how often these flaky tests case pain.
379 "Only you can prevent forest fires." -- Smokey the Bear
384 class Rollout(AbstractRolloutPrepCommand):
386 show_in_main_help = True
387 help_text = "Revert the given revision(s) in the working copy and optionally commit the revert and re-open the original bug"
388 long_help = """Updates the working copy.
389 Applies the inverse diff for the provided revision.
390 Creates an appropriate rollout ChangeLog, including a trac link and bug link.
391 Opens the generated ChangeLogs in $EDITOR.
392 Shows the prepared diff for confirmation.
393 Commits the revert and updates the bug (including re-opening the bug if necessary)."""
395 steps.CleanWorkingDirectory,
397 steps.RevertRevision,
398 steps.PrepareChangeLogForRevert,
403 steps.ReopenBugAfterRollout,