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