machenbach@chromium.org | 935a779 | 2013-11-12 09:05:18 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # Copyright 2013 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 | |
| 29 | import os |
| 30 | import re |
| 31 | import subprocess |
| 32 | import sys |
| 33 | |
| 34 | PERSISTFILE_BASENAME = "PERSISTFILE_BASENAME" |
| 35 | TEMP_BRANCH = "TEMP_BRANCH" |
| 36 | BRANCHNAME = "BRANCHNAME" |
| 37 | DOT_GIT_LOCATION = "DOT_GIT_LOCATION" |
| 38 | VERSION_FILE = "VERSION_FILE" |
| 39 | CHANGELOG_FILE = "CHANGELOG_FILE" |
| 40 | CHANGELOG_ENTRY_FILE = "CHANGELOG_ENTRY_FILE" |
| 41 | COMMITMSG_FILE = "COMMITMSG_FILE" |
| 42 | PATCH_FILE = "PATCH_FILE" |
| 43 | |
| 44 | |
| 45 | def TextToFile(text, file_name): |
| 46 | with open(file_name, "w") as f: |
| 47 | f.write(text) |
| 48 | |
| 49 | |
| 50 | def AppendToFile(text, file_name): |
| 51 | with open(file_name, "a") as f: |
| 52 | f.write(text) |
| 53 | |
| 54 | |
| 55 | def LinesInFile(file_name): |
| 56 | with open(file_name) as f: |
| 57 | for line in f: |
| 58 | yield line |
| 59 | |
| 60 | |
| 61 | def FileToText(file_name): |
| 62 | with open(file_name) as f: |
| 63 | return f.read() |
| 64 | |
| 65 | |
| 66 | def MSub(rexp, replacement, text): |
| 67 | return re.sub(rexp, replacement, text, flags=re.MULTILINE) |
| 68 | |
| 69 | |
machenbach@chromium.org | 0cc0950 | 2013-11-13 12:20:55 +0000 | [diff] [blame] | 70 | def GetLastChangeLogEntries(change_log_file): |
| 71 | result = [] |
| 72 | for line in LinesInFile(change_log_file): |
| 73 | if re.search(r"^\d{4}-\d{2}-\d{2}:", line) and result: break |
| 74 | result.append(line) |
| 75 | return "".join(result) |
| 76 | |
| 77 | |
machenbach@chromium.org | af9cfcb | 2013-11-19 11:05:18 +0000 | [diff] [blame^] | 78 | def MakeChangeLogBody(commit_generator): |
| 79 | result = "" |
| 80 | for (title, body, author) in commit_generator(): |
| 81 | # Add the commit's title line. |
| 82 | result += "%s\n" % title.rstrip() |
| 83 | |
| 84 | # Grep for "BUG=xxxx" lines in the commit message and convert them to |
| 85 | # "(issue xxxx)". |
| 86 | out = body.splitlines() |
| 87 | out = filter(lambda x: re.search(r"^BUG=", x), out) |
| 88 | out = filter(lambda x: not re.search(r"BUG=$", x), out) |
| 89 | out = filter(lambda x: not re.search(r"BUG=none$", x), out) |
| 90 | |
| 91 | # TODO(machenbach): Handle multiple entries (e.g. BUG=123, 234). |
| 92 | def FormatIssue(text): |
| 93 | text = re.sub(r"BUG=v8:(.*)$", r"(issue \1)", text) |
| 94 | text = re.sub(r"BUG=chromium:(.*)$", r"(Chromium issue \1)", text) |
| 95 | text = re.sub(r"BUG=(.*)$", r"(Chromium issue \1)", text) |
| 96 | return " %s\n" % text |
| 97 | |
| 98 | for line in map(FormatIssue, out): |
| 99 | result += line |
| 100 | |
| 101 | # Append the commit's author for reference. |
| 102 | result += "%s\n\n" % author.rstrip() |
| 103 | return result |
| 104 | |
| 105 | |
machenbach@chromium.org | 935a779 | 2013-11-12 09:05:18 +0000 | [diff] [blame] | 106 | # Some commands don't like the pipe, e.g. calling vi from within the script or |
| 107 | # from subscripts like git cl upload. |
| 108 | def Command(cmd, args="", prefix="", pipe=True): |
| 109 | cmd_line = "%s %s %s" % (prefix, cmd, args) |
| 110 | print "Command: %s" % cmd_line |
| 111 | try: |
| 112 | if pipe: |
| 113 | return subprocess.check_output(cmd_line, shell=True) |
| 114 | else: |
| 115 | return subprocess.check_call(cmd_line, shell=True) |
| 116 | except subprocess.CalledProcessError: |
| 117 | return None |
| 118 | |
| 119 | |
| 120 | # Wrapper for side effects. |
| 121 | class SideEffectHandler(object): |
| 122 | def Command(self, cmd, args="", prefix="", pipe=True): |
| 123 | return Command(cmd, args, prefix, pipe) |
| 124 | |
| 125 | def ReadLine(self): |
| 126 | return sys.stdin.readline().strip() |
| 127 | |
| 128 | DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler() |
| 129 | |
| 130 | |
| 131 | class Step(object): |
| 132 | def __init__(self, text="", requires=None): |
| 133 | self._text = text |
| 134 | self._number = -1 |
| 135 | self._requires = requires |
| 136 | self._side_effect_handler = DEFAULT_SIDE_EFFECT_HANDLER |
| 137 | |
| 138 | def SetNumber(self, number): |
| 139 | self._number = number |
| 140 | |
| 141 | def SetConfig(self, config): |
| 142 | self._config = config |
| 143 | |
| 144 | def SetState(self, state): |
| 145 | self._state = state |
| 146 | |
| 147 | def SetOptions(self, options): |
| 148 | self._options = options |
| 149 | |
| 150 | def SetSideEffectHandler(self, handler): |
| 151 | self._side_effect_handler = handler |
| 152 | |
| 153 | def Config(self, key): |
| 154 | return self._config[key] |
| 155 | |
| 156 | def Run(self): |
| 157 | assert self._number >= 0 |
| 158 | assert self._config is not None |
| 159 | assert self._state is not None |
| 160 | assert self._side_effect_handler is not None |
| 161 | if self._requires: |
| 162 | self.RestoreIfUnset(self._requires) |
| 163 | if not self._state[self._requires]: |
| 164 | return |
| 165 | print ">>> Step %d: %s" % (self._number, self._text) |
| 166 | self.RunStep() |
| 167 | |
| 168 | def RunStep(self): |
| 169 | raise NotImplementedError |
| 170 | |
| 171 | def ReadLine(self): |
| 172 | return self._side_effect_handler.ReadLine() |
| 173 | |
| 174 | def Git(self, args="", prefix="", pipe=True): |
| 175 | return self._side_effect_handler.Command("git", args, prefix, pipe) |
| 176 | |
| 177 | def Editor(self, args): |
| 178 | return self._side_effect_handler.Command(os.environ["EDITOR"], args, |
| 179 | pipe=False) |
| 180 | |
| 181 | def Die(self, msg=""): |
| 182 | if msg != "": |
| 183 | print "Error: %s" % msg |
| 184 | print "Exiting" |
| 185 | raise Exception(msg) |
| 186 | |
| 187 | def Confirm(self, msg): |
| 188 | print "%s [Y/n] " % msg, |
| 189 | answer = self.ReadLine() |
| 190 | return answer == "" or answer == "Y" or answer == "y" |
| 191 | |
| 192 | def DeleteBranch(self, name): |
| 193 | git_result = self.Git("branch").strip() |
| 194 | for line in git_result.splitlines(): |
| 195 | if re.match(r".*\s+%s$" % name, line): |
| 196 | msg = "Branch %s exists, do you want to delete it?" % name |
| 197 | if self.Confirm(msg): |
| 198 | if self.Git("branch -D %s" % name) is None: |
| 199 | self.Die("Deleting branch '%s' failed." % name) |
| 200 | print "Branch %s deleted." % name |
| 201 | else: |
| 202 | msg = "Can't continue. Please delete branch %s and try again." % name |
| 203 | self.Die(msg) |
| 204 | |
| 205 | def Persist(self, var, value): |
| 206 | value = value or "__EMPTY__" |
| 207 | TextToFile(value, "%s-%s" % (self._config[PERSISTFILE_BASENAME], var)) |
| 208 | |
| 209 | def Restore(self, var): |
| 210 | value = FileToText("%s-%s" % (self._config[PERSISTFILE_BASENAME], var)) |
| 211 | value = value or self.Die("Variable '%s' could not be restored." % var) |
| 212 | return "" if value == "__EMPTY__" else value |
| 213 | |
| 214 | def RestoreIfUnset(self, var_name): |
| 215 | if self._state.get(var_name) is None: |
| 216 | self._state[var_name] = self.Restore(var_name) |
| 217 | |
| 218 | def InitialEnvironmentChecks(self): |
| 219 | # Cancel if this is not a git checkout. |
| 220 | if not os.path.exists(self._config[DOT_GIT_LOCATION]): |
| 221 | self.Die("This is not a git checkout, this script won't work for you.") |
| 222 | |
| 223 | # Cancel if EDITOR is unset or not executable. |
| 224 | if (not os.environ.get("EDITOR") or |
| 225 | Command("which", os.environ["EDITOR"]) is None): |
| 226 | self.Die("Please set your EDITOR environment variable, you'll need it.") |
| 227 | |
| 228 | def CommonPrepare(self): |
| 229 | # Check for a clean workdir. |
| 230 | if self.Git("status -s -uno").strip() != "": |
| 231 | self.Die("Workspace is not clean. Please commit or undo your changes.") |
| 232 | |
| 233 | # Persist current branch. |
| 234 | current_branch = "" |
| 235 | git_result = self.Git("status -s -b -uno").strip() |
| 236 | for line in git_result.splitlines(): |
| 237 | match = re.match(r"^## (.+)", line) |
| 238 | if match: |
| 239 | current_branch = match.group(1) |
| 240 | break |
| 241 | self.Persist("current_branch", current_branch) |
| 242 | |
| 243 | # Fetch unfetched revisions. |
| 244 | if self.Git("svn fetch") is None: |
| 245 | self.Die("'git svn fetch' failed.") |
| 246 | |
machenbach@chromium.org | af9cfcb | 2013-11-19 11:05:18 +0000 | [diff] [blame^] | 247 | def PrepareBranch(self): |
machenbach@chromium.org | 935a779 | 2013-11-12 09:05:18 +0000 | [diff] [blame] | 248 | # Get ahold of a safe temporary branch and check it out. |
machenbach@chromium.org | af9cfcb | 2013-11-19 11:05:18 +0000 | [diff] [blame^] | 249 | self.RestoreIfUnset("current_branch") |
| 250 | if self._state["current_branch"] != self._config[TEMP_BRANCH]: |
machenbach@chromium.org | 935a779 | 2013-11-12 09:05:18 +0000 | [diff] [blame] | 251 | self.DeleteBranch(self._config[TEMP_BRANCH]) |
| 252 | self.Git("checkout -b %s" % self._config[TEMP_BRANCH]) |
| 253 | |
| 254 | # Delete the branch that will be created later if it exists already. |
| 255 | self.DeleteBranch(self._config[BRANCHNAME]) |
| 256 | |
| 257 | def CommonCleanup(self): |
| 258 | self.RestoreIfUnset("current_branch") |
| 259 | self.Git("checkout -f %s" % self._state["current_branch"]) |
| 260 | if self._config[TEMP_BRANCH] != self._state["current_branch"]: |
| 261 | self.Git("branch -D %s" % self._config[TEMP_BRANCH]) |
| 262 | if self._config[BRANCHNAME] != self._state["current_branch"]: |
| 263 | self.Git("branch -D %s" % self._config[BRANCHNAME]) |
| 264 | |
| 265 | # Clean up all temporary files. |
| 266 | Command("rm", "-f %s*" % self._config[PERSISTFILE_BASENAME]) |
| 267 | |
| 268 | def ReadAndPersistVersion(self, prefix=""): |
| 269 | def ReadAndPersist(var_name, def_name): |
| 270 | match = re.match(r"^#define %s\s+(\d*)" % def_name, line) |
| 271 | if match: |
| 272 | value = match.group(1) |
| 273 | self.Persist("%s%s" % (prefix, var_name), value) |
| 274 | self._state["%s%s" % (prefix, var_name)] = value |
| 275 | for line in LinesInFile(self._config[VERSION_FILE]): |
| 276 | for (var_name, def_name) in [("major", "MAJOR_VERSION"), |
| 277 | ("minor", "MINOR_VERSION"), |
| 278 | ("build", "BUILD_NUMBER"), |
| 279 | ("patch", "PATCH_LEVEL")]: |
| 280 | ReadAndPersist(var_name, def_name) |
| 281 | |
| 282 | def RestoreVersionIfUnset(self, prefix=""): |
| 283 | for v in ["major", "minor", "build", "patch"]: |
| 284 | self.RestoreIfUnset("%s%s" % (prefix, v)) |
| 285 | |
| 286 | def WaitForLGTM(self): |
| 287 | print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit " |
| 288 | "your change. (If you need to iterate on the patch or double check " |
| 289 | "that it's sane, do so in another shell, but remember to not " |
| 290 | "change the headline of the uploaded CL.") |
| 291 | answer = "" |
| 292 | while answer != "LGTM": |
| 293 | print "> ", |
| 294 | answer = self.ReadLine() |
| 295 | if answer != "LGTM": |
| 296 | print "That was not 'LGTM'." |
| 297 | |
| 298 | def WaitForResolvingConflicts(self, patch_file): |
| 299 | print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", " |
| 300 | "or resolve the conflicts, stage *all* touched files with " |
| 301 | "'git add', and type \"RESOLVED<Return>\"") |
| 302 | answer = "" |
| 303 | while answer != "RESOLVED": |
| 304 | if answer == "ABORT": |
| 305 | self.Die("Applying the patch failed.") |
| 306 | if answer != "": |
| 307 | print "That was not 'RESOLVED' or 'ABORT'." |
| 308 | print "> ", |
| 309 | answer = self.ReadLine() |
| 310 | |
| 311 | # Takes a file containing the patch to apply as first argument. |
| 312 | def ApplyPatch(self, patch_file, reverse_patch=""): |
| 313 | args = "apply --index --reject %s \"%s\"" % (reverse_patch, patch_file) |
| 314 | if self.Git(args) is None: |
| 315 | self.WaitForResolvingConflicts(patch_file) |
| 316 | |
| 317 | |
| 318 | class UploadStep(Step): |
| 319 | def __init__(self): |
| 320 | Step.__init__(self, "Upload for code review.") |
| 321 | |
| 322 | def RunStep(self): |
| 323 | print "Please enter the email address of a V8 reviewer for your patch: ", |
| 324 | reviewer = self.ReadLine() |
| 325 | args = "cl upload -r \"%s\" --send-mail" % reviewer |
| 326 | if self.Git(args,pipe=False) is None: |
| 327 | self.Die("'git cl upload' failed, please try again.") |
machenbach@chromium.org | af9cfcb | 2013-11-19 11:05:18 +0000 | [diff] [blame^] | 328 | |
| 329 | |
| 330 | def RunScript(step_classes, |
| 331 | config, |
| 332 | options, |
| 333 | side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): |
| 334 | state = {} |
| 335 | steps = [] |
| 336 | number = 0 |
| 337 | |
| 338 | for step_class in step_classes: |
| 339 | # TODO(machenbach): Factory methods. |
| 340 | step = step_class() |
| 341 | step.SetNumber(number) |
| 342 | step.SetConfig(config) |
| 343 | step.SetOptions(options) |
| 344 | step.SetState(state) |
| 345 | step.SetSideEffectHandler(side_effect_handler) |
| 346 | steps.append(step) |
| 347 | number += 1 |
| 348 | |
| 349 | for step in steps[options.s:]: |
| 350 | step.Run() |