Home | History | Annotate | Download | only in commands
      1 # Copyright (c) 2009 Google Inc. All rights reserved.
      2 # Copyright (c) 2009 Apple Inc. All rights reserved.
      3 #
      4 # Redistribution and use in source and binary forms, with or without
      5 # modification, are permitted provided that the following conditions are
      6 # met:
      7 # 
      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
     13 # distribution.
     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.
     17 # 
     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.
     29 
     30 import os
     31 
     32 from webkitpy.tool import steps
     33 
     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
     43 
     44 
     45 class Clean(AbstractSequencedCommand):
     46     name = "clean"
     47     help_text = "Clean the working copy"
     48     steps = [
     49         steps.CleanWorkingDirectory,
     50     ]
     51 
     52     def _prepare_state(self, options, args, tool):
     53         options.force_clean = True
     54 
     55 
     56 class Update(AbstractSequencedCommand):
     57     name = "update"
     58     help_text = "Update working copy (used internally)"
     59     steps = [
     60         steps.CleanWorkingDirectory,
     61         steps.Update,
     62     ]
     63 
     64 
     65 class Build(AbstractSequencedCommand):
     66     name = "build"
     67     help_text = "Update working copy and build"
     68     steps = [
     69         steps.CleanWorkingDirectory,
     70         steps.Update,
     71         steps.Build,
     72     ]
     73 
     74     def _prepare_state(self, options, args, tool):
     75         options.build = True
     76 
     77 
     78 class BuildAndTest(AbstractSequencedCommand):
     79     name = "build-and-test"
     80     help_text = "Update working copy, build, and run the tests"
     81     steps = [
     82         steps.CleanWorkingDirectory,
     83         steps.Update,
     84         steps.Build,
     85         steps.RunTests,
     86     ]
     87 
     88 
     89 class Land(AbstractSequencedCommand):
     90     name = "land"
     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
     94     steps = [
     95         steps.EnsureBuildersAreGreen,
     96         steps.UpdateChangeLogsWithReviewer,
     97         steps.ValidateReviewer,
     98         steps.ValidateChangeLogs, # We do this after UpdateChangeLogsWithReviewer to avoid not having to cache the diff twice.
     99         steps.Build,
    100         steps.RunTests,
    101         steps.Commit,
    102         steps.CloseBugForLandDiff,
    103     ]
    104     long_help = """land commits the current working copy diff (just as svn or git commit would).
    105 land will NOT build and run the tests before committing, but you can use the --build option for that.
    106 If a bug id is provided, or one can be found in the ChangeLog land will update the bug after committing."""
    107 
    108     def _prepare_state(self, options, args, tool):
    109         changed_files = self._tool.scm().changed_files(options.git_commit)
    110         return {
    111             "changed_files": changed_files,
    112             "bug_id": (args and args[0]) or tool.checkout().bug_id_for_this_commit(options.git_commit, changed_files),
    113         }
    114 
    115 
    116 class LandCowboy(AbstractSequencedCommand):
    117     name = "land-cowboy"
    118     help_text = "Prepares a ChangeLog and lands the current working directory diff."
    119     steps = [
    120         steps.PrepareChangeLog,
    121         steps.EditChangeLog,
    122         steps.ConfirmDiff,
    123         steps.Build,
    124         steps.RunTests,
    125         steps.Commit,
    126     ]
    127 
    128 
    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.
    132     #
    133     # def _fetch_list_of_patches_to_process(self, options, args, tool):
    134     # def _prepare_to_process(self, options, args, tool):
    135 
    136     @staticmethod
    137     def _collect_patches_by_bug(patches):
    138         bugs_to_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
    142 
    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)
    146 
    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))))
    150 
    151         for patch in patches:
    152             self._process_patch(patch, options, args, tool)
    153 
    154 
    155 class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand):
    156     prepare_steps = None
    157     main_steps = None
    158 
    159     def __init__(self):
    160         options = []
    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)
    165 
    166     def _prepare_to_process(self, options, args, tool):
    167         self._prepare_sequence.run_and_handle_errors(tool, options)
    168 
    169     def _process_patch(self, patch, options, args, tool):
    170         state = { "patch" : patch }
    171         self._main_sequence.run_and_handle_errors(tool, options, state)
    172 
    173 
    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)
    177 
    178 
    179 class ProcessBugsMixin(object):
    180     def _fetch_list_of_patches_to_process(self, options, args, tool):
    181         all_patches = []
    182         for bug_id in args:
    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
    186         return all_patches
    187 
    188 
    189 class CheckStyle(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
    190     name = "check-style"
    191     help_text = "Run check-webkit-style on the specified attachments"
    192     argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
    193     main_steps = [
    194         steps.CleanWorkingDirectory,
    195         steps.Update,
    196         steps.ApplyPatch,
    197         steps.CheckStyle,
    198     ]
    199 
    200 
    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]"
    205     main_steps = [
    206         steps.CleanWorkingDirectory,
    207         steps.Update,
    208         steps.ApplyPatch,
    209         steps.Build,
    210     ]
    211 
    212 
    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]"
    217     main_steps = [
    218         steps.CleanWorkingDirectory,
    219         steps.Update,
    220         steps.ApplyPatch,
    221         steps.Build,
    222         steps.RunTests,
    223     ]
    224 
    225 
    226 class AbstractPatchApplyingCommand(AbstractPatchSequencingCommand):
    227     prepare_steps = [
    228         steps.EnsureLocalCommitIfNeeded,
    229         steps.CleanWorkingDirectoryWithLocalCommits,
    230         steps.Update,
    231     ]
    232     main_steps = [
    233         steps.ApplyPatchWithLocalCommit,
    234     ]
    235     long_help = """Updates the working copy.
    236 Downloads and applies the patches, creating local commits if necessary."""
    237 
    238 
    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
    244 
    245 
    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
    251 
    252 
    253 class AbstractPatchLandingCommand(AbstractPatchSequencingCommand):
    254     prepare_steps = [
    255         steps.EnsureBuildersAreGreen,
    256     ]
    257     main_steps = [
    258         steps.CleanWorkingDirectory,
    259         steps.Update,
    260         steps.ApplyPatch,
    261         steps.ValidateChangeLogs,
    262         steps.ValidateReviewer,
    263         steps.Build,
    264         steps.RunTests,
    265         steps.Commit,
    266         steps.ClosePatch,
    267         steps.CloseBug,
    268     ]
    269     long_help = """Checks to make sure builders are green.
    270 Updates the working copy.
    271 Applies the patch.
    272 Builds.
    273 Runs the layout tests.
    274 Commits the patch.
    275 Clears the flags on the patch.
    276 Closes the bug if no patches are marked for review."""
    277 
    278 
    279 class LandAttachment(AbstractPatchLandingCommand, ProcessAttachmentsMixin):
    280     name = "land-attachment"
    281     help_text = "Land patches from bugzilla, optionally building and testing them first"
    282     argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
    283     show_in_main_help = True
    284 
    285 
    286 class LandFromBug(AbstractPatchLandingCommand, ProcessBugsMixin):
    287     name = "land-from-bug"
    288     help_text = "Land all patches on the given bugs, optionally building and testing them first"
    289     argument_names = "BUGID [BUGIDS]"
    290     show_in_main_help = True
    291 
    292 
    293 class AbstractRolloutPrepCommand(AbstractSequencedCommand):
    294     argument_names = "REVISION [REVISIONS] REASON"
    295 
    296     def _commit_info(self, revision):
    297         commit_info = self._tool.checkout().commit_info_for_revision(revision)
    298         if commit_info and commit_info.bug_id():
    299             # Note: Don't print a bug URL here because it will confuse the
    300             #       SheriffBot because the SheriffBot just greps the output
    301             #       of create-rollout for bug URLs.  It should do better
    302             #       parsing instead.
    303             log("Preparing rollout for bug %s." % commit_info.bug_id())
    304         else:
    305             log("Unable to parse bug number from diff.")
    306         return commit_info
    307 
    308     def _prepare_state(self, options, args, tool):
    309         revision_list = []
    310         for revision in str(args[0]).split():
    311             if revision.isdigit():
    312                 revision_list.append(int(revision))
    313             else:
    314                 raise ScriptError(message="Invalid svn revision number: " + revision)
    315         revision_list.sort()
    316 
    317         # We use the earliest revision for the bug info
    318         earliest_revision = revision_list[0]
    319         commit_info = self._commit_info(earliest_revision)
    320         cc_list = sorted([party.bugzilla_email()
    321                           for party in commit_info.responsible_parties()
    322                           if party.bugzilla_email()])
    323         return {
    324             "revision": earliest_revision,
    325             "revision_list": revision_list,
    326             "bug_id": commit_info.bug_id(),
    327             # FIXME: We should used the list as the canonical representation.
    328             "bug_cc": ",".join(cc_list),
    329             "reason": args[1],
    330         }
    331 
    332 
    333 class PrepareRollout(AbstractRolloutPrepCommand):
    334     name = "prepare-rollout"
    335     help_text = "Revert the given revision(s) in the working copy and prepare ChangeLogs with revert reason"
    336     long_help = """Updates the working copy.
    337 Applies the inverse diff for the provided revision(s).
    338 Creates an appropriate rollout ChangeLog, including a trac link and bug link.
    339 """
    340     steps = [
    341         steps.CleanWorkingDirectory,
    342         steps.Update,
    343         steps.RevertRevision,
    344         steps.PrepareChangeLogForRevert,
    345     ]
    346 
    347 
    348 class CreateRollout(AbstractRolloutPrepCommand):
    349     name = "create-rollout"
    350     help_text = "Creates a bug to track the broken SVN revision(s) and uploads a rollout patch."
    351     steps = [
    352         steps.CleanWorkingDirectory,
    353         steps.Update,
    354         steps.RevertRevision,
    355         steps.CreateBug,
    356         steps.PrepareChangeLogForRevert,
    357         steps.PostDiffForRevert,
    358     ]
    359 
    360     def _prepare_state(self, options, args, tool):
    361         state = AbstractRolloutPrepCommand._prepare_state(self, options, args, tool)
    362         # Currently, state["bug_id"] points to the bug that caused the
    363         # regression.  We want to create a new bug that blocks the old bug
    364         # so we move state["bug_id"] to state["bug_blocked"] and delete the
    365         # old state["bug_id"] so that steps.CreateBug will actually create
    366         # the new bug that we want (and subsequently store its bug id into
    367         # state["bug_id"])
    368         state["bug_blocked"] = state["bug_id"]
    369         del state["bug_id"]
    370         state["bug_title"] = "REGRESSION(r%s): %s" % (state["revision"], state["reason"])
    371         state["bug_description"] = "%s broke the build:\n%s" % (urls.view_revision_url(state["revision"]), state["reason"])
    372         # FIXME: If we had more context here, we could link to other open bugs
    373         #        that mention the test that regressed.
    374         if options.parent_command == "sheriff-bot":
    375             state["bug_description"] += """
    376 
    377 This is an automatic bug report generated by the sheriff-bot. If this bug
    378 report was created because of a flaky test, please file a bug for the flaky
    379 test (if we don't already have one on file) and dup this bug against that bug
    380 so that we can track how often these flaky tests case pain.
    381 
    382 "Only you can prevent forest fires." -- Smokey the Bear
    383 """
    384         return state
    385 
    386 
    387 class Rollout(AbstractRolloutPrepCommand):
    388     name = "rollout"
    389     show_in_main_help = True
    390     help_text = "Revert the given revision(s) in the working copy and optionally commit the revert and re-open the original bug"
    391     long_help = """Updates the working copy.
    392 Applies the inverse diff for the provided revision.
    393 Creates an appropriate rollout ChangeLog, including a trac link and bug link.
    394 Opens the generated ChangeLogs in $EDITOR.
    395 Shows the prepared diff for confirmation.
    396 Commits the revert and updates the bug (including re-opening the bug if necessary)."""
    397     steps = [
    398         steps.CleanWorkingDirectory,
    399         steps.Update,
    400         steps.RevertRevision,
    401         steps.PrepareChangeLogForRevert,
    402         steps.EditChangeLog,
    403         steps.ConfirmDiff,
    404         steps.Build,
    405         steps.Commit,
    406         steps.ReopenBugAfterRollout,
    407     ]
    408