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