Clean up ChunkedUpdateDrawingAreaProxy
[WebKit-https.git] / WebKitTools / Scripts / webkitpy / tool / commands / upload.py
1 #!/usr/bin/env python
2 # Copyright (c) 2009, 2010 Google Inc. All rights reserved.
3 # Copyright (c) 2009 Apple Inc. All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
7 # met:
8
9 #     * Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 #     * Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following disclaimer
13 # in the documentation and/or other materials provided with the
14 # distribution.
15 #     * Neither the name of Google Inc. nor the names of its
16 # contributors may be used to endorse or promote products derived from
17 # this software without specific prior written permission.
18
19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31 import os
32 import re
33 import sys
34
35 from optparse import make_option
36
37 import webkitpy.tool.steps as steps
38
39 from webkitpy.common.config.committers import CommitterList
40 from webkitpy.common.net.bugzilla import parse_bug_id
41 from webkitpy.common.system.user import User
42 from webkitpy.thirdparty.mock import Mock
43 from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
44 from webkitpy.tool.grammar import pluralize, join_with_separators
45 from webkitpy.tool.comments import bug_comment_from_svn_revision
46 from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
47 from webkitpy.common.system.deprecated_logging import error, log
48
49
50 class CommitMessageForCurrentDiff(AbstractDeclarativeCommand):
51     name = "commit-message"
52     help_text = "Print a commit message suitable for the uncommitted changes"
53
54     def __init__(self):
55         options = [
56             steps.Options.git_commit,
57         ]
58         AbstractDeclarativeCommand.__init__(self, options=options)
59
60     def execute(self, options, args, tool):
61         # This command is a useful test to make sure commit_message_for_this_commit
62         # always returns the right value regardless of the current working directory.
63         print "%s" % tool.checkout().commit_message_for_this_commit(options.git_commit).message()
64
65
66 class CleanPendingCommit(AbstractDeclarativeCommand):
67     name = "clean-pending-commit"
68     help_text = "Clear r+ on obsolete patches so they do not appear in the pending-commit list."
69
70     # NOTE: This was designed to be generic, but right now we're only processing patches from the pending-commit list, so only r+ matters.
71     def _flags_to_clear_on_patch(self, patch):
72         if not patch.is_obsolete():
73             return None
74         what_was_cleared = []
75         if patch.review() == "+":
76             if patch.reviewer():
77                 what_was_cleared.append("%s's review+" % patch.reviewer().full_name)
78             else:
79                 what_was_cleared.append("review+")
80         return join_with_separators(what_was_cleared)
81
82     def execute(self, options, args, tool):
83         committers = CommitterList()
84         for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
85             bug = self._tool.bugs.fetch_bug(bug_id)
86             patches = bug.patches(include_obsolete=True)
87             for patch in patches:
88                 flags_to_clear = self._flags_to_clear_on_patch(patch)
89                 if not flags_to_clear:
90                     continue
91                 message = "Cleared %s from obsolete attachment %s so that this bug does not appear in http://webkit.org/pending-commit." % (flags_to_clear, patch.id())
92                 self._tool.bugs.obsolete_attachment(patch.id(), message)
93
94
95 # FIXME: This should be share more logic with AssignToCommitter and CleanPendingCommit
96 class CleanReviewQueue(AbstractDeclarativeCommand):
97     name = "clean-review-queue"
98     help_text = "Clear r? on obsolete patches so they do not appear in the pending-commit list."
99
100     def execute(self, options, args, tool):
101         queue_url = "http://webkit.org/pending-review"
102         # We do this inefficient dance to be more like webkit.org/pending-review
103         # bugs.queries.fetch_bug_ids_from_review_queue() doesn't return
104         # closed bugs, but folks using /pending-review will see them. :(
105         for patch_id in tool.bugs.queries.fetch_attachment_ids_from_review_queue():
106             patch = self._tool.bugs.fetch_attachment(patch_id)
107             if not patch.review() == "?":
108                 continue
109             attachment_obsolete_modifier = ""
110             if patch.is_obsolete():
111                 attachment_obsolete_modifier = "obsolete "
112             elif patch.bug().is_closed():
113                 bug_closed_explanation = "  If you would like this patch reviewed, please attach it to a new bug (or re-open this bug before marking it for review again)."
114             else:
115                 # Neither the patch was obsolete or the bug was closed, next patch...
116                 continue
117             message = "Cleared review? from %sattachment %s so that this bug does not appear in %s.%s" % (attachment_obsolete_modifier, patch.id(), queue_url, bug_closed_explanation)
118             self._tool.bugs.obsolete_attachment(patch.id(), message)
119
120
121 class AssignToCommitter(AbstractDeclarativeCommand):
122     name = "assign-to-committer"
123     help_text = "Assign bug to whoever attached the most recent r+'d patch"
124
125     def _patches_have_commiters(self, reviewed_patches):
126         for patch in reviewed_patches:
127             if not patch.committer():
128                 return False
129         return True
130
131     def _assign_bug_to_last_patch_attacher(self, bug_id):
132         committers = CommitterList()
133         bug = self._tool.bugs.fetch_bug(bug_id)
134         if not bug.is_unassigned():
135             assigned_to_email = bug.assigned_to_email()
136             log("Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email)))
137             return
138
139         reviewed_patches = bug.reviewed_patches()
140         if not reviewed_patches:
141             log("Bug %s has no non-obsolete patches, ignoring." % bug_id)
142             return
143
144         # We only need to do anything with this bug if one of the r+'d patches does not have a valid committer (cq+ set).
145         if self._patches_have_commiters(reviewed_patches):
146             log("All reviewed patches on bug %s already have commit-queue+, ignoring." % bug_id)
147             return
148
149         latest_patch = reviewed_patches[-1]
150         attacher_email = latest_patch.attacher_email()
151         committer = committers.committer_by_email(attacher_email)
152         if not committer:
153             log("Attacher %s is not a committer.  Bug %s likely needs commit-queue+." % (attacher_email, bug_id))
154             return
155
156         reassign_message = "Attachment %s was posted by a committer and has review+, assigning to %s for commit." % (latest_patch.id(), committer.full_name)
157         self._tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message)
158
159     def execute(self, options, args, tool):
160         for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
161             self._assign_bug_to_last_patch_attacher(bug_id)
162
163
164 class ObsoleteAttachments(AbstractSequencedCommand):
165     name = "obsolete-attachments"
166     help_text = "Mark all attachments on a bug as obsolete"
167     argument_names = "BUGID"
168     steps = [
169         steps.ObsoletePatches,
170     ]
171
172     def _prepare_state(self, options, args, tool):
173         return { "bug_id" : args[0] }
174
175
176 class AbstractPatchUploadingCommand(AbstractSequencedCommand):
177     def _bug_id(self, options, args, tool, state):
178         # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs).
179         bug_id = args and args[0]
180         if not bug_id:
181             changed_files = self._tool.scm().changed_files(options.git_commit)
182             state["changed_files"] = changed_files
183             bug_id = tool.checkout().bug_id_for_this_commit(options.git_commit, changed_files)
184         return bug_id
185
186     def _prepare_state(self, options, args, tool):
187         state = {}
188         state["bug_id"] = self._bug_id(options, args, tool, state)
189         if not state["bug_id"]:
190             error("No bug id passed and no bug url found in ChangeLogs.")
191         return state
192
193
194 class Post(AbstractPatchUploadingCommand):
195     name = "post"
196     help_text = "Attach the current working directory diff to a bug as a patch file"
197     argument_names = "[BUGID]"
198     steps = [
199         steps.CheckStyle,
200         steps.ConfirmDiff,
201         steps.ObsoletePatches,
202         steps.SuggestReviewers,
203         steps.PostDiff,
204     ]
205
206
207 class LandSafely(AbstractPatchUploadingCommand):
208     name = "land-safely"
209     help_text = "Land the current diff via the commit-queue"
210     argument_names = "[BUGID]"
211     long_help = """land-safely updates the ChangeLog with the reviewer listed
212     in bugs.webkit.org for BUGID (or the bug ID detected from the ChangeLog).
213     The command then uploads the current diff to the bug and marks it for
214     commit by the commit-queue."""
215     show_in_main_help = True
216     steps = [
217         steps.UpdateChangeLogsWithReviewer,
218         steps.ObsoletePatches,
219         steps.PostDiffForCommit,
220     ]
221
222
223 class Prepare(AbstractSequencedCommand):
224     name = "prepare"
225     help_text = "Creates a bug (or prompts for an existing bug) and prepares the ChangeLogs"
226     argument_names = "[BUGID]"
227     steps = [
228         steps.PromptForBugOrTitle,
229         steps.CreateBug,
230         steps.PrepareChangeLog,
231     ]
232
233     def _prepare_state(self, options, args, tool):
234         bug_id = args and args[0]
235         return { "bug_id" : bug_id }
236
237
238 class Upload(AbstractPatchUploadingCommand):
239     name = "upload"
240     help_text = "Automates the process of uploading a patch for review"
241     argument_names = "[BUGID]"
242     show_in_main_help = True
243     steps = [
244         steps.CheckStyle,
245         steps.PromptForBugOrTitle,
246         steps.CreateBug,
247         steps.PrepareChangeLog,
248         steps.EditChangeLog,
249         steps.ConfirmDiff,
250         steps.ObsoletePatches,
251         steps.SuggestReviewers,
252         steps.PostDiff,
253     ]
254     long_help = """upload uploads the current diff to bugs.webkit.org.
255     If no bug id is provided, upload will create a bug.
256     If the current diff does not have a ChangeLog, upload
257     will prepare a ChangeLog.  Once a patch is read, upload
258     will open the ChangeLogs for editing using the command in the
259     EDITOR environment variable and will display the diff using the
260     command in the PAGER environment variable."""
261
262     def _prepare_state(self, options, args, tool):
263         state = {}
264         state["bug_id"] = self._bug_id(options, args, tool, state)
265         return state
266
267
268 class EditChangeLogs(AbstractSequencedCommand):
269     name = "edit-changelogs"
270     help_text = "Opens modified ChangeLogs in $EDITOR"
271     show_in_main_help = True
272     steps = [
273         steps.EditChangeLog,
274     ]
275
276
277 class PostCommits(AbstractDeclarativeCommand):
278     name = "post-commits"
279     help_text = "Attach a range of local commits to bugs as patch files"
280     argument_names = "COMMITISH"
281
282     def __init__(self):
283         options = [
284             make_option("-b", "--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
285             make_option("--add-log-as-comment", action="store_true", dest="add_log_as_comment", default=False, help="Add commit log message as a comment when uploading the patch."),
286             make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"),
287             steps.Options.obsolete_patches,
288             steps.Options.review,
289             steps.Options.request_commit,
290         ]
291         AbstractDeclarativeCommand.__init__(self, options=options, requires_local_commits=True)
292
293     def _comment_text_for_commit(self, options, commit_message, tool, commit_id):
294         comment_text = None
295         if (options.add_log_as_comment):
296             comment_text = commit_message.body(lstrip=True)
297             comment_text += "---\n"
298             comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
299         return comment_text
300
301     def execute(self, options, args, tool):
302         commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
303         if len(commit_ids) > 10: # We could lower this limit, 10 is too many for one bug as-is.
304             error("webkit-patch does not support attaching %s at once.  Are you sure you passed the right commit range?" % (pluralize("patch", len(commit_ids))))
305
306         have_obsoleted_patches = set()
307         for commit_id in commit_ids:
308             commit_message = tool.scm().commit_message_for_local_commit(commit_id)
309
310             # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs).
311             bug_id = options.bug_id or parse_bug_id(commit_message.message()) or parse_bug_id(tool.scm().create_patch(git_commit=commit_id))
312             if not bug_id:
313                 log("Skipping %s: No bug id found in commit or specified with --bug-id." % commit_id)
314                 continue
315
316             if options.obsolete_patches and bug_id not in have_obsoleted_patches:
317                 state = { "bug_id": bug_id }
318                 steps.ObsoletePatches(tool, options).run(state)
319                 have_obsoleted_patches.add(bug_id)
320
321             diff = tool.scm().create_patch(git_commit=commit_id)
322             description = options.description or commit_message.description(lstrip=True, strip_url=True)
323             comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id)
324             tool.bugs.add_patch_to_bug(bug_id, diff, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
325
326
327 # FIXME: This command needs to be brought into the modern age with steps and CommitInfo.
328 class MarkBugFixed(AbstractDeclarativeCommand):
329     name = "mark-bug-fixed"
330     help_text = "Mark the specified bug as fixed"
331     argument_names = "[SVN_REVISION]"
332     def __init__(self):
333         options = [
334             make_option("--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
335             make_option("--comment", action="store", type="string", dest="comment", help="Text to include in bug comment."),
336             make_option("--open", action="store_true", default=False, dest="open_bug", help="Open bug in default web browser (Mac only)."),
337             make_option("--update-only", action="store_true", default=False, dest="update_only", help="Add comment to the bug, but do not close it."),
338         ]
339         AbstractDeclarativeCommand.__init__(self, options=options)
340
341     # FIXME: We should be using checkout().changelog_entries_for_revision(...) instead here.
342     def _fetch_commit_log(self, tool, svn_revision):
343         if not svn_revision:
344             return tool.scm().last_svn_commit_log()
345         return tool.scm().svn_commit_log(svn_revision)
346
347     def _determine_bug_id_and_svn_revision(self, tool, bug_id, svn_revision):
348         commit_log = self._fetch_commit_log(tool, svn_revision)
349
350         if not bug_id:
351             bug_id = parse_bug_id(commit_log)
352
353         if not svn_revision:
354             match = re.search("^r(?P<svn_revision>\d+) \|", commit_log, re.MULTILINE)
355             if match:
356                 svn_revision = match.group('svn_revision')
357
358         if not bug_id or not svn_revision:
359             not_found = []
360             if not bug_id:
361                 not_found.append("bug id")
362             if not svn_revision:
363                 not_found.append("svn revision")
364             error("Could not find %s on command-line or in %s."
365                   % (" or ".join(not_found), "r%s" % svn_revision if svn_revision else "last commit"))
366
367         return (bug_id, svn_revision)
368
369     def execute(self, options, args, tool):
370         bug_id = options.bug_id
371
372         svn_revision = args and args[0]
373         if svn_revision:
374             if re.match("^r[0-9]+$", svn_revision, re.IGNORECASE):
375                 svn_revision = svn_revision[1:]
376             if not re.match("^[0-9]+$", svn_revision):
377                 error("Invalid svn revision: '%s'" % svn_revision)
378
379         needs_prompt = False
380         if not bug_id or not svn_revision:
381             needs_prompt = True
382             (bug_id, svn_revision) = self._determine_bug_id_and_svn_revision(tool, bug_id, svn_revision)
383
384         log("Bug: <%s> %s" % (tool.bugs.bug_url_for_bug_id(bug_id), tool.bugs.fetch_bug_dictionary(bug_id)["title"]))
385         log("Revision: %s" % svn_revision)
386
387         if options.open_bug:
388             tool.user.open_url(tool.bugs.bug_url_for_bug_id(bug_id))
389
390         if needs_prompt:
391             if not tool.user.confirm("Is this correct?"):
392                 exit(1)
393
394         bug_comment = bug_comment_from_svn_revision(svn_revision)
395         if options.comment:
396             bug_comment = "%s\n\n%s" % (options.comment, bug_comment)
397
398         if options.update_only:
399             log("Adding comment to Bug %s." % bug_id)
400             tool.bugs.post_comment_to_bug(bug_id, bug_comment)
401         else:
402             log("Adding comment to Bug %s and marking as Resolved/Fixed." % bug_id)
403             tool.bugs.close_bug_as_fixed(bug_id, bug_comment)
404
405
406 # FIXME: Requires unit test.  Blocking issue: too complex for now.
407 class CreateBug(AbstractDeclarativeCommand):
408     name = "create-bug"
409     help_text = "Create a bug from local changes or local commits"
410     argument_names = "[COMMITISH]"
411
412     def __init__(self):
413         options = [
414             steps.Options.cc,
415             steps.Options.component,
416             make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."),
417             make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
418             make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
419         ]
420         AbstractDeclarativeCommand.__init__(self, options=options)
421
422     def create_bug_from_commit(self, options, args, tool):
423         commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
424         if len(commit_ids) > 3:
425             error("Are you sure you want to create one bug with %s patches?" % len(commit_ids))
426
427         commit_id = commit_ids[0]
428
429         bug_title = ""
430         comment_text = ""
431         if options.prompt:
432             (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
433         else:
434             commit_message = tool.scm().commit_message_for_local_commit(commit_id)
435             bug_title = commit_message.description(lstrip=True, strip_url=True)
436             comment_text = commit_message.body(lstrip=True)
437             comment_text += "---\n"
438             comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
439
440         diff = tool.scm().create_patch(git_commit=commit_id)
441         bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
442
443         if bug_id and len(commit_ids) > 1:
444             options.bug_id = bug_id
445             options.obsolete_patches = False
446             # FIXME: We should pass through --no-comment switch as well.
447             PostCommits.execute(self, options, commit_ids[1:], tool)
448
449     def create_bug_from_patch(self, options, args, tool):
450         bug_title = ""
451         comment_text = ""
452         if options.prompt:
453             (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
454         else:
455             commit_message = tool.checkout().commit_message_for_this_commit(options.git_commit)
456             bug_title = commit_message.description(lstrip=True, strip_url=True)
457             comment_text = commit_message.body(lstrip=True)
458
459         diff = tool.scm().create_patch(options.git_commit)
460         bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
461
462     def prompt_for_bug_title_and_comment(self):
463         bug_title = User.prompt("Bug title: ")
464         print "Bug comment (hit ^D on blank line to end):"
465         lines = sys.stdin.readlines()
466         try:
467             sys.stdin.seek(0, os.SEEK_END)
468         except IOError:
469             # Cygwin raises an Illegal Seek (errno 29) exception when the above
470             # seek() call is made. Ignoring it seems to cause no harm.
471             # FIXME: Figure out a way to get avoid the exception in the first
472             # place.
473             pass
474         comment_text = "".join(lines)
475         return (bug_title, comment_text)
476
477     def execute(self, options, args, tool):
478         if len(args):
479             if (not tool.scm().supports_local_commits()):
480                 error("Extra arguments not supported; patch is taken from working directory.")
481             self.create_bug_from_commit(options, args, tool)
482         else:
483             self.create_bug_from_patch(options, args, tool)