blob: 750794eabdf2d0a61a9a583ab7e403792a7bb877 [file] [log] [blame]
Ben Murdoch4a90d5f2016-03-22 12:00:34 +00001#!/usr/bin/env python
2# Copyright 2013 the V8 project authors. All rights reserved.
3# Redistribution and use in source and binary forms, with or without
4# modification, are permitted provided that the following conditions are
5# met:
6#
7# * Redistributions of source code must retain the above copyright
8# notice, this list of conditions and the following disclaimer.
9# * Redistributions in binary form must reproduce the above
10# copyright notice, this list of conditions and the following
11# disclaimer in the documentation and/or other materials provided
12# with the distribution.
13# * Neither the name of Google Inc. nor the names of its
14# contributors may be used to endorse or promote products derived
15# from this software without specific prior written permission.
16#
17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29import argparse
30import os
31import sys
32import tempfile
33import urllib2
34
35from common_includes import *
36
37PUSH_MSG_GIT_SUFFIX = " (based on %s)"
38
39
40class Preparation(Step):
41 MESSAGE = "Preparation."
42
43 def RunStep(self):
44 self.InitialEnvironmentChecks(self.default_cwd)
45 self.CommonPrepare()
46
47 if(self["current_branch"] == self.Config("CANDIDATESBRANCH")
48 or self["current_branch"] == self.Config("BRANCHNAME")):
49 print "Warning: Script started on branch %s" % self["current_branch"]
50
51 self.PrepareBranch()
52 self.DeleteBranch(self.Config("CANDIDATESBRANCH"))
53
54
55class FreshBranch(Step):
56 MESSAGE = "Create a fresh branch."
57
58 def RunStep(self):
59 self.GitCreateBranch(self.Config("BRANCHNAME"),
60 self.vc.RemoteMasterBranch())
61
62
63class PreparePushRevision(Step):
64 MESSAGE = "Check which revision to push."
65
66 def RunStep(self):
67 if self._options.revision:
68 self["push_hash"] = self._options.revision
69 else:
70 self["push_hash"] = self.GitLog(n=1, format="%H", git_hash="HEAD")
71 if not self["push_hash"]: # pragma: no cover
72 self.Die("Could not determine the git hash for the push.")
73
74
75class IncrementVersion(Step):
76 MESSAGE = "Increment version number."
77
78 def RunStep(self):
79 latest_version = self.GetLatestVersion()
80
81 # The version file on master can be used to bump up major/minor at
82 # branch time.
83 self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteMasterBranch())
84 self.ReadAndPersistVersion("master_")
85 master_version = self.ArrayToVersion("master_")
86
87 # Use the highest version from master or from tags to determine the new
88 # version.
89 authoritative_version = sorted(
90 [master_version, latest_version], key=SortingKey)[1]
91 self.StoreVersion(authoritative_version, "authoritative_")
92
93 # Variables prefixed with 'new_' contain the new version numbers for the
94 # ongoing candidates push.
95 self["new_major"] = self["authoritative_major"]
96 self["new_minor"] = self["authoritative_minor"]
97 self["new_build"] = str(int(self["authoritative_build"]) + 1)
98
99 # Make sure patch level is 0 in a new push.
100 self["new_patch"] = "0"
101
102 self["version"] = "%s.%s.%s" % (self["new_major"],
103 self["new_minor"],
104 self["new_build"])
105
106 print ("Incremented version to %s" % self["version"])
107
108
109class DetectLastRelease(Step):
110 MESSAGE = "Detect commit ID of last release base."
111
112 def RunStep(self):
113 if self._options.last_master:
114 self["last_push_master"] = self._options.last_master
115 else:
116 self["last_push_master"] = self.GetLatestReleaseBase()
117
118
119class PrepareChangeLog(Step):
120 MESSAGE = "Prepare raw ChangeLog entry."
121
122 def Reload(self, body):
123 """Attempts to reload the commit message from rietveld in order to allow
124 late changes to the LOG flag. Note: This is brittle to future changes of
125 the web page name or structure.
126 """
127 match = re.search(r"^Review URL: https://codereview\.chromium\.org/(\d+)$",
128 body, flags=re.M)
129 if match:
130 cl_url = ("https://codereview.chromium.org/%s/description"
131 % match.group(1))
132 try:
133 # Fetch from Rietveld but only retry once with one second delay since
134 # there might be many revisions.
135 body = self.ReadURL(cl_url, wait_plan=[1])
136 except urllib2.URLError: # pragma: no cover
137 pass
138 return body
139
140 def RunStep(self):
141 self["date"] = self.GetDate()
142 output = "%s: Version %s\n\n" % (self["date"], self["version"])
143 TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE"))
144 commits = self.GitLog(format="%H",
145 git_hash="%s..%s" % (self["last_push_master"],
146 self["push_hash"]))
147
148 # Cache raw commit messages.
149 commit_messages = [
150 [
151 self.GitLog(n=1, format="%s", git_hash=commit),
152 self.Reload(self.GitLog(n=1, format="%B", git_hash=commit)),
153 self.GitLog(n=1, format="%an", git_hash=commit),
154 ] for commit in commits.splitlines()
155 ]
156
157 # Auto-format commit messages.
158 body = MakeChangeLogBody(commit_messages, auto_format=True)
159 AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE"))
160
161 msg = (" Performance and stability improvements on all platforms."
162 "\n#\n# The change log above is auto-generated. Please review if "
163 "all relevant\n# commit messages from the list below are included."
164 "\n# All lines starting with # will be stripped.\n#\n")
165 AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE"))
166
167 # Include unformatted commit messages as a reference in a comment.
168 comment_body = MakeComment(MakeChangeLogBody(commit_messages))
169 AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE"))
170
171
172class EditChangeLog(Step):
173 MESSAGE = "Edit ChangeLog entry."
174
175 def RunStep(self):
176 print ("Please press <Return> to have your EDITOR open the ChangeLog "
177 "entry, then edit its contents to your liking. When you're done, "
178 "save the file and exit your EDITOR. ")
179 self.ReadLine(default="")
180 self.Editor(self.Config("CHANGELOG_ENTRY_FILE"))
181
182 # Strip comments and reformat with correct indentation.
183 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip()
184 changelog_entry = StripComments(changelog_entry)
185 changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines()))
186 changelog_entry = changelog_entry.lstrip()
187
188 if changelog_entry == "": # pragma: no cover
189 self.Die("Empty ChangeLog entry.")
190
191 # Safe new change log for adding it later to the candidates patch.
192 TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE"))
193
194
195class StragglerCommits(Step):
196 MESSAGE = ("Fetch straggler commits that sneaked in since this script was "
197 "started.")
198
199 def RunStep(self):
200 self.vc.Fetch()
201 self.GitCheckout(self.vc.RemoteMasterBranch())
202
203
204class SquashCommits(Step):
205 MESSAGE = "Squash commits into one."
206
207 def RunStep(self):
208 # Instead of relying on "git rebase -i", we'll just create a diff, because
209 # that's easier to automate.
210 TextToFile(self.GitDiff(self.vc.RemoteCandidateBranch(),
211 self["push_hash"]),
212 self.Config("PATCH_FILE"))
213
214 # Convert the ChangeLog entry to commit message format.
215 text = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
216
217 # Remove date and trailing white space.
218 text = re.sub(r"^%s: " % self["date"], "", text.rstrip())
219
220 # Show the used master hash in the commit message.
221 suffix = PUSH_MSG_GIT_SUFFIX % self["push_hash"]
222 text = MSub(r"^(Version \d+\.\d+\.\d+)$", "\\1%s" % suffix, text)
223
224 # Remove indentation and merge paragraphs into single long lines, keeping
225 # empty lines between them.
226 def SplitMapJoin(split_text, fun, join_text):
227 return lambda text: join_text.join(map(fun, text.split(split_text)))
228 strip = lambda line: line.strip()
229 text = SplitMapJoin("\n\n", SplitMapJoin("\n", strip, " "), "\n\n")(text)
230
231 if not text: # pragma: no cover
232 self.Die("Commit message editing failed.")
233 self["commit_title"] = text.splitlines()[0]
234 TextToFile(text, self.Config("COMMITMSG_FILE"))
235
236
237class NewBranch(Step):
238 MESSAGE = "Create a new branch from candidates."
239
240 def RunStep(self):
241 self.GitCreateBranch(self.Config("CANDIDATESBRANCH"),
242 self.vc.RemoteCandidateBranch())
243
244
245class ApplyChanges(Step):
246 MESSAGE = "Apply squashed changes."
247
248 def RunStep(self):
249 self.ApplyPatch(self.Config("PATCH_FILE"))
250 os.remove(self.Config("PATCH_FILE"))
251 # The change log has been modified by the patch. Reset it to the version
252 # on candidates and apply the exact changes determined by this
253 # PrepareChangeLog step above.
254 self.GitCheckoutFile(CHANGELOG_FILE, self.vc.RemoteCandidateBranch())
255 # The version file has been modified by the patch. Reset it to the version
256 # on candidates.
257 self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteCandidateBranch())
258
259
260class CommitSquash(Step):
261 MESSAGE = "Commit to local candidates branch."
262
263 def RunStep(self):
264 # Make a first commit with a slightly different title to not confuse
265 # the tagging.
266 msg = FileToText(self.Config("COMMITMSG_FILE")).splitlines()
267 msg[0] = msg[0].replace("(based on", "(squashed - based on")
268 self.GitCommit(message = "\n".join(msg))
269
270
271class PrepareVersionBranch(Step):
272 MESSAGE = "Prepare new branch to commit version and changelog file."
273
274 def RunStep(self):
275 self.GitCheckout("master")
276 self.Git("fetch")
277 self.GitDeleteBranch(self.Config("CANDIDATESBRANCH"))
278 self.GitCreateBranch(self.Config("CANDIDATESBRANCH"),
279 self.vc.RemoteCandidateBranch())
280
281
282class AddChangeLog(Step):
283 MESSAGE = "Add ChangeLog changes to candidates branch."
284
285 def RunStep(self):
286 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
287 old_change_log = FileToText(os.path.join(self.default_cwd, CHANGELOG_FILE))
288 new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log)
289 TextToFile(new_change_log, os.path.join(self.default_cwd, CHANGELOG_FILE))
290 os.remove(self.Config("CHANGELOG_ENTRY_FILE"))
291
292
293class SetVersion(Step):
294 MESSAGE = "Set correct version for candidates."
295
296 def RunStep(self):
297 self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_")
298
299
300class CommitCandidate(Step):
301 MESSAGE = "Commit version and changelog to local candidates branch."
302
303 def RunStep(self):
304 self.GitCommit(file_name = self.Config("COMMITMSG_FILE"))
305 os.remove(self.Config("COMMITMSG_FILE"))
306
307
308class SanityCheck(Step):
309 MESSAGE = "Sanity check."
310
311 def RunStep(self):
312 # TODO(machenbach): Run presubmit script here as it is now missing in the
313 # prepare push process.
314 if not self.Confirm("Please check if your local checkout is sane: Inspect "
315 "%s, compile, run tests. Do you want to commit this new candidates "
316 "revision to the repository?" % VERSION_FILE):
317 self.Die("Execution canceled.") # pragma: no cover
318
319
320class Land(Step):
321 MESSAGE = "Land the patch."
322
323 def RunStep(self):
324 self.vc.CLLand()
325
326
327class TagRevision(Step):
328 MESSAGE = "Tag the new revision."
329
330 def RunStep(self):
331 self.vc.Tag(
332 self["version"], self.vc.RemoteCandidateBranch(), self["commit_title"])
333
334
335class CleanUp(Step):
336 MESSAGE = "Done!"
337
338 def RunStep(self):
339 print("Congratulations, you have successfully created the candidates "
340 "revision %s."
341 % self["version"])
342
343 self.CommonCleanup()
344 if self.Config("CANDIDATESBRANCH") != self["current_branch"]:
345 self.GitDeleteBranch(self.Config("CANDIDATESBRANCH"))
346
347
348class PushToCandidates(ScriptsBase):
349 def _PrepareOptions(self, parser):
350 group = parser.add_mutually_exclusive_group()
351 group.add_argument("-f", "--force",
352 help="Don't prompt the user.",
353 default=False, action="store_true")
354 group.add_argument("-m", "--manual",
355 help="Prompt the user at every important step.",
356 default=False, action="store_true")
357 parser.add_argument("-b", "--last-master",
358 help=("The git commit ID of the last master "
359 "revision that was pushed to candidates. This is"
360 " used for the auto-generated ChangeLog entry."))
361 parser.add_argument("-l", "--last-push",
362 help="The git commit ID of the last candidates push.")
363 parser.add_argument("-R", "--revision",
364 help="The git commit ID to push (defaults to HEAD).")
365
366 def _ProcessOptions(self, options): # pragma: no cover
367 if not options.manual and not options.reviewer:
368 print "A reviewer (-r) is required in (semi-)automatic mode."
369 return False
370 if not options.manual and not options.author:
371 print "Specify your chromium.org email with -a in (semi-)automatic mode."
372 return False
373
374 options.tbr_commit = not options.manual
375 return True
376
377 def _Config(self):
378 return {
379 "BRANCHNAME": "prepare-push",
380 "CANDIDATESBRANCH": "candidates-push",
381 "PERSISTFILE_BASENAME": "/tmp/v8-push-to-candidates-tempfile",
382 "CHANGELOG_ENTRY_FILE":
383 "/tmp/v8-push-to-candidates-tempfile-changelog-entry",
384 "PATCH_FILE": "/tmp/v8-push-to-candidates-tempfile-patch-file",
385 "COMMITMSG_FILE": "/tmp/v8-push-to-candidates-tempfile-commitmsg",
386 }
387
388 def _Steps(self):
389 return [
390 Preparation,
391 FreshBranch,
392 PreparePushRevision,
393 IncrementVersion,
394 DetectLastRelease,
395 PrepareChangeLog,
396 EditChangeLog,
397 StragglerCommits,
398 SquashCommits,
399 NewBranch,
400 ApplyChanges,
401 CommitSquash,
402 SanityCheck,
403 Land,
404 PrepareVersionBranch,
405 AddChangeLog,
406 SetVersion,
407 CommitCandidate,
408 Land,
409 TagRevision,
410 CleanUp,
411 ]
412
413
414if __name__ == "__main__": # pragma: no cover
415 sys.exit(PushToCandidates().Run())