• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 Reload(self, body):
81    """Attempts to reload the commit message from rietveld in order to allow
82    late changes to the LOG flag. Note: This is brittle to future changes of
83    the web page name or structure.
84    """
85    match = re.search(r"^Review URL: https://codereview\.chromium\.org/(\d+)$",
86                      body, flags=re.M)
87    if match:
88      cl_url = ("https://codereview.chromium.org/%s/description"
89                % match.group(1))
90      try:
91        # Fetch from Rietveld but only retry once with one second delay since
92        # there might be many revisions.
93        body = self.ReadURL(cl_url, wait_plan=[1])
94      except urllib2.URLError:  # pragma: no cover
95        pass
96    return body
97
98  def RunStep(self):
99    self["date"] = self.GetDate()
100    output = "%s: Version %s\n\n" % (self["date"], self["version"])
101    TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE"))
102    commits = self.GitLog(format="%H",
103        git_hash="%s..%s" % (self["last_push_master"],
104                             self["push_hash"]))
105
106    # Cache raw commit messages.
107    commit_messages = [
108      [
109        self.GitLog(n=1, format="%s", git_hash=commit),
110        self.Reload(self.GitLog(n=1, format="%B", git_hash=commit)),
111        self.GitLog(n=1, format="%an", git_hash=commit),
112      ] for commit in commits.splitlines()
113    ]
114
115    # Auto-format commit messages.
116    body = MakeChangeLogBody(commit_messages, auto_format=True)
117    AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE"))
118
119    msg = ("        Performance and stability improvements on all platforms."
120           "\n#\n# The change log above is auto-generated. Please review if "
121           "all relevant\n# commit messages from the list below are included."
122           "\n# All lines starting with # will be stripped.\n#\n")
123    AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE"))
124
125    # Include unformatted commit messages as a reference in a comment.
126    comment_body = MakeComment(MakeChangeLogBody(commit_messages))
127    AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE"))
128
129
130class EditChangeLog(Step):
131  MESSAGE = "Edit ChangeLog entry."
132
133  def RunStep(self):
134    print ("Please press <Return> to have your EDITOR open the ChangeLog "
135           "entry, then edit its contents to your liking. When you're done, "
136           "save the file and exit your EDITOR. ")
137    self.ReadLine(default="")
138    self.Editor(self.Config("CHANGELOG_ENTRY_FILE"))
139
140    # Strip comments and reformat with correct indentation.
141    changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip()
142    changelog_entry = StripComments(changelog_entry)
143    changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines()))
144    changelog_entry = changelog_entry.lstrip()
145
146    if changelog_entry == "":  # pragma: no cover
147      self.Die("Empty ChangeLog entry.")
148
149    # Safe new change log for adding it later to the candidates patch.
150    TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE"))
151
152
153class PushBranchRef(Step):
154  MESSAGE = "Create branch ref."
155
156  def RunStep(self):
157    cmd = "push origin %s:refs/heads/%s" % (self["push_hash"], self["version"])
158    if self._options.dry_run:
159      print "Dry run. Command:\ngit %s" % cmd
160    else:
161      self.Git(cmd)
162
163
164class MakeBranch(Step):
165  MESSAGE = "Create the branch."
166
167  def RunStep(self):
168    self.Git("reset --hard origin/master")
169    self.Git("new-branch work-branch --upstream origin/%s" % self["version"])
170    self.GitCheckoutFile(CHANGELOG_FILE, self["latest_version"])
171    self.GitCheckoutFile(VERSION_FILE, self["latest_version"])
172    self.GitCheckoutFile(WATCHLISTS_FILE, self["latest_version"])
173
174
175class AddChangeLog(Step):
176  MESSAGE = "Add ChangeLog changes to release branch."
177
178  def RunStep(self):
179    changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
180    old_change_log = FileToText(os.path.join(self.default_cwd, CHANGELOG_FILE))
181    new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log)
182    TextToFile(new_change_log, os.path.join(self.default_cwd, CHANGELOG_FILE))
183
184
185class SetVersion(Step):
186  MESSAGE = "Set correct version for candidates."
187
188  def RunStep(self):
189    self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_")
190
191
192class EnableMergeWatchlist(Step):
193  MESSAGE = "Enable watchlist entry for merge notifications."
194
195  def RunStep(self):
196    old_watchlist_content = FileToText(os.path.join(self.default_cwd,
197                                                    WATCHLISTS_FILE))
198    new_watchlist_content = re.sub("(# 'v8-merges@googlegroups\.com',)",
199                                   "'v8-merges@googlegroups.com',",
200                                   old_watchlist_content)
201    TextToFile(new_watchlist_content, os.path.join(self.default_cwd,
202                                                   WATCHLISTS_FILE))
203
204
205class CommitBranch(Step):
206  MESSAGE = "Commit version and changelog to new branch."
207
208  def RunStep(self):
209    # Convert the ChangeLog entry to commit message format.
210    text = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
211
212    # Remove date and trailing white space.
213    text = re.sub(r"^%s: " % self["date"], "", text.rstrip())
214
215    # Remove indentation and merge paragraphs into single long lines, keeping
216    # empty lines between them.
217    def SplitMapJoin(split_text, fun, join_text):
218      return lambda text: join_text.join(map(fun, text.split(split_text)))
219    text = SplitMapJoin(
220        "\n\n", SplitMapJoin("\n", str.strip, " "), "\n\n")(text)
221
222    if not text:  # pragma: no cover
223      self.Die("Commit message editing failed.")
224    self["commit_title"] = text.splitlines()[0]
225    TextToFile(text, self.Config("COMMITMSG_FILE"))
226
227    self.GitCommit(file_name = self.Config("COMMITMSG_FILE"))
228    os.remove(self.Config("COMMITMSG_FILE"))
229    os.remove(self.Config("CHANGELOG_ENTRY_FILE"))
230
231
232class PushBranch(Step):
233  MESSAGE = "Push changes."
234
235  def RunStep(self):
236    cmd = "cl land --bypass-hooks -f"
237    if self._options.dry_run:
238      print "Dry run. Command:\ngit %s" % cmd
239    else:
240      self.Git(cmd)
241
242
243class TagRevision(Step):
244  MESSAGE = "Tag the new revision."
245
246  def RunStep(self):
247    if self._options.dry_run:
248      print ("Dry run. Tagging \"%s\" with %s" %
249             (self["commit_title"], self["version"]))
250    else:
251      self.vc.Tag(self["version"],
252                  "origin/%s" % self["version"],
253                  self["commit_title"])
254
255
256class CleanUp(Step):
257  MESSAGE = "Done!"
258
259  def RunStep(self):
260    print("Congratulations, you have successfully created version %s."
261          % self["version"])
262
263    self.GitCheckout("origin/master")
264    self.DeleteBranch("work-branch")
265    self.Git("gc")
266
267
268class CreateRelease(ScriptsBase):
269  def _PrepareOptions(self, parser):
270    group = parser.add_mutually_exclusive_group()
271    group.add_argument("-f", "--force",
272                      help="Don't prompt the user.",
273                      default=True, action="store_true")
274    group.add_argument("-m", "--manual",
275                      help="Prompt the user at every important step.",
276                      default=False, action="store_true")
277    parser.add_argument("-R", "--revision",
278                        help="The git commit ID to push (defaults to HEAD).")
279
280  def _ProcessOptions(self, options):  # pragma: no cover
281    if not options.author or not options.reviewer:
282      print "Reviewer (-r) and author (-a) are required."
283      return False
284    return True
285
286  def _Config(self):
287    return {
288      "PERSISTFILE_BASENAME": "/tmp/create-releases-tempfile",
289      "CHANGELOG_ENTRY_FILE":
290          "/tmp/v8-create-releases-tempfile-changelog-entry",
291      "COMMITMSG_FILE": "/tmp/v8-create-releases-tempfile-commitmsg",
292    }
293
294  def _Steps(self):
295    return [
296      Preparation,
297      PrepareBranchRevision,
298      IncrementVersion,
299      DetectLastRelease,
300      PrepareChangeLog,
301      EditChangeLog,
302      PushBranchRef,
303      MakeBranch,
304      AddChangeLog,
305      SetVersion,
306      EnableMergeWatchlist,
307      CommitBranch,
308      PushBranch,
309      TagRevision,
310      CleanUp,
311    ]
312
313
314if __name__ == "__main__":  # pragma: no cover
315  sys.exit(CreateRelease().Run())
316