push_to_candidates.py 13.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
#!/usr/bin/env python
# Copyright 2013 the V8 project authors. All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
#       copyright notice, this list of conditions and the following
#       disclaimer in the documentation and/or other materials provided
#       with the distribution.
#     * Neither the name of Google Inc. nor the names of its
#       contributors may be used to endorse or promote products derived
#       from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

29
import argparse
30
import os
31 32
import sys
import tempfile
33
import urllib2
34 35 36

from common_includes import *

37
PUSH_MSG_GIT_SUFFIX = " (based on %s)"
38

39

40
class Preparation(Step):
41
  MESSAGE = "Preparation."
42 43

  def RunStep(self):
44
    self.InitialEnvironmentChecks(self.default_cwd)
45
    self.CommonPrepare()
46

machenbach's avatar
machenbach committed
47
    if(self["current_branch"] == self.Config("CANDIDATESBRANCH")
48
       or self["current_branch"] == self.Config("BRANCHNAME")):
49 50
      print "Warning: Script started on branch %s" % self["current_branch"]

51
    self.PrepareBranch()
machenbach's avatar
machenbach committed
52
    self.DeleteBranch(self.Config("CANDIDATESBRANCH"))
53 54 55


class FreshBranch(Step):
56
  MESSAGE = "Create a fresh branch."
57 58

  def RunStep(self):
59 60
    self.GitCreateBranch(self.Config("BRANCHNAME"),
                         self.vc.RemoteMasterBranch())
61 62


63 64 65 66 67
class PreparePushRevision(Step):
  MESSAGE = "Check which revision to push."

  def RunStep(self):
    if self._options.revision:
68
      self["push_hash"] = self._options.revision
69 70 71 72 73 74
    else:
      self["push_hash"] = self.GitLog(n=1, format="%H", git_hash="HEAD")
    if not self["push_hash"]:  # pragma: no cover
      self.Die("Could not determine the git hash for the push.")


75 76
class IncrementVersion(Step):
  MESSAGE = "Increment version number."
77 78

  def RunStep(self):
79
    latest_version = self.GetLatestVersion()
80 81 82

    # The version file on master can be used to bump up major/minor at
    # branch time.
83
    self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteMasterBranch())
84
    self.ReadAndPersistVersion("master_")
85
    master_version = self.ArrayToVersion("master_")
86

87 88 89 90 91
    # Use the highest version from master or from tags to determine the new
    # version.
    authoritative_version = sorted(
        [master_version, latest_version], key=SortingKey)[1]
    self.StoreVersion(authoritative_version, "authoritative_")
92

93
    # Variables prefixed with 'new_' contain the new version numbers for the
machenbach's avatar
machenbach committed
94
    # ongoing candidates push.
95 96 97
    self["new_major"] = self["authoritative_major"]
    self["new_minor"] = self["authoritative_minor"]
    self["new_build"] = str(int(self["authoritative_build"]) + 1)
98 99 100 101

    # Make sure patch level is 0 in a new push.
    self["new_patch"] = "0"

102 103 104 105
    self["version"] = "%s.%s.%s" % (self["new_major"],
                                    self["new_minor"],
                                    self["new_build"])

106 107
    print ("Incremented version to %s" % self["version"])

108

109 110 111 112 113 114 115 116 117 118
class DetectLastRelease(Step):
  MESSAGE = "Detect commit ID of last release base."

  def RunStep(self):
    if self._options.last_master:
      self["last_push_master"] = self._options.last_master
    else:
      self["last_push_master"] = self.GetLatestReleaseBase()


119
class 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",
machenbach's avatar
machenbach committed
127
        git_hash="%s..%s" % (self["last_push_master"],
128
                             self["push_hash"]))
129

130 131 132
    # Cache raw commit messages.
    commit_messages = [
      [
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 137 138 139 140
      ] for commit in commits.splitlines()
    ]

    # Auto-format commit messages.
    body = MakeChangeLogBody(commit_messages, auto_format=True)
141
    AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE"))
142

143 144 145 146
    msg = ("        Performance and stability improvements on all platforms."
           "\n#\n# The change log above is auto-generated. Please review if "
           "all relevant\n# commit messages from the list below are included."
           "\n# All lines starting with # will be stripped.\n#\n")
147
    AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE"))
148

149 150
    # Include unformatted commit messages as a reference in a comment.
    comment_body = MakeComment(MakeChangeLogBody(commit_messages))
151
    AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE"))
152

153

154
class EditChangeLog(Step):
155
  MESSAGE = "Edit ChangeLog entry."
156 157 158 159 160

  def RunStep(self):
    print ("Please press <Return> to have your EDITOR open the ChangeLog "
           "entry, then edit its contents to your liking. When you're done, "
           "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 172
      self.Die("Empty ChangeLog entry.")

machenbach's avatar
machenbach committed
173
    # Safe new change log for adding it later to the candidates patch.
174
    TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE"))
175 176 177


class StragglerCommits(Step):
178 179
  MESSAGE = ("Fetch straggler commits that sneaked in since this script was "
             "started.")
180 181

  def RunStep(self):
182 183
    self.vc.Fetch()
    self.GitCheckout(self.vc.RemoteMasterBranch())
184 185 186


class SquashCommits(Step):
187
  MESSAGE = "Squash commits into one."
188 189 190

  def RunStep(self):
    # Instead of relying on "git rebase -i", we'll just create a diff, because
191
    # that's easier to automate.
192 193
    TextToFile(self.GitDiff(self.vc.RemoteCandidateBranch(),
                            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 203
    # Show the used master hash in the commit message.
    suffix = PUSH_MSG_GIT_SUFFIX % self["push_hash"]
204
    text = MSub(r"^(Version \d+\.\d+\.\d+)$", "\\1%s" % suffix, text)
205

206 207 208 209 210 211
    # Remove indentation and merge paragraphs into single long lines, keeping
    # empty lines between them.
    def SplitMapJoin(split_text, fun, join_text):
      return lambda text: join_text.join(map(fun, text.split(split_text)))
    strip = lambda line: line.strip()
    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 219


class NewBranch(Step):
machenbach's avatar
machenbach committed
220
  MESSAGE = "Create a new branch from candidates."
221 222

  def RunStep(self):
machenbach's avatar
machenbach committed
223
    self.GitCreateBranch(self.Config("CANDIDATESBRANCH"),
224
                         self.vc.RemoteCandidateBranch())
225 226 227


class ApplyChanges(Step):
228
  MESSAGE = "Apply squashed changes."
229 230

  def RunStep(self):
231 232
    self.ApplyPatch(self.Config("PATCH_FILE"))
    os.remove(self.Config("PATCH_FILE"))
233
    # The change log has been modified by the patch. Reset it to the version
machenbach's avatar
machenbach committed
234 235
    # on candidates and apply the exact changes determined by this
    # PrepareChangeLog step above.
236 237
    self.GitCheckoutFile(CHANGELOG_FILE, self.vc.RemoteCandidateBranch())
    # The version file has been modified by the patch. Reset it to the version
machenbach's avatar
machenbach committed
238
    # on candidates.
239
    self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteCandidateBranch())
240 241


242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
class CommitSquash(Step):
  MESSAGE = "Commit to local candidates branch."

  def RunStep(self):
    # Make a first commit with a slightly different title to not confuse
    # the tagging.
    msg = FileToText(self.Config("COMMITMSG_FILE")).splitlines()
    msg[0] = msg[0].replace("(based on", "(squashed - based on")
    self.GitCommit(message = "\n".join(msg))


class PrepareVersionBranch(Step):
  MESSAGE = "Prepare new branch to commit version and changelog file."

  def RunStep(self):
    self.GitCheckout("master")
    self.Git("fetch")
machenbach's avatar
machenbach committed
259 260
    self.GitDeleteBranch(self.Config("CANDIDATESBRANCH"))
    self.GitCreateBranch(self.Config("CANDIDATESBRANCH"),
261 262 263
                         self.vc.RemoteCandidateBranch())


264
class AddChangeLog(Step):
machenbach's avatar
machenbach committed
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


275
class SetVersion(Step):
machenbach's avatar
machenbach committed
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


282 283
class CommitCandidate(Step):
  MESSAGE = "Commit version and changelog to local candidates branch."
284 285

  def RunStep(self):
286 287
    self.GitCommit(file_name = self.Config("COMMITMSG_FILE"))
    os.remove(self.Config("COMMITMSG_FILE"))
288 289 290


class SanityCheck(Step):
291
  MESSAGE = "Sanity check."
292 293

  def RunStep(self):
294 295
    # TODO(machenbach): Run presubmit script here as it is now missing in the
    # prepare push process.
296
    if not self.Confirm("Please check if your local checkout is sane: Inspect "
machenbach's avatar
machenbach committed
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


302 303
class Land(Step):
  MESSAGE = "Land the patch."
304 305

  def RunStep(self):
306
    self.vc.CLLand()
307 308 309


class TagRevision(Step):
310
  MESSAGE = "Tag the new revision."
311 312

  def RunStep(self):
313 314
    self.vc.Tag(
        self["version"], self.vc.RemoteCandidateBranch(), self["commit_title"])
315 316 317


class CleanUp(Step):
318
  MESSAGE = "Done!"
319 320

  def RunStep(self):
machenbach's avatar
machenbach committed
321
    print("Congratulations, you have successfully created the candidates "
322
          "revision %s."
323
          % self["version"])
324 325

    self.CommonCleanup()
machenbach's avatar
machenbach committed
326 327
    if self.Config("CANDIDATESBRANCH") != self["current_branch"]:
      self.GitDeleteBranch(self.Config("CANDIDATESBRANCH"))
328 329


machenbach's avatar
machenbach committed
330
class PushToCandidates(ScriptsBase):
331 332 333 334 335 336 337 338
  def _PrepareOptions(self, parser):
    group = parser.add_mutually_exclusive_group()
    group.add_argument("-f", "--force",
                      help="Don't prompt the user.",
                      default=False, action="store_true")
    group.add_argument("-m", "--manual",
                      help="Prompt the user at every important step.",
                      default=False, action="store_true")
machenbach's avatar
machenbach committed
339 340 341 342
    parser.add_argument("-b", "--last-master",
                        help=("The git commit ID of the last master "
                              "revision that was pushed to candidates. This is"
                              " used for the auto-generated ChangeLog entry."))
343
    parser.add_argument("-l", "--last-push",
machenbach's avatar
machenbach committed
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 350 351 352 353 354 355 356 357 358
    if not options.manual and not options.reviewer:
      print "A reviewer (-r) is required in (semi-)automatic mode."
      return False
    if not options.manual and not options.author:
      print "Specify your chromium.org email with -a in (semi-)automatic mode."
      return False

    options.tbr_commit = not options.manual
    return True

359 360 361
  def _Config(self):
    return {
      "BRANCHNAME": "prepare-push",
machenbach's avatar
machenbach committed
362 363 364 365 366 367
      "CANDIDATESBRANCH": "candidates-push",
      "PERSISTFILE_BASENAME": "/tmp/v8-push-to-candidates-tempfile",
      "CHANGELOG_ENTRY_FILE":
          "/tmp/v8-push-to-candidates-tempfile-changelog-entry",
      "PATCH_FILE": "/tmp/v8-push-to-candidates-tempfile-patch-file",
      "COMMITMSG_FILE": "/tmp/v8-push-to-candidates-tempfile-commitmsg",
368 369
    }

370 371 372 373
  def _Steps(self):
    return [
      Preparation,
      FreshBranch,
374
      PreparePushRevision,
375
      IncrementVersion,
376
      DetectLastRelease,
377 378 379 380 381 382
      PrepareChangeLog,
      EditChangeLog,
      StragglerCommits,
      SquashCommits,
      NewBranch,
      ApplyChanges,
383 384 385 386
      CommitSquash,
      SanityCheck,
      Land,
      PrepareVersionBranch,
387
      AddChangeLog,
388
      SetVersion,
389
      CommitCandidate,
390
      Land,
391 392 393 394
      TagRevision,
      CleanUp,
    ]

395

396
if __name__ == "__main__":  # pragma: no cover
machenbach's avatar
machenbach committed
397
  sys.exit(PushToCandidates().Run())