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