summaryrefslogtreecommitdiffstats
path: root/Tools/Scripts/webkitpy/tool/commands/upload.py
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/Scripts/webkitpy/tool/commands/upload.py')
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/upload.py483
1 files changed, 483 insertions, 0 deletions
diff --git a/Tools/Scripts/webkitpy/tool/commands/upload.py b/Tools/Scripts/webkitpy/tool/commands/upload.py
new file mode 100644
index 0000000..e12c8e2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/upload.py
@@ -0,0 +1,483 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, 2010 Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import re
+import sys
+
+from optparse import make_option
+
+import webkitpy.tool.steps as steps
+
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.net.bugzilla import parse_bug_id
+from webkitpy.common.system.user import User
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
+from webkitpy.tool.grammar import pluralize, join_with_separators
+from webkitpy.tool.comments import bug_comment_from_svn_revision
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+from webkitpy.common.system.deprecated_logging import error, log
+
+
+class CommitMessageForCurrentDiff(AbstractDeclarativeCommand):
+ name = "commit-message"
+ help_text = "Print a commit message suitable for the uncommitted changes"
+
+ def __init__(self):
+ options = [
+ steps.Options.git_commit,
+ ]
+ AbstractDeclarativeCommand.__init__(self, options=options)
+
+ def execute(self, options, args, tool):
+ # This command is a useful test to make sure commit_message_for_this_commit
+ # always returns the right value regardless of the current working directory.
+ print "%s" % tool.checkout().commit_message_for_this_commit(options.git_commit).message()
+
+
+class CleanPendingCommit(AbstractDeclarativeCommand):
+ name = "clean-pending-commit"
+ help_text = "Clear r+ on obsolete patches so they do not appear in the pending-commit list."
+
+ # NOTE: This was designed to be generic, but right now we're only processing patches from the pending-commit list, so only r+ matters.
+ def _flags_to_clear_on_patch(self, patch):
+ if not patch.is_obsolete():
+ return None
+ what_was_cleared = []
+ if patch.review() == "+":
+ if patch.reviewer():
+ what_was_cleared.append("%s's review+" % patch.reviewer().full_name)
+ else:
+ what_was_cleared.append("review+")
+ return join_with_separators(what_was_cleared)
+
+ def execute(self, options, args, tool):
+ committers = CommitterList()
+ for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
+ bug = self._tool.bugs.fetch_bug(bug_id)
+ patches = bug.patches(include_obsolete=True)
+ for patch in patches:
+ flags_to_clear = self._flags_to_clear_on_patch(patch)
+ if not flags_to_clear:
+ continue
+ 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())
+ self._tool.bugs.obsolete_attachment(patch.id(), message)
+
+
+# FIXME: This should be share more logic with AssignToCommitter and CleanPendingCommit
+class CleanReviewQueue(AbstractDeclarativeCommand):
+ name = "clean-review-queue"
+ help_text = "Clear r? on obsolete patches so they do not appear in the pending-commit list."
+
+ def execute(self, options, args, tool):
+ queue_url = "http://webkit.org/pending-review"
+ # We do this inefficient dance to be more like webkit.org/pending-review
+ # bugs.queries.fetch_bug_ids_from_review_queue() doesn't return
+ # closed bugs, but folks using /pending-review will see them. :(
+ for patch_id in tool.bugs.queries.fetch_attachment_ids_from_review_queue():
+ patch = self._tool.bugs.fetch_attachment(patch_id)
+ if not patch.review() == "?":
+ continue
+ attachment_obsolete_modifier = ""
+ if patch.is_obsolete():
+ attachment_obsolete_modifier = "obsolete "
+ elif patch.bug().is_closed():
+ 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)."
+ else:
+ # Neither the patch was obsolete or the bug was closed, next patch...
+ continue
+ 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)
+ self._tool.bugs.obsolete_attachment(patch.id(), message)
+
+
+class AssignToCommitter(AbstractDeclarativeCommand):
+ name = "assign-to-committer"
+ help_text = "Assign bug to whoever attached the most recent r+'d patch"
+
+ def _patches_have_commiters(self, reviewed_patches):
+ for patch in reviewed_patches:
+ if not patch.committer():
+ return False
+ return True
+
+ def _assign_bug_to_last_patch_attacher(self, bug_id):
+ committers = CommitterList()
+ bug = self._tool.bugs.fetch_bug(bug_id)
+ if not bug.is_unassigned():
+ assigned_to_email = bug.assigned_to_email()
+ log("Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email)))
+ return
+
+ reviewed_patches = bug.reviewed_patches()
+ if not reviewed_patches:
+ log("Bug %s has no non-obsolete patches, ignoring." % bug_id)
+ return
+
+ # We only need to do anything with this bug if one of the r+'d patches does not have a valid committer (cq+ set).
+ if self._patches_have_commiters(reviewed_patches):
+ log("All reviewed patches on bug %s already have commit-queue+, ignoring." % bug_id)
+ return
+
+ latest_patch = reviewed_patches[-1]
+ attacher_email = latest_patch.attacher_email()
+ committer = committers.committer_by_email(attacher_email)
+ if not committer:
+ log("Attacher %s is not a committer. Bug %s likely needs commit-queue+." % (attacher_email, bug_id))
+ return
+
+ reassign_message = "Attachment %s was posted by a committer and has review+, assigning to %s for commit." % (latest_patch.id(), committer.full_name)
+ self._tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message)
+
+ def execute(self, options, args, tool):
+ for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
+ self._assign_bug_to_last_patch_attacher(bug_id)
+
+
+class ObsoleteAttachments(AbstractSequencedCommand):
+ name = "obsolete-attachments"
+ help_text = "Mark all attachments on a bug as obsolete"
+ argument_names = "BUGID"
+ steps = [
+ steps.ObsoletePatches,
+ ]
+
+ def _prepare_state(self, options, args, tool):
+ return { "bug_id" : args[0] }
+
+
+class AbstractPatchUploadingCommand(AbstractSequencedCommand):
+ def _bug_id(self, options, args, tool, state):
+ # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs).
+ bug_id = args and args[0]
+ if not bug_id:
+ changed_files = self._tool.scm().changed_files(options.git_commit)
+ state["changed_files"] = changed_files
+ bug_id = tool.checkout().bug_id_for_this_commit(options.git_commit, changed_files)
+ return bug_id
+
+ def _prepare_state(self, options, args, tool):
+ state = {}
+ state["bug_id"] = self._bug_id(options, args, tool, state)
+ if not state["bug_id"]:
+ error("No bug id passed and no bug url found in ChangeLogs.")
+ return state
+
+
+class Post(AbstractPatchUploadingCommand):
+ name = "post"
+ help_text = "Attach the current working directory diff to a bug as a patch file"
+ argument_names = "[BUGID]"
+ steps = [
+ steps.CheckStyle,
+ steps.ConfirmDiff,
+ steps.ObsoletePatches,
+ steps.SuggestReviewers,
+ steps.PostDiff,
+ ]
+
+
+class LandSafely(AbstractPatchUploadingCommand):
+ name = "land-safely"
+ help_text = "Land the current diff via the commit-queue"
+ argument_names = "[BUGID]"
+ long_help = """land-safely updates the ChangeLog with the reviewer listed
+ in bugs.webkit.org for BUGID (or the bug ID detected from the ChangeLog).
+ The command then uploads the current diff to the bug and marks it for
+ commit by the commit-queue."""
+ show_in_main_help = True
+ steps = [
+ steps.UpdateChangeLogsWithReviewer,
+ steps.ObsoletePatches,
+ steps.PostDiffForCommit,
+ ]
+
+
+class Prepare(AbstractSequencedCommand):
+ name = "prepare"
+ help_text = "Creates a bug (or prompts for an existing bug) and prepares the ChangeLogs"
+ argument_names = "[BUGID]"
+ steps = [
+ steps.PromptForBugOrTitle,
+ steps.CreateBug,
+ steps.PrepareChangeLog,
+ ]
+
+ def _prepare_state(self, options, args, tool):
+ bug_id = args and args[0]
+ return { "bug_id" : bug_id }
+
+
+class Upload(AbstractPatchUploadingCommand):
+ name = "upload"
+ help_text = "Automates the process of uploading a patch for review"
+ argument_names = "[BUGID]"
+ show_in_main_help = True
+ steps = [
+ steps.CheckStyle,
+ steps.PromptForBugOrTitle,
+ steps.CreateBug,
+ steps.PrepareChangeLog,
+ steps.EditChangeLog,
+ steps.ConfirmDiff,
+ steps.ObsoletePatches,
+ steps.SuggestReviewers,
+ steps.PostDiff,
+ ]
+ long_help = """upload uploads the current diff to bugs.webkit.org.
+ If no bug id is provided, upload will create a bug.
+ If the current diff does not have a ChangeLog, upload
+ will prepare a ChangeLog. Once a patch is read, upload
+ will open the ChangeLogs for editing using the command in the
+ EDITOR environment variable and will display the diff using the
+ command in the PAGER environment variable."""
+
+ def _prepare_state(self, options, args, tool):
+ state = {}
+ state["bug_id"] = self._bug_id(options, args, tool, state)
+ return state
+
+
+class EditChangeLogs(AbstractSequencedCommand):
+ name = "edit-changelogs"
+ help_text = "Opens modified ChangeLogs in $EDITOR"
+ show_in_main_help = True
+ steps = [
+ steps.EditChangeLog,
+ ]
+
+
+class PostCommits(AbstractDeclarativeCommand):
+ name = "post-commits"
+ help_text = "Attach a range of local commits to bugs as patch files"
+ argument_names = "COMMITISH"
+
+ def __init__(self):
+ options = [
+ 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."),
+ 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."),
+ make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"),
+ steps.Options.obsolete_patches,
+ steps.Options.review,
+ steps.Options.request_commit,
+ ]
+ AbstractDeclarativeCommand.__init__(self, options=options, requires_local_commits=True)
+
+ def _comment_text_for_commit(self, options, commit_message, tool, commit_id):
+ comment_text = None
+ if (options.add_log_as_comment):
+ comment_text = commit_message.body(lstrip=True)
+ comment_text += "---\n"
+ comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
+ return comment_text
+
+ def execute(self, options, args, tool):
+ commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
+ if len(commit_ids) > 10: # We could lower this limit, 10 is too many for one bug as-is.
+ error("webkit-patch does not support attaching %s at once. Are you sure you passed the right commit range?" % (pluralize("patch", len(commit_ids))))
+
+ have_obsoleted_patches = set()
+ for commit_id in commit_ids:
+ commit_message = tool.scm().commit_message_for_local_commit(commit_id)
+
+ # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs).
+ bug_id = options.bug_id or parse_bug_id(commit_message.message()) or parse_bug_id(tool.scm().create_patch(git_commit=commit_id))
+ if not bug_id:
+ log("Skipping %s: No bug id found in commit or specified with --bug-id." % commit_id)
+ continue
+
+ if options.obsolete_patches and bug_id not in have_obsoleted_patches:
+ state = { "bug_id": bug_id }
+ steps.ObsoletePatches(tool, options).run(state)
+ have_obsoleted_patches.add(bug_id)
+
+ diff = tool.scm().create_patch(git_commit=commit_id)
+ description = options.description or commit_message.description(lstrip=True, strip_url=True)
+ comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id)
+ tool.bugs.add_patch_to_bug(bug_id, diff, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+
+# FIXME: This command needs to be brought into the modern age with steps and CommitInfo.
+class MarkBugFixed(AbstractDeclarativeCommand):
+ name = "mark-bug-fixed"
+ help_text = "Mark the specified bug as fixed"
+ argument_names = "[SVN_REVISION]"
+ def __init__(self):
+ options = [
+ make_option("--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
+ make_option("--comment", action="store", type="string", dest="comment", help="Text to include in bug comment."),
+ make_option("--open", action="store_true", default=False, dest="open_bug", help="Open bug in default web browser (Mac only)."),
+ make_option("--update-only", action="store_true", default=False, dest="update_only", help="Add comment to the bug, but do not close it."),
+ ]
+ AbstractDeclarativeCommand.__init__(self, options=options)
+
+ # FIXME: We should be using checkout().changelog_entries_for_revision(...) instead here.
+ def _fetch_commit_log(self, tool, svn_revision):
+ if not svn_revision:
+ return tool.scm().last_svn_commit_log()
+ return tool.scm().svn_commit_log(svn_revision)
+
+ def _determine_bug_id_and_svn_revision(self, tool, bug_id, svn_revision):
+ commit_log = self._fetch_commit_log(tool, svn_revision)
+
+ if not bug_id:
+ bug_id = parse_bug_id(commit_log)
+
+ if not svn_revision:
+ match = re.search("^r(?P<svn_revision>\d+) \|", commit_log, re.MULTILINE)
+ if match:
+ svn_revision = match.group('svn_revision')
+
+ if not bug_id or not svn_revision:
+ not_found = []
+ if not bug_id:
+ not_found.append("bug id")
+ if not svn_revision:
+ not_found.append("svn revision")
+ error("Could not find %s on command-line or in %s."
+ % (" or ".join(not_found), "r%s" % svn_revision if svn_revision else "last commit"))
+
+ return (bug_id, svn_revision)
+
+ def execute(self, options, args, tool):
+ bug_id = options.bug_id
+
+ svn_revision = args and args[0]
+ if svn_revision:
+ if re.match("^r[0-9]+$", svn_revision, re.IGNORECASE):
+ svn_revision = svn_revision[1:]
+ if not re.match("^[0-9]+$", svn_revision):
+ error("Invalid svn revision: '%s'" % svn_revision)
+
+ needs_prompt = False
+ if not bug_id or not svn_revision:
+ needs_prompt = True
+ (bug_id, svn_revision) = self._determine_bug_id_and_svn_revision(tool, bug_id, svn_revision)
+
+ log("Bug: <%s> %s" % (tool.bugs.bug_url_for_bug_id(bug_id), tool.bugs.fetch_bug_dictionary(bug_id)["title"]))
+ log("Revision: %s" % svn_revision)
+
+ if options.open_bug:
+ tool.user.open_url(tool.bugs.bug_url_for_bug_id(bug_id))
+
+ if needs_prompt:
+ if not tool.user.confirm("Is this correct?"):
+ exit(1)
+
+ bug_comment = bug_comment_from_svn_revision(svn_revision)
+ if options.comment:
+ bug_comment = "%s\n\n%s" % (options.comment, bug_comment)
+
+ if options.update_only:
+ log("Adding comment to Bug %s." % bug_id)
+ tool.bugs.post_comment_to_bug(bug_id, bug_comment)
+ else:
+ log("Adding comment to Bug %s and marking as Resolved/Fixed." % bug_id)
+ tool.bugs.close_bug_as_fixed(bug_id, bug_comment)
+
+
+# FIXME: Requires unit test. Blocking issue: too complex for now.
+class CreateBug(AbstractDeclarativeCommand):
+ name = "create-bug"
+ help_text = "Create a bug from local changes or local commits"
+ argument_names = "[COMMITISH]"
+
+ def __init__(self):
+ options = [
+ steps.Options.cc,
+ steps.Options.component,
+ make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."),
+ make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
+ make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
+ ]
+ AbstractDeclarativeCommand.__init__(self, options=options)
+
+ def create_bug_from_commit(self, options, args, tool):
+ commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
+ if len(commit_ids) > 3:
+ error("Are you sure you want to create one bug with %s patches?" % len(commit_ids))
+
+ commit_id = commit_ids[0]
+
+ bug_title = ""
+ comment_text = ""
+ if options.prompt:
+ (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
+ else:
+ commit_message = tool.scm().commit_message_for_local_commit(commit_id)
+ bug_title = commit_message.description(lstrip=True, strip_url=True)
+ comment_text = commit_message.body(lstrip=True)
+ comment_text += "---\n"
+ comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
+
+ diff = tool.scm().create_patch(git_commit=commit_id)
+ 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)
+
+ if bug_id and len(commit_ids) > 1:
+ options.bug_id = bug_id
+ options.obsolete_patches = False
+ # FIXME: We should pass through --no-comment switch as well.
+ PostCommits.execute(self, options, commit_ids[1:], tool)
+
+ def create_bug_from_patch(self, options, args, tool):
+ bug_title = ""
+ comment_text = ""
+ if options.prompt:
+ (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
+ else:
+ commit_message = tool.checkout().commit_message_for_this_commit(options.git_commit)
+ bug_title = commit_message.description(lstrip=True, strip_url=True)
+ comment_text = commit_message.body(lstrip=True)
+
+ diff = tool.scm().create_patch(options.git_commit)
+ 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)
+
+ def prompt_for_bug_title_and_comment(self):
+ bug_title = User.prompt("Bug title: ")
+ print "Bug comment (hit ^D on blank line to end):"
+ lines = sys.stdin.readlines()
+ try:
+ sys.stdin.seek(0, os.SEEK_END)
+ except IOError:
+ # Cygwin raises an Illegal Seek (errno 29) exception when the above
+ # seek() call is made. Ignoring it seems to cause no harm.
+ # FIXME: Figure out a way to get avoid the exception in the first
+ # place.
+ pass
+ comment_text = "".join(lines)
+ return (bug_title, comment_text)
+
+ def execute(self, options, args, tool):
+ if len(args):
+ if (not tool.scm().supports_local_commits()):
+ error("Extra arguments not supported; patch is taken from working directory.")
+ self.create_bug_from_commit(options, args, tool)
+ else:
+ self.create_bug_from_patch(options, args, tool)