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 optparse 30 import sys 31 import tempfile 32 import urllib2 33 34 from common_includes import * 35 36 TRUNKBRANCH = "TRUNKBRANCH" 37 CHROMIUM = "CHROMIUM" 38 DEPS_FILE = "DEPS_FILE" 39 40 CONFIG = { 41 BRANCHNAME: "prepare-push", 42 TRUNKBRANCH: "trunk-push", 43 PERSISTFILE_BASENAME: "/tmp/v8-push-to-trunk-tempfile", 44 TEMP_BRANCH: "prepare-push-temporary-branch-created-by-script", 45 DOT_GIT_LOCATION: ".git", 46 VERSION_FILE: "src/version.cc", 47 CHANGELOG_FILE: "ChangeLog", 48 CHANGELOG_ENTRY_FILE: "/tmp/v8-push-to-trunk-tempfile-changelog-entry", 49 PATCH_FILE: "/tmp/v8-push-to-trunk-tempfile-patch-file", 50 COMMITMSG_FILE: "/tmp/v8-push-to-trunk-tempfile-commitmsg", 51 DEPS_FILE: "DEPS", 52 } 53 54 55 class Preparation(Step): 56 MESSAGE = "Preparation." 57 58 def RunStep(self): 59 self.InitialEnvironmentChecks() 60 self.CommonPrepare() 61 self.PrepareBranch() 62 self.DeleteBranch(self.Config(TRUNKBRANCH)) 63 64 65 class FreshBranch(Step): 66 MESSAGE = "Create a fresh branch." 67 68 def RunStep(self): 69 args = "checkout -b %s svn/bleeding_edge" % self.Config(BRANCHNAME) 70 if self.Git(args) is None: 71 self.Die("Creating branch %s failed." % self.Config(BRANCHNAME)) 72 73 74 class DetectLastPush(Step): 75 MESSAGE = "Detect commit ID of last push to trunk." 76 77 def RunStep(self): 78 last_push = (self._options.l or 79 self.Git("log -1 --format=%H ChangeLog").strip()) 80 while True: 81 # Print assumed commit, circumventing git's pager. 82 print self.Git("log -1 %s" % last_push) 83 if self.Confirm("Is the commit printed above the last push to trunk?"): 84 break 85 args = "log -1 --format=%H %s^ ChangeLog" % last_push 86 last_push = self.Git(args).strip() 87 self.Persist("last_push", last_push) 88 self._state["last_push"] = last_push 89 90 91 class PrepareChangeLog(Step): 92 MESSAGE = "Prepare raw ChangeLog entry." 93 94 def Reload(self, body): 95 """Attempts to reload the commit message from rietveld in order to allow 96 late changes to the LOG flag. Note: This is brittle to future changes of 97 the web page name or structure. 98 """ 99 match = re.search(r"^Review URL: https://codereview\.chromium\.org/(\d+)$", 100 body, flags=re.M) 101 if match: 102 cl_url = "https://codereview.chromium.org/%s/description" % match.group(1) 103 try: 104 # Fetch from Rietveld but only retry once with one second delay since 105 # there might be many revisions. 106 body = self.ReadURL(cl_url, wait_plan=[1]) 107 except urllib2.URLError: 108 pass 109 return body 110 111 def RunStep(self): 112 self.RestoreIfUnset("last_push") 113 114 # These version numbers are used again later for the trunk commit. 115 self.ReadAndPersistVersion() 116 117 date = self.GetDate() 118 self.Persist("date", date) 119 output = "%s: Version %s.%s.%s\n\n" % (date, 120 self._state["major"], 121 self._state["minor"], 122 self._state["build"]) 123 TextToFile(output, self.Config(CHANGELOG_ENTRY_FILE)) 124 125 args = "log %s..HEAD --format=%%H" % self._state["last_push"] 126 commits = self.Git(args).strip() 127 128 # Cache raw commit messages. 129 commit_messages = [ 130 [ 131 self.Git("log -1 %s --format=\"%%s\"" % commit), 132 self.Reload(self.Git("log -1 %s --format=\"%%B\"" % commit)), 133 self.Git("log -1 %s --format=\"%%an\"" % commit), 134 ] for commit in commits.splitlines() 135 ] 136 137 # Auto-format commit messages. 138 body = MakeChangeLogBody(commit_messages, auto_format=True) 139 AppendToFile(body, self.Config(CHANGELOG_ENTRY_FILE)) 140 141 msg = (" Performance and stability improvements on all platforms." 142 "\n#\n# The change log above is auto-generated. Please review if " 143 "all relevant\n# commit messages from the list below are included." 144 "\n# All lines starting with # will be stripped.\n#\n") 145 AppendToFile(msg, self.Config(CHANGELOG_ENTRY_FILE)) 146 147 # Include unformatted commit messages as a reference in a comment. 148 comment_body = MakeComment(MakeChangeLogBody(commit_messages)) 149 AppendToFile(comment_body, self.Config(CHANGELOG_ENTRY_FILE)) 150 151 152 class EditChangeLog(Step): 153 MESSAGE = "Edit ChangeLog entry." 154 155 def RunStep(self): 156 print ("Please press <Return> to have your EDITOR open the ChangeLog " 157 "entry, then edit its contents to your liking. When you're done, " 158 "save the file and exit your EDITOR. ") 159 self.ReadLine(default="") 160 self.Editor(self.Config(CHANGELOG_ENTRY_FILE)) 161 handle, new_changelog = tempfile.mkstemp() 162 os.close(handle) 163 164 # Strip comments and reformat with correct indentation. 165 changelog_entry = FileToText(self.Config(CHANGELOG_ENTRY_FILE)).rstrip() 166 changelog_entry = StripComments(changelog_entry) 167 changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines())) 168 changelog_entry = changelog_entry.lstrip() 169 170 if changelog_entry == "": 171 self.Die("Empty ChangeLog entry.") 172 173 with open(new_changelog, "w") as f: 174 f.write(changelog_entry) 175 f.write("\n\n\n") # Explicitly insert two empty lines. 176 177 AppendToFile(FileToText(self.Config(CHANGELOG_FILE)), new_changelog) 178 TextToFile(FileToText(new_changelog), self.Config(CHANGELOG_FILE)) 179 os.remove(new_changelog) 180 181 182 class IncrementVersion(Step): 183 MESSAGE = "Increment version number." 184 185 def RunStep(self): 186 self.RestoreIfUnset("build") 187 new_build = str(int(self._state["build"]) + 1) 188 189 if self.Confirm(("Automatically increment BUILD_NUMBER? (Saying 'n' will " 190 "fire up your EDITOR on %s so you can make arbitrary " 191 "changes. When you're done, save the file and exit your " 192 "EDITOR.)" % self.Config(VERSION_FILE))): 193 text = FileToText(self.Config(VERSION_FILE)) 194 text = MSub(r"(?<=#define BUILD_NUMBER)(?P<space>\s+)\d*$", 195 r"\g<space>%s" % new_build, 196 text) 197 TextToFile(text, self.Config(VERSION_FILE)) 198 else: 199 self.Editor(self.Config(VERSION_FILE)) 200 201 self.ReadAndPersistVersion("new_") 202 203 204 class CommitLocal(Step): 205 MESSAGE = "Commit to local branch." 206 207 def RunStep(self): 208 self.RestoreVersionIfUnset("new_") 209 prep_commit_msg = ("Prepare push to trunk. " 210 "Now working on version %s.%s.%s." % (self._state["new_major"], 211 self._state["new_minor"], 212 self._state["new_build"])) 213 self.Persist("prep_commit_msg", prep_commit_msg) 214 215 # Include optional TBR only in the git command. The persisted commit 216 # message is used for finding the commit again later. 217 review = "\n\nTBR=%s" % self._options.r if not self.IsManual() else "" 218 if self.Git("commit -a -m \"%s%s\"" % (prep_commit_msg, review)) is None: 219 self.Die("'git commit -a' failed.") 220 221 222 class CommitRepository(Step): 223 MESSAGE = "Commit to the repository." 224 225 def RunStep(self): 226 self.WaitForLGTM() 227 # Re-read the ChangeLog entry (to pick up possible changes). 228 # FIXME(machenbach): This was hanging once with a broken pipe. 229 TextToFile(GetLastChangeLogEntries(self.Config(CHANGELOG_FILE)), 230 self.Config(CHANGELOG_ENTRY_FILE)) 231 232 if self.Git("cl dcommit -f", "PRESUBMIT_TREE_CHECK=\"skip\"") is None: 233 self.Die("'git cl dcommit' failed, please try again.") 234 235 236 class StragglerCommits(Step): 237 MESSAGE = ("Fetch straggler commits that sneaked in since this script was " 238 "started.") 239 240 def RunStep(self): 241 if self.Git("svn fetch") is None: 242 self.Die("'git svn fetch' failed.") 243 self.Git("checkout svn/bleeding_edge") 244 self.RestoreIfUnset("prep_commit_msg") 245 args = "log -1 --format=%%H --grep=\"%s\"" % self._state["prep_commit_msg"] 246 prepare_commit_hash = self.Git(args).strip() 247 self.Persist("prepare_commit_hash", prepare_commit_hash) 248 249 250 class SquashCommits(Step): 251 MESSAGE = "Squash commits into one." 252 253 def RunStep(self): 254 # Instead of relying on "git rebase -i", we'll just create a diff, because 255 # that's easier to automate. 256 self.RestoreIfUnset("prepare_commit_hash") 257 args = "diff svn/trunk %s" % self._state["prepare_commit_hash"] 258 TextToFile(self.Git(args), self.Config(PATCH_FILE)) 259 260 # Convert the ChangeLog entry to commit message format: 261 # - remove date 262 # - remove indentation 263 # - merge paragraphs into single long lines, keeping empty lines between 264 # them. 265 self.RestoreIfUnset("date") 266 changelog_entry = FileToText(self.Config(CHANGELOG_ENTRY_FILE)) 267 268 # TODO(machenbach): This could create a problem if the changelog contained 269 # any quotation marks. 270 text = Command("echo \"%s\" \ 271 | sed -e \"s/^%s: //\" \ 272 | sed -e 's/^ *//' \ 273 | awk '{ \ 274 if (need_space == 1) {\ 275 printf(\" \");\ 276 };\ 277 printf(\"%%s\", $0);\ 278 if ($0 ~ /^$/) {\ 279 printf(\"\\n\\n\");\ 280 need_space = 0;\ 281 } else {\ 282 need_space = 1;\ 283 }\ 284 }'" % (changelog_entry, self._state["date"])) 285 286 if not text: 287 self.Die("Commit message editing failed.") 288 TextToFile(text, self.Config(COMMITMSG_FILE)) 289 os.remove(self.Config(CHANGELOG_ENTRY_FILE)) 290 291 292 class NewBranch(Step): 293 MESSAGE = "Create a new branch from trunk." 294 295 def RunStep(self): 296 if self.Git("checkout -b %s svn/trunk" % self.Config(TRUNKBRANCH)) is None: 297 self.Die("Checking out a new branch '%s' failed." % 298 self.Config(TRUNKBRANCH)) 299 300 301 class ApplyChanges(Step): 302 MESSAGE = "Apply squashed changes." 303 304 def RunStep(self): 305 self.ApplyPatch(self.Config(PATCH_FILE)) 306 Command("rm", "-f %s*" % self.Config(PATCH_FILE)) 307 308 309 class SetVersion(Step): 310 MESSAGE = "Set correct version for trunk." 311 312 def RunStep(self): 313 self.RestoreVersionIfUnset() 314 output = "" 315 for line in FileToText(self.Config(VERSION_FILE)).splitlines(): 316 if line.startswith("#define MAJOR_VERSION"): 317 line = re.sub("\d+$", self._state["major"], line) 318 elif line.startswith("#define MINOR_VERSION"): 319 line = re.sub("\d+$", self._state["minor"], line) 320 elif line.startswith("#define BUILD_NUMBER"): 321 line = re.sub("\d+$", self._state["build"], line) 322 elif line.startswith("#define PATCH_LEVEL"): 323 line = re.sub("\d+$", "0", line) 324 elif line.startswith("#define IS_CANDIDATE_VERSION"): 325 line = re.sub("\d+$", "0", line) 326 output += "%s\n" % line 327 TextToFile(output, self.Config(VERSION_FILE)) 328 329 330 class CommitTrunk(Step): 331 MESSAGE = "Commit to local trunk branch." 332 333 def RunStep(self): 334 self.Git("add \"%s\"" % self.Config(VERSION_FILE)) 335 if self.Git("commit -F \"%s\"" % self.Config(COMMITMSG_FILE)) is None: 336 self.Die("'git commit' failed.") 337 Command("rm", "-f %s*" % self.Config(COMMITMSG_FILE)) 338 339 340 class SanityCheck(Step): 341 MESSAGE = "Sanity check." 342 343 def RunStep(self): 344 if not self.Confirm("Please check if your local checkout is sane: Inspect " 345 "%s, compile, run tests. Do you want to commit this new trunk " 346 "revision to the repository?" % self.Config(VERSION_FILE)): 347 self.Die("Execution canceled.") 348 349 350 class CommitSVN(Step): 351 MESSAGE = "Commit to SVN." 352 353 def RunStep(self): 354 result = self.Git("svn dcommit 2>&1") 355 if not result: 356 self.Die("'git svn dcommit' failed.") 357 result = filter(lambda x: re.search(r"^Committed r[0-9]+", x), 358 result.splitlines()) 359 if len(result) > 0: 360 trunk_revision = re.sub(r"^Committed r([0-9]+)", r"\1", result[0]) 361 362 # Sometimes grepping for the revision fails. No idea why. If you figure 363 # out why it is flaky, please do fix it properly. 364 if not trunk_revision: 365 print("Sorry, grepping for the SVN revision failed. Please look for it " 366 "in the last command's output above and provide it manually (just " 367 "the number, without the leading \"r\").") 368 self.DieNoManualMode("Can't prompt in forced mode.") 369 while not trunk_revision: 370 print "> ", 371 trunk_revision = self.ReadLine() 372 self.Persist("trunk_revision", trunk_revision) 373 374 375 class TagRevision(Step): 376 MESSAGE = "Tag the new revision." 377 378 def RunStep(self): 379 self.RestoreVersionIfUnset() 380 ver = "%s.%s.%s" % (self._state["major"], 381 self._state["minor"], 382 self._state["build"]) 383 if self.Git("svn tag %s -m \"Tagging version %s\"" % (ver, ver)) is None: 384 self.Die("'git svn tag' failed.") 385 386 387 class CheckChromium(Step): 388 MESSAGE = "Ask for chromium checkout." 389 390 def Run(self): 391 chrome_path = self._options.c 392 if not chrome_path: 393 self.DieNoManualMode("Please specify the path to a Chromium checkout in " 394 "forced mode.") 395 print ("Do you have a \"NewGit\" Chromium checkout and want " 396 "this script to automate creation of the roll CL? If yes, enter the " 397 "path to (and including) the \"src\" directory here, otherwise just " 398 "press <Return>: "), 399 chrome_path = self.ReadLine() 400 self.Persist("chrome_path", chrome_path) 401 402 403 class SwitchChromium(Step): 404 MESSAGE = "Switch to Chromium checkout." 405 REQUIRES = "chrome_path" 406 407 def RunStep(self): 408 v8_path = os.getcwd() 409 self.Persist("v8_path", v8_path) 410 os.chdir(self._state["chrome_path"]) 411 self.InitialEnvironmentChecks() 412 # Check for a clean workdir. 413 if self.Git("status -s -uno").strip() != "": 414 self.Die("Workspace is not clean. Please commit or undo your changes.") 415 # Assert that the DEPS file is there. 416 if not os.path.exists(self.Config(DEPS_FILE)): 417 self.Die("DEPS file not present.") 418 419 420 class UpdateChromiumCheckout(Step): 421 MESSAGE = "Update the checkout and create a new branch." 422 REQUIRES = "chrome_path" 423 424 def RunStep(self): 425 os.chdir(self._state["chrome_path"]) 426 if self.Git("checkout master") is None: 427 self.Die("'git checkout master' failed.") 428 if self.Git("pull") is None: 429 self.Die("'git pull' failed, please try again.") 430 431 self.RestoreIfUnset("trunk_revision") 432 args = "checkout -b v8-roll-%s" % self._state["trunk_revision"] 433 if self.Git(args) is None: 434 self.Die("Failed to checkout a new branch.") 435 436 437 class UploadCL(Step): 438 MESSAGE = "Create and upload CL." 439 REQUIRES = "chrome_path" 440 441 def RunStep(self): 442 os.chdir(self._state["chrome_path"]) 443 444 # Patch DEPS file. 445 self.RestoreIfUnset("trunk_revision") 446 deps = FileToText(self.Config(DEPS_FILE)) 447 deps = re.sub("(?<=\"v8_revision\": \")([0-9]+)(?=\")", 448 self._state["trunk_revision"], 449 deps) 450 TextToFile(deps, self.Config(DEPS_FILE)) 451 452 self.RestoreVersionIfUnset() 453 ver = "%s.%s.%s" % (self._state["major"], 454 self._state["minor"], 455 self._state["build"]) 456 if self._options and self._options.r: 457 print "Using account %s for review." % self._options.r 458 rev = self._options.r 459 else: 460 print "Please enter the email address of a reviewer for the roll CL: ", 461 self.DieNoManualMode("A reviewer must be specified in forced mode.") 462 rev = self.ReadLine() 463 args = "commit -am \"Update V8 to version %s.\n\nTBR=%s\"" % (ver, rev) 464 if self.Git(args) is None: 465 self.Die("'git commit' failed.") 466 force_flag = " -f" if not self.IsManual() else "" 467 if self.Git("cl upload --send-mail%s" % force_flag, pipe=False) is None: 468 self.Die("'git cl upload' failed, please try again.") 469 print "CL uploaded." 470 471 472 class SwitchV8(Step): 473 MESSAGE = "Returning to V8 checkout." 474 REQUIRES = "chrome_path" 475 476 def RunStep(self): 477 self.RestoreIfUnset("v8_path") 478 os.chdir(self._state["v8_path"]) 479 480 481 class CleanUp(Step): 482 MESSAGE = "Done!" 483 484 def RunStep(self): 485 self.RestoreVersionIfUnset() 486 ver = "%s.%s.%s" % (self._state["major"], 487 self._state["minor"], 488 self._state["build"]) 489 self.RestoreIfUnset("trunk_revision") 490 self.RestoreIfUnset("chrome_path") 491 492 if self._state["chrome_path"]: 493 print("Congratulations, you have successfully created the trunk " 494 "revision %s and rolled it into Chromium. Please don't forget to " 495 "update the v8rel spreadsheet:" % ver) 496 else: 497 print("Congratulations, you have successfully created the trunk " 498 "revision %s. Please don't forget to roll this new version into " 499 "Chromium, and to update the v8rel spreadsheet:" % ver) 500 print "%s\ttrunk\t%s" % (ver, self._state["trunk_revision"]) 501 502 self.CommonCleanup() 503 if self.Config(TRUNKBRANCH) != self._state["current_branch"]: 504 self.Git("branch -D %s" % self.Config(TRUNKBRANCH)) 505 506 507 def RunPushToTrunk(config, 508 options, 509 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): 510 step_classes = [ 511 Preparation, 512 FreshBranch, 513 DetectLastPush, 514 PrepareChangeLog, 515 EditChangeLog, 516 IncrementVersion, 517 CommitLocal, 518 UploadStep, 519 CommitRepository, 520 StragglerCommits, 521 SquashCommits, 522 NewBranch, 523 ApplyChanges, 524 SetVersion, 525 CommitTrunk, 526 SanityCheck, 527 CommitSVN, 528 TagRevision, 529 CheckChromium, 530 SwitchChromium, 531 UpdateChromiumCheckout, 532 UploadCL, 533 SwitchV8, 534 CleanUp, 535 ] 536 537 RunScript(step_classes, config, options, side_effect_handler) 538 539 540 def BuildOptions(): 541 result = optparse.OptionParser() 542 result.add_option("-c", "--chromium", dest="c", 543 help=("Specify the path to your Chromium src/ " 544 "directory to automate the V8 roll.")) 545 result.add_option("-f", "--force", dest="f", 546 help="Don't prompt the user.", 547 default=False, action="store_true") 548 result.add_option("-l", "--last-push", dest="l", 549 help=("Manually specify the git commit ID " 550 "of the last push to trunk.")) 551 result.add_option("-m", "--manual", dest="m", 552 help="Prompt the user at every important step.", 553 default=False, action="store_true") 554 result.add_option("-r", "--reviewer", dest="r", 555 help=("Specify the account name to be used for reviews.")) 556 result.add_option("-s", "--step", dest="s", 557 help="Specify the step where to start work. Default: 0.", 558 default=0, type="int") 559 return result 560 561 562 def ProcessOptions(options): 563 if options.s < 0: 564 print "Bad step number %d" % options.s 565 return False 566 if not options.m and not options.r: 567 print "A reviewer (-r) is required in (semi-)automatic mode." 568 return False 569 if options.f and options.m: 570 print "Manual and forced mode cannot be combined." 571 return False 572 if not options.m and not options.c: 573 print "A chromium checkout (-c) is required in (semi-)automatic mode." 574 return False 575 return True 576 577 578 def Main(): 579 parser = BuildOptions() 580 (options, args) = parser.parse_args() 581 if not ProcessOptions(options): 582 parser.print_help() 583 return 1 584 RunPushToTrunk(CONFIG, options) 585 586 if __name__ == "__main__": 587 sys.exit(Main()) 588