blob: 6e821f2a0bff9b8f31c784750227ed517555aa69 [file] [log] [blame]
Ben Murdochb8a8cc12014-11-26 15:28:44 +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
Emily Bernierd0a1eb72015-03-24 16:35:39 -040037PUSH_MSG_GIT_SUFFIX = " (based on %s)"
38PUSH_MSG_GIT_RE = re.compile(r".* \(based on (?P<git_rev>[a-fA-F0-9]+)\)$")
Ben Murdochb8a8cc12014-11-26 15:28:44 +000039
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("TRUNKBRANCH")
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("TRUNKBRANCH"))
53
54
55class FreshBranch(Step):
56 MESSAGE = "Create a fresh branch."
57
58 def RunStep(self):
Emily Bernierd0a1eb72015-03-24 16:35:39 -040059 self.GitCreateBranch(self.Config("BRANCHNAME"),
60 self.vc.RemoteMasterBranch())
Ben Murdochb8a8cc12014-11-26 15:28:44 +000061
62
63class PreparePushRevision(Step):
64 MESSAGE = "Check which revision to push."
65
66 def RunStep(self):
67 if self._options.revision:
Emily Bernierd0a1eb72015-03-24 16:35:39 -040068 self["push_hash"] = self._options.revision
Ben Murdochb8a8cc12014-11-26 15:28:44 +000069 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 DetectLastPush(Step):
76 MESSAGE = "Detect commit ID of last push to trunk."
77
78 def RunStep(self):
79 last_push = self._options.last_push or self.FindLastTrunkPush()
80 while True:
81 # Print assumed commit, circumventing git's pager.
82 print self.GitLog(n=1, git_hash=last_push)
83 if self.Confirm("Is the commit printed above the last push to trunk?"):
84 break
85 last_push = self.FindLastTrunkPush(parent_hash=last_push)
86
87 if self._options.last_bleeding_edge:
88 # Read the bleeding edge revision of the last push from a command-line
89 # option.
90 last_push_bleeding_edge = self._options.last_bleeding_edge
91 else:
92 # Retrieve the bleeding edge revision of the last push from the text in
93 # the push commit message.
94 last_push_title = self.GitLog(n=1, format="%s", git_hash=last_push)
Emily Bernierd0a1eb72015-03-24 16:35:39 -040095 last_push_bleeding_edge = PUSH_MSG_GIT_RE.match(
96 last_push_title).group("git_rev")
97
Ben Murdochb8a8cc12014-11-26 15:28:44 +000098 if not last_push_bleeding_edge: # pragma: no cover
99 self.Die("Could not retrieve bleeding edge git hash for trunk push %s"
100 % last_push)
101
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400102 # This points to the git hash of the last push on trunk.
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000103 self["last_push_trunk"] = last_push
104 # This points to the last bleeding_edge revision that went into the last
105 # push.
106 # TODO(machenbach): Do we need a check to make sure we're not pushing a
107 # revision older than the last push? If we do this, the output of the
108 # current change log preparation won't make much sense.
109 self["last_push_bleeding_edge"] = last_push_bleeding_edge
110
111
112# TODO(machenbach): Code similarities with bump_up_version.py. Merge after
113# turning this script into a pure git script.
114class GetCurrentBleedingEdgeVersion(Step):
115 MESSAGE = "Get latest bleeding edge version."
116
117 def RunStep(self):
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400118 self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteMasterBranch())
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000119
120 # Store latest version.
121 self.ReadAndPersistVersion("latest_")
122 self["latest_version"] = self.ArrayToVersion("latest_")
123 print "Bleeding edge version: %s" % self["latest_version"]
124
125
126class IncrementVersion(Step):
127 MESSAGE = "Increment version number."
128
129 def RunStep(self):
130 # Retrieve current version from last trunk push.
131 self.GitCheckoutFile(VERSION_FILE, self["last_push_trunk"])
132 self.ReadAndPersistVersion()
133 self["trunk_version"] = self.ArrayToVersion("")
134
135 if self["latest_build"] == "9999": # pragma: no cover
136 # If version control on bleeding edge was switched off, just use the last
137 # trunk version.
138 self["latest_version"] = self["trunk_version"]
139
140 if SortingKey(self["trunk_version"]) < SortingKey(self["latest_version"]):
141 # If the version on bleeding_edge is newer than on trunk, use it.
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400142 self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteMasterBranch())
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000143 self.ReadAndPersistVersion()
144
145 if self.Confirm(("Automatically increment BUILD_NUMBER? (Saying 'n' will "
146 "fire up your EDITOR on %s so you can make arbitrary "
147 "changes. When you're done, save the file and exit your "
148 "EDITOR.)" % VERSION_FILE)):
149
150 text = FileToText(os.path.join(self.default_cwd, VERSION_FILE))
151 text = MSub(r"(?<=#define BUILD_NUMBER)(?P<space>\s+)\d*$",
152 r"\g<space>%s" % str(int(self["build"]) + 1),
153 text)
154 TextToFile(text, os.path.join(self.default_cwd, VERSION_FILE))
155 else:
156 self.Editor(os.path.join(self.default_cwd, VERSION_FILE))
157
158 # Variables prefixed with 'new_' contain the new version numbers for the
159 # ongoing trunk push.
160 self.ReadAndPersistVersion("new_")
161
162 # Make sure patch level is 0 in a new push.
163 self["new_patch"] = "0"
164
165 self["version"] = "%s.%s.%s" % (self["new_major"],
166 self["new_minor"],
167 self["new_build"])
168
169
170class PrepareChangeLog(Step):
171 MESSAGE = "Prepare raw ChangeLog entry."
172
173 def Reload(self, body):
174 """Attempts to reload the commit message from rietveld in order to allow
175 late changes to the LOG flag. Note: This is brittle to future changes of
176 the web page name or structure.
177 """
178 match = re.search(r"^Review URL: https://codereview\.chromium\.org/(\d+)$",
179 body, flags=re.M)
180 if match:
181 cl_url = ("https://codereview.chromium.org/%s/description"
182 % match.group(1))
183 try:
184 # Fetch from Rietveld but only retry once with one second delay since
185 # there might be many revisions.
186 body = self.ReadURL(cl_url, wait_plan=[1])
187 except urllib2.URLError: # pragma: no cover
188 pass
189 return body
190
191 def RunStep(self):
192 self["date"] = self.GetDate()
193 output = "%s: Version %s\n\n" % (self["date"], self["version"])
194 TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE"))
195 commits = self.GitLog(format="%H",
196 git_hash="%s..%s" % (self["last_push_bleeding_edge"],
197 self["push_hash"]))
198
199 # Cache raw commit messages.
200 commit_messages = [
201 [
202 self.GitLog(n=1, format="%s", git_hash=commit),
203 self.Reload(self.GitLog(n=1, format="%B", git_hash=commit)),
204 self.GitLog(n=1, format="%an", git_hash=commit),
205 ] for commit in commits.splitlines()
206 ]
207
208 # Auto-format commit messages.
209 body = MakeChangeLogBody(commit_messages, auto_format=True)
210 AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE"))
211
212 msg = (" Performance and stability improvements on all platforms."
213 "\n#\n# The change log above is auto-generated. Please review if "
214 "all relevant\n# commit messages from the list below are included."
215 "\n# All lines starting with # will be stripped.\n#\n")
216 AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE"))
217
218 # Include unformatted commit messages as a reference in a comment.
219 comment_body = MakeComment(MakeChangeLogBody(commit_messages))
220 AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE"))
221
222
223class EditChangeLog(Step):
224 MESSAGE = "Edit ChangeLog entry."
225
226 def RunStep(self):
227 print ("Please press <Return> to have your EDITOR open the ChangeLog "
228 "entry, then edit its contents to your liking. When you're done, "
229 "save the file and exit your EDITOR. ")
230 self.ReadLine(default="")
231 self.Editor(self.Config("CHANGELOG_ENTRY_FILE"))
232
233 # Strip comments and reformat with correct indentation.
234 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip()
235 changelog_entry = StripComments(changelog_entry)
236 changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines()))
237 changelog_entry = changelog_entry.lstrip()
238
239 if changelog_entry == "": # pragma: no cover
240 self.Die("Empty ChangeLog entry.")
241
242 # Safe new change log for adding it later to the trunk patch.
243 TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE"))
244
245
246class StragglerCommits(Step):
247 MESSAGE = ("Fetch straggler commits that sneaked in since this script was "
248 "started.")
249
250 def RunStep(self):
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400251 self.vc.Fetch()
252 self.GitCheckout(self.vc.RemoteMasterBranch())
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000253
254
255class SquashCommits(Step):
256 MESSAGE = "Squash commits into one."
257
258 def RunStep(self):
259 # Instead of relying on "git rebase -i", we'll just create a diff, because
260 # that's easier to automate.
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400261 TextToFile(self.GitDiff(self.vc.RemoteCandidateBranch(),
262 self["push_hash"]),
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000263 self.Config("PATCH_FILE"))
264
265 # Convert the ChangeLog entry to commit message format.
266 text = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
267
268 # Remove date and trailing white space.
269 text = re.sub(r"^%s: " % self["date"], "", text.rstrip())
270
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400271 # Show the used master hash in the commit message.
272 suffix = PUSH_MSG_GIT_SUFFIX % self["push_hash"]
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000273 text = MSub(r"^(Version \d+\.\d+\.\d+)$", "\\1%s" % suffix, text)
274
275 # Remove indentation and merge paragraphs into single long lines, keeping
276 # empty lines between them.
277 def SplitMapJoin(split_text, fun, join_text):
278 return lambda text: join_text.join(map(fun, text.split(split_text)))
279 strip = lambda line: line.strip()
280 text = SplitMapJoin("\n\n", SplitMapJoin("\n", strip, " "), "\n\n")(text)
281
282 if not text: # pragma: no cover
283 self.Die("Commit message editing failed.")
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400284 self["commit_title"] = text.splitlines()[0]
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000285 TextToFile(text, self.Config("COMMITMSG_FILE"))
286
287
288class NewBranch(Step):
289 MESSAGE = "Create a new branch from trunk."
290
291 def RunStep(self):
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400292 self.GitCreateBranch(self.Config("TRUNKBRANCH"),
293 self.vc.RemoteCandidateBranch())
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000294
295
296class ApplyChanges(Step):
297 MESSAGE = "Apply squashed changes."
298
299 def RunStep(self):
300 self.ApplyPatch(self.Config("PATCH_FILE"))
301 os.remove(self.Config("PATCH_FILE"))
302
303
304class AddChangeLog(Step):
305 MESSAGE = "Add ChangeLog changes to trunk branch."
306
307 def RunStep(self):
308 # The change log has been modified by the patch. Reset it to the version
309 # on trunk and apply the exact changes determined by this PrepareChangeLog
310 # step above.
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400311 self.GitCheckoutFile(CHANGELOG_FILE, self.vc.RemoteCandidateBranch())
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000312 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400313 old_change_log = FileToText(os.path.join(self.default_cwd, CHANGELOG_FILE))
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000314 new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log)
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400315 TextToFile(new_change_log, os.path.join(self.default_cwd, CHANGELOG_FILE))
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000316 os.remove(self.Config("CHANGELOG_ENTRY_FILE"))
317
318
319class SetVersion(Step):
320 MESSAGE = "Set correct version for trunk."
321
322 def RunStep(self):
323 # The version file has been modified by the patch. Reset it to the version
324 # on trunk and apply the correct version.
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400325 self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteCandidateBranch())
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000326 self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_")
327
328
329class CommitTrunk(Step):
330 MESSAGE = "Commit to local trunk branch."
331
332 def RunStep(self):
333 self.GitCommit(file_name = self.Config("COMMITMSG_FILE"))
334 os.remove(self.Config("COMMITMSG_FILE"))
335
336
337class SanityCheck(Step):
338 MESSAGE = "Sanity check."
339
340 def RunStep(self):
341 # TODO(machenbach): Run presubmit script here as it is now missing in the
342 # prepare push process.
343 if not self.Confirm("Please check if your local checkout is sane: Inspect "
344 "%s, compile, run tests. Do you want to commit this new trunk "
345 "revision to the repository?" % VERSION_FILE):
346 self.Die("Execution canceled.") # pragma: no cover
347
348
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400349class Land(Step):
350 MESSAGE = "Land the patch."
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000351
352 def RunStep(self):
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400353 self.vc.CLLand()
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000354
355
356class TagRevision(Step):
357 MESSAGE = "Tag the new revision."
358
359 def RunStep(self):
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400360 self.vc.Tag(
361 self["version"], self.vc.RemoteCandidateBranch(), self["commit_title"])
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000362
363
364class CleanUp(Step):
365 MESSAGE = "Done!"
366
367 def RunStep(self):
368 print("Congratulations, you have successfully created the trunk "
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400369 "revision %s."
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000370 % self["version"])
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000371
372 self.CommonCleanup()
373 if self.Config("TRUNKBRANCH") != self["current_branch"]:
374 self.GitDeleteBranch(self.Config("TRUNKBRANCH"))
375
376
377class PushToTrunk(ScriptsBase):
378 def _PrepareOptions(self, parser):
379 group = parser.add_mutually_exclusive_group()
380 group.add_argument("-f", "--force",
381 help="Don't prompt the user.",
382 default=False, action="store_true")
383 group.add_argument("-m", "--manual",
384 help="Prompt the user at every important step.",
385 default=False, action="store_true")
386 parser.add_argument("-b", "--last-bleeding-edge",
387 help=("The git commit ID of the last bleeding edge "
388 "revision that was pushed to trunk. This is "
389 "used for the auto-generated ChangeLog entry."))
390 parser.add_argument("-l", "--last-push",
391 help="The git commit ID of the last push to trunk.")
392 parser.add_argument("-R", "--revision",
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400393 help="The git commit ID to push (defaults to HEAD).")
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000394
395 def _ProcessOptions(self, options): # pragma: no cover
396 if not options.manual and not options.reviewer:
397 print "A reviewer (-r) is required in (semi-)automatic mode."
398 return False
399 if not options.manual and not options.author:
400 print "Specify your chromium.org email with -a in (semi-)automatic mode."
401 return False
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000402
403 options.tbr_commit = not options.manual
404 return True
405
406 def _Config(self):
407 return {
408 "BRANCHNAME": "prepare-push",
409 "TRUNKBRANCH": "trunk-push",
410 "PERSISTFILE_BASENAME": "/tmp/v8-push-to-trunk-tempfile",
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000411 "CHANGELOG_ENTRY_FILE": "/tmp/v8-push-to-trunk-tempfile-changelog-entry",
412 "PATCH_FILE": "/tmp/v8-push-to-trunk-tempfile-patch-file",
413 "COMMITMSG_FILE": "/tmp/v8-push-to-trunk-tempfile-commitmsg",
414 }
415
416 def _Steps(self):
417 return [
418 Preparation,
419 FreshBranch,
420 PreparePushRevision,
421 DetectLastPush,
422 GetCurrentBleedingEdgeVersion,
423 IncrementVersion,
424 PrepareChangeLog,
425 EditChangeLog,
426 StragglerCommits,
427 SquashCommits,
428 NewBranch,
429 ApplyChanges,
430 AddChangeLog,
431 SetVersion,
432 CommitTrunk,
433 SanityCheck,
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400434 Land,
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000435 TagRevision,
436 CleanUp,
437 ]
438
439
440if __name__ == "__main__": # pragma: no cover
441 sys.exit(PushToTrunk().Run())