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 * 34from git_recipes import GetCommitMessageFooterMap 35 36def IsSvnNumber(rev): 37 return rev.isdigit() and len(rev) < 8 38 39class Preparation(Step): 40 MESSAGE = "Preparation." 41 42 def RunStep(self): 43 if os.path.exists(self.Config("ALREADY_MERGING_SENTINEL_FILE")): 44 if self._options.force: 45 os.remove(self.Config("ALREADY_MERGING_SENTINEL_FILE")) 46 elif self._options.step == 0: # pragma: no cover 47 self.Die("A merge is already in progress") 48 open(self.Config("ALREADY_MERGING_SENTINEL_FILE"), "a").close() 49 50 self.InitialEnvironmentChecks(self.default_cwd) 51 52 self["merge_to_branch"] = self._options.branch 53 54 self.CommonPrepare() 55 self.PrepareBranch() 56 57 58class CreateBranch(Step): 59 MESSAGE = "Create a fresh branch for the patch." 60 61 def RunStep(self): 62 self.GitCreateBranch(self.Config("BRANCHNAME"), 63 self.vc.RemoteBranch(self["merge_to_branch"])) 64 65 66class 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 XXX" pattern. 75 git_hashes = self.GitLog(reverse=True, format="%H", 76 grep="^[Pp]ort %s" % revision, 77 branch=self.vc.RemoteMasterBranch()) 78 for git_hash in git_hashes.splitlines(): 79 revision_title = self.GitLog(n=1, format="%s", git_hash=git_hash) 80 81 # Is this revision included in the original revision list? 82 if git_hash in self["full_revision_list"]: 83 print("Found port of %s -> %s (already included): %s" 84 % (revision, git_hash, revision_title)) 85 else: 86 print("Found port of %s -> %s: %s" 87 % (revision, git_hash, revision_title)) 88 port_revision_list.append(git_hash) 89 90 # Do we find any port? 91 if len(port_revision_list) > 0: 92 if self.Confirm("Automatically add corresponding ports (%s)?" 93 % ", ".join(port_revision_list)): 94 #: 'y': Add ports to revision list. 95 self["full_revision_list"].extend(port_revision_list) 96 97 98class CreateCommitMessage(Step): 99 MESSAGE = "Create commit message." 100 101 def _create_commit_description(self, commit_hash): 102 patch_merge_desc = self.GitLog(n=1, format="%s", git_hash=commit_hash) 103 description = "Merged: " + patch_merge_desc + "\n" 104 description += "Revision: " + commit_hash + "\n\n" 105 return description 106 107 def RunStep(self): 108 109 # Stringify: ["abcde", "12345"] -> "abcde, 12345" 110 self["revision_list"] = ", ".join(self["full_revision_list"]) 111 112 if not self["revision_list"]: # pragma: no cover 113 self.Die("Revision list is empty.") 114 115 msg_pieces = [] 116 117 if len(self["full_revision_list"]) > 1: 118 self["commit_title"] = "Merged: Squashed multiple commits." 119 for commit_hash in self["full_revision_list"]: 120 msg_pieces.append(self._create_commit_description(commit_hash)) 121 else: 122 commit_hash = self["full_revision_list"][0] 123 full_description = self._create_commit_description(commit_hash).split("\n") 124 125 #Truncate title because of code review tool 126 title = full_description[0] 127 if len(title) > 100: 128 title = title[:96] + " ..." 129 130 self["commit_title"] = title 131 msg_pieces.append(full_description[1] + "\n\n") 132 133 bugs = [] 134 for commit_hash in self["full_revision_list"]: 135 msg = self.GitLog(n=1, git_hash=commit_hash) 136 for bug in re.findall(r"^[ \t]*BUG[ \t]*=[ \t]*(.*?)[ \t]*$", msg, re.M): 137 bugs.extend(s.strip() for s in bug.split(",")) 138 gerrit_bug = GetCommitMessageFooterMap(msg).get('Bug', '') 139 bugs.extend(s.strip() for s in gerrit_bug.split(",")) 140 bug_aggregate = ",".join( 141 sorted(filter(lambda s: s and s != "none", set(bugs)))) 142 if bug_aggregate: 143 # TODO(machenbach): Use proper gerrit footer for bug after switch to 144 # gerrit. Keep BUG= for now for backwards-compatibility. 145 msg_pieces.append("BUG=%s\nLOG=N\n" % bug_aggregate) 146 147 msg_pieces.append("NOTRY=true\nNOPRESUBMIT=true\nNOTREECHECKS=true\n") 148 149 self["new_commit_msg"] = "".join(msg_pieces) 150 151 152class ApplyPatches(Step): 153 MESSAGE = "Apply patches for selected revisions." 154 155 def RunStep(self): 156 for commit_hash in self["full_revision_list"]: 157 print("Applying patch for %s to %s..." 158 % (commit_hash, self["merge_to_branch"])) 159 patch = self.GitGetPatch(commit_hash) 160 TextToFile(patch, self.Config("TEMPORARY_PATCH_FILE")) 161 self.ApplyPatch(self.Config("TEMPORARY_PATCH_FILE")) 162 if self._options.patch: 163 self.ApplyPatch(self._options.patch) 164 165class CommitLocal(Step): 166 MESSAGE = "Commit to local branch." 167 168 def RunStep(self): 169 # Add a commit message title. 170 self["new_commit_msg"] = "%s\n\n%s" % (self["commit_title"], 171 self["new_commit_msg"]) 172 TextToFile(self["new_commit_msg"], self.Config("COMMITMSG_FILE")) 173 self.GitCommit(file_name=self.Config("COMMITMSG_FILE")) 174 175class CommitRepository(Step): 176 MESSAGE = "Commit to the repository." 177 178 def RunStep(self): 179 self.GitCheckout(self.Config("BRANCHNAME")) 180 self.WaitForLGTM() 181 self.GitPresubmit() 182 self.vc.CLLand() 183 184class CleanUp(Step): 185 MESSAGE = "Cleanup." 186 187 def RunStep(self): 188 self.CommonCleanup() 189 print "*** SUMMARY ***" 190 print "branch: %s" % self["merge_to_branch"] 191 if self["revision_list"]: 192 print "patches: %s" % self["revision_list"] 193 194 195class MergeToBranch(ScriptsBase): 196 def _Description(self): 197 return ("Performs the necessary steps to merge revisions from " 198 "master to release branches like 4.5. This script does not " 199 "version the commit. See http://goo.gl/9ke2Vw for more " 200 "information.") 201 202 def _PrepareOptions(self, parser): 203 group = parser.add_mutually_exclusive_group(required=True) 204 group.add_argument("--branch", help="The branch to merge to.") 205 parser.add_argument("revisions", nargs="*", 206 help="The revisions to merge.") 207 parser.add_argument("-f", "--force", 208 help="Delete sentinel file.", 209 default=False, action="store_true") 210 parser.add_argument("-m", "--message", 211 help="A commit message for the patch.") 212 parser.add_argument("-p", "--patch", 213 help="A patch file to apply as part of the merge.") 214 215 def _ProcessOptions(self, options): 216 if len(options.revisions) < 1: 217 if not options.patch: 218 print "Either a patch file or revision numbers must be specified" 219 return False 220 if not options.message: 221 print "You must specify a merge comment if no patches are specified" 222 return False 223 options.bypass_upload_hooks = True 224 # CC ulan to make sure that fixes are merged to Google3. 225 options.cc = "ulan@chromium.org" 226 227 if len(options.branch.split('.')) > 2: 228 print ("This script does not support merging to roll branches. " 229 "Please use tools/release/roll_merge.py for this use case.") 230 return False 231 232 # Make sure to use git hashes in the new workflows. 233 for revision in options.revisions: 234 if (IsSvnNumber(revision) or 235 (revision[0:1] == "r" and IsSvnNumber(revision[1:]))): 236 print "Please provide full git hashes of the patches to merge." 237 print "Got: %s" % revision 238 return False 239 return True 240 241 def _Config(self): 242 return { 243 "BRANCHNAME": "prepare-merge", 244 "PERSISTFILE_BASENAME": RELEASE_WORKDIR + "v8-merge-to-branch-tempfile", 245 "ALREADY_MERGING_SENTINEL_FILE": 246 RELEASE_WORKDIR + "v8-merge-to-branch-tempfile-already-merging", 247 "TEMPORARY_PATCH_FILE": 248 RELEASE_WORKDIR + "v8-prepare-merge-tempfile-temporary-patch", 249 "COMMITMSG_FILE": RELEASE_WORKDIR + "v8-prepare-merge-tempfile-commitmsg", 250 } 251 252 def _Steps(self): 253 return [ 254 Preparation, 255 CreateBranch, 256 SearchArchitecturePorts, 257 CreateCommitMessage, 258 ApplyPatches, 259 CommitLocal, 260 UploadStep, 261 CommitRepository, 262 CleanUp, 263 ] 264 265 266if __name__ == "__main__": # pragma: no cover 267 sys.exit(MergeToBranch().Run()) 268