From: machenbach@chromium.org Date: Fri, 8 Nov 2013 14:27:16 +0000 (+0000) Subject: Add push-to-trunk python port. X-Git-Tag: upstream/4.7.83~11818 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=a8f77a54cf3bc99b442c6fab3914f2ecaa908045;p=platform%2Fupstream%2Fv8.git Add push-to-trunk python port. Preliminary version. TODO: Add wrapper script in tools dir. TODO: Some refactoring. R=jkummerow@chromium.org Review URL: https://codereview.chromium.org/49653002 git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@17600 ce2b1a6d-e550-0410-aec6-3dcde31c8c00 --- diff --git a/tools/push-to-trunk/common_includes.py b/tools/push-to-trunk/common_includes.py new file mode 100644 index 0000000..3b2eab0 --- /dev/null +++ b/tools/push-to-trunk/common_includes.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python +# Copyright 2013 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. + +import os +import re +import subprocess +import sys + +PERSISTFILE_BASENAME = "PERSISTFILE_BASENAME" +TEMP_BRANCH = "TEMP_BRANCH" +BRANCHNAME = "BRANCHNAME" +DOT_GIT_LOCATION = "DOT_GIT_LOCATION" +VERSION_FILE = "VERSION_FILE" +CHANGELOG_FILE = "CHANGELOG_FILE" +CHANGELOG_ENTRY_FILE = "CHANGELOG_ENTRY_FILE" +COMMITMSG_FILE = "COMMITMSG_FILE" +PATCH_FILE = "PATCH_FILE" + + +def TextToFile(text, file_name): + with open(file_name, "w") as f: + f.write(text) + + +def AppendToFile(text, file_name): + with open(file_name, "a") as f: + f.write(text) + + +def LinesInFile(file_name): + with open(file_name) as f: + for line in f: + yield line + + +def FileToText(file_name): + with open(file_name) as f: + return f.read() + + +def MSub(rexp, replacement, text): + return re.sub(rexp, replacement, text, flags=re.MULTILINE) + + +# Some commands don't like the pipe, e.g. calling vi from within the script or +# from subscripts like git cl upload. +def Command(cmd, args="", prefix="", pipe=True): + cmd_line = "%s %s %s" % (prefix, cmd, args) + print "Command: %s" % cmd_line + try: + if pipe: + return subprocess.check_output(cmd_line, shell=True) + else: + return subprocess.check_call(cmd_line, shell=True) + except subprocess.CalledProcessError: + return None + + +# Wrapper for side effects. +class SideEffectHandler(object): + def Command(self, cmd, args="", prefix="", pipe=True): + return Command(cmd, args, prefix, pipe) + + def ReadLine(self): + return sys.stdin.readline().strip() + +DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler() + + +class Step(object): + def __init__(self, text="", requires=None): + self._text = text + self._number = -1 + self._requires = requires + self._side_effect_handler = DEFAULT_SIDE_EFFECT_HANDLER + + def SetNumber(self, number): + self._number = number + + def SetConfig(self, config): + self._config = config + + def SetState(self, state): + self._state = state + + def SetOptions(self, options): + self._options = options + + def SetSideEffectHandler(self, handler): + self._side_effect_handler = handler + + def Config(self, key): + return self._config[key] + + def Run(self): + assert self._number >= 0 + assert self._config is not None + assert self._state is not None + assert self._side_effect_handler is not None + if self._requires: + self.RestoreIfUnset(self._requires) + if not self._state[self._requires]: + return + print ">>> Step %d: %s" % (self._number, self._text) + self.RunStep() + + def RunStep(self): + raise NotImplementedError + + def ReadLine(self): + return self._side_effect_handler.ReadLine() + + def Git(self, args="", prefix="", pipe=True): + return self._side_effect_handler.Command("git", args, prefix, pipe) + + def Editor(self, args): + return self._side_effect_handler.Command(os.environ["EDITOR"], args, + pipe=False) + + def Die(self, msg=""): + if msg != "": + print "Error: %s" % msg + print "Exiting" + raise Exception(msg) + + def Confirm(self, msg): + print "%s [Y/n] " % msg, + answer = self.ReadLine() + return answer == "" or answer == "Y" or answer == "y" + + def DeleteBranch(self, name): + git_result = self.Git("branch").strip() + for line in git_result.splitlines(): + if re.match(r".*\s+%s$" % name, line): + msg = "Branch %s exists, do you want to delete it?" % name + if self.Confirm(msg): + if self.Git("branch -D %s" % name) is None: + self.Die("Deleting branch '%s' failed." % name) + print "Branch %s deleted." % name + else: + msg = "Can't continue. Please delete branch %s and try again." % name + self.Die(msg) + + def Persist(self, var, value): + value = value or "__EMPTY__" + TextToFile(value, "%s-%s" % (self._config[PERSISTFILE_BASENAME], var)) + + def Restore(self, var): + value = FileToText("%s-%s" % (self._config[PERSISTFILE_BASENAME], var)) + value = value or self.Die("Variable '%s' could not be restored." % var) + return "" if value == "__EMPTY__" else value + + def RestoreIfUnset(self, var_name): + if self._state.get(var_name) is None: + self._state[var_name] = self.Restore(var_name) + + def InitialEnvironmentChecks(self): + # Cancel if this is not a git checkout. + if not os.path.exists(self._config[DOT_GIT_LOCATION]): + self.Die("This is not a git checkout, this script won't work for you.") + + # Cancel if EDITOR is unset or not executable. + if (not os.environ.get("EDITOR") or + Command("which", os.environ["EDITOR"]) is None): + self.Die("Please set your EDITOR environment variable, you'll need it.") + + def CommonPrepare(self): + # Check for a clean workdir. + if self.Git("status -s -uno").strip() != "": + self.Die("Workspace is not clean. Please commit or undo your changes.") + + # Persist current branch. + current_branch = "" + git_result = self.Git("status -s -b -uno").strip() + for line in git_result.splitlines(): + match = re.match(r"^## (.+)", line) + if match: + current_branch = match.group(1) + break + self.Persist("current_branch", current_branch) + + # Fetch unfetched revisions. + if self.Git("svn fetch") is None: + self.Die("'git svn fetch' failed.") + + # Get ahold of a safe temporary branch and check it out. + if current_branch != self._config[TEMP_BRANCH]: + self.DeleteBranch(self._config[TEMP_BRANCH]) + self.Git("checkout -b %s" % self._config[TEMP_BRANCH]) + + # Delete the branch that will be created later if it exists already. + self.DeleteBranch(self._config[BRANCHNAME]) + + def CommonCleanup(self): + self.RestoreIfUnset("current_branch") + self.Git("checkout -f %s" % self._state["current_branch"]) + if self._config[TEMP_BRANCH] != self._state["current_branch"]: + self.Git("branch -D %s" % self._config[TEMP_BRANCH]) + if self._config[BRANCHNAME] != self._state["current_branch"]: + self.Git("branch -D %s" % self._config[BRANCHNAME]) + + # Clean up all temporary files. + Command("rm", "-f %s*" % self._config[PERSISTFILE_BASENAME]) + + def ReadAndPersistVersion(self, prefix=""): + def ReadAndPersist(var_name, def_name): + match = re.match(r"^#define %s\s+(\d*)" % def_name, line) + if match: + value = match.group(1) + self.Persist("%s%s" % (prefix, var_name), value) + self._state["%s%s" % (prefix, var_name)] = value + for line in LinesInFile(self._config[VERSION_FILE]): + for (var_name, def_name) in [("major", "MAJOR_VERSION"), + ("minor", "MINOR_VERSION"), + ("build", "BUILD_NUMBER"), + ("patch", "PATCH_LEVEL")]: + ReadAndPersist(var_name, def_name) + + def RestoreVersionIfUnset(self, prefix=""): + for v in ["major", "minor", "build", "patch"]: + self.RestoreIfUnset("%s%s" % (prefix, v)) + + def WaitForLGTM(self): + print ("Please wait for an LGTM, then type \"LGTM\" to commit " + "your change. (If you need to iterate on the patch or double check " + "that it's sane, do so in another shell, but remember to not " + "change the headline of the uploaded CL.") + answer = "" + while answer != "LGTM": + print "> ", + answer = self.ReadLine() + if answer != "LGTM": + print "That was not 'LGTM'." + + def WaitForResolvingConflicts(self, patch_file): + print("Applying the patch \"%s\" failed. Either type \"ABORT\", " + "or resolve the conflicts, stage *all* touched files with " + "'git add', and type \"RESOLVED\"") + answer = "" + while answer != "RESOLVED": + if answer == "ABORT": + self.Die("Applying the patch failed.") + if answer != "": + print "That was not 'RESOLVED' or 'ABORT'." + print "> ", + answer = self.ReadLine() + + # Takes a file containing the patch to apply as first argument. + def ApplyPatch(self, patch_file, reverse_patch=""): + args = "apply --index --reject %s \"%s\"" % (reverse_patch, patch_file) + if self.Git(args) is None: + self.WaitForResolvingConflicts(patch_file) + + +class UploadStep(Step): + def __init__(self): + Step.__init__(self, "Upload for code review.") + + def RunStep(self): + print "Please enter the email address of a V8 reviewer for your patch: ", + reviewer = self.ReadLine() + args = "cl upload -r \"%s\" --send-mail" % reviewer + if self.Git(args,pipe=False) is None: + self.Die("'git cl upload' failed, please try again.") diff --git a/tools/push-to-trunk/push_to_trunk.py b/tools/push-to-trunk/push_to_trunk.py new file mode 100755 index 0000000..6ec03cb --- /dev/null +++ b/tools/push-to-trunk/push_to_trunk.py @@ -0,0 +1,592 @@ +#!/usr/bin/env python +# Copyright 2013 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. + +import datetime +import optparse +import sys +import tempfile + +from common_includes import * + +TRUNKBRANCH = "TRUNKBRANCH" +CHROMIUM = "CHROMIUM" +DEPS_FILE = "DEPS_FILE" + +CONFIG = { + BRANCHNAME: "prepare-push", + TRUNKBRANCH: "trunk-push", + PERSISTFILE_BASENAME: "/tmp/v8-push-to-trunk-tempfile", + TEMP_BRANCH: "prepare-push-temporary-branch-created-by-script", + DOT_GIT_LOCATION: ".git", + VERSION_FILE: "src/version.cc", + CHANGELOG_FILE: "ChangeLog", + CHANGELOG_ENTRY_FILE: "/tmp/v8-push-to-trunk-tempfile-changelog-entry", + PATCH_FILE: "/tmp/v8-push-to-trunk-tempfile-patch-file", + COMMITMSG_FILE: "/tmp/v8-push-to-trunk-tempfile-commitmsg", + DEPS_FILE: "DEPS", +} + + +class Preparation(Step): + def __init__(self): + Step.__init__(self, "Preparation.") + + def RunStep(self): + self.InitialEnvironmentChecks() + self.CommonPrepare() + self.DeleteBranch(self.Config(TRUNKBRANCH)) + + +class FreshBranch(Step): + def __init__(self): + Step.__init__(self, "Create a fresh branch.") + + def RunStep(self): + args = "checkout -b %s svn/bleeding_edge" % self.Config(BRANCHNAME) + if self.Git(args) is None: + self.Die("Creating branch %s failed." % self.Config(BRANCHNAME)) + + +class DetectLastPush(Step): + def __init__(self): + Step.__init__(self, "Detect commit ID of last push to trunk.") + + def RunStep(self): + last_push = (self._options.l or + self.Git("log -1 --format=%H ChangeLog").strip()) + while True: + # Print assumed commit, circumventing git's pager. + print self.Git("log -1 %s" % last_push) + if self.Confirm("Is the commit printed above the last push to trunk?"): + break + args = "log -1 --format=%H %s^ ChangeLog" % last_push + last_push = self.Git(args).strip() + self.Persist("last_push", last_push) + self._state["last_push"] = last_push + + +class PrepareChangeLog(Step): + def __init__(self): + Step.__init__(self, "Prepare raw ChangeLog entry.") + + def RunStep(self): + self.RestoreIfUnset("last_push") + + # These version numbers are used again later for the trunk commit. + self.ReadAndPersistVersion() + + date = datetime.date.today().strftime("%Y-%m-%d") + self.Persist("date", date) + output = "%s: Version %s.%s.%s\n\n" % (date, + self._state["major"], + self._state["minor"], + self._state["build"]) + TextToFile(output, self.Config(CHANGELOG_ENTRY_FILE)) + + args = "log %s..HEAD --format=%%H" % self._state["last_push"] + commits = self.Git(args).strip() + for commit in commits.splitlines(): + # Get the commit's title line. + args = "log -1 %s --format=\"%%w(80,8,8)%%s\"" % commit + title = "%s\n" % self.Git(args).rstrip() + AppendToFile(title, self.Config(CHANGELOG_ENTRY_FILE)) + + # Grep for "BUG=xxxx" lines in the commit message and convert them to + # "(issue xxxx)". + out = self.Git("log -1 %s --format=\"%%B\"" % commit).splitlines() + out = filter(lambda x: re.search(r"^BUG=", x), out) + out = filter(lambda x: not re.search(r"BUG=$", x), out) + out = filter(lambda x: not re.search(r"BUG=none$", x), out) + + # TODO(machenbach): Handle multiple entries (e.g. BUG=123, 234). + def FormatIssue(text): + text = re.sub(r"BUG=v8:(.*)$", r"(issue \1)", text) + text = re.sub(r"BUG=chromium:(.*)$", r"(Chromium issue \1)", text) + text = re.sub(r"BUG=(.*)$", r"(Chromium issue \1)", text) + return " %s\n" % text + + for line in map(FormatIssue, out): + AppendToFile(line, self.Config(CHANGELOG_ENTRY_FILE)) + + # Append the commit's author for reference. + args = "log -1 %s --format=\"%%w(80,8,8)(%%an)\"" % commit + author = self.Git(args).rstrip() + AppendToFile("%s\n\n" % author, self.Config(CHANGELOG_ENTRY_FILE)) + + msg = " Performance and stability improvements on all platforms.\n" + AppendToFile(msg, self.Config(CHANGELOG_ENTRY_FILE)) + +class EditChangeLog(Step): + def __init__(self): + Step.__init__(self, "Edit ChangeLog entry.") + + def RunStep(self): + print ("Please press to have your EDITOR open the ChangeLog " + "entry, then edit its contents to your liking. When you're done, " + "save the file and exit your EDITOR. ") + self.ReadLine() + + self.Editor(self.Config(CHANGELOG_ENTRY_FILE)) + handle, new_changelog = tempfile.mkstemp() + os.close(handle) + + # (1) Eliminate tabs, (2) fix too little and (3) too much indentation, and + # (4) eliminate trailing whitespace. + changelog_entry = FileToText(self.Config(CHANGELOG_ENTRY_FILE)).rstrip() + changelog_entry = MSub(r"\t", r" ", changelog_entry) + changelog_entry = MSub(r"^ {1,7}([^ ])", r" \1", changelog_entry) + changelog_entry = MSub(r"^ {9,80}([^ ])", r" \1", changelog_entry) + changelog_entry = MSub(r" +$", r"", changelog_entry) + + if changelog_entry == "": + self.Die("Empty ChangeLog entry.") + + with open(new_changelog, "w") as f: + f.write(changelog_entry) + f.write("\n\n\n") # Explicitly insert two empty lines. + + AppendToFile(FileToText(self.Config(CHANGELOG_FILE)), new_changelog) + TextToFile(FileToText(new_changelog), self.Config(CHANGELOG_FILE)) + os.remove(new_changelog) + + +class IncrementVersion(Step): + def __init__(self): + Step.__init__(self, "Increment version number.") + + def RunStep(self): + self.RestoreIfUnset("build") + new_build = str(int(self._state["build"]) + 1) + + if self.Confirm(("Automatically increment BUILD_NUMBER? (Saying 'n' will " + "fire up your EDITOR on %s so you can make arbitrary " + "changes. When you're done, save the file and exit your " + "EDITOR.)" % self.Config(VERSION_FILE))): + text = FileToText(self.Config(VERSION_FILE)) + text = MSub(r"(?<=#define BUILD_NUMBER)(?P\s+)\d*$", + r"\g%s" % new_build, + text) + TextToFile(text, self.Config(VERSION_FILE)) + else: + self.Editor(self.Config(VERSION_FILE)) + + self.ReadAndPersistVersion("new_") + + +class CommitLocal(Step): + def __init__(self): + Step.__init__(self, "Commit to local branch.") + + def RunStep(self): + self.RestoreVersionIfUnset("new_") + prep_commit_msg = ("Prepare push to trunk. " + "Now working on version %s.%s.%s." % (self._state["new_major"], + self._state["new_minor"], + self._state["new_build"])) + self.Persist("prep_commit_msg", prep_commit_msg) + if self.Git("commit -a -m \"%s\"" % prep_commit_msg) is None: + self.Die("'git commit -a' failed.") + + +class CommitRepository(Step): + def __init__(self): + Step.__init__(self, "Commit to the repository.") + + def RunStep(self): + self.WaitForLGTM() + # Re-read the ChangeLog entry (to pick up possible changes). + # FIXME(machenbach): This was hanging once with a broken pipe. + TextToFile(Command("cat %s | awk --posix '{\ + if ($0 ~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}:/) {\ + if (in_firstblock == 1) {\ + exit 0;\ + } else {\ + in_firstblock = 1;\ + }\ + };\ + print $0;\ + }'" % self.Config(CHANGELOG_FILE)), self.Config(CHANGELOG_ENTRY_FILE)) + + if self.Git("cl dcommit", "PRESUBMIT_TREE_CHECK=\"skip\"") is None: + self.Die("'git cl dcommit' failed, please try again.") + + +class StragglerCommits(Step): + def __init__(self): + Step.__init__(self, "Fetch straggler commits that sneaked in since this " + "script was started.") + + def RunStep(self): + if self.Git("svn fetch") is None: + self.Die("'git svn fetch' failed.") + self.Git("checkout svn/bleeding_edge") + self.RestoreIfUnset("prep_commit_msg") + args = "log -1 --format=%%H --grep=\"%s\"" % self._state["prep_commit_msg"] + prepare_commit_hash = self.Git(args).strip() + self.Persist("prepare_commit_hash", prepare_commit_hash) + + +class SquashCommits(Step): + def __init__(self): + Step.__init__(self, "Squash commits into one.") + + def RunStep(self): + # Instead of relying on "git rebase -i", we'll just create a diff, because + # that's easier to automate. + self.RestoreIfUnset("prepare_commit_hash") + args = "diff svn/trunk %s" % self._state["prepare_commit_hash"] + TextToFile(self.Git(args), self.Config(PATCH_FILE)) + + # Convert the ChangeLog entry to commit message format: + # - remove date + # - remove indentation + # - merge paragraphs into single long lines, keeping empty lines between + # them. + self.RestoreIfUnset("date") + changelog_entry = FileToText(self.Config(CHANGELOG_ENTRY_FILE)) + + # TODO(machenbach): This could create a problem if the changelog contained + # any quotation marks. + text = Command("echo \"%s\" \ + | sed -e \"s/^%s: //\" \ + | sed -e 's/^ *//' \ + | awk '{ \ + if (need_space == 1) {\ + printf(\" \");\ + };\ + printf(\"%%s\", $0);\ + if ($0 ~ /^$/) {\ + printf(\"\\n\\n\");\ + need_space = 0;\ + } else {\ + need_space = 1;\ + }\ + }'" % (changelog_entry, self._state["date"])) + + if not text: + self.Die("Commit message editing failed.") + TextToFile(text, self.Config(COMMITMSG_FILE)) + os.remove(self.Config(CHANGELOG_ENTRY_FILE)) + + +class NewBranch(Step): + def __init__(self): + Step.__init__(self, "Create a new branch from trunk.") + + def RunStep(self): + if self.Git("checkout -b %s svn/trunk" % self.Config(TRUNKBRANCH)) is None: + self.Die("Checking out a new branch '%s' failed." % + self.Config(TRUNKBRANCH)) + + +class ApplyChanges(Step): + def __init__(self): + Step.__init__(self, "Apply squashed changes.") + + def RunStep(self): + self.ApplyPatch(self.Config(PATCH_FILE)) + Command("rm", "-f %s*" % self.Config(PATCH_FILE)) + + +class SetVersion(Step): + def __init__(self): + Step.__init__(self, "Set correct version for trunk.") + + def RunStep(self): + self.RestoreVersionIfUnset() + output = "" + for line in FileToText(self.Config(VERSION_FILE)).splitlines(): + if line.startswith("#define MAJOR_VERSION"): + line = re.sub("\d+$", self._state["major"], line) + elif line.startswith("#define MINOR_VERSION"): + line = re.sub("\d+$", self._state["minor"], line) + elif line.startswith("#define BUILD_NUMBER"): + line = re.sub("\d+$", self._state["build"], line) + elif line.startswith("#define PATCH_LEVEL"): + line = re.sub("\d+$", "0", line) + elif line.startswith("#define IS_CANDIDATE_VERSION"): + line = re.sub("\d+$", "0", line) + output += "%s\n" % line + TextToFile(output, self.Config(VERSION_FILE)) + + +class CommitTrunk(Step): + def __init__(self): + Step.__init__(self, "Commit to local trunk branch.") + + def RunStep(self): + self.Git("add \"%s\"" % self.Config(VERSION_FILE)) + if self.Git("commit -F \"%s\"" % self.Config(COMMITMSG_FILE)) is None: + self.Die("'git commit' failed.") + Command("rm", "-f %s*" % self.Config(COMMITMSG_FILE)) + + +class SanityCheck(Step): + def __init__(self): + Step.__init__(self, "Sanity check.") + + def RunStep(self): + if not self.Confirm("Please check if your local checkout is sane: Inspect " + "%s, compile, run tests. Do you want to commit this new trunk " + "revision to the repository?" % self.Config(VERSION_FILE)): + self.Die("Execution canceled.") + + +class CommitSVN(Step): + def __init__(self): + Step.__init__(self, "Commit to SVN.") + + def RunStep(self): + result = self.Git("svn dcommit 2>&1") + if not result: + self.Die("'git svn dcommit' failed.") + result = filter(lambda x: re.search(r"^Committed r[0-9]+", x), + result.splitlines()) + if len(result) > 0: + trunk_revision = re.sub(r"^Committed r([0-9]+)", r"\1", result[0]) + + # Sometimes grepping for the revision fails. No idea why. If you figure + # out why it is flaky, please do fix it properly. + if not trunk_revision: + print("Sorry, grepping for the SVN revision failed. Please look for it " + "in the last command's output above and provide it manually (just " + "the number, without the leading \"r\").") + while not trunk_revision: + print "> ", + trunk_revision = self.ReadLine() + self.Persist("trunk_revision", trunk_revision) + + +class TagRevision(Step): + def __init__(self): + Step.__init__(self, "Tag the new revision.") + + def RunStep(self): + self.RestoreVersionIfUnset() + ver = "%s.%s.%s" % (self._state["major"], + self._state["minor"], + self._state["build"]) + if self.Git("svn tag %s -m \"Tagging version %s\"" % (ver, ver)) is None: + self.Die("'git svn tag' failed.") + + +class CheckChromium(Step): + def __init__(self): + Step.__init__(self, "Ask for chromium checkout.") + + def Run(self): + chrome_path = self._options.c + if not chrome_path: + print ("Do you have a \"NewGit\" Chromium checkout and want " + "this script to automate creation of the roll CL? If yes, enter the " + "path to (and including) the \"src\" directory here, otherwise just " + "press : "), + chrome_path = self.ReadLine() + self.Persist("chrome_path", chrome_path) + + +class SwitchChromium(Step): + def __init__(self): + Step.__init__(self, "Switch to Chromium checkout.", requires="chrome_path") + + def RunStep(self): + v8_path = os.getcwd() + self.Persist("v8_path", v8_path) + os.chdir(self._state["chrome_path"]) + self.InitialEnvironmentChecks() + # Check for a clean workdir. + if self.Git("status -s -uno").strip() != "": + self.Die("Workspace is not clean. Please commit or undo your changes.") + # Assert that the DEPS file is there. + if not os.path.exists(self.Config(DEPS_FILE)): + self.Die("DEPS file not present.") + + +class UpdateChromiumCheckout(Step): + def __init__(self): + Step.__init__(self, "Update the checkout and create a new branch.", + requires="chrome_path") + + def RunStep(self): + os.chdir(self._state["chrome_path"]) + if self.Git("checkout master") is None: + self.Die("'git checkout master' failed.") + if self.Git("pull") is None: + self.Die("'git pull' failed, please try again.") + + self.RestoreIfUnset("trunk_revision") + args = "checkout -b v8-roll-%s" % self._state["trunk_revision"] + if self.Git(args) is None: + self.Die("Failed to checkout a new branch.") + + +class UploadCL(Step): + def __init__(self): + Step.__init__(self, "Create and upload CL.", requires="chrome_path") + + def RunStep(self): + os.chdir(self._state["chrome_path"]) + + # Patch DEPS file. + self.RestoreIfUnset("trunk_revision") + deps = FileToText(self.Config(DEPS_FILE)) + deps = re.sub("(?<=\"v8_revision\": \")([0-9]+)(?=\")", + self._state["trunk_revision"], + deps) + TextToFile(deps, self.Config(DEPS_FILE)) + + self.RestoreVersionIfUnset() + ver = "%s.%s.%s" % (self._state["major"], + self._state["minor"], + self._state["build"]) + print "Please enter the email address of a reviewer for the roll CL: ", + rev = self.ReadLine() + args = "commit -am \"Update V8 to version %s.\n\nTBR=%s\"" % (ver, rev) + if self.Git(args) is None: + self.Die("'git commit' failed.") + if self.Git("cl upload --send-mail", pipe=False) is None: + self.Die("'git cl upload' failed, please try again.") + print "CL uploaded." + + +class SwitchV8(Step): + def __init__(self): + Step.__init__(self, "Returning to V8 checkout.", requires="chrome_path") + + def RunStep(self): + self.RestoreIfUnset("v8_path") + os.chdir(self._state["v8_path"]) + + +class CleanUp(Step): + def __init__(self): + Step.__init__(self, "Done!") + + def RunStep(self): + self.RestoreVersionIfUnset() + ver = "%s.%s.%s" % (self._state["major"], + self._state["minor"], + self._state["build"]) + self.RestoreIfUnset("trunk_revision") + self.RestoreIfUnset("chrome_path") + + if self._state["chrome_path"]: + print("Congratulations, you have successfully created the trunk " + "revision %s and rolled it into Chromium. Please don't forget to " + "update the v8rel spreadsheet:" % ver) + else: + print("Congratulations, you have successfully created the trunk " + "revision %s. Please don't forget to roll this new version into " + "Chromium, and to update the v8rel spreadsheet:" % ver) + print "%s\ttrunk\t%s" % (ver, self._state["trunk_revision"]) + + self.CommonCleanup() + if self.Config(TRUNKBRANCH) != self._state["current_branch"]: + self.Git("branch -D %s" % self.Config(TRUNKBRANCH)) + + +def RunScript(config, + options, + side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): + step_classes = [ + Preparation, + FreshBranch, + DetectLastPush, + PrepareChangeLog, + EditChangeLog, + IncrementVersion, + CommitLocal, + UploadStep, + CommitRepository, + StragglerCommits, + SquashCommits, + NewBranch, + ApplyChanges, + SetVersion, + CommitTrunk, + SanityCheck, + CommitSVN, + TagRevision, + CheckChromium, + SwitchChromium, + UpdateChromiumCheckout, + UploadCL, + SwitchV8, + CleanUp, + ] + + state = {} + steps = [] + number = 0 + + for step_class in step_classes: + # TODO(machenbach): Factory methods. + step = step_class() + step.SetNumber(number) + step.SetConfig(config) + step.SetOptions(options) + step.SetState(state) + step.SetSideEffectHandler(side_effect_handler) + steps.append(step) + number += 1 + + for step in steps[options.s:]: + step.Run() + + +def BuildOptions(): + result = optparse.OptionParser() + result.add_option("-s", "--step", dest="s", + help="Specify the step where to start work. Default: 0.", + default=0, type="int") + result.add_option("-l", "--last-push", dest="l", + help=("Manually specify the git commit ID " + "of the last push to trunk.")) + result.add_option("-c", "--chromium", dest="c", + help=("Specify the path to your Chromium src/ " + "directory to automate the V8 roll.")) + return result + + +def ProcessOptions(options): + if options.s < 0: + print "Bad step number %d" % options.s + return False + return True + + +def Main(): + parser = BuildOptions() + (options, args) = parser.parse_args() + if not ProcessOptions(options): + parser.print_help() + return 1 + RunScript(CONFIG, options) + +if __name__ == "__main__": + sys.exit(Main()) diff --git a/tools/push-to-trunk/test_scripts.py b/tools/push-to-trunk/test_scripts.py new file mode 100644 index 0000000..9865ada --- /dev/null +++ b/tools/push-to-trunk/test_scripts.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python +# Copyright 2013 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. + +import os +import tempfile +import unittest + +import common_includes +from common_includes import * +import push_to_trunk +from push_to_trunk import * + + +TEST_CONFIG = { + BRANCHNAME: "test-prepare-push", + TRUNKBRANCH: "test-trunk-push", + PERSISTFILE_BASENAME: "/tmp/test-v8-push-to-trunk-tempfile", + TEMP_BRANCH: "test-prepare-push-temporary-branch-created-by-script", + DOT_GIT_LOCATION: None, + VERSION_FILE: None, + CHANGELOG_FILE: None, + CHANGELOG_ENTRY_FILE: "/tmp/test-v8-push-to-trunk-tempfile-changelog-entry", + PATCH_FILE: "/tmp/test-v8-push-to-trunk-tempfile-patch", + COMMITMSG_FILE: "/tmp/test-v8-push-to-trunk-tempfile-commitmsg", + CHROMIUM: "/tmp/test-v8-push-to-trunk-tempfile-chromium", + DEPS_FILE: "/tmp/test-v8-push-to-trunk-tempfile-chromium/DEPS", +} + + +class ScriptTest(unittest.TestCase): + def MakeEmptyTempFile(self): + handle, name = tempfile.mkstemp() + os.close(handle) + self._tmp_files.append(name) + return name + + def MakeTempVersionFile(self): + name = self.MakeEmptyTempFile() + with open(name, "w") as f: + f.write(" // Some line...\n") + f.write("\n") + f.write("#define MAJOR_VERSION 3\n") + f.write("#define MINOR_VERSION 22\n") + f.write("#define BUILD_NUMBER 5\n") + f.write("#define PATCH_LEVEL 0\n") + f.write(" // Some line...\n") + f.write("#define IS_CANDIDATE_VERSION 0\n") + return name + + def MakeStep(self, step_class=Step, state=None): + state = state or {} + step = step_class() + step.SetConfig(TEST_CONFIG) + step.SetState(state) + step.SetNumber(0) + step.SetSideEffectHandler(self) + return step + + def GitMock(self, cmd, args="", pipe=True): + self._git_index += 1 + try: + git_invocation = self._git_recipe[self._git_index] + except IndexError: + raise Exception("Calling git %s" % args) + if git_invocation[0] != args: + raise Exception("Expected: %s - Actual: %s" % (git_invocation[0], args)) + if len(git_invocation) == 3: + # Run optional function checking the context during this git command. + git_invocation[2]() + return git_invocation[1] + + def LogMock(self, cmd, args=""): + print "Log: %s %s" % (cmd, args) + + MOCKS = { + "git": GitMock, + "vi": LogMock, + } + + def Command(self, cmd, args="", prefix="", pipe=True): + return ScriptTest.MOCKS[cmd](self, cmd, args) + + def ReadLine(self): + self._rl_index += 1 + try: + return self._rl_recipe[self._rl_index] + except IndexError: + raise Exception("Calling readline too often") + + def setUp(self): + self._git_recipe = [] + self._git_index = -1 + self._rl_recipe = [] + self._rl_index = -1 + self._tmp_files = [] + + def tearDown(self): + Command("rm", "-rf %s*" % TEST_CONFIG[PERSISTFILE_BASENAME]) + + # Clean up temps. Doesn't work automatically. + for name in self._tmp_files: + if os.path.exists(name): + os.remove(name) + + if self._git_index < len(self._git_recipe) -1: + raise Exception("Called git too seldom: %d vs. %d" % + (self._git_index, len(self._git_recipe))) + if self._rl_index < len(self._rl_recipe) -1: + raise Exception("Too little input: %d vs. %d" % + (self._rl_index, len(self._rl_recipe))) + + def testPersistRestore(self): + self.MakeStep().Persist("test1", "") + self.assertEquals("", self.MakeStep().Restore("test1")) + self.MakeStep().Persist("test2", "AB123") + self.assertEquals("AB123", self.MakeStep().Restore("test2")) + + def testGitOrig(self): + self.assertTrue(Command("git", "--version").startswith("git version")) + + def testGitMock(self): + self._git_recipe = [["--version", "git version 1.2.3"], ["dummy", ""]] + self.assertEquals("git version 1.2.3", self.MakeStep().Git("--version")) + self.assertEquals("", self.MakeStep().Git("dummy")) + + def testCommonPrepareDefault(self): + self._git_recipe = [ + ["status -s -uno", ""], + ["status -s -b -uno", "## some_branch"], + ["svn fetch", ""], + ["branch", " branch1\n* %s" % TEST_CONFIG[TEMP_BRANCH]], + ["branch -D %s" % TEST_CONFIG[TEMP_BRANCH], ""], + ["checkout -b %s" % TEST_CONFIG[TEMP_BRANCH], ""], + ["branch", ""], + ] + self._rl_recipe = ["Y"] + self.MakeStep().CommonPrepare() + self.assertEquals("some_branch", self.MakeStep().Restore("current_branch")) + + def testCommonPrepareNoConfirm(self): + self._git_recipe = [ + ["status -s -uno", ""], + ["status -s -b -uno", "## some_branch"], + ["svn fetch", ""], + ["branch", " branch1\n* %s" % TEST_CONFIG[TEMP_BRANCH]], + ] + self._rl_recipe = ["n"] + self.assertRaises(Exception, self.MakeStep().CommonPrepare) + self.assertEquals("some_branch", self.MakeStep().Restore("current_branch")) + + def testCommonPrepareDeleteBranchFailure(self): + self._git_recipe = [ + ["status -s -uno", ""], + ["status -s -b -uno", "## some_branch"], + ["svn fetch", ""], + ["branch", " branch1\n* %s" % TEST_CONFIG[TEMP_BRANCH]], + ["branch -D %s" % TEST_CONFIG[TEMP_BRANCH], None], + ] + self._rl_recipe = ["Y"] + self.assertRaises(Exception, self.MakeStep().CommonPrepare) + self.assertEquals("some_branch", self.MakeStep().Restore("current_branch")) + + def testInitialEnvironmentChecks(self): + TEST_CONFIG[DOT_GIT_LOCATION] = self.MakeEmptyTempFile() + os.environ["EDITOR"] = "vi" + self.MakeStep().InitialEnvironmentChecks() + + def testReadAndPersistVersion(self): + TEST_CONFIG[VERSION_FILE] = self.MakeTempVersionFile() + step = self.MakeStep() + step.ReadAndPersistVersion() + self.assertEquals("3", self.MakeStep().Restore("major")) + self.assertEquals("22", self.MakeStep().Restore("minor")) + self.assertEquals("5", self.MakeStep().Restore("build")) + self.assertEquals("0", self.MakeStep().Restore("patch")) + self.assertEquals("3", step._state["major"]) + self.assertEquals("22", step._state["minor"]) + self.assertEquals("5", step._state["build"]) + self.assertEquals("0", step._state["patch"]) + + def testRegex(self): + self.assertEqual("(issue 321)", + re.sub(r"BUG=v8:(.*)$", r"(issue \1)", "BUG=v8:321")) + self.assertEqual("(Chromium issue 321)", + re.sub(r"BUG=(.*)$", r"(Chromium issue \1)", "BUG=321")) + + cl = " too little\n\ttab\ttab\n too much\n trailing " + cl = MSub(r"\t", r" ", cl) + cl = MSub(r"^ {1,7}([^ ])", r" \1", cl) + cl = MSub(r"^ {9,80}([^ ])", r" \1", cl) + cl = MSub(r" +$", r"", cl) + self.assertEqual(" too little\n" + " tab tab\n" + " too much\n" + " trailing", cl) + + self.assertEqual("//\n#define BUILD_NUMBER 3\n", + MSub(r"(?<=#define BUILD_NUMBER)(?P\s+)\d*$", + r"\g3", + "//\n#define BUILD_NUMBER 321\n")) + + def testPrepareChangeLog(self): + TEST_CONFIG[VERSION_FILE] = self.MakeTempVersionFile() + TEST_CONFIG[CHANGELOG_ENTRY_FILE] = self.MakeEmptyTempFile() + + self._git_recipe = [ + ["log 1234..HEAD --format=%H", "rev1\nrev2"], + ["log -1 rev1 --format=\"%w(80,8,8)%s\"", " Title text 1"], + ["log -1 rev1 --format=\"%B\"", "Title\n\nBUG=\n"], + ["log -1 rev1 --format=\"%w(80,8,8)(%an)\"", + " author1@chromium.org"], + ["log -1 rev2 --format=\"%w(80,8,8)%s\"", " Title text 2"], + ["log -1 rev2 --format=\"%B\"", "Title\n\nBUG=321\n"], + ["log -1 rev2 --format=\"%w(80,8,8)(%an)\"", + " author2@chromium.org"], + ] + + self.MakeStep().Persist("last_push", "1234") + self.MakeStep(PrepareChangeLog).Run() + + cl = FileToText(TEST_CONFIG[CHANGELOG_ENTRY_FILE]) + self.assertTrue(re.search(r"\d+\-\d+\-\d+: Version 3\.22\.5", cl)) + self.assertTrue(re.search(r" Title text 1", cl)) + self.assertTrue(re.search(r" Title text 2", cl)) + self.assertTrue(re.search(r" author1@chromium.org", cl)) + self.assertTrue(re.search(r" author2@chromium.org", cl)) + self.assertTrue(re.search(r" \(Chromium issue 321\)", cl)) + self.assertFalse(re.search(r"BUG=", cl)) + self.assertEquals("3", self.MakeStep().Restore("major")) + self.assertEquals("22", self.MakeStep().Restore("minor")) + self.assertEquals("5", self.MakeStep().Restore("build")) + self.assertEquals("0", self.MakeStep().Restore("patch")) + + def testEditChangeLog(self): + TEST_CONFIG[CHANGELOG_ENTRY_FILE] = self.MakeEmptyTempFile() + TEST_CONFIG[CHANGELOG_FILE] = self.MakeEmptyTempFile() + TextToFile(" Original CL", TEST_CONFIG[CHANGELOG_FILE]) + TextToFile(" New \n\tLines \n", TEST_CONFIG[CHANGELOG_ENTRY_FILE]) + os.environ["EDITOR"] = "vi" + + self._rl_recipe = [ + "", # Open editor. + ] + + self.MakeStep(EditChangeLog).Run() + + self.assertEquals(" New\n Lines\n\n\n Original CL", + FileToText(TEST_CONFIG[CHANGELOG_FILE])) + + def testIncrementVersion(self): + TEST_CONFIG[VERSION_FILE] = self.MakeTempVersionFile() + self.MakeStep().Persist("build", "5") + + self._rl_recipe = [ + "Y", # Increment build number. + ] + + self.MakeStep(IncrementVersion).Run() + + self.assertEquals("3", self.MakeStep().Restore("new_major")) + self.assertEquals("22", self.MakeStep().Restore("new_minor")) + self.assertEquals("6", self.MakeStep().Restore("new_build")) + self.assertEquals("0", self.MakeStep().Restore("new_patch")) + + def testSquashCommits(self): + TEST_CONFIG[CHANGELOG_ENTRY_FILE] = self.MakeEmptyTempFile() + with open(TEST_CONFIG[CHANGELOG_ENTRY_FILE], "w") as f: + f.write("1999-11-11: Version 3.22.5\n") + f.write("\n") + f.write(" Log text 1.\n") + f.write(" Chromium issue 12345\n") + f.write("\n") + f.write(" Performance and stability improvements on all " + "platforms.\n") + + self._git_recipe = [ + ["diff svn/trunk hash1", "patch content"], + ] + + self.MakeStep().Persist("prepare_commit_hash", "hash1") + self.MakeStep().Persist("date", "1999-11-11") + + self.MakeStep(SquashCommits).Run() + + msg = FileToText(TEST_CONFIG[COMMITMSG_FILE]) + self.assertTrue(re.search(r"Version 3\.22\.5", msg)) + self.assertTrue(re.search(r"Performance and stability", msg)) + self.assertTrue(re.search(r"Log text 1\. Chromium issue 12345", msg)) + self.assertFalse(re.search(r"\d+\-\d+\-\d+", msg)) + + patch = FileToText(TEST_CONFIG[ PATCH_FILE]) + self.assertTrue(re.search(r"patch content", patch)) + + def testPushToTrunk(self): + TEST_CONFIG[DOT_GIT_LOCATION] = self.MakeEmptyTempFile() + TEST_CONFIG[VERSION_FILE] = self.MakeTempVersionFile() + TEST_CONFIG[CHANGELOG_ENTRY_FILE] = self.MakeEmptyTempFile() + TEST_CONFIG[CHANGELOG_FILE] = self.MakeEmptyTempFile() + if not os.path.exists(TEST_CONFIG[CHROMIUM]): + os.makedirs(TEST_CONFIG[CHROMIUM]) + TextToFile("1999-04-05: Version 3.22.4", TEST_CONFIG[CHANGELOG_FILE]) + TextToFile("Some line\n \"v8_revision\": \"123444\",\n some line", + TEST_CONFIG[DEPS_FILE]) + os.environ["EDITOR"] = "vi" + + def CheckPreparePush(): + cl = FileToText(TEST_CONFIG[CHANGELOG_FILE]) + self.assertTrue(re.search(r"Version 3.22.5", cl)) + self.assertTrue(re.search(r" Log text 1", cl)) + self.assertTrue(re.search(r" \(issue 321\)", cl)) + version = FileToText(TEST_CONFIG[VERSION_FILE]) + self.assertTrue(re.search(r"#define BUILD_NUMBER\s+6", version)) + + def CheckSVNCommit(): + commit = FileToText(TEST_CONFIG[COMMITMSG_FILE]) + self.assertTrue(re.search(r"Version 3.22.5", commit)) + self.assertTrue(re.search(r"Log text 1. \(issue 321\)", commit)) + version = FileToText(TEST_CONFIG[VERSION_FILE]) + self.assertTrue(re.search(r"#define MINOR_VERSION\s+22", version)) + self.assertTrue(re.search(r"#define BUILD_NUMBER\s+5", version)) + self.assertFalse(re.search(r"#define BUILD_NUMBER\s+6", version)) + self.assertTrue(re.search(r"#define PATCH_LEVEL\s+0", version)) + self.assertTrue(re.search(r"#define IS_CANDIDATE_VERSION\s+0", version)) + + self._git_recipe = [ + ["status -s -uno", ""], + ["status -s -b -uno", "## some_branch\n"], + ["svn fetch", ""], + ["branch", " branch1\n* branch2\n"], + ["checkout -b %s" % TEST_CONFIG[TEMP_BRANCH], ""], + ["branch", " branch1\n* branch2\n"], + ["branch", " branch1\n* branch2\n"], + ["checkout -b %s svn/bleeding_edge" % TEST_CONFIG[BRANCHNAME], ""], + ["log -1 --format=%H ChangeLog", "1234\n"], + ["log -1 1234", "Last push ouput\n"], + ["log 1234..HEAD --format=%H", "rev1\n"], + ["log -1 rev1 --format=\"%w(80,8,8)%s\"", " Log text 1.\n"], + ["log -1 rev1 --format=\"%B\"", "Text\nBUG=v8:321\nText\n"], + ["log -1 rev1 --format=\"%w(80,8,8)(%an)\"", + " author1@chromium.org\n"], + [("commit -a -m \"Prepare push to trunk. " + "Now working on version 3.22.6.\""), + " 2 files changed\n", + CheckPreparePush], + ["cl upload -r \"reviewer@chromium.org\" --send-mail", "done\n"], + ["cl dcommit", "Closing issue\n"], + ["svn fetch", "fetch result\n"], + ["checkout svn/bleeding_edge", ""], + [("log -1 --format=%H --grep=\"Prepare push to trunk. " + "Now working on version 3.22.6.\""), + "hash1\n"], + ["diff svn/trunk hash1", "patch content\n"], + ["checkout -b %s svn/trunk" % TEST_CONFIG[TRUNKBRANCH], ""], + ["apply --index --reject \"%s\"" % TEST_CONFIG[PATCH_FILE], ""], + ["add \"%s\"" % TEST_CONFIG[VERSION_FILE], ""], + ["commit -F \"%s\"" % TEST_CONFIG[COMMITMSG_FILE], "", CheckSVNCommit], + ["svn dcommit 2>&1", "Some output\nCommitted r123456\nSome output\n"], + ["svn tag 3.22.5 -m \"Tagging version 3.22.5\"", ""], + ["status -s -uno", ""], + ["checkout master", ""], + ["pull", ""], + ["checkout -b v8-roll-123456", ""], + [("commit -am \"Update V8 to version 3.22.5.\n\n" + "TBR=reviewer@chromium.org\""), + ""], + ["cl upload --send-mail", ""], + ["checkout -f some_branch", ""], + ["branch -D %s" % TEST_CONFIG[TEMP_BRANCH], ""], + ["branch -D %s" % TEST_CONFIG[BRANCHNAME], ""], + ["branch -D %s" % TEST_CONFIG[TRUNKBRANCH], ""], + ] + self._rl_recipe = [ + "Y", # Confirm last push. + "", # Open editor. + "Y", # Increment build number. + "reviewer@chromium.org", # V8 reviewer. + "LGTX", # Enter LGTM for V8 CL (wrong). + "LGTM", # Enter LGTM for V8 CL. + "Y", # Sanity check. + "reviewer@chromium.org", # Chromium reviewer. + ] + + class Options( object ): + pass + + options = Options() + options.s = 0 + options.l = None + options.c = TEST_CONFIG[CHROMIUM] + RunScript(TEST_CONFIG, options, self) + + deps = FileToText(TEST_CONFIG[DEPS_FILE]) + self.assertTrue(re.search("\"v8_revision\": \"123456\"", deps)) + + cl = FileToText(TEST_CONFIG[CHANGELOG_FILE]) + self.assertTrue(re.search(r"\d\d\d\d\-\d+\-\d+: Version 3\.22\.5", cl)) + self.assertTrue(re.search(r" Log text 1", cl)) + self.assertTrue(re.search(r" \(issue 321\)", cl)) + self.assertTrue(re.search(r"1999\-04\-05: Version 3\.22\.4", cl)) + + # Note: The version file is on build number 5 again in the end of this test + # since the git command that merges to the bleeding edge branch is mocked + # out.