Home | History | Annotate | Download | only in push-to-trunk
      1 #!/usr/bin/env python
      2 # Copyright 2014 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 from collections import OrderedDict
     31 import sys
     32 
     33 from common_includes import *
     34 
     35 ALREADY_MERGING_SENTINEL_FILE = "ALREADY_MERGING_SENTINEL_FILE"
     36 COMMIT_HASHES_FILE = "COMMIT_HASHES_FILE"
     37 TEMPORARY_PATCH_FILE = "TEMPORARY_PATCH_FILE"
     38 
     39 CONFIG = {
     40   BRANCHNAME: "prepare-merge",
     41   PERSISTFILE_BASENAME: "/tmp/v8-merge-to-branch-tempfile",
     42   ALREADY_MERGING_SENTINEL_FILE:
     43       "/tmp/v8-merge-to-branch-tempfile-already-merging",
     44   DOT_GIT_LOCATION: ".git",
     45   VERSION_FILE: "src/version.cc",
     46   TEMPORARY_PATCH_FILE: "/tmp/v8-prepare-merge-tempfile-temporary-patch",
     47   COMMITMSG_FILE: "/tmp/v8-prepare-merge-tempfile-commitmsg",
     48   COMMIT_HASHES_FILE: "/tmp/v8-merge-to-branch-tempfile-PATCH_COMMIT_HASHES",
     49 }
     50 
     51 
     52 class Preparation(Step):
     53   MESSAGE = "Preparation."
     54 
     55   def RunStep(self):
     56     if os.path.exists(self.Config(ALREADY_MERGING_SENTINEL_FILE)):
     57       if self._options.force:
     58         os.remove(self.Config(ALREADY_MERGING_SENTINEL_FILE))
     59       elif self._options.step == 0:  # pragma: no cover
     60         self.Die("A merge is already in progress")
     61     open(self.Config(ALREADY_MERGING_SENTINEL_FILE), "a").close()
     62 
     63     self.InitialEnvironmentChecks()
     64     if self._options.revert_bleeding_edge:
     65       self["merge_to_branch"] = "bleeding_edge"
     66     elif self._options.branch:
     67       self["merge_to_branch"] = self._options.branch
     68     else:  # pragma: no cover
     69       self.Die("Please specify a branch to merge to")
     70 
     71     self.CommonPrepare()
     72     self.PrepareBranch()
     73 
     74 
     75 class CreateBranch(Step):
     76   MESSAGE = "Create a fresh branch for the patch."
     77 
     78   def RunStep(self):
     79     self.GitCreateBranch(self.Config(BRANCHNAME),
     80                          "svn/%s" % self["merge_to_branch"])
     81 
     82 
     83 class SearchArchitecturePorts(Step):
     84   MESSAGE = "Search for corresponding architecture ports."
     85 
     86   def RunStep(self):
     87     self["full_revision_list"] = list(OrderedDict.fromkeys(
     88         self._options.revisions))
     89     port_revision_list = []
     90     for revision in self["full_revision_list"]:
     91       # Search for commits which matches the "Port rXXX" pattern.
     92       git_hashes = self.GitLog(reverse=True, format="%H",
     93                                grep="Port r%d" % int(revision),
     94                                branch="svn/bleeding_edge")
     95       for git_hash in git_hashes.splitlines():
     96         svn_revision = self.GitSVNFindSVNRev(git_hash, "svn/bleeding_edge")
     97         if not svn_revision:  # pragma: no cover
     98           self.Die("Cannot determine svn revision for %s" % git_hash)
     99         revision_title = self.GitLog(n=1, format="%s", git_hash=git_hash)
    100 
    101         # Is this revision included in the original revision list?
    102         if svn_revision in self["full_revision_list"]:
    103           print("Found port of r%s -> r%s (already included): %s"
    104                 % (revision, svn_revision, revision_title))
    105         else:
    106           print("Found port of r%s -> r%s: %s"
    107                 % (revision, svn_revision, revision_title))
    108           port_revision_list.append(svn_revision)
    109 
    110     # Do we find any port?
    111     if len(port_revision_list) > 0:
    112       if self.Confirm("Automatically add corresponding ports (%s)?"
    113                       % ", ".join(port_revision_list)):
    114         #: 'y': Add ports to revision list.
    115         self["full_revision_list"].extend(port_revision_list)
    116 
    117 
    118 class FindGitRevisions(Step):
    119   MESSAGE = "Find the git revisions associated with the patches."
    120 
    121   def RunStep(self):
    122     self["patch_commit_hashes"] = []
    123     for revision in self["full_revision_list"]:
    124       next_hash = self.GitSVNFindGitHash(revision, "svn/bleeding_edge")
    125       if not next_hash:  # pragma: no cover
    126         self.Die("Cannot determine git hash for r%s" % revision)
    127       self["patch_commit_hashes"].append(next_hash)
    128 
    129     # Stringify: [123, 234] -> "r123, r234"
    130     self["revision_list"] = ", ".join(map(lambda s: "r%s" % s,
    131                                       self["full_revision_list"]))
    132 
    133     if not self["revision_list"]:  # pragma: no cover
    134       self.Die("Revision list is empty.")
    135 
    136     # The commit message title is added below after the version is specified.
    137     self["new_commit_msg"] = ""
    138 
    139     for commit_hash in self["patch_commit_hashes"]:
    140       patch_merge_desc = self.GitLog(n=1, format="%s", git_hash=commit_hash)
    141       self["new_commit_msg"] += "%s\n\n" % patch_merge_desc
    142 
    143     bugs = []
    144     for commit_hash in self["patch_commit_hashes"]:
    145       msg = self.GitLog(n=1, git_hash=commit_hash)
    146       for bug in re.findall(r"^[ \t]*BUG[ \t]*=[ \t]*(.*?)[ \t]*$", msg,
    147                             re.M):
    148         bugs.extend(map(lambda s: s.strip(), bug.split(",")))
    149     bug_aggregate = ",".join(sorted(filter(lambda s: s and s != "none", bugs)))
    150     if bug_aggregate:
    151       self["new_commit_msg"] += "BUG=%s\nLOG=N\n" % bug_aggregate
    152 
    153 
    154 class ApplyPatches(Step):
    155   MESSAGE = "Apply patches for selected revisions."
    156 
    157   def RunStep(self):
    158     for commit_hash in self["patch_commit_hashes"]:
    159       print("Applying patch for %s to %s..."
    160             % (commit_hash, self["merge_to_branch"]))
    161       patch = self.GitGetPatch(commit_hash)
    162       TextToFile(patch, self.Config(TEMPORARY_PATCH_FILE))
    163       self.ApplyPatch(self.Config(TEMPORARY_PATCH_FILE), self._options.revert)
    164     if self._options.patch:
    165       self.ApplyPatch(self._options.patch, self._options.revert)
    166 
    167 
    168 class PrepareVersion(Step):
    169   MESSAGE = "Prepare version file."
    170 
    171   def RunStep(self):
    172     if self._options.revert_bleeding_edge:
    173       return
    174     # This is used to calculate the patch level increment.
    175     self.ReadAndPersistVersion()
    176 
    177 
    178 class IncrementVersion(Step):
    179   MESSAGE = "Increment version number."
    180 
    181   def RunStep(self):
    182     if self._options.revert_bleeding_edge:
    183       return
    184     new_patch = str(int(self["patch"]) + 1)
    185     if self.Confirm("Automatically increment PATCH_LEVEL? (Saying 'n' will "
    186                     "fire up your EDITOR on %s so you can make arbitrary "
    187                     "changes. When you're done, save the file and exit your "
    188                     "EDITOR.)" % self.Config(VERSION_FILE)):
    189       text = FileToText(self.Config(VERSION_FILE))
    190       text = MSub(r"(?<=#define PATCH_LEVEL)(?P<space>\s+)\d*$",
    191                   r"\g<space>%s" % new_patch,
    192                   text)
    193       TextToFile(text, self.Config(VERSION_FILE))
    194     else:
    195       self.Editor(self.Config(VERSION_FILE))
    196     self.ReadAndPersistVersion("new_")
    197     self["version"] = "%s.%s.%s.%s" % (self["new_major"],
    198                                        self["new_minor"],
    199                                        self["new_build"],
    200                                        self["new_patch"])
    201 
    202 
    203 class CommitLocal(Step):
    204   MESSAGE = "Commit to local branch."
    205 
    206   def RunStep(self):
    207     # Add a commit message title.
    208     if self._options.revert:
    209       if not self._options.revert_bleeding_edge:
    210         title = ("Version %s (rollback of %s)"
    211                  % (self["version"], self["revision_list"]))
    212       else:
    213         title = "Revert %s." % self["revision_list"]
    214     else:
    215       title = ("Version %s (merged %s)"
    216                % (self["version"], self["revision_list"]))
    217     self["new_commit_msg"] = "%s\n\n%s" % (title, self["new_commit_msg"])
    218     TextToFile(self["new_commit_msg"], self.Config(COMMITMSG_FILE))
    219     self.GitCommit(file_name=self.Config(COMMITMSG_FILE))
    220 
    221 
    222 class CommitRepository(Step):
    223   MESSAGE = "Commit to the repository."
    224 
    225   def RunStep(self):
    226     self.GitCheckout(self.Config(BRANCHNAME))
    227     self.WaitForLGTM()
    228     self.GitPresubmit()
    229     self.GitDCommit()
    230 
    231 
    232 class PrepareSVN(Step):
    233   MESSAGE = "Determine svn commit revision."
    234 
    235   def RunStep(self):
    236     if self._options.revert_bleeding_edge:
    237       return
    238     self.GitSVNFetch()
    239     commit_hash = self.GitLog(n=1, format="%H", grep=self["new_commit_msg"],
    240                               branch="svn/%s" % self["merge_to_branch"])
    241     if not commit_hash:  # pragma: no cover
    242       self.Die("Unable to map git commit to svn revision.")
    243     self["svn_revision"] = self.GitSVNFindSVNRev(commit_hash)
    244     print "subversion revision number is r%s" % self["svn_revision"]
    245 
    246 
    247 class TagRevision(Step):
    248   MESSAGE = "Create the tag."
    249 
    250   def RunStep(self):
    251     if self._options.revert_bleeding_edge:
    252       return
    253     print "Creating tag svn/tags/%s" % self["version"]
    254     if self["merge_to_branch"] == "trunk":
    255       self["to_url"] = "trunk"
    256     else:
    257       self["to_url"] = "branches/%s" % self["merge_to_branch"]
    258     self.SVN("copy -r %s https://v8.googlecode.com/svn/%s "
    259              "https://v8.googlecode.com/svn/tags/%s -m "
    260              "\"Tagging version %s\""
    261              % (self["svn_revision"], self["to_url"],
    262                 self["version"], self["version"]))
    263 
    264 
    265 class CleanUp(Step):
    266   MESSAGE = "Cleanup."
    267 
    268   def RunStep(self):
    269     self.CommonCleanup()
    270     if not self._options.revert_bleeding_edge:
    271       print "*** SUMMARY ***"
    272       print "version: %s" % self["version"]
    273       print "branch: %s" % self["to_url"]
    274       print "svn revision: %s" % self["svn_revision"]
    275       if self["revision_list"]:
    276         print "patches: %s" % self["revision_list"]
    277 
    278 
    279 class MergeToBranch(ScriptsBase):
    280   def _Description(self):
    281     return ("Performs the necessary steps to merge revisions from "
    282             "bleeding_edge to other branches, including trunk.")
    283 
    284   def _PrepareOptions(self, parser):
    285     group = parser.add_mutually_exclusive_group(required=True)
    286     group.add_argument("--branch", help="The branch to merge to.")
    287     group.add_argument("-R", "--revert-bleeding-edge",
    288                        help="Revert specified patches from bleeding edge.",
    289                        default=False, action="store_true")
    290     parser.add_argument("revisions", nargs="*",
    291                         help="The revisions to merge.")
    292     parser.add_argument("-f", "--force",
    293                         help="Delete sentinel file.",
    294                         default=False, action="store_true")
    295     parser.add_argument("-m", "--message",
    296                         help="A commit message for the patch.")
    297     parser.add_argument("--revert",
    298                         help="Revert specified patches.",
    299                         default=False, action="store_true")
    300     parser.add_argument("-p", "--patch",
    301                         help="A patch file to apply as part of the merge.")
    302 
    303   def _ProcessOptions(self, options):
    304     # TODO(machenbach): Add a test that covers revert from bleeding_edge
    305     if len(options.revisions) < 1:
    306       if not options.patch:
    307         print "Either a patch file or revision numbers must be specified"
    308         return False
    309       if not options.message:
    310         print "You must specify a merge comment if no patches are specified"
    311         return False
    312     return True
    313 
    314   def _Steps(self):
    315     return [
    316       Preparation,
    317       CreateBranch,
    318       SearchArchitecturePorts,
    319       FindGitRevisions,
    320       ApplyPatches,
    321       PrepareVersion,
    322       IncrementVersion,
    323       CommitLocal,
    324       UploadStep,
    325       CommitRepository,
    326       PrepareSVN,
    327       TagRevision,
    328       CleanUp,
    329     ]
    330 
    331 
    332 if __name__ == "__main__":  # pragma: no cover
    333   sys.exit(MergeToBranch(CONFIG).Run())
    334