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