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 30import os 31 32from webkitpy.tool import steps 33 34from webkitpy.common.checkout.changelog import ChangeLog 35from webkitpy.common.config import urls 36from webkitpy.common.system.executive import ScriptError 37from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand 38from webkitpy.tool.commands.stepsequence import StepSequence 39from webkitpy.tool.comments import bug_comment_from_commit_text 40from webkitpy.tool.grammar import pluralize 41from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand 42from webkitpy.common.system.deprecated_logging import error, log 43 44 45class 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 56class Update(AbstractSequencedCommand): 57 name = "update" 58 help_text = "Update working copy (used internally)" 59 steps = [ 60 steps.CleanWorkingDirectory, 61 steps.Update, 62 ] 63 64 65class 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 78class 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 89class 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). 105land will NOT build and run the tests before committing, but you can use the --build option for that. 106If 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 116class 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 129class 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 155class 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 174class 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 179class 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 189class 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 201class 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 213class 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 226class 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. 236Downloads and applies the patches, creating local commits if necessary.""" 237 238 239class 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 246class 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 253class 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. 270Updates the working copy. 271Applies the patch. 272Builds. 273Runs the layout tests. 274Commits the patch. 275Clears the flags on the patch. 276Closes the bug if no patches are marked for review.""" 277 278 279class 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 286class 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 293class 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 333class 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. 337Applies the inverse diff for the provided revision(s). 338Creates 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 348class 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 377This is an automatic bug report generated by the sheriff-bot. If this bug 378report was created because of a flaky test, please file a bug for the flaky 379test (if we don't already have one on file) and dup this bug against that bug 380so 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 387class 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. 392Applies the inverse diff for the provided revision. 393Creates an appropriate rollout ChangeLog, including a trac link and bug link. 394Opens the generated ChangeLogs in $EDITOR. 395Shows the prepared diff for confirmation. 396Commits 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