1#!/usr/bin/env python
2# Copyright 2015 the V8 project authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import argparse
7import os
8import sys
9import tempfile
10import urllib2
11
12from common_includes import *
13
14class Preparation(Step):
15  MESSAGE = "Preparation."
16
17  def RunStep(self):
18    self.Git("fetch origin +refs/heads/*:refs/heads/*")
19    self.GitCheckout("origin/master")
20    self.DeleteBranch("work-branch")
21
22
23class PrepareBranchRevision(Step):
24  MESSAGE = "Check from which revision to branch off."
25
26  def RunStep(self):
27    self["push_hash"] = (self._options.revision or
28                         self.GitLog(n=1, format="%H", branch="origin/master"))
29    assert self["push_hash"]
30    print "Release revision %s" % self["push_hash"]
31
32
33class IncrementVersion(Step):
34  MESSAGE = "Increment version number."
35
36  def RunStep(self):
37    latest_version = self.GetLatestVersion()
38
39    # The version file on master can be used to bump up major/minor at
40    # branch time.
41    self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteMasterBranch())
42    self.ReadAndPersistVersion("master_")
43    master_version = self.ArrayToVersion("master_")
44
45    # Use the highest version from master or from tags to determine the new
46    # version.
47    authoritative_version = sorted(
48        [master_version, latest_version], key=SortingKey)[1]
49    self.StoreVersion(authoritative_version, "authoritative_")
50
51    # Variables prefixed with 'new_' contain the new version numbers for the
52    # ongoing candidates push.
53    self["new_major"] = self["authoritative_major"]
54    self["new_minor"] = self["authoritative_minor"]
55    self["new_build"] = str(int(self["authoritative_build"]) + 1)
56
57    # Make sure patch level is 0 in a new push.
58    self["new_patch"] = "0"
59
60    # The new version is not a candidate.
61    self["new_candidate"] = "0"
62
63    self["version"] = "%s.%s.%s" % (self["new_major"],
64                                    self["new_minor"],
65                                    self["new_build"])
66
67    print ("Incremented version to %s" % self["version"])
68
69
70class DetectLastRelease(Step):
71  MESSAGE = "Detect commit ID of last release base."
72
73  def RunStep(self):
74    self["last_push_master"] = self.GetLatestReleaseBase()
75
76
77class PrepareChangeLog(Step):
78  MESSAGE = "Prepare raw ChangeLog entry."
79
80  def RunStep(self):
81    self["date"] = self.GetDate()
82    output = "%s: Version %s\n\n" % (self["date"], self["version"])
83    TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE"))
84    commits = self.GitLog(format="%H",
85        git_hash="%s..%s" % (self["last_push_master"],
86                             self["push_hash"]))
87
88    # Cache raw commit messages.
89    commit_messages = [
90      [
91        self.GitLog(n=1, format="%s", git_hash=commit),
92        self.GitLog(n=1, format="%B", git_hash=commit),
93        self.GitLog(n=1, format="%an", git_hash=commit),
94      ] for commit in commits.splitlines()
95    ]
96
97    # Auto-format commit messages.
98    body = MakeChangeLogBody(commit_messages, auto_format=True)
99    AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE"))
100
101    msg = ("        Performance and stability improvements on all platforms."
102           "\n#\n# The change log above is auto-generated. Please review if "
103           "all relevant\n# commit messages from the list below are included."
104           "\n# All lines starting with # will be stripped.\n#\n")
105    AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE"))
106
107    # Include unformatted commit messages as a reference in a comment.
108    comment_body = MakeComment(MakeChangeLogBody(commit_messages))
109    AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE"))
110
111
112class EditChangeLog(Step):
113  MESSAGE = "Edit ChangeLog entry."
114
115  def RunStep(self):
116    print ("Please press <Return> to have your EDITOR open the ChangeLog "
117           "entry, then edit its contents to your liking. When you're done, "
118           "save the file and exit your EDITOR. ")
119    self.ReadLine(default="")
120    self.Editor(self.Config("CHANGELOG_ENTRY_FILE"))
121
122    # Strip comments and reformat with correct indentation.
123    changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip()
124    changelog_entry = StripComments(changelog_entry)
125    changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines()))
126    changelog_entry = changelog_entry.lstrip()
127
128    if changelog_entry == "":  # pragma: no cover
129      self.Die("Empty ChangeLog entry.")
130
131    # Safe new change log for adding it later to the candidates patch.
132    TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE"))
133
134
135class PushBranchRef(Step):
136  MESSAGE = "Create branch ref."
137
138  def RunStep(self):
139    cmd = "push origin %s:refs/heads/%s" % (self["push_hash"], self["version"])
140    if self._options.dry_run:
141      print "Dry run. Command:\ngit %s" % cmd
142    else:
143      self.Git(cmd)
144
145
146class MakeBranch(Step):
147  MESSAGE = "Create the branch."
148
149  def RunStep(self):
150    self.Git("reset --hard origin/master")
151    self.Git("new-branch work-branch --upstream origin/%s" % self["version"])
152    self.GitCheckoutFile(CHANGELOG_FILE, self["latest_version"])
153    self.GitCheckoutFile(VERSION_FILE, self["latest_version"])
154    self.GitCheckoutFile(WATCHLISTS_FILE, self["latest_version"])
155
156
157class AddChangeLog(Step):
158  MESSAGE = "Add ChangeLog changes to release branch."
159
160  def RunStep(self):
161    changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
162    old_change_log = FileToText(os.path.join(self.default_cwd, CHANGELOG_FILE))
163    new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log)
164    TextToFile(new_change_log, os.path.join(self.default_cwd, CHANGELOG_FILE))
165
166
167class SetVersion(Step):
168  MESSAGE = "Set correct version for candidates."
169
170  def RunStep(self):
171    self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_")
172
173
174class EnableMergeWatchlist(Step):
175  MESSAGE = "Enable watchlist entry for merge notifications."
176
177  def RunStep(self):
178    old_watchlist_content = FileToText(os.path.join(self.default_cwd,
179                                                    WATCHLISTS_FILE))
180    new_watchlist_content = re.sub("(# 'v8-merges@googlegroups\.com',)",
181                                   "'v8-merges@googlegroups.com',",
182                                   old_watchlist_content)
183    TextToFile(new_watchlist_content, os.path.join(self.default_cwd,
184                                                   WATCHLISTS_FILE))
185
186
187class CommitBranch(Step):
188  MESSAGE = "Commit version and changelog to new branch."
189
190  def RunStep(self):
191    # Convert the ChangeLog entry to commit message format.
192    text = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
193
194    # Remove date and trailing white space.
195    text = re.sub(r"^%s: " % self["date"], "", text.rstrip())
196
197    # Remove indentation and merge paragraphs into single long lines, keeping
198    # empty lines between them.
199    def SplitMapJoin(split_text, fun, join_text):
200      return lambda text: join_text.join(map(fun, text.split(split_text)))
201    text = SplitMapJoin(
202        "\n\n", SplitMapJoin("\n", str.strip, " "), "\n\n")(text)
203
204    if not text:  # pragma: no cover
205      self.Die("Commit message editing failed.")
206    text += "\n\nTBR=%s" % self._options.reviewer
207    self["commit_title"] = text.splitlines()[0]
208    TextToFile(text, self.Config("COMMITMSG_FILE"))
209
210    self.GitCommit(file_name = self.Config("COMMITMSG_FILE"))
211    os.remove(self.Config("COMMITMSG_FILE"))
212    os.remove(self.Config("CHANGELOG_ENTRY_FILE"))
213
214
215class LandBranch(Step):
216  MESSAGE = "Upload and land changes."
217
218  def RunStep(self):
219    if self._options.dry_run:
220      print "Dry run - upload CL."
221    else:
222      self.GitUpload(author=self._options.author,
223                     force=True,
224                     bypass_hooks=True,
225                     private=True)
226    cmd = "cl land --bypass-hooks -f"
227    if self._options.dry_run:
228      print "Dry run. Command:\ngit %s" % cmd
229    else:
230      self.Git(cmd)
231
232
233class TagRevision(Step):
234  MESSAGE = "Tag the new revision."
235
236  def RunStep(self):
237    if self._options.dry_run:
238      print ("Dry run. Tagging \"%s\" with %s" %
239             (self["commit_title"], self["version"]))
240    else:
241      self.vc.Tag(self["version"],
242                  "origin/%s" % self["version"],
243                  self["commit_title"])
244
245
246class CleanUp(Step):
247  MESSAGE = "Done!"
248
249  def RunStep(self):
250    print("Congratulations, you have successfully created version %s."
251          % self["version"])
252
253    self.GitCheckout("origin/master")
254    self.DeleteBranch("work-branch")
255    self.Git("gc")
256
257
258class CreateRelease(ScriptsBase):
259  def _PrepareOptions(self, parser):
260    group = parser.add_mutually_exclusive_group()
261    group.add_argument("-f", "--force",
262                      help="Don't prompt the user.",
263                      default=True, action="store_true")
264    group.add_argument("-m", "--manual",
265                      help="Prompt the user at every important step.",
266                      default=False, action="store_true")
267    parser.add_argument("-R", "--revision",
268                        help="The git commit ID to push (defaults to HEAD).")
269
270  def _ProcessOptions(self, options):  # pragma: no cover
271    if not options.author or not options.reviewer:
272      print "Reviewer (-r) and author (-a) are required."
273      return False
274    return True
275
276  def _Config(self):
277    return {
278      "PERSISTFILE_BASENAME": "/tmp/create-releases-tempfile",
279      "CHANGELOG_ENTRY_FILE":
280          "/tmp/v8-create-releases-tempfile-changelog-entry",
281      "COMMITMSG_FILE": "/tmp/v8-create-releases-tempfile-commitmsg",
282    }
283
284  def _Steps(self):
285    return [
286      Preparation,
287      PrepareBranchRevision,
288      IncrementVersion,
289      DetectLastRelease,
290      PrepareChangeLog,
291      EditChangeLog,
292      PushBranchRef,
293      MakeBranch,
294      AddChangeLog,
295      SetVersion,
296      EnableMergeWatchlist,
297      CommitBranch,
298      LandBranch,
299      TagRevision,
300      CleanUp,
301    ]
302
303
304if __name__ == "__main__":  # pragma: no cover
305  sys.exit(CreateRelease().Run())
306