1
# Copyright (c) 2009, 2011 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
43
_log = logging.getLogger(__name__)
46
class Clean(AbstractSequencedCommand):
48
help_text = "Clean the working copy"
50
steps.CleanWorkingDirectory,
53
def _prepare_state(self, options, args, tool):
54
options.force_clean = True
57
class Update(AbstractSequencedCommand):
59
help_text = "Update working copy (used internally)"
61
steps.CleanWorkingDirectory,
66
class Build(AbstractSequencedCommand):
68
help_text = "Update working copy and build"
70
steps.CleanWorkingDirectory,
75
def _prepare_state(self, options, args, tool):
79
class BuildAndTest(AbstractSequencedCommand):
80
name = "build-and-test"
81
help_text = "Update working copy, build, and run the tests"
83
steps.CleanWorkingDirectory,
90
class Land(AbstractSequencedCommand):
92
help_text = "Land the current working directory diff and updates the associated bug if any"
93
argument_names = "[BUGID]"
94
show_in_main_help = True
96
steps.AddSvnMimetypeForPng,
97
steps.UpdateChangeLogsWithReviewer,
98
steps.ValidateReviewer,
99
steps.ValidateChangeLogs, # We do this after UpdateChangeLogsWithReviewer to avoid not having to cache the diff twice.
103
steps.CloseBugForLandDiff,
105
long_help = """land commits the current working copy diff (just as svn or git commit would).
106
land will NOT build and run the tests before committing, but you can use the --build option for that.
107
If a bug id is provided, or one can be found in the ChangeLog land will update the bug after committing."""
109
def _prepare_state(self, options, args, tool):
110
changed_files = self._tool.scm().changed_files(options.git_commit)
112
"changed_files": changed_files,
113
"bug_id": (args and args[0]) or tool.checkout().bug_id_for_this_commit(options.git_commit, changed_files),
117
class LandCowboy(AbstractSequencedCommand):
119
help_text = "Prepares a ChangeLog and lands the current working directory diff."
121
steps.PrepareChangeLog,
128
steps.CloseBugForLandDiff,
131
def _prepare_state(self, options, args, tool):
132
options.check_style_filter = "-changelog"
135
class LandCowhand(LandCowboy):
136
# Gender-blind term for cowboy, see: http://en.wiktionary.org/wiki/cowhand
137
name = "land-cowhand"
140
class CheckStyleLocal(AbstractSequencedCommand):
141
name = "check-style-local"
142
help_text = "Run check-webkit-style on the current working directory diff"
148
class AbstractPatchProcessingCommand(AbstractDeclarativeCommand):
149
# Subclasses must implement the methods below. We don't declare them here
150
# because we want to be able to implement them with mix-ins.
152
# pylint: disable-msg=E1101
153
# def _fetch_list_of_patches_to_process(self, options, args, tool):
154
# def _prepare_to_process(self, options, args, tool):
155
# def _process_patch(self, options, args, tool):
158
def _collect_patches_by_bug(patches):
160
for patch in patches:
161
bugs_to_patches[patch.bug_id()] = bugs_to_patches.get(patch.bug_id(), []) + [patch]
162
return bugs_to_patches
164
def execute(self, options, args, tool):
165
self._prepare_to_process(options, args, tool)
166
patches = self._fetch_list_of_patches_to_process(options, args, tool)
168
# It's nice to print out total statistics.
169
bugs_to_patches = self._collect_patches_by_bug(patches)
170
_log.info("Processing %s from %s." % (pluralize("patch", len(patches)), pluralize("bug", len(bugs_to_patches))))
172
for patch in patches:
173
self._process_patch(patch, options, args, tool)
176
class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand):
182
self._prepare_sequence = StepSequence(self.prepare_steps)
183
self._main_sequence = StepSequence(self.main_steps)
184
options = sorted(set(self._prepare_sequence.options() + self._main_sequence.options()))
185
AbstractPatchProcessingCommand.__init__(self, options)
187
def _prepare_to_process(self, options, args, tool):
188
self._prepare_sequence.run_and_handle_errors(tool, options)
190
def _process_patch(self, patch, options, args, tool):
191
state = { "patch" : patch }
192
self._main_sequence.run_and_handle_errors(tool, options, state)
195
class ProcessAttachmentsMixin(object):
196
def _fetch_list_of_patches_to_process(self, options, args, tool):
197
return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args)
200
class ProcessBugsMixin(object):
201
def _fetch_list_of_patches_to_process(self, options, args, tool):
204
patches = tool.bugs.fetch_bug(bug_id).reviewed_patches()
205
_log.info("%s found on bug %s." % (pluralize("reviewed patch", len(patches)), bug_id))
206
all_patches += patches
208
_log.info("No reviewed patches found, looking for unreviewed patches.")
210
patches = tool.bugs.fetch_bug(bug_id).patches()
211
_log.info("%s found on bug %s." % (pluralize("patch", len(patches)), bug_id))
212
all_patches += patches
216
class ProcessURLsMixin(object):
217
def _fetch_list_of_patches_to_process(self, options, args, tool):
220
bug_id = urls.parse_bug_id(url)
222
patches = tool.bugs.fetch_bug(bug_id).patches()
223
_log.info("%s found on bug %s." % (pluralize("patch", len(patches)), bug_id))
224
all_patches += patches
226
attachment_id = urls.parse_attachment_id(url)
228
all_patches += tool.bugs.fetch_attachment(attachment_id)
233
class CheckStyle(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
235
help_text = "Run check-webkit-style on the specified attachments"
236
argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
238
steps.CleanWorkingDirectory,
245
class BuildAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
246
name = "build-attachment"
247
help_text = "Apply and build patches from bugzilla"
248
argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
250
steps.CleanWorkingDirectory,
257
class BuildAndTestAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
258
name = "build-and-test-attachment"
259
help_text = "Apply, build, and test patches from bugzilla"
260
argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
262
steps.CleanWorkingDirectory,
270
class AbstractPatchApplyingCommand(AbstractPatchSequencingCommand):
272
steps.EnsureLocalCommitIfNeeded,
273
steps.CleanWorkingDirectoryWithLocalCommits,
277
steps.ApplyPatchWithLocalCommit,
279
long_help = """Updates the working copy.
280
Downloads and applies the patches, creating local commits if necessary."""
283
class ApplyAttachment(AbstractPatchApplyingCommand, ProcessAttachmentsMixin):
284
name = "apply-attachment"
285
help_text = "Apply an attachment to the local working directory"
286
argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
287
show_in_main_help = True
290
class ApplyFromBug(AbstractPatchApplyingCommand, ProcessBugsMixin):
291
name = "apply-from-bug"
292
help_text = "Apply reviewed patches from provided bugs to the local working directory"
293
argument_names = "BUGID [BUGIDS]"
294
show_in_main_help = True
297
class ApplyWatchList(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
298
name = "apply-watchlist"
299
help_text = "Applies the watchlist to the specified attachments"
300
argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
302
steps.CleanWorkingDirectory,
305
steps.ApplyWatchList,
307
long_help = """"Applies the watchlist to the specified attachments.
308
Downloads the attachment, applies it locally, runs the watchlist against it, and updates the bug with the result."""
311
class AbstractPatchLandingCommand(AbstractPatchSequencingCommand):
313
steps.CleanWorkingDirectory,
316
steps.ValidateChangeLogs,
317
steps.ValidateReviewer,
324
long_help = """Checks to make sure builders are green.
325
Updates the working copy.
328
Runs the layout tests.
330
Clears the flags on the patch.
331
Closes the bug if no patches are marked for review."""
334
class LandAttachment(AbstractPatchLandingCommand, ProcessAttachmentsMixin):
335
name = "land-attachment"
336
help_text = "Land patches from bugzilla, optionally building and testing them first"
337
argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
338
show_in_main_help = True
341
class LandFromBug(AbstractPatchLandingCommand, ProcessBugsMixin):
342
name = "land-from-bug"
343
help_text = "Land all patches on the given bugs, optionally building and testing them first"
344
argument_names = "BUGID [BUGIDS]"
345
show_in_main_help = True
348
class LandFromURL(AbstractPatchLandingCommand, ProcessURLsMixin):
349
name = "land-from-url"
350
help_text = "Land all patches on the given URLs, optionally building and testing them first"
351
argument_names = "URL [URLS]"
354
class ValidateChangelog(AbstractSequencedCommand):
355
name = "validate-changelog"
356
help_text = "Validate that the ChangeLogs and reviewers look reasonable"
357
long_help = """Examines the current diff to see whether the ChangeLogs
358
and the reviewers listed in the ChangeLogs look reasonable.
361
steps.ValidateChangeLogs,
362
steps.ValidateReviewer,
366
class AbstractRolloutPrepCommand(AbstractSequencedCommand):
367
argument_names = "REVISION [REVISIONS] REASON"
369
def _commit_info(self, revision):
370
commit_info = self._tool.checkout().commit_info_for_revision(revision)
371
if commit_info and commit_info.bug_id():
372
# Note: Don't print a bug URL here because it will confuse the
373
# SheriffBot because the SheriffBot just greps the output
374
# of create-rollout for bug URLs. It should do better
376
_log.info("Preparing rollout for bug %s." % commit_info.bug_id())
378
_log.info("Unable to parse bug number from diff.")
381
def _prepare_state(self, options, args, tool):
383
for revision in str(args[0]).split():
384
if revision.isdigit():
385
revision_list.append(int(revision))
387
raise ScriptError(message="Invalid svn revision number: " + revision)
390
# We use the earliest revision for the bug info
391
earliest_revision = revision_list[0]
393
"revision": earliest_revision,
394
"revision_list": revision_list,
397
commit_info = self._commit_info(earliest_revision)
399
state["bug_id"] = commit_info.bug_id()
400
cc_list = sorted([party.bugzilla_email()
401
for party in commit_info.responsible_parties()
402
if party.bugzilla_email()])
403
# FIXME: We should used the list as the canonical representation.
404
state["bug_cc"] = ",".join(cc_list)
408
class PrepareRollout(AbstractRolloutPrepCommand):
409
name = "prepare-rollout"
410
help_text = "Revert the given revision(s) in the working copy and prepare ChangeLogs with revert reason"
411
long_help = """Updates the working copy.
412
Applies the inverse diff for the provided revision(s).
413
Creates an appropriate rollout ChangeLog, including a trac link and bug link.
416
steps.CleanWorkingDirectory,
418
steps.RevertRevision,
419
steps.PrepareChangeLogForRevert,
423
class CreateRollout(AbstractRolloutPrepCommand):
424
name = "create-rollout"
425
help_text = "Creates a bug to track the broken SVN revision(s) and uploads a rollout patch."
427
steps.CleanWorkingDirectory,
429
steps.RevertRevision,
431
steps.PrepareChangeLogForRevert,
432
steps.PostDiffForRevert,
435
def _prepare_state(self, options, args, tool):
436
state = AbstractRolloutPrepCommand._prepare_state(self, options, args, tool)
437
# Currently, state["bug_id"] points to the bug that caused the
438
# regression. We want to create a new bug that blocks the old bug
439
# so we move state["bug_id"] to state["bug_blocked"] and delete the
440
# old state["bug_id"] so that steps.CreateBug will actually create
441
# the new bug that we want (and subsequently store its bug id into
443
state["bug_blocked"] = state["bug_id"]
445
state["bug_title"] = "REGRESSION(r%s): %s" % (state["revision"], state["reason"])
446
state["bug_description"] = "%s broke the build:\n%s" % (urls.view_revision_url(state["revision"]), state["reason"])
447
# FIXME: If we had more context here, we could link to other open bugs
448
# that mention the test that regressed.
449
if options.parent_command == "sheriff-bot":
450
state["bug_description"] += """
452
This is an automatic bug report generated by the sheriff-bot. If this bug
453
report was created because of a flaky test, please file a bug for the flaky
454
test (if we don't already have one on file) and dup this bug against that bug
455
so that we can track how often these flaky tests case pain.
457
"Only you can prevent forest fires." -- Smokey the Bear
462
class Rollout(AbstractRolloutPrepCommand):
464
show_in_main_help = True
465
help_text = "Revert the given revision(s) in the working copy and optionally commit the revert and re-open the original bug"
466
long_help = """Updates the working copy.
467
Applies the inverse diff for the provided revision.
468
Creates an appropriate rollout ChangeLog, including a trac link and bug link.
469
Opens the generated ChangeLogs in $EDITOR.
470
Shows the prepared diff for confirmation.
471
Commits the revert and updates the bug (including re-opening the bug if necessary)."""
473
steps.CleanWorkingDirectory,
475
steps.RevertRevision,
476
steps.PrepareChangeLogForRevert,
481
steps.ReopenBugAfterRollout,