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