diff options
Diffstat (limited to 'chromium/v8/tools/push-to-trunk/common_includes.py')
-rw-r--r-- | chromium/v8/tools/push-to-trunk/common_includes.py | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/chromium/v8/tools/push-to-trunk/common_includes.py b/chromium/v8/tools/push-to-trunk/common_includes.py new file mode 100644 index 00000000000..196593758e1 --- /dev/null +++ b/chromium/v8/tools/push-to-trunk/common_includes.py @@ -0,0 +1,486 @@ +#!/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 +import textwrap +import time +import urllib2 + +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) + + +def Fill80(line): + # Replace tabs and remove surrounding space. + line = re.sub(r"\t", r" ", line.strip()) + + # Format with 8 characters indentation and line width 80. + return textwrap.fill(line, width=80, initial_indent=" ", + subsequent_indent=" ") + + +def GetLastChangeLogEntries(change_log_file): + result = [] + for line in LinesInFile(change_log_file): + if re.search(r"^\d{4}-\d{2}-\d{2}:", line) and result: break + result.append(line) + return "".join(result) + + +def MakeComment(text): + return MSub(r"^( ?)", "#", text) + + +def StripComments(text): + # Use split not splitlines to keep terminal newlines. + return "\n".join(filter(lambda x: not x.startswith("#"), text.split("\n"))) + + +def MakeChangeLogBody(commit_messages, auto_format=False): + result = "" + added_titles = set() + for (title, body, author) in commit_messages: + # TODO(machenbach): Better check for reverts. A revert should remove the + # original CL from the actual log entry. + title = title.strip() + if auto_format: + # Only add commits that set the LOG flag correctly. + log_exp = r"^[ \t]*LOG[ \t]*=[ \t]*(?:Y(?:ES)?)|TRUE" + if not re.search(log_exp, body, flags=re.I | re.M): + continue + # Never include reverts. + if title.startswith("Revert "): + continue + # Don't include duplicates. + if title in added_titles: + continue + + # Add and format the commit's title and bug reference. Move dot to the end. + added_titles.add(title) + raw_title = re.sub(r"(\.|\?|!)$", "", title) + bug_reference = MakeChangeLogBugReference(body) + space = " " if bug_reference else "" + result += "%s\n" % Fill80("%s%s%s." % (raw_title, space, bug_reference)) + + # Append the commit's author for reference if not in auto-format mode. + if not auto_format: + result += "%s\n" % Fill80("(%s)" % author.strip()) + + result += "\n" + return result + + +def MakeChangeLogBugReference(body): + """Grep for "BUG=xxxx" lines in the commit message and convert them to + "(issue xxxx)". + """ + crbugs = [] + v8bugs = [] + + def AddIssues(text): + ref = re.match(r"^BUG[ \t]*=[ \t]*(.+)$", text.strip()) + if not ref: + return + for bug in ref.group(1).split(","): + bug = bug.strip() + match = re.match(r"^v8:(\d+)$", bug) + if match: v8bugs.append(int(match.group(1))) + else: + match = re.match(r"^(?:chromium:)?(\d+)$", bug) + if match: crbugs.append(int(match.group(1))) + + # Add issues to crbugs and v8bugs. + map(AddIssues, body.splitlines()) + + # Filter duplicates, sort, stringify. + crbugs = map(str, sorted(set(crbugs))) + v8bugs = map(str, sorted(set(v8bugs))) + + bug_groups = [] + def FormatIssues(prefix, bugs): + if len(bugs) > 0: + plural = "s" if len(bugs) > 1 else "" + bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs))) + + FormatIssues("", v8bugs) + FormatIssues("Chromium ", crbugs) + + if len(bug_groups) > 0: + return "(%s)" % ", ".join(bug_groups) + else: + return "" + + +# 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): + # TODO(machenbach): Use timeout. + 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() + + def ReadURL(self, url): + # pylint: disable=E1121 + url_fh = urllib2.urlopen(url, None, 60) + try: + return url_fh.read() + finally: + url_fh.close() + + def Sleep(seconds): + time.sleep(seconds) + +DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler() + + +class Step(object): + def __init__(self, text, requires, number, config, state, options, handler): + self._text = text + self._requires = requires + self._number = number + self._config = config + self._state = state + self._options = options + self._side_effect_handler = handler + assert self._number >= 0 + assert self._config is not None + assert self._state is not None + assert self._side_effect_handler is not None + + def Config(self, key): + return self._config[key] + + def Run(self): + 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 Retry(self, cb, retry_on=None, wait_plan=None): + """ Retry a function. + Params: + cb: The function to retry. + retry_on: A callback that takes the result of the function and returns + True if the function should be retried. A function throwing an + exception is always retried. + wait_plan: A list of waiting delays between retries in seconds. The + maximum number of retries is len(wait_plan). + """ + retry_on = retry_on or (lambda x: False) + wait_plan = list(wait_plan or []) + wait_plan.reverse() + while True: + got_exception = False + try: + result = cb() + except Exception: + got_exception = True + if got_exception or retry_on(result): + if not wait_plan: + raise Exception("Retried too often. Giving up.") + wait_time = wait_plan.pop() + print "Waiting for %f seconds." % wait_time + self._side_effect_handler.Sleep(wait_time) + print "Retrying..." + else: + return result + + def ReadLine(self, default=None): + # Don't prompt in forced mode. + if self._options and self._options.f and default is not None: + print "%s (forced)" % default + return default + else: + return self._side_effect_handler.ReadLine() + + def Git(self, args="", prefix="", pipe=True, retry_on=None): + cmd = lambda: self._side_effect_handler.Command("git", args, prefix, pipe) + return self.Retry(cmd, retry_on, [5, 30]) + + def Editor(self, args): + return self._side_effect_handler.Command(os.environ["EDITOR"], args, + pipe=False) + + def ReadURL(self, url, retry_on=None, wait_plan=None): + wait_plan = wait_plan or [3, 60, 600] + cmd = lambda: self._side_effect_handler.ReadURL(url) + return self.Retry(cmd, retry_on, wait_plan) + + def Die(self, msg=""): + if msg != "": + print "Error: %s" % msg + print "Exiting" + raise Exception(msg) + + def DieInForcedMode(self, msg=""): + if self._options and self._options.f: + msg = msg or "Not implemented in forced mode." + self.Die(msg) + + def Confirm(self, msg): + print "%s [Y/n] " % msg, + answer = self.ReadLine(default="Y") + 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.") + + # TODO(machenbach): Don't use EDITOR in forced mode as soon as script is + # well tested. + # 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.") + + def PrepareBranch(self): + # Get ahold of a safe temporary branch and check it out. + self.RestoreIfUnset("current_branch") + if self._state["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<Return>\" 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 "> ", + # TODO(machenbach): Add default="LGTM" to avoid prompt when script is + # well tested and when prepare push cl has TBR flag. + 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<Return>\", " + "or resolve the conflicts, stage *all* touched files with " + "'git add', and type \"RESOLVED<Return>\"") + self.DieInForcedMode() + 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): + MESSAGE = "Upload for code review." + + def RunStep(self): + if self._options.r: + print "Using account %s for review." % self._options.r + reviewer = self._options.r + else: + print "Please enter the email address of a V8 reviewer for your patch: ", + self.DieInForcedMode("A reviewer must be specified in forced mode.") + reviewer = self.ReadLine() + force_flag = " -f" if self._options.f else "" + args = "cl upload -r \"%s\" --send-mail%s" % (reviewer, force_flag) + # TODO(machenbach): Check output in forced mode. Verify that all required + # base files were uploaded, if not retry. + if self.Git(args, pipe=False) is None: + self.Die("'git cl upload' failed, please try again.") + + +def MakeStep(step_class=Step, number=0, state=None, config=None, + options=None, side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): + # Allow to pass in empty dictionaries. + state = state if state is not None else {} + config = config if config is not None else {} + + try: + message = step_class.MESSAGE + except AttributeError: + message = step_class.__name__ + try: + requires = step_class.REQUIRES + except AttributeError: + requires = None + + return step_class(message, requires, number=number, config=config, + state=state, options=options, + handler=side_effect_handler) + + +def RunScript(step_classes, + config, + options, + side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): + state = {} + steps = [] + for (number, step_class) in enumerate(step_classes): + steps.append(MakeStep(step_class, number, state, config, + options, side_effect_handler)) + + for step in steps[options.s:]: + step.Run() |