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 optparse import make_option
     33 
     34 import webkitpy.steps as steps
     35 
     36 from webkitpy.bugzilla import parse_bug_id
     37 # We could instead use from modules import buildsteps and then prefix every buildstep with "buildsteps."
     38 from webkitpy.changelogs import ChangeLog
     39 from webkitpy.commands.abstractsequencedcommand import AbstractSequencedCommand
     40 from webkitpy.comments import bug_comment_from_commit_text
     41 from webkitpy.executive import ScriptError
     42 from webkitpy.grammar import pluralize
     43 from webkitpy.webkit_logging import error, log
     44 from webkitpy.multicommandtool import AbstractDeclarativeCommand
     45 from webkitpy.stepsequence import StepSequence
     46 
     47 
     48 class Build(AbstractSequencedCommand):
     49     name = "build"
     50     help_text = "Update working copy and build"
     51     steps = [
     52         steps.CleanWorkingDirectory,
     53         steps.Update,
     54         steps.Build,
     55     ]
     56 
     57 
     58 class BuildAndTest(AbstractSequencedCommand):
     59     name = "build-and-test"
     60     help_text = "Update working copy, build, and run the tests"
     61     steps = [
     62         steps.CleanWorkingDirectory,
     63         steps.Update,
     64         steps.Build,
     65         steps.RunTests,
     66     ]
     67 
     68 
     69 class Land(AbstractSequencedCommand):
     70     name = "land"
     71     help_text = "Land the current working directory diff and updates the associated bug if any"
     72     argument_names = "[BUGID]"
     73     show_in_main_help = True
     74     steps = [
     75         steps.EnsureBuildersAreGreen,
     76         steps.UpdateChangeLogsWithReviewer,
     77         steps.EnsureBuildersAreGreen,
     78         steps.Build,
     79         steps.RunTests,
     80         steps.Commit,
     81         steps.CloseBugForLandDiff,
     82     ]
     83     long_help = """land commits the current working copy diff (just as svn or git commit would).
     84 land will build and run the tests before committing.
     85 If a bug id is provided, or one can be found in the ChangeLog land will update the bug after committing."""
     86 
     87     def _prepare_state(self, options, args, tool):
     88         return {
     89             "bug_id" : (args and args[0]) or parse_bug_id(tool.scm().create_patch()),
     90         }
     91 
     92 
     93 class AbstractPatchProcessingCommand(AbstractDeclarativeCommand):
     94     # Subclasses must implement the methods below.  We don't declare them here
     95     # because we want to be able to implement them with mix-ins.
     96     #
     97     # def _fetch_list_of_patches_to_process(self, options, args, tool):
     98     # def _prepare_to_process(self, options, args, tool):
     99 
    100     @staticmethod
    101     def _collect_patches_by_bug(patches):
    102         bugs_to_patches = {}
    103         for patch in patches:
    104             bugs_to_patches[patch.bug_id()] = bugs_to_patches.get(patch.bug_id(), []) + [patch]
    105         return bugs_to_patches
    106 
    107     def execute(self, options, args, tool):
    108         self._prepare_to_process(options, args, tool)
    109         patches = self._fetch_list_of_patches_to_process(options, args, tool)
    110 
    111         # It's nice to print out total statistics.
    112         bugs_to_patches = self._collect_patches_by_bug(patches)
    113         log("Processing %s from %s." % (pluralize("patch", len(patches)), pluralize("bug", len(bugs_to_patches))))
    114 
    115         for patch in patches:
    116             self._process_patch(patch, options, args, tool)
    117 
    118 
    119 class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand):
    120     prepare_steps = None
    121     main_steps = None
    122 
    123     def __init__(self):
    124         options = []
    125         self._prepare_sequence = StepSequence(self.prepare_steps)
    126         self._main_sequence = StepSequence(self.main_steps)
    127         options = sorted(set(self._prepare_sequence.options() + self._main_sequence.options()))
    128         AbstractPatchProcessingCommand.__init__(self, options)
    129 
    130     def _prepare_to_process(self, options, args, tool):
    131         self._prepare_sequence.run_and_handle_errors(tool, options)
    132 
    133     def _process_patch(self, patch, options, args, tool):
    134         state = { "patch" : patch }
    135         self._main_sequence.run_and_handle_errors(tool, options, state)
    136 
    137 
    138 class ProcessAttachmentsMixin(object):
    139     def _fetch_list_of_patches_to_process(self, options, args, tool):
    140         return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args)
    141 
    142 
    143 class ProcessBugsMixin(object):
    144     def _fetch_list_of_patches_to_process(self, options, args, tool):
    145         all_patches = []
    146         for bug_id in args:
    147             patches = tool.bugs.fetch_bug(bug_id).reviewed_patches()
    148             log("%s found on bug %s." % (pluralize("reviewed patch", len(patches)), bug_id))
    149             all_patches += patches
    150         return all_patches
    151 
    152 
    153 class CheckStyle(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
    154     name = "check-style"
    155     help_text = "Run check-webkit-style on the specified attachments"
    156     argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
    157     main_steps = [
    158         steps.CleanWorkingDirectory,
    159         steps.Update,
    160         steps.ApplyPatch,
    161         steps.CheckStyle,
    162     ]
    163 
    164 
    165 class BuildAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
    166     name = "build-attachment"
    167     help_text = "Apply and build patches from bugzilla"
    168     argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
    169     main_steps = [
    170         steps.CleanWorkingDirectory,
    171         steps.Update,
    172         steps.ApplyPatch,
    173         steps.Build,
    174     ]
    175 
    176 
    177 class AbstractPatchApplyingCommand(AbstractPatchSequencingCommand):
    178     prepare_steps = [
    179         steps.EnsureLocalCommitIfNeeded,
    180         steps.CleanWorkingDirectoryWithLocalCommits,
    181         steps.Update,
    182     ]
    183     main_steps = [
    184         steps.ApplyPatchWithLocalCommit,
    185     ]
    186     long_help = """Updates the working copy.
    187 Downloads and applies the patches, creating local commits if necessary."""
    188 
    189 
    190 class ApplyAttachment(AbstractPatchApplyingCommand, ProcessAttachmentsMixin):
    191     name = "apply-attachment"
    192     help_text = "Apply an attachment to the local working directory"
    193     argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
    194     show_in_main_help = True
    195 
    196 
    197 class ApplyFromBug(AbstractPatchApplyingCommand, ProcessBugsMixin):
    198     name = "apply-from-bug"
    199     help_text = "Apply reviewed patches from provided bugs to the local working directory"
    200     argument_names = "BUGID [BUGIDS]"
    201     show_in_main_help = True
    202 
    203 
    204 class AbstractPatchLandingCommand(AbstractPatchSequencingCommand):
    205     prepare_steps = [
    206         steps.EnsureBuildersAreGreen,
    207     ]
    208     main_steps = [
    209         steps.CleanWorkingDirectory,
    210         steps.Update,
    211         steps.ApplyPatch,
    212         steps.EnsureBuildersAreGreen,
    213         steps.Build,
    214         steps.RunTests,
    215         steps.Commit,
    216         steps.ClosePatch,
    217         steps.CloseBug,
    218     ]
    219     long_help = """Checks to make sure builders are green.
    220 Updates the working copy.
    221 Applies the patch.
    222 Builds.
    223 Runs the layout tests.
    224 Commits the patch.
    225 Clears the flags on the patch.
    226 Closes the bug if no patches are marked for review."""
    227 
    228 
    229 class LandAttachment(AbstractPatchLandingCommand, ProcessAttachmentsMixin):
    230     name = "land-attachment"
    231     help_text = "Land patches from bugzilla, optionally building and testing them first"
    232     argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
    233     show_in_main_help = True
    234 
    235 
    236 class LandFromBug(AbstractPatchLandingCommand, ProcessBugsMixin):
    237     name = "land-from-bug"
    238     help_text = "Land all patches on the given bugs, optionally building and testing them first"
    239     argument_names = "BUGID [BUGIDS]"
    240     show_in_main_help = True
    241 
    242 
    243 class Rollout(AbstractSequencedCommand):
    244     name = "rollout"
    245     show_in_main_help = True
    246     help_text = "Revert the given revision in the working copy and optionally commit the revert and re-open the original bug"
    247     argument_names = "REVISION REASON"
    248     long_help = """Updates the working copy.
    249 Applies the inverse diff for the provided revision.
    250 Creates an appropriate rollout ChangeLog, including a trac link and bug link.
    251 Opens the generated ChangeLogs in $EDITOR.
    252 Shows the prepared diff for confirmation.
    253 Commits the revert and updates the bug (including re-opening the bug if necessary)."""
    254     steps = [
    255         steps.CleanWorkingDirectory,
    256         steps.Update,
    257         steps.RevertRevision,
    258         steps.PrepareChangeLogForRevert,
    259         steps.EditChangeLog,
    260         steps.ConfirmDiff,
    261         steps.CompleteRollout,
    262     ]
    263 
    264     @staticmethod
    265     def _parse_bug_id_from_revision_diff(tool, revision):
    266         original_diff = tool.scm().diff_for_revision(revision)
    267         return parse_bug_id(original_diff)
    268 
    269     def execute(self, options, args, tool):
    270         revision = args[0]
    271         reason = args[1]
    272         bug_id = self._parse_bug_id_from_revision_diff(tool, revision)
    273         if options.complete_rollout:
    274             if bug_id:
    275                 log("Will re-open bug %s after rollout." % bug_id)
    276             else:
    277                 log("Failed to parse bug number from diff.  No bugs will be updated/reopened after the rollout.")
    278 
    279         state = {
    280             "revision" : revision,
    281             "bug_id" : bug_id,
    282             "reason" : reason,
    283         }
    284         self._sequence.run_and_handle_errors(tool, options, state)
    285