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