#!/usr/bin/env python # Copyright 2014 the V8 project authors. All rights reserved. # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # for py2/py3 compatibility from __future__ import print_function import argparse from collections import OrderedDict import sys from common_includes import * from git_recipes import GetCommitMessageFooterMap def IsSvnNumber(rev): return rev.isdigit() and len(rev) < 8 class Preparation(Step): MESSAGE = "Preparation." def RunStep(self): if os.path.exists(self.Config("ALREADY_MERGING_SENTINEL_FILE")): if self._options.force: os.remove(self.Config("ALREADY_MERGING_SENTINEL_FILE")) elif self._options.step == 0: # pragma: no cover self.Die("A merge is already in progress. Use -f to continue") open(self.Config("ALREADY_MERGING_SENTINEL_FILE"), "a").close() self.InitialEnvironmentChecks(self.default_cwd) self["merge_to_branch"] = self._options.branch self.CommonPrepare() self.PrepareBranch() class CreateBranch(Step): MESSAGE = "Create a fresh branch for the patch." def RunStep(self): self.GitCreateBranch(self.Config("BRANCHNAME"), self.vc.RemoteBranch(self["merge_to_branch"])) class SearchArchitecturePorts(Step): MESSAGE = "Search for corresponding architecture ports." def RunStep(self): self["full_revision_list"] = list(OrderedDict.fromkeys( self._options.revisions)) port_revision_list = [] for revision in self["full_revision_list"]: # Search for commits which matches the "Port XXX" pattern. git_hashes = self.GitLog(reverse=True, format="%H", grep="^[Pp]ort %s" % revision, branch=self.vc.RemoteMasterBranch()) for git_hash in git_hashes.splitlines(): revision_title = self.GitLog(n=1, format="%s", git_hash=git_hash) # Is this revision included in the original revision list? if git_hash in self["full_revision_list"]: print("Found port of %s -> %s (already included): %s" % (revision, git_hash, revision_title)) else: print("Found port of %s -> %s: %s" % (revision, git_hash, revision_title)) port_revision_list.append(git_hash) # Do we find any port? if len(port_revision_list) > 0: if self.Confirm("Automatically add corresponding ports (%s)?" % ", ".join(port_revision_list)): #: 'y': Add ports to revision list. self["full_revision_list"].extend(port_revision_list) class CreateCommitMessage(Step): MESSAGE = "Create commit message." def _create_commit_description(self, commit_hash): patch_merge_desc = self.GitLog(n=1, format="%s", git_hash=commit_hash) description = "Merged: " + patch_merge_desc + "\n" description += "Revision: " + commit_hash + "\n\n" return description def RunStep(self): # Stringify: ["abcde", "12345"] -> "abcde, 12345" self["revision_list"] = ", ".join(self["full_revision_list"]) if not self["revision_list"]: # pragma: no cover self.Die("Revision list is empty.") msg_pieces = [] if len(self["full_revision_list"]) > 1: self["commit_title"] = "Merged: Squashed multiple commits." for commit_hash in self["full_revision_list"]: msg_pieces.append(self._create_commit_description(commit_hash)) else: commit_hash = self["full_revision_list"][0] full_description = self._create_commit_description(commit_hash).split("\n") #Truncate title because of code review tool title = full_description[0] if len(title) > 100: title = title[:96] + " ..." self["commit_title"] = title msg_pieces.append(full_description[1] + "\n\n") bugs = [] for commit_hash in self["full_revision_list"]: msg = self.GitLog(n=1, git_hash=commit_hash) for bug in re.findall(r"^[ \t]*BUG[ \t]*=[ \t]*(.*?)[ \t]*$", msg, re.M): bugs.extend(s.strip() for s in bug.split(",")) gerrit_bug = GetCommitMessageFooterMap(msg).get('Bug', '') bugs.extend(s.strip() for s in gerrit_bug.split(",")) bug_aggregate = ",".join( sorted(filter(lambda s: s and s != "none", set(bugs)))) if bug_aggregate: # TODO(machenbach): Use proper gerrit footer for bug after switch to # gerrit. Keep BUG= for now for backwards-compatibility. msg_pieces.append("BUG=%s\nLOG=N\n" % bug_aggregate) msg_pieces.append("NOTRY=true\nNOPRESUBMIT=true\nNOTREECHECKS=true\n") self["new_commit_msg"] = "".join(msg_pieces) class ApplyPatches(Step): MESSAGE = "Apply patches for selected revisions." def RunStep(self): for commit_hash in self["full_revision_list"]: print("Applying patch for %s to %s..." % (commit_hash, self["merge_to_branch"])) patch = self.GitGetPatch(commit_hash) TextToFile(patch, self.Config("TEMPORARY_PATCH_FILE")) self.ApplyPatch(self.Config("TEMPORARY_PATCH_FILE")) if self._options.patch: self.ApplyPatch(self._options.patch) class CommitLocal(Step): MESSAGE = "Commit to local branch." def RunStep(self): # Add a commit message title. self["new_commit_msg"] = "%s\n\n%s" % (self["commit_title"], self["new_commit_msg"]) TextToFile(self["new_commit_msg"], self.Config("COMMITMSG_FILE")) self.GitCommit(file_name=self.Config("COMMITMSG_FILE")) class CommitRepository(Step): MESSAGE = "Commit to the repository." def RunStep(self): self.GitCheckout(self.Config("BRANCHNAME")) self.WaitForLGTM() self.GitPresubmit() self.vc.CLLand() class CleanUp(Step): MESSAGE = "Cleanup." def RunStep(self): self.CommonCleanup() print("*** SUMMARY ***") print("branch: %s" % self["merge_to_branch"]) if self["revision_list"]: print("patches: %s" % self["revision_list"]) class MergeToBranch(ScriptsBase): def _Description(self): return ("Performs the necessary steps to merge revisions from " "master to release branches like 4.5. This script does not " "version the commit. See http://goo.gl/9ke2Vw for more " "information.") def _PrepareOptions(self, parser): group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--branch", help="The branch to merge to.") parser.add_argument("revisions", nargs="*", help="The revisions to merge.") parser.add_argument("-f", "--force", help="Delete sentinel file.", default=False, action="store_true") parser.add_argument("-m", "--message", help="A commit message for the patch.") parser.add_argument("-p", "--patch", help="A patch file to apply as part of the merge.") def _ProcessOptions(self, options): if len(options.revisions) < 1: if not options.patch: print("Either a patch file or revision numbers must be specified") return False if not options.message: print("You must specify a merge comment if no patches are specified") return False options.bypass_upload_hooks = True # CC ulan to make sure that fixes are merged to Google3. options.cc = "ulan@chromium.org" if len(options.branch.split('.')) > 2: print ("This script does not support merging to roll branches. " "Please use tools/release/roll_merge.py for this use case.") return False # Make sure to use git hashes in the new workflows. for revision in options.revisions: if (IsSvnNumber(revision) or (revision[0:1] == "r" and IsSvnNumber(revision[1:]))): print("Please provide full git hashes of the patches to merge.") print("Got: %s" % revision) return False return True def _Config(self): return { "BRANCHNAME": "prepare-merge", "PERSISTFILE_BASENAME": RELEASE_WORKDIR + "v8-merge-to-branch-tempfile", "ALREADY_MERGING_SENTINEL_FILE": RELEASE_WORKDIR + "v8-merge-to-branch-tempfile-already-merging", "TEMPORARY_PATCH_FILE": RELEASE_WORKDIR + "v8-prepare-merge-tempfile-temporary-patch", "COMMITMSG_FILE": RELEASE_WORKDIR + "v8-prepare-merge-tempfile-commitmsg", } def _Steps(self): return [ Preparation, CreateBranch, SearchArchitecturePorts, CreateCommitMessage, ApplyPatches, CommitLocal, UploadStep, CommitRepository, CleanUp, ] if __name__ == "__main__": # pragma: no cover sys.exit(MergeToBranch().Run())