WebKitTools/Scripts/webkitpy/tool/commands/upload.py
changeset 0 4f2f89ce4247
equal deleted inserted replaced
-1:000000000000 0:4f2f89ce4247
       
     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 class AssignToCommitter(AbstractDeclarativeCommand):
       
    96     name = "assign-to-committer"
       
    97     help_text = "Assign bug to whoever attached the most recent r+'d patch"
       
    98 
       
    99     def _patches_have_commiters(self, reviewed_patches):
       
   100         for patch in reviewed_patches:
       
   101             if not patch.committer():
       
   102                 return False
       
   103         return True
       
   104 
       
   105     def _assign_bug_to_last_patch_attacher(self, bug_id):
       
   106         committers = CommitterList()
       
   107         bug = self.tool.bugs.fetch_bug(bug_id)
       
   108         if not bug.is_unassigned():
       
   109             assigned_to_email = bug.assigned_to_email()
       
   110             log("Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email)))
       
   111             return
       
   112 
       
   113         reviewed_patches = bug.reviewed_patches()
       
   114         if not reviewed_patches:
       
   115             log("Bug %s has no non-obsolete patches, ignoring." % bug_id)
       
   116             return
       
   117 
       
   118         # We only need to do anything with this bug if one of the r+'d patches does not have a valid committer (cq+ set).
       
   119         if self._patches_have_commiters(reviewed_patches):
       
   120             log("All reviewed patches on bug %s already have commit-queue+, ignoring." % bug_id)
       
   121             return
       
   122 
       
   123         latest_patch = reviewed_patches[-1]
       
   124         attacher_email = latest_patch.attacher_email()
       
   125         committer = committers.committer_by_email(attacher_email)
       
   126         if not committer:
       
   127             log("Attacher %s is not a committer.  Bug %s likely needs commit-queue+." % (attacher_email, bug_id))
       
   128             return
       
   129 
       
   130         reassign_message = "Attachment %s was posted by a committer and has review+, assigning to %s for commit." % (latest_patch.id(), committer.full_name)
       
   131         self.tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message)
       
   132 
       
   133     def execute(self, options, args, tool):
       
   134         for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
       
   135             self._assign_bug_to_last_patch_attacher(bug_id)
       
   136 
       
   137 
       
   138 class ObsoleteAttachments(AbstractSequencedCommand):
       
   139     name = "obsolete-attachments"
       
   140     help_text = "Mark all attachments on a bug as obsolete"
       
   141     argument_names = "BUGID"
       
   142     steps = [
       
   143         steps.ObsoletePatches,
       
   144     ]
       
   145 
       
   146     def _prepare_state(self, options, args, tool):
       
   147         return { "bug_id" : args[0] }
       
   148 
       
   149 
       
   150 class AbstractPatchUploadingCommand(AbstractSequencedCommand):
       
   151     def _bug_id(self, options, args, tool, state):
       
   152         # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs).
       
   153         bug_id = args and args[0]
       
   154         if not bug_id:
       
   155             bug_id = tool.checkout().bug_id_for_this_commit(options.git_commit)
       
   156         return bug_id
       
   157 
       
   158     def _prepare_state(self, options, args, tool):
       
   159         state = {}
       
   160         state["bug_id"] = self._bug_id(options, args, tool, state)
       
   161         if not state["bug_id"]:
       
   162             error("No bug id passed and no bug url found in ChangeLogs.")
       
   163         return state
       
   164 
       
   165 
       
   166 class Post(AbstractPatchUploadingCommand):
       
   167     name = "post"
       
   168     help_text = "Attach the current working directory diff to a bug as a patch file"
       
   169     argument_names = "[BUGID]"
       
   170     steps = [
       
   171         steps.CheckStyle,
       
   172         steps.ConfirmDiff,
       
   173         steps.ObsoletePatches,
       
   174         steps.PostDiff,
       
   175     ]
       
   176 
       
   177 
       
   178 class LandSafely(AbstractPatchUploadingCommand):
       
   179     name = "land-safely"
       
   180     help_text = "Land the current diff via the commit-queue"
       
   181     argument_names = "[BUGID]"
       
   182     long_help = """land-safely updates the ChangeLog with the reviewer listed
       
   183     in bugs.webkit.org for BUGID (or the bug ID detected from the ChangeLog).
       
   184     The command then uploads the current diff to the bug and marks it for
       
   185     commit by the commit-queue."""
       
   186     show_in_main_help = True
       
   187     steps = [
       
   188         steps.UpdateChangeLogsWithReviewer,
       
   189         steps.ObsoletePatches,
       
   190         steps.PostDiffForCommit,
       
   191     ]
       
   192 
       
   193 
       
   194 class Prepare(AbstractSequencedCommand):
       
   195     name = "prepare"
       
   196     help_text = "Creates a bug (or prompts for an existing bug) and prepares the ChangeLogs"
       
   197     argument_names = "[BUGID]"
       
   198     steps = [
       
   199         steps.PromptForBugOrTitle,
       
   200         steps.CreateBug,
       
   201         steps.PrepareChangeLog,
       
   202     ]
       
   203 
       
   204     def _prepare_state(self, options, args, tool):
       
   205         bug_id = args and args[0]
       
   206         return { "bug_id" : bug_id }
       
   207 
       
   208 
       
   209 class Upload(AbstractPatchUploadingCommand):
       
   210     name = "upload"
       
   211     help_text = "Automates the process of uploading a patch for review"
       
   212     argument_names = "[BUGID]"
       
   213     show_in_main_help = True
       
   214     steps = [
       
   215         steps.CheckStyle,
       
   216         steps.PromptForBugOrTitle,
       
   217         steps.CreateBug,
       
   218         steps.PrepareChangeLog,
       
   219         steps.EditChangeLog,
       
   220         steps.ConfirmDiff,
       
   221         steps.ObsoletePatches,
       
   222         steps.PostDiff,
       
   223     ]
       
   224     long_help = """upload uploads the current diff to bugs.webkit.org.
       
   225     If no bug id is provided, upload will create a bug.
       
   226     If the current diff does not have a ChangeLog, upload
       
   227     will prepare a ChangeLog.  Once a patch is read, upload
       
   228     will open the ChangeLogs for editing using the command in the
       
   229     EDITOR environment variable and will display the diff using the
       
   230     command in the PAGER environment variable."""
       
   231 
       
   232     def _prepare_state(self, options, args, tool):
       
   233         state = {}
       
   234         state["bug_id"] = self._bug_id(options, args, tool, state)
       
   235         return state
       
   236 
       
   237 
       
   238 class EditChangeLogs(AbstractSequencedCommand):
       
   239     name = "edit-changelogs"
       
   240     help_text = "Opens modified ChangeLogs in $EDITOR"
       
   241     show_in_main_help = True
       
   242     steps = [
       
   243         steps.EditChangeLog,
       
   244     ]
       
   245 
       
   246 
       
   247 class PostCommits(AbstractDeclarativeCommand):
       
   248     name = "post-commits"
       
   249     help_text = "Attach a range of local commits to bugs as patch files"
       
   250     argument_names = "COMMITISH"
       
   251 
       
   252     def __init__(self):
       
   253         options = [
       
   254             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."),
       
   255             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."),
       
   256             make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"),
       
   257             steps.Options.obsolete_patches,
       
   258             steps.Options.review,
       
   259             steps.Options.request_commit,
       
   260         ]
       
   261         AbstractDeclarativeCommand.__init__(self, options=options, requires_local_commits=True)
       
   262 
       
   263     def _comment_text_for_commit(self, options, commit_message, tool, commit_id):
       
   264         comment_text = None
       
   265         if (options.add_log_as_comment):
       
   266             comment_text = commit_message.body(lstrip=True)
       
   267             comment_text += "---\n"
       
   268             comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
       
   269         return comment_text
       
   270 
       
   271     def execute(self, options, args, tool):
       
   272         commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
       
   273         if len(commit_ids) > 10: # We could lower this limit, 10 is too many for one bug as-is.
       
   274             error("webkit-patch does not support attaching %s at once.  Are you sure you passed the right commit range?" % (pluralize("patch", len(commit_ids))))
       
   275 
       
   276         have_obsoleted_patches = set()
       
   277         for commit_id in commit_ids:
       
   278             commit_message = tool.scm().commit_message_for_local_commit(commit_id)
       
   279 
       
   280             # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs).
       
   281             bug_id = options.bug_id or parse_bug_id(commit_message.message()) or parse_bug_id(tool.scm().create_patch(git_commit=commit_id))
       
   282             if not bug_id:
       
   283                 log("Skipping %s: No bug id found in commit or specified with --bug-id." % commit_id)
       
   284                 continue
       
   285 
       
   286             if options.obsolete_patches and bug_id not in have_obsoleted_patches:
       
   287                 state = { "bug_id": bug_id }
       
   288                 steps.ObsoletePatches(tool, options).run(state)
       
   289                 have_obsoleted_patches.add(bug_id)
       
   290 
       
   291             diff = tool.scm().create_patch(git_commit=commit_id)
       
   292             description = options.description or commit_message.description(lstrip=True, strip_url=True)
       
   293             comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id)
       
   294             tool.bugs.add_patch_to_bug(bug_id, diff, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
       
   295 
       
   296 
       
   297 # FIXME: This command needs to be brought into the modern age with steps and CommitInfo.
       
   298 class MarkBugFixed(AbstractDeclarativeCommand):
       
   299     name = "mark-bug-fixed"
       
   300     help_text = "Mark the specified bug as fixed"
       
   301     argument_names = "[SVN_REVISION]"
       
   302     def __init__(self):
       
   303         options = [
       
   304             make_option("--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
       
   305             make_option("--comment", action="store", type="string", dest="comment", help="Text to include in bug comment."),
       
   306             make_option("--open", action="store_true", default=False, dest="open_bug", help="Open bug in default web browser (Mac only)."),
       
   307             make_option("--update-only", action="store_true", default=False, dest="update_only", help="Add comment to the bug, but do not close it."),
       
   308         ]
       
   309         AbstractDeclarativeCommand.__init__(self, options=options)
       
   310 
       
   311     # FIXME: We should be using checkout().changelog_entries_for_revision(...) instead here.
       
   312     def _fetch_commit_log(self, tool, svn_revision):
       
   313         if not svn_revision:
       
   314             return tool.scm().last_svn_commit_log()
       
   315         return tool.scm().svn_commit_log(svn_revision)
       
   316 
       
   317     def _determine_bug_id_and_svn_revision(self, tool, bug_id, svn_revision):
       
   318         commit_log = self._fetch_commit_log(tool, svn_revision)
       
   319 
       
   320         if not bug_id:
       
   321             bug_id = parse_bug_id(commit_log)
       
   322 
       
   323         if not svn_revision:
       
   324             match = re.search("^r(?P<svn_revision>\d+) \|", commit_log, re.MULTILINE)
       
   325             if match:
       
   326                 svn_revision = match.group('svn_revision')
       
   327 
       
   328         if not bug_id or not svn_revision:
       
   329             not_found = []
       
   330             if not bug_id:
       
   331                 not_found.append("bug id")
       
   332             if not svn_revision:
       
   333                 not_found.append("svn revision")
       
   334             error("Could not find %s on command-line or in %s."
       
   335                   % (" or ".join(not_found), "r%s" % svn_revision if svn_revision else "last commit"))
       
   336 
       
   337         return (bug_id, svn_revision)
       
   338 
       
   339     def execute(self, options, args, tool):
       
   340         bug_id = options.bug_id
       
   341 
       
   342         svn_revision = args and args[0]
       
   343         if svn_revision:
       
   344             if re.match("^r[0-9]+$", svn_revision, re.IGNORECASE):
       
   345                 svn_revision = svn_revision[1:]
       
   346             if not re.match("^[0-9]+$", svn_revision):
       
   347                 error("Invalid svn revision: '%s'" % svn_revision)
       
   348 
       
   349         needs_prompt = False
       
   350         if not bug_id or not svn_revision:
       
   351             needs_prompt = True
       
   352             (bug_id, svn_revision) = self._determine_bug_id_and_svn_revision(tool, bug_id, svn_revision)
       
   353 
       
   354         log("Bug: <%s> %s" % (tool.bugs.bug_url_for_bug_id(bug_id), tool.bugs.fetch_bug_dictionary(bug_id)["title"]))
       
   355         log("Revision: %s" % svn_revision)
       
   356 
       
   357         if options.open_bug:
       
   358             tool.user.open_url(tool.bugs.bug_url_for_bug_id(bug_id))
       
   359 
       
   360         if needs_prompt:
       
   361             if not tool.user.confirm("Is this correct?"):
       
   362                 exit(1)
       
   363 
       
   364         bug_comment = bug_comment_from_svn_revision(svn_revision)
       
   365         if options.comment:
       
   366             bug_comment = "%s\n\n%s" % (options.comment, bug_comment)
       
   367 
       
   368         if options.update_only:
       
   369             log("Adding comment to Bug %s." % bug_id)
       
   370             tool.bugs.post_comment_to_bug(bug_id, bug_comment)
       
   371         else:
       
   372             log("Adding comment to Bug %s and marking as Resolved/Fixed." % bug_id)
       
   373             tool.bugs.close_bug_as_fixed(bug_id, bug_comment)
       
   374 
       
   375 
       
   376 # FIXME: Requires unit test.  Blocking issue: too complex for now.
       
   377 class CreateBug(AbstractDeclarativeCommand):
       
   378     name = "create-bug"
       
   379     help_text = "Create a bug from local changes or local commits"
       
   380     argument_names = "[COMMITISH]"
       
   381 
       
   382     def __init__(self):
       
   383         options = [
       
   384             steps.Options.cc,
       
   385             steps.Options.component,
       
   386             make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."),
       
   387             make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
       
   388             make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
       
   389         ]
       
   390         AbstractDeclarativeCommand.__init__(self, options=options)
       
   391 
       
   392     def create_bug_from_commit(self, options, args, tool):
       
   393         commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
       
   394         if len(commit_ids) > 3:
       
   395             error("Are you sure you want to create one bug with %s patches?" % len(commit_ids))
       
   396 
       
   397         commit_id = commit_ids[0]
       
   398 
       
   399         bug_title = ""
       
   400         comment_text = ""
       
   401         if options.prompt:
       
   402             (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
       
   403         else:
       
   404             commit_message = tool.scm().commit_message_for_local_commit(commit_id)
       
   405             bug_title = commit_message.description(lstrip=True, strip_url=True)
       
   406             comment_text = commit_message.body(lstrip=True)
       
   407             comment_text += "---\n"
       
   408             comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
       
   409 
       
   410         diff = tool.scm().create_patch(git_commit=commit_id)
       
   411         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)
       
   412 
       
   413         if bug_id and len(commit_ids) > 1:
       
   414             options.bug_id = bug_id
       
   415             options.obsolete_patches = False
       
   416             # FIXME: We should pass through --no-comment switch as well.
       
   417             PostCommits.execute(self, options, commit_ids[1:], tool)
       
   418 
       
   419     def create_bug_from_patch(self, options, args, tool):
       
   420         bug_title = ""
       
   421         comment_text = ""
       
   422         if options.prompt:
       
   423             (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
       
   424         else:
       
   425             commit_message = tool.checkout().commit_message_for_this_commit(options.git_commit)
       
   426             bug_title = commit_message.description(lstrip=True, strip_url=True)
       
   427             comment_text = commit_message.body(lstrip=True)
       
   428 
       
   429         diff = tool.scm().create_patch(options.git_commit)
       
   430         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)
       
   431 
       
   432     def prompt_for_bug_title_and_comment(self):
       
   433         bug_title = User.prompt("Bug title: ")
       
   434         print "Bug comment (hit ^D on blank line to end):"
       
   435         lines = sys.stdin.readlines()
       
   436         try:
       
   437             sys.stdin.seek(0, os.SEEK_END)
       
   438         except IOError:
       
   439             # Cygwin raises an Illegal Seek (errno 29) exception when the above
       
   440             # seek() call is made. Ignoring it seems to cause no harm.
       
   441             # FIXME: Figure out a way to get avoid the exception in the first
       
   442             # place.
       
   443             pass
       
   444         comment_text = "".join(lines)
       
   445         return (bug_title, comment_text)
       
   446 
       
   447     def execute(self, options, args, tool):
       
   448         if len(args):
       
   449             if (not tool.scm().supports_local_commits()):
       
   450                 error("Extra arguments not supported; patch is taken from working directory.")
       
   451             self.create_bug_from_commit(options, args, tool)
       
   452         else:
       
   453             self.create_bug_from_patch(options, args, tool)