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