2009-11-25 Eric Seidel <eric@webkit.org>
[WebKit-https.git] / WebKitTools / Scripts / modules / commands / upload.py
1 #!/usr/bin/env python
2 # Copyright (c) 2009, 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 # FIXME: Trim down this import list once we have unit tests.
32 import os
33 import re
34 import StringIO
35 import subprocess
36 import sys
37 import time
38
39 from datetime import datetime, timedelta
40 from optparse import make_option
41
42 from modules.bugzilla import Bugzilla, parse_bug_id
43 from modules.buildbot import BuildBot
44 from modules.changelogs import ChangeLog
45 from modules.comments import bug_comment_from_commit_text
46 from modules.grammar import pluralize
47 from modules.landingsequence import LandingSequence, ConditionalLandingSequence
48 from modules.logging import error, log, tee
49 from modules.multicommandtool import MultiCommandTool, Command
50 from modules.patchcollection import PatchCollection
51 from modules.scm import CommitMessage, detect_scm_system, ScriptError, CheckoutNeedsUpdate
52 from modules.statusbot import StatusBot
53 from modules.webkitlandingscripts import WebKitLandingScripts, commit_message_for_this_commit
54 from modules.webkitport import WebKitPort
55 from modules.workqueue import WorkQueue, WorkQueueDelegate
56
57 class CommitMessageForCurrentDiff(Command):
58     name = "commit-message"
59     show_in_main_help = False
60     def __init__(self):
61         Command.__init__(self, "Print a commit message suitable for the uncommitted changes")
62
63     def execute(self, options, args, tool):
64         os.chdir(tool.scm().checkout_root)
65         print "%s" % commit_message_for_this_commit(tool.scm()).message()
66
67
68 class ObsoleteAttachments(Command):
69     name = "obsolete-attachments"
70     show_in_main_help = False
71     def __init__(self):
72         Command.__init__(self, "Mark all attachments on a bug as obsolete", "BUGID")
73
74     def execute(self, options, args, tool):
75         bug_id = args[0]
76         attachments = tool.bugs.fetch_attachments_from_bug(bug_id)
77         for attachment in attachments:
78             if not attachment["is_obsolete"]:
79                 tool.bugs.obsolete_attachment(attachment["id"])
80
81
82 class PostDiff(Command):
83     name = "post-diff"
84     show_in_main_help = True
85     def __init__(self):
86         options = [
87             make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: \"patch\")"),
88         ]
89         options += self.posting_options()
90         Command.__init__(self, "Attach the current working directory diff to a bug as a patch file", "[BUGID]", options=options)
91
92     @staticmethod
93     def posting_options():
94         return [
95             make_option("--no-obsolete", action="store_false", dest="obsolete_patches", default=True, help="Do not obsolete old patches before posting this one."),
96             make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
97             make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
98         ]
99
100     @staticmethod
101     def obsolete_patches_on_bug(bug_id, bugs):
102         patches = bugs.fetch_patches_from_bug(bug_id)
103         if len(patches):
104             log("Obsoleting %s on bug %s" % (pluralize("old patch", len(patches)), bug_id))
105             for patch in patches:
106                 bugs.obsolete_attachment(patch["id"])
107
108     def execute(self, options, args, tool):
109         # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs).
110         bug_id = (args and args[0]) or parse_bug_id(tool.scm().create_patch())
111         if not bug_id:
112             error("No bug id passed and no bug url found in diff, can't post.")
113
114         if options.obsolete_patches:
115             self.obsolete_patches_on_bug(bug_id, tool.bugs)
116
117         diff = tool.scm().create_patch()
118         diff_file = StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object
119
120         description = options.description or "Patch"
121         tool.bugs.add_patch_to_bug(bug_id, diff_file, description, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
122
123
124 class PostCommits(Command):
125     name = "post-commits"
126     show_in_main_help = True
127     def __init__(self):
128         options = [
129             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."),
130             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."),
131             make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"),
132         ]
133         options += PostDiff.posting_options()
134         Command.__init__(self, "Attach a range of local commits to bugs as patch files", "COMMITISH", options=options, requires_local_commits=True)
135
136     def _comment_text_for_commit(self, options, commit_message, tool, commit_id):
137         comment_text = None
138         if (options.add_log_as_comment):
139             comment_text = commit_message.body(lstrip=True)
140             comment_text += "---\n"
141             comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
142         return comment_text
143
144     def _diff_file_for_commit(self, tool, commit_id):
145         diff = tool.scm().create_patch_from_local_commit(commit_id)
146         return StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object
147
148     def execute(self, options, args, tool):
149         commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
150         if len(commit_ids) > 10: # We could lower this limit, 10 is too many for one bug as-is.
151             error("bugzilla-tool does not support attaching %s at once.  Are you sure you passed the right commit range?" % (pluralize("patch", len(commit_ids))))
152
153         have_obsoleted_patches = set()
154         for commit_id in commit_ids:
155             commit_message = tool.scm().commit_message_for_local_commit(commit_id)
156
157             # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs).
158             bug_id = options.bug_id or parse_bug_id(commit_message.message()) or parse_bug_id(tool.scm().create_patch_from_local_commit(commit_id))
159             if not bug_id:
160                 log("Skipping %s: No bug id found in commit or specified with --bug-id." % commit_id)
161                 continue
162
163             if options.obsolete_patches and bug_id not in have_obsoleted_patches:
164                 PostDiff.obsolete_patches_on_bug(bug_id, tool.bugs)
165                 have_obsoleted_patches.add(bug_id)
166
167             diff_file = self._diff_file_for_commit(tool, commit_id)
168             description = options.description or commit_message.description(lstrip=True, strip_url=True)
169             comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id)
170             tool.bugs.add_patch_to_bug(bug_id, diff_file, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
171
172
173 class CreateBug(Command):
174     name = "create-bug"
175     show_in_main_help = True
176     def __init__(self):
177         options = [
178             make_option("--cc", action="store", type="string", dest="cc", help="Comma-separated list of email addresses to carbon-copy."),
179             make_option("--component", action="store", type="string", dest="component", help="Component for the new bug."),
180             make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."),
181             make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
182             make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
183         ]
184         Command.__init__(self, "Create a bug from local changes or local commits", "[COMMITISH]", options=options)
185
186     def create_bug_from_commit(self, options, args, tool):
187         commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
188         if len(commit_ids) > 3:
189             error("Are you sure you want to create one bug with %s patches?" % len(commit_ids))
190
191         commit_id = commit_ids[0]
192
193         bug_title = ""
194         comment_text = ""
195         if options.prompt:
196             (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
197         else:
198             commit_message = tool.scm().commit_message_for_local_commit(commit_id)
199             bug_title = commit_message.description(lstrip=True, strip_url=True)
200             comment_text = commit_message.body(lstrip=True)
201             comment_text += "---\n"
202             comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
203
204         diff = tool.scm().create_patch_from_local_commit(commit_id)
205         diff_file = StringIO.StringIO(diff) # create_bug_with_patch expects a file-like object
206         bug_id = tool.bugs.create_bug_with_patch(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
207
208         if bug_id and len(commit_ids) > 1:
209             options.bug_id = bug_id
210             options.obsolete_patches = False
211             # FIXME: We should pass through --no-comment switch as well.
212             PostCommits.execute(self, options, commit_ids[1:], tool)
213
214     def create_bug_from_patch(self, options, args, tool):
215         bug_title = ""
216         comment_text = ""
217         if options.prompt:
218             (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
219         else:
220             commit_message = commit_message_for_this_commit(tool.scm())
221             bug_title = commit_message.description(lstrip=True, strip_url=True)
222             comment_text = commit_message.body(lstrip=True)
223
224         diff = tool.scm().create_patch()
225         diff_file = StringIO.StringIO(diff) # create_bug_with_patch expects a file-like object
226         bug_id = tool.bugs.create_bug_with_patch(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
227
228     def prompt_for_bug_title_and_comment(self):
229         bug_title = raw_input("Bug title: ")
230         print "Bug comment (hit ^D on blank line to end):"
231         lines = sys.stdin.readlines()
232         try:
233             sys.stdin.seek(0, os.SEEK_END)
234         except IOError:
235             # Cygwin raises an Illegal Seek (errno 29) exception when the above
236             # seek() call is made. Ignoring it seems to cause no harm.
237             # FIXME: Figure out a way to get avoid the exception in the first
238             # place.
239             pass
240         comment_text = "".join(lines)
241         return (bug_title, comment_text)
242
243     def execute(self, options, args, tool):
244         if len(args):
245             if (not tool.scm().supports_local_commits()):
246                 error("Extra arguments not supported; patch is taken from working directory.")
247             self.create_bug_from_commit(options, args, tool)
248         else:
249             self.create_bug_from_patch(options, args, tool)