Rename WebKitTools to Tools
[WebKit-https.git] / Tools / Scripts / webkitpy / tool / commands / download.py
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 import webkitpy.tool.steps as 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.Build,
99         steps.RunTests,
100         steps.Commit,
101         steps.CloseBugForLandDiff,
102     ]
103     long_help = """land commits the current working copy diff (just as svn or git commit would).
104 land will NOT build and run the tests before committing, but you can use the --build option for that.
105 If a bug id is provided, or one can be found in the ChangeLog land will update the bug after committing."""
106
107     def _prepare_state(self, options, args, tool):
108         changed_files = self._tool.scm().changed_files(options.git_commit)
109         return {
110             "changed_files": changed_files,
111             "bug_id": (args and args[0]) or tool.checkout().bug_id_for_this_commit(options.git_commit, changed_files),
112         }
113
114
115 class LandCowboy(AbstractSequencedCommand):
116     name = "land-cowboy"
117     help_text = "Prepares a ChangeLog and lands the current working directory diff."
118     steps = [
119         steps.PrepareChangeLog,
120         steps.EditChangeLog,
121         steps.ConfirmDiff,
122         steps.Build,
123         steps.RunTests,
124         steps.Commit,
125     ]
126
127
128 class AbstractPatchProcessingCommand(AbstractDeclarativeCommand):
129     # Subclasses must implement the methods below.  We don't declare them here
130     # because we want to be able to implement them with mix-ins.
131     #
132     # def _fetch_list_of_patches_to_process(self, options, args, tool):
133     # def _prepare_to_process(self, options, args, tool):
134
135     @staticmethod
136     def _collect_patches_by_bug(patches):
137         bugs_to_patches = {}
138         for patch in patches:
139             bugs_to_patches[patch.bug_id()] = bugs_to_patches.get(patch.bug_id(), []) + [patch]
140         return bugs_to_patches
141
142     def execute(self, options, args, tool):
143         self._prepare_to_process(options, args, tool)
144         patches = self._fetch_list_of_patches_to_process(options, args, tool)
145
146         # It's nice to print out total statistics.
147         bugs_to_patches = self._collect_patches_by_bug(patches)
148         log("Processing %s from %s." % (pluralize("patch", len(patches)), pluralize("bug", len(bugs_to_patches))))
149
150         for patch in patches:
151             self._process_patch(patch, options, args, tool)
152
153
154 class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand):
155     prepare_steps = None
156     main_steps = None
157
158     def __init__(self):
159         options = []
160         self._prepare_sequence = StepSequence(self.prepare_steps)
161         self._main_sequence = StepSequence(self.main_steps)
162         options = sorted(set(self._prepare_sequence.options() + self._main_sequence.options()))
163         AbstractPatchProcessingCommand.__init__(self, options)
164
165     def _prepare_to_process(self, options, args, tool):
166         self._prepare_sequence.run_and_handle_errors(tool, options)
167
168     def _process_patch(self, patch, options, args, tool):
169         state = { "patch" : patch }
170         self._main_sequence.run_and_handle_errors(tool, options, state)
171
172
173 class ProcessAttachmentsMixin(object):
174     def _fetch_list_of_patches_to_process(self, options, args, tool):
175         return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args)
176
177
178 class ProcessBugsMixin(object):
179     def _fetch_list_of_patches_to_process(self, options, args, tool):
180         all_patches = []
181         for bug_id in args:
182             patches = tool.bugs.fetch_bug(bug_id).reviewed_patches()
183             log("%s found on bug %s." % (pluralize("reviewed patch", len(patches)), bug_id))
184             all_patches += patches
185         return all_patches
186
187
188 class CheckStyle(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
189     name = "check-style"
190     help_text = "Run check-webkit-style on the specified attachments"
191     argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
192     main_steps = [
193         steps.CleanWorkingDirectory,
194         steps.Update,
195         steps.ApplyPatch,
196         steps.CheckStyle,
197     ]
198
199
200 class BuildAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
201     name = "build-attachment"
202     help_text = "Apply and build patches from bugzilla"
203     argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
204     main_steps = [
205         steps.CleanWorkingDirectory,
206         steps.Update,
207         steps.ApplyPatch,
208         steps.Build,
209     ]
210
211
212 class BuildAndTestAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
213     name = "build-and-test-attachment"
214     help_text = "Apply, build, and test patches from bugzilla"
215     argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
216     main_steps = [
217         steps.CleanWorkingDirectory,
218         steps.Update,
219         steps.ApplyPatch,
220         steps.Build,
221         steps.RunTests,
222     ]
223
224
225 class AbstractPatchApplyingCommand(AbstractPatchSequencingCommand):
226     prepare_steps = [
227         steps.EnsureLocalCommitIfNeeded,
228         steps.CleanWorkingDirectoryWithLocalCommits,
229         steps.Update,
230     ]
231     main_steps = [
232         steps.ApplyPatchWithLocalCommit,
233     ]
234     long_help = """Updates the working copy.
235 Downloads and applies the patches, creating local commits if necessary."""
236
237
238 class ApplyAttachment(AbstractPatchApplyingCommand, ProcessAttachmentsMixin):
239     name = "apply-attachment"
240     help_text = "Apply an attachment to the local working directory"
241     argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
242     show_in_main_help = True
243
244
245 class ApplyFromBug(AbstractPatchApplyingCommand, ProcessBugsMixin):
246     name = "apply-from-bug"
247     help_text = "Apply reviewed patches from provided bugs to the local working directory"
248     argument_names = "BUGID [BUGIDS]"
249     show_in_main_help = True
250
251
252 class AbstractPatchLandingCommand(AbstractPatchSequencingCommand):
253     prepare_steps = [
254         steps.EnsureBuildersAreGreen,
255     ]
256     main_steps = [
257         steps.CleanWorkingDirectory,
258         steps.Update,
259         steps.ApplyPatch,
260         steps.ValidateReviewer,
261         steps.Build,
262         steps.RunTests,
263         steps.Commit,
264         steps.ClosePatch,
265         steps.CloseBug,
266     ]
267     long_help = """Checks to make sure builders are green.
268 Updates the working copy.
269 Applies the patch.
270 Builds.
271 Runs the layout tests.
272 Commits the patch.
273 Clears the flags on the patch.
274 Closes the bug if no patches are marked for review."""
275
276
277 class LandAttachment(AbstractPatchLandingCommand, ProcessAttachmentsMixin):
278     name = "land-attachment"
279     help_text = "Land patches from bugzilla, optionally building and testing them first"
280     argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
281     show_in_main_help = True
282
283
284 class LandFromBug(AbstractPatchLandingCommand, ProcessBugsMixin):
285     name = "land-from-bug"
286     help_text = "Land all patches on the given bugs, optionally building and testing them first"
287     argument_names = "BUGID [BUGIDS]"
288     show_in_main_help = True
289
290
291 class AbstractRolloutPrepCommand(AbstractSequencedCommand):
292     argument_names = "REVISION [REVISIONS] REASON"
293
294     def _commit_info(self, revision):
295         commit_info = self._tool.checkout().commit_info_for_revision(revision)
296         if commit_info and commit_info.bug_id():
297             # Note: Don't print a bug URL here because it will confuse the
298             #       SheriffBot because the SheriffBot just greps the output
299             #       of create-rollout for bug URLs.  It should do better
300             #       parsing instead.
301             log("Preparing rollout for bug %s." % commit_info.bug_id())
302         else:
303             log("Unable to parse bug number from diff.")
304         return commit_info
305
306     def _prepare_state(self, options, args, tool):
307         revision_list = []
308         for revision in str(args[0]).split():
309             if revision.isdigit():
310                 revision_list.append(int(revision))
311             else:
312                 raise ScriptError(message="Invalid svn revision number: " + revision)
313         revision_list.sort()
314
315         # We use the earliest revision for the bug info
316         earliest_revision = revision_list[0]
317         commit_info = self._commit_info(earliest_revision)
318         cc_list = sorted([party.bugzilla_email()
319                           for party in commit_info.responsible_parties()
320                           if party.bugzilla_email()])
321         return {
322             "revision": earliest_revision,
323             "revision_list": revision_list,
324             "bug_id": commit_info.bug_id(),
325             # FIXME: We should used the list as the canonical representation.
326             "bug_cc": ",".join(cc_list),
327             "reason": args[1],
328         }
329
330
331 class PrepareRollout(AbstractRolloutPrepCommand):
332     name = "prepare-rollout"
333     help_text = "Revert the given revision(s) in the working copy and prepare ChangeLogs with revert reason"
334     long_help = """Updates the working copy.
335 Applies the inverse diff for the provided revision(s).
336 Creates an appropriate rollout ChangeLog, including a trac link and bug link.
337 """
338     steps = [
339         steps.CleanWorkingDirectory,
340         steps.Update,
341         steps.RevertRevision,
342         steps.PrepareChangeLogForRevert,
343     ]
344
345
346 class CreateRollout(AbstractRolloutPrepCommand):
347     name = "create-rollout"
348     help_text = "Creates a bug to track the broken SVN revision(s) and uploads a rollout patch."
349     steps = [
350         steps.CleanWorkingDirectory,
351         steps.Update,
352         steps.RevertRevision,
353         steps.CreateBug,
354         steps.PrepareChangeLogForRevert,
355         steps.PostDiffForRevert,
356     ]
357
358     def _prepare_state(self, options, args, tool):
359         state = AbstractRolloutPrepCommand._prepare_state(self, options, args, tool)
360         # Currently, state["bug_id"] points to the bug that caused the
361         # regression.  We want to create a new bug that blocks the old bug
362         # so we move state["bug_id"] to state["bug_blocked"] and delete the
363         # old state["bug_id"] so that steps.CreateBug will actually create
364         # the new bug that we want (and subsequently store its bug id into
365         # state["bug_id"])
366         state["bug_blocked"] = state["bug_id"]
367         del state["bug_id"]
368         state["bug_title"] = "REGRESSION(r%s): %s" % (state["revision"], state["reason"])
369         state["bug_description"] = "%s broke the build:\n%s" % (urls.view_revision_url(state["revision"]), state["reason"])
370         # FIXME: If we had more context here, we could link to other open bugs
371         #        that mention the test that regressed.
372         if options.parent_command == "sheriff-bot":
373             state["bug_description"] += """
374
375 This is an automatic bug report generated by the sheriff-bot. If this bug
376 report was created because of a flaky test, please file a bug for the flaky
377 test (if we don't already have one on file) and dup this bug against that bug
378 so that we can track how often these flaky tests case pain.
379
380 "Only you can prevent forest fires." -- Smokey the Bear
381 """
382         return state
383
384
385 class Rollout(AbstractRolloutPrepCommand):
386     name = "rollout"
387     show_in_main_help = True
388     help_text = "Revert the given revision(s) in the working copy and optionally commit the revert and re-open the original bug"
389     long_help = """Updates the working copy.
390 Applies the inverse diff for the provided revision.
391 Creates an appropriate rollout ChangeLog, including a trac link and bug link.
392 Opens the generated ChangeLogs in $EDITOR.
393 Shows the prepared diff for confirmation.
394 Commits the revert and updates the bug (including re-opening the bug if necessary)."""
395     steps = [
396         steps.CleanWorkingDirectory,
397         steps.Update,
398         steps.RevertRevision,
399         steps.PrepareChangeLogForRevert,
400         steps.EditChangeLog,
401         steps.ConfirmDiff,
402         steps.Build,
403         steps.Commit,
404         steps.ReopenBugAfterRollout,
405     ]