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