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