1#!/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 RunStep(self):
123    self["date"] = self.GetDate()
124    output = "%s: Version %s\n\n" % (self["date"], self["version"])
125    TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE"))
126    commits = self.GitLog(format="%H",
127        git_hash="%s..%s" % (self["last_push_master"],
128                             self["push_hash"]))
129
130    # Cache raw commit messages.
131    commit_messages = [
132      [
133        self.GitLog(n=1, format="%s", git_hash=commit),
134        self.GitLog(n=1, format="%B", git_hash=commit),
135        self.GitLog(n=1, format="%an", git_hash=commit),
136      ] for commit in commits.splitlines()
137    ]
138
139    # Auto-format commit messages.
140    body = MakeChangeLogBody(commit_messages, auto_format=True)
141    AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE"))
142
143    msg = ("        Performance and stability improvements on all platforms."
144           "\n#\n# The change log above is auto-generated. Please review if "
145           "all relevant\n# commit messages from the list below are included."
146           "\n# All lines starting with # will be stripped.\n#\n")
147    AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE"))
148
149    # Include unformatted commit messages as a reference in a comment.
150    comment_body = MakeComment(MakeChangeLogBody(commit_messages))
151    AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE"))
152
153
154class EditChangeLog(Step):
155  MESSAGE = "Edit ChangeLog entry."
156
157  def RunStep(self):
158    print ("Please press <Return> to have your EDITOR open the ChangeLog "
159           "entry, then edit its contents to your liking. When you're done, "
160           "save the file and exit your EDITOR. ")
161    self.ReadLine(default="")
162    self.Editor(self.Config("CHANGELOG_ENTRY_FILE"))
163
164    # Strip comments and reformat with correct indentation.
165    changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip()
166    changelog_entry = StripComments(changelog_entry)
167    changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines()))
168    changelog_entry = changelog_entry.lstrip()
169
170    if changelog_entry == "":  # pragma: no cover
171      self.Die("Empty ChangeLog entry.")
172
173    # Safe new change log for adding it later to the candidates patch.
174    TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE"))
175
176
177class StragglerCommits(Step):
178  MESSAGE = ("Fetch straggler commits that sneaked in since this script was "
179             "started.")
180
181  def RunStep(self):
182    self.vc.Fetch()
183    self.GitCheckout(self.vc.RemoteMasterBranch())
184
185
186class SquashCommits(Step):
187  MESSAGE = "Squash commits into one."
188
189  def RunStep(self):
190    # Instead of relying on "git rebase -i", we'll just create a diff, because
191    # that's easier to automate.
192    TextToFile(self.GitDiff(self.vc.RemoteCandidateBranch(),
193                            self["push_hash"]),
194               self.Config("PATCH_FILE"))
195
196    # Convert the ChangeLog entry to commit message format.
197    text = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
198
199    # Remove date and trailing white space.
200    text = re.sub(r"^%s: " % self["date"], "", text.rstrip())
201
202    # Show the used master hash in the commit message.
203    suffix = PUSH_MSG_GIT_SUFFIX % self["push_hash"]
204    text = MSub(r"^(Version \d+\.\d+\.\d+)$", "\\1%s" % suffix, text)
205
206    # Remove indentation and merge paragraphs into single long lines, keeping
207    # empty lines between them.
208    def SplitMapJoin(split_text, fun, join_text):
209      return lambda text: join_text.join(map(fun, text.split(split_text)))
210    strip = lambda line: line.strip()
211    text = SplitMapJoin("\n\n", SplitMapJoin("\n", strip, " "), "\n\n")(text)
212
213    if not text:  # pragma: no cover
214      self.Die("Commit message editing failed.")
215    self["commit_title"] = text.splitlines()[0]
216    TextToFile(text, self.Config("COMMITMSG_FILE"))
217
218
219class NewBranch(Step):
220  MESSAGE = "Create a new branch from candidates."
221
222  def RunStep(self):
223    self.GitCreateBranch(self.Config("CANDIDATESBRANCH"),
224                         self.vc.RemoteCandidateBranch())
225
226
227class ApplyChanges(Step):
228  MESSAGE = "Apply squashed changes."
229
230  def RunStep(self):
231    self.ApplyPatch(self.Config("PATCH_FILE"))
232    os.remove(self.Config("PATCH_FILE"))
233    # The change log has been modified by the patch. Reset it to the version
234    # on candidates and apply the exact changes determined by this
235    # PrepareChangeLog step above.
236    self.GitCheckoutFile(CHANGELOG_FILE, self.vc.RemoteCandidateBranch())
237    # The version file has been modified by the patch. Reset it to the version
238    # on candidates.
239    self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteCandidateBranch())
240
241
242class CommitSquash(Step):
243  MESSAGE = "Commit to local candidates branch."
244
245  def RunStep(self):
246    # Make a first commit with a slightly different title to not confuse
247    # the tagging.
248    msg = FileToText(self.Config("COMMITMSG_FILE")).splitlines()
249    msg[0] = msg[0].replace("(based on", "(squashed - based on")
250    self.GitCommit(message = "\n".join(msg))
251
252
253class PrepareVersionBranch(Step):
254  MESSAGE = "Prepare new branch to commit version and changelog file."
255
256  def RunStep(self):
257    self.GitCheckout("master")
258    self.Git("fetch")
259    self.GitDeleteBranch(self.Config("CANDIDATESBRANCH"))
260    self.GitCreateBranch(self.Config("CANDIDATESBRANCH"),
261                         self.vc.RemoteCandidateBranch())
262
263
264class AddChangeLog(Step):
265  MESSAGE = "Add ChangeLog changes to candidates branch."
266
267  def RunStep(self):
268    changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
269    old_change_log = FileToText(os.path.join(self.default_cwd, CHANGELOG_FILE))
270    new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log)
271    TextToFile(new_change_log, os.path.join(self.default_cwd, CHANGELOG_FILE))
272    os.remove(self.Config("CHANGELOG_ENTRY_FILE"))
273
274
275class SetVersion(Step):
276  MESSAGE = "Set correct version for candidates."
277
278  def RunStep(self):
279    self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_")
280
281
282class CommitCandidate(Step):
283  MESSAGE = "Commit version and changelog to local candidates branch."
284
285  def RunStep(self):
286    self.GitCommit(file_name = self.Config("COMMITMSG_FILE"))
287    os.remove(self.Config("COMMITMSG_FILE"))
288
289
290class SanityCheck(Step):
291  MESSAGE = "Sanity check."
292
293  def RunStep(self):
294    # TODO(machenbach): Run presubmit script here as it is now missing in the
295    # prepare push process.
296    if not self.Confirm("Please check if your local checkout is sane: Inspect "
297        "%s, compile, run tests. Do you want to commit this new candidates "
298        "revision to the repository?" % VERSION_FILE):
299      self.Die("Execution canceled.")  # pragma: no cover
300
301
302class Land(Step):
303  MESSAGE = "Land the patch."
304
305  def RunStep(self):
306    self.vc.CLLand()
307
308
309class TagRevision(Step):
310  MESSAGE = "Tag the new revision."
311
312  def RunStep(self):
313    self.vc.Tag(
314        self["version"], self.vc.RemoteCandidateBranch(), self["commit_title"])
315
316
317class CleanUp(Step):
318  MESSAGE = "Done!"
319
320  def RunStep(self):
321    print("Congratulations, you have successfully created the candidates "
322          "revision %s."
323          % self["version"])
324
325    self.CommonCleanup()
326    if self.Config("CANDIDATESBRANCH") != self["current_branch"]:
327      self.GitDeleteBranch(self.Config("CANDIDATESBRANCH"))
328
329
330class PushToCandidates(ScriptsBase):
331  def _PrepareOptions(self, parser):
332    group = parser.add_mutually_exclusive_group()
333    group.add_argument("-f", "--force",
334                      help="Don't prompt the user.",
335                      default=False, action="store_true")
336    group.add_argument("-m", "--manual",
337                      help="Prompt the user at every important step.",
338                      default=False, action="store_true")
339    parser.add_argument("-b", "--last-master",
340                        help=("The git commit ID of the last master "
341                              "revision that was pushed to candidates. This is"
342                              " used for the auto-generated ChangeLog entry."))
343    parser.add_argument("-l", "--last-push",
344                        help="The git commit ID of the last candidates push.")
345    parser.add_argument("-R", "--revision",
346                        help="The git commit ID to push (defaults to HEAD).")
347
348  def _ProcessOptions(self, options):  # pragma: no cover
349    if not options.manual and not options.reviewer:
350      print "A reviewer (-r) is required in (semi-)automatic mode."
351      return False
352    if not options.manual and not options.author:
353      print "Specify your chromium.org email with -a in (semi-)automatic mode."
354      return False
355
356    options.tbr_commit = not options.manual
357    return True
358
359  def _Config(self):
360    return {
361      "BRANCHNAME": "prepare-push",
362      "CANDIDATESBRANCH": "candidates-push",
363      "PERSISTFILE_BASENAME": "/tmp/v8-push-to-candidates-tempfile",
364      "CHANGELOG_ENTRY_FILE":
365          "/tmp/v8-push-to-candidates-tempfile-changelog-entry",
366      "PATCH_FILE": "/tmp/v8-push-to-candidates-tempfile-patch-file",
367      "COMMITMSG_FILE": "/tmp/v8-push-to-candidates-tempfile-commitmsg",
368    }
369
370  def _Steps(self):
371    return [
372      Preparation,
373      FreshBranch,
374      PreparePushRevision,
375      IncrementVersion,
376      DetectLastRelease,
377      PrepareChangeLog,
378      EditChangeLog,
379      StragglerCommits,
380      SquashCommits,
381      NewBranch,
382      ApplyChanges,
383      CommitSquash,
384      SanityCheck,
385      Land,
386      PrepareVersionBranch,
387      AddChangeLog,
388      SetVersion,
389      CommitCandidate,
390      Land,
391      TagRevision,
392      CleanUp,
393    ]
394
395
396if __name__ == "__main__":  # pragma: no cover
397  sys.exit(PushToCandidates().Run())
398