Home | History | Annotate | Download | only in push-to-trunk
      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_MESSAGE_SUFFIX = " (based on bleeding_edge revision r%d)"
     38 PUSH_MESSAGE_RE = re.compile(r".* \(based on bleeding_edge revision r(\d+)\)$")
     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("TRUNKBRANCH")
     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("TRUNKBRANCH"))
     53 
     54 
     55 class FreshBranch(Step):
     56   MESSAGE = "Create a fresh branch."
     57 
     58   def RunStep(self):
     59     self.GitCreateBranch(self.Config("BRANCHNAME"), "svn/bleeding_edge")
     60 
     61 
     62 class PreparePushRevision(Step):
     63   MESSAGE = "Check which revision to push."
     64 
     65   def RunStep(self):
     66     if self._options.revision:
     67       self["push_hash"] = self.GitSVNFindGitHash(self._options.revision)
     68     else:
     69       self["push_hash"] = self.GitLog(n=1, format="%H", git_hash="HEAD")
     70     if not self["push_hash"]:  # pragma: no cover
     71       self.Die("Could not determine the git hash for the push.")
     72 
     73 
     74 class DetectLastPush(Step):
     75   MESSAGE = "Detect commit ID of last push to trunk."
     76 
     77   def RunStep(self):
     78     last_push = self._options.last_push or self.FindLastTrunkPush()
     79     while True:
     80       # Print assumed commit, circumventing git's pager.
     81       print self.GitLog(n=1, git_hash=last_push)
     82       if self.Confirm("Is the commit printed above the last push to trunk?"):
     83         break
     84       last_push = self.FindLastTrunkPush(parent_hash=last_push)
     85 
     86     if self._options.last_bleeding_edge:
     87       # Read the bleeding edge revision of the last push from a command-line
     88       # option.
     89       last_push_bleeding_edge = self._options.last_bleeding_edge
     90     else:
     91       # Retrieve the bleeding edge revision of the last push from the text in
     92       # the push commit message.
     93       last_push_title = self.GitLog(n=1, format="%s", git_hash=last_push)
     94       last_push_be_svn = PUSH_MESSAGE_RE.match(last_push_title).group(1)
     95       if not last_push_be_svn:  # pragma: no cover
     96         self.Die("Could not retrieve bleeding edge revision for trunk push %s"
     97                  % last_push)
     98       last_push_bleeding_edge = self.GitSVNFindGitHash(last_push_be_svn)
     99       if not last_push_bleeding_edge:  # pragma: no cover
    100         self.Die("Could not retrieve bleeding edge git hash for trunk push %s"
    101                  % last_push)
    102 
    103     # This points to the svn revision of the last push on trunk.
    104     self["last_push_trunk"] = last_push
    105     # This points to the last bleeding_edge revision that went into the last
    106     # push.
    107     # TODO(machenbach): Do we need a check to make sure we're not pushing a
    108     # revision older than the last push? If we do this, the output of the
    109     # current change log preparation won't make much sense.
    110     self["last_push_bleeding_edge"] = last_push_bleeding_edge
    111 
    112 
    113 # TODO(machenbach): Code similarities with bump_up_version.py. Merge after
    114 # turning this script into a pure git script.
    115 class GetCurrentBleedingEdgeVersion(Step):
    116   MESSAGE = "Get latest bleeding edge version."
    117 
    118   def RunStep(self):
    119     self.GitCheckoutFile(VERSION_FILE, "svn/bleeding_edge")
    120 
    121     # Store latest version.
    122     self.ReadAndPersistVersion("latest_")
    123     self["latest_version"] = self.ArrayToVersion("latest_")
    124     print "Bleeding edge version: %s" % self["latest_version"]
    125 
    126 
    127 class IncrementVersion(Step):
    128   MESSAGE = "Increment version number."
    129 
    130   def RunStep(self):
    131     # Retrieve current version from last trunk push.
    132     self.GitCheckoutFile(VERSION_FILE, self["last_push_trunk"])
    133     self.ReadAndPersistVersion()
    134     self["trunk_version"] = self.ArrayToVersion("")
    135 
    136     if self["latest_build"] == "9999":  # pragma: no cover
    137       # If version control on bleeding edge was switched off, just use the last
    138       # trunk version.
    139       self["latest_version"] = self["trunk_version"]
    140 
    141     if SortingKey(self["trunk_version"]) < SortingKey(self["latest_version"]):
    142       # If the version on bleeding_edge is newer than on trunk, use it.
    143       self.GitCheckoutFile(VERSION_FILE, "svn/bleeding_edge")
    144       self.ReadAndPersistVersion()
    145 
    146     if self.Confirm(("Automatically increment BUILD_NUMBER? (Saying 'n' will "
    147                      "fire up your EDITOR on %s so you can make arbitrary "
    148                      "changes. When you're done, save the file and exit your "
    149                      "EDITOR.)" % VERSION_FILE)):
    150 
    151       text = FileToText(os.path.join(self.default_cwd, VERSION_FILE))
    152       text = MSub(r"(?<=#define BUILD_NUMBER)(?P<space>\s+)\d*$",
    153                   r"\g<space>%s" % str(int(self["build"]) + 1),
    154                   text)
    155       TextToFile(text, os.path.join(self.default_cwd, VERSION_FILE))
    156     else:
    157       self.Editor(os.path.join(self.default_cwd, VERSION_FILE))
    158 
    159     # Variables prefixed with 'new_' contain the new version numbers for the
    160     # ongoing trunk push.
    161     self.ReadAndPersistVersion("new_")
    162 
    163     # Make sure patch level is 0 in a new push.
    164     self["new_patch"] = "0"
    165 
    166     self["version"] = "%s.%s.%s" % (self["new_major"],
    167                                     self["new_minor"],
    168                                     self["new_build"])
    169 
    170 
    171 class PrepareChangeLog(Step):
    172   MESSAGE = "Prepare raw ChangeLog entry."
    173 
    174   def Reload(self, body):
    175     """Attempts to reload the commit message from rietveld in order to allow
    176     late changes to the LOG flag. Note: This is brittle to future changes of
    177     the web page name or structure.
    178     """
    179     match = re.search(r"^Review URL: https://codereview\.chromium\.org/(\d+)$",
    180                       body, flags=re.M)
    181     if match:
    182       cl_url = ("https://codereview.chromium.org/%s/description"
    183                 % match.group(1))
    184       try:
    185         # Fetch from Rietveld but only retry once with one second delay since
    186         # there might be many revisions.
    187         body = self.ReadURL(cl_url, wait_plan=[1])
    188       except urllib2.URLError:  # pragma: no cover
    189         pass
    190     return body
    191 
    192   def RunStep(self):
    193     self["date"] = self.GetDate()
    194     output = "%s: Version %s\n\n" % (self["date"], self["version"])
    195     TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE"))
    196     commits = self.GitLog(format="%H",
    197         git_hash="%s..%s" % (self["last_push_bleeding_edge"],
    198                              self["push_hash"]))
    199 
    200     # Cache raw commit messages.
    201     commit_messages = [
    202       [
    203         self.GitLog(n=1, format="%s", git_hash=commit),
    204         self.Reload(self.GitLog(n=1, format="%B", git_hash=commit)),
    205         self.GitLog(n=1, format="%an", git_hash=commit),
    206       ] for commit in commits.splitlines()
    207     ]
    208 
    209     # Auto-format commit messages.
    210     body = MakeChangeLogBody(commit_messages, auto_format=True)
    211     AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE"))
    212 
    213     msg = ("        Performance and stability improvements on all platforms."
    214            "\n#\n# The change log above is auto-generated. Please review if "
    215            "all relevant\n# commit messages from the list below are included."
    216            "\n# All lines starting with # will be stripped.\n#\n")
    217     AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE"))
    218 
    219     # Include unformatted commit messages as a reference in a comment.
    220     comment_body = MakeComment(MakeChangeLogBody(commit_messages))
    221     AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE"))
    222 
    223 
    224 class EditChangeLog(Step):
    225   MESSAGE = "Edit ChangeLog entry."
    226 
    227   def RunStep(self):
    228     print ("Please press <Return> to have your EDITOR open the ChangeLog "
    229            "entry, then edit its contents to your liking. When you're done, "
    230            "save the file and exit your EDITOR. ")
    231     self.ReadLine(default="")
    232     self.Editor(self.Config("CHANGELOG_ENTRY_FILE"))
    233 
    234     # Strip comments and reformat with correct indentation.
    235     changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip()
    236     changelog_entry = StripComments(changelog_entry)
    237     changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines()))
    238     changelog_entry = changelog_entry.lstrip()
    239 
    240     if changelog_entry == "":  # pragma: no cover
    241       self.Die("Empty ChangeLog entry.")
    242 
    243     # Safe new change log for adding it later to the trunk patch.
    244     TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE"))
    245 
    246 
    247 class StragglerCommits(Step):
    248   MESSAGE = ("Fetch straggler commits that sneaked in since this script was "
    249              "started.")
    250 
    251   def RunStep(self):
    252     self.GitSVNFetch()
    253     self.GitCheckout("svn/bleeding_edge")
    254 
    255 
    256 class SquashCommits(Step):
    257   MESSAGE = "Squash commits into one."
    258 
    259   def RunStep(self):
    260     # Instead of relying on "git rebase -i", we'll just create a diff, because
    261     # that's easier to automate.
    262     TextToFile(self.GitDiff("svn/trunk", self["push_hash"]),
    263                self.Config("PATCH_FILE"))
    264 
    265     # Convert the ChangeLog entry to commit message format.
    266     text = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
    267 
    268     # Remove date and trailing white space.
    269     text = re.sub(r"^%s: " % self["date"], "", text.rstrip())
    270 
    271     # Retrieve svn revision for showing the used bleeding edge revision in the
    272     # commit message.
    273     self["svn_revision"] = self.GitSVNFindSVNRev(self["push_hash"])
    274     suffix = PUSH_MESSAGE_SUFFIX % int(self["svn_revision"])
    275     text = MSub(r"^(Version \d+\.\d+\.\d+)$", "\\1%s" % suffix, text)
    276 
    277     # Remove indentation and merge paragraphs into single long lines, keeping
    278     # empty lines between them.
    279     def SplitMapJoin(split_text, fun, join_text):
    280       return lambda text: join_text.join(map(fun, text.split(split_text)))
    281     strip = lambda line: line.strip()
    282     text = SplitMapJoin("\n\n", SplitMapJoin("\n", strip, " "), "\n\n")(text)
    283 
    284     if not text:  # pragma: no cover
    285       self.Die("Commit message editing failed.")
    286     TextToFile(text, self.Config("COMMITMSG_FILE"))
    287 
    288 
    289 class NewBranch(Step):
    290   MESSAGE = "Create a new branch from trunk."
    291 
    292   def RunStep(self):
    293     self.GitCreateBranch(self.Config("TRUNKBRANCH"), "svn/trunk")
    294 
    295 
    296 class ApplyChanges(Step):
    297   MESSAGE = "Apply squashed changes."
    298 
    299   def RunStep(self):
    300     self.ApplyPatch(self.Config("PATCH_FILE"))
    301     os.remove(self.Config("PATCH_FILE"))
    302 
    303 
    304 class AddChangeLog(Step):
    305   MESSAGE = "Add ChangeLog changes to trunk branch."
    306 
    307   def RunStep(self):
    308     # The change log has been modified by the patch. Reset it to the version
    309     # on trunk and apply the exact changes determined by this PrepareChangeLog
    310     # step above.
    311     self.GitCheckoutFile(self.Config("CHANGELOG_FILE"), "svn/trunk")
    312     changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
    313     old_change_log = FileToText(self.Config("CHANGELOG_FILE"))
    314     new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log)
    315     TextToFile(new_change_log, self.Config("CHANGELOG_FILE"))
    316     os.remove(self.Config("CHANGELOG_ENTRY_FILE"))
    317 
    318 
    319 class SetVersion(Step):
    320   MESSAGE = "Set correct version for trunk."
    321 
    322   def RunStep(self):
    323     # The version file has been modified by the patch. Reset it to the version
    324     # on trunk and apply the correct version.
    325     self.GitCheckoutFile(VERSION_FILE, "svn/trunk")
    326     self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_")
    327 
    328 
    329 class CommitTrunk(Step):
    330   MESSAGE = "Commit to local trunk branch."
    331 
    332   def RunStep(self):
    333     self.GitCommit(file_name = self.Config("COMMITMSG_FILE"))
    334     os.remove(self.Config("COMMITMSG_FILE"))
    335 
    336 
    337 class SanityCheck(Step):
    338   MESSAGE = "Sanity check."
    339 
    340   def RunStep(self):
    341     # TODO(machenbach): Run presubmit script here as it is now missing in the
    342     # prepare push process.
    343     if not self.Confirm("Please check if your local checkout is sane: Inspect "
    344         "%s, compile, run tests. Do you want to commit this new trunk "
    345         "revision to the repository?" % VERSION_FILE):
    346       self.Die("Execution canceled.")  # pragma: no cover
    347 
    348 
    349 class CommitSVN(Step):
    350   MESSAGE = "Commit to SVN."
    351 
    352   def RunStep(self):
    353     result = self.GitSVNDCommit()
    354     if not result:  # pragma: no cover
    355       self.Die("'git svn dcommit' failed.")
    356     result = filter(lambda x: re.search(r"^Committed r[0-9]+", x),
    357                     result.splitlines())
    358     if len(result) > 0:
    359       self["trunk_revision"] = re.sub(r"^Committed r([0-9]+)", r"\1",result[0])
    360 
    361     # Sometimes grepping for the revision fails. No idea why. If you figure
    362     # out why it is flaky, please do fix it properly.
    363     if not self["trunk_revision"]:
    364       print("Sorry, grepping for the SVN revision failed. Please look for it "
    365             "in the last command's output above and provide it manually (just "
    366             "the number, without the leading \"r\").")
    367       self.DieNoManualMode("Can't prompt in forced mode.")
    368       while not self["trunk_revision"]:
    369         print "> ",
    370         self["trunk_revision"] = self.ReadLine()
    371 
    372 
    373 class TagRevision(Step):
    374   MESSAGE = "Tag the new revision."
    375 
    376   def RunStep(self):
    377     self.GitSVNTag(self["version"])
    378 
    379 
    380 class CleanUp(Step):
    381   MESSAGE = "Done!"
    382 
    383   def RunStep(self):
    384     print("Congratulations, you have successfully created the trunk "
    385           "revision %s. Please don't forget to roll this new version into "
    386           "Chromium, and to update the v8rel spreadsheet:"
    387           % self["version"])
    388     print "%s\ttrunk\t%s" % (self["version"], self["trunk_revision"])
    389 
    390     self.CommonCleanup()
    391     if self.Config("TRUNKBRANCH") != self["current_branch"]:
    392       self.GitDeleteBranch(self.Config("TRUNKBRANCH"))
    393 
    394 
    395 class PushToTrunk(ScriptsBase):
    396   def _PrepareOptions(self, parser):
    397     group = parser.add_mutually_exclusive_group()
    398     group.add_argument("-f", "--force",
    399                       help="Don't prompt the user.",
    400                       default=False, action="store_true")
    401     group.add_argument("-m", "--manual",
    402                       help="Prompt the user at every important step.",
    403                       default=False, action="store_true")
    404     parser.add_argument("-b", "--last-bleeding-edge",
    405                         help=("The git commit ID of the last bleeding edge "
    406                               "revision that was pushed to trunk. This is "
    407                               "used for the auto-generated ChangeLog entry."))
    408     parser.add_argument("-l", "--last-push",
    409                         help="The git commit ID of the last push to trunk.")
    410     parser.add_argument("-R", "--revision",
    411                         help="The svn revision to push (defaults to HEAD).")
    412 
    413   def _ProcessOptions(self, options):  # pragma: no cover
    414     if not options.manual and not options.reviewer:
    415       print "A reviewer (-r) is required in (semi-)automatic mode."
    416       return False
    417     if not options.manual and not options.author:
    418       print "Specify your chromium.org email with -a in (semi-)automatic mode."
    419       return False
    420     if options.revision and not int(options.revision) > 0:
    421       print("The --revision flag must be a positiv integer pointing to a "
    422             "valid svn revision.")
    423       return False
    424 
    425     options.tbr_commit = not options.manual
    426     return True
    427 
    428   def _Config(self):
    429     return {
    430       "BRANCHNAME": "prepare-push",
    431       "TRUNKBRANCH": "trunk-push",
    432       "PERSISTFILE_BASENAME": "/tmp/v8-push-to-trunk-tempfile",
    433       "CHANGELOG_FILE": "ChangeLog",
    434       "CHANGELOG_ENTRY_FILE": "/tmp/v8-push-to-trunk-tempfile-changelog-entry",
    435       "PATCH_FILE": "/tmp/v8-push-to-trunk-tempfile-patch-file",
    436       "COMMITMSG_FILE": "/tmp/v8-push-to-trunk-tempfile-commitmsg",
    437     }
    438 
    439   def _Steps(self):
    440     return [
    441       Preparation,
    442       FreshBranch,
    443       PreparePushRevision,
    444       DetectLastPush,
    445       GetCurrentBleedingEdgeVersion,
    446       IncrementVersion,
    447       PrepareChangeLog,
    448       EditChangeLog,
    449       StragglerCommits,
    450       SquashCommits,
    451       NewBranch,
    452       ApplyChanges,
    453       AddChangeLog,
    454       SetVersion,
    455       CommitTrunk,
    456       SanityCheck,
    457       CommitSVN,
    458       TagRevision,
    459       CleanUp,
    460     ]
    461 
    462 
    463 if __name__ == "__main__":  # pragma: no cover
    464   sys.exit(PushToTrunk().Run())
    465