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 argparse 30 import os 31 import sys 32 import tempfile 33 import urllib2 34 35 from common_includes import * 36 37 PUSH_MESSAGE_SUFFIX = " (based on bleeding_edge revision r%d)" 38 PUSH_MESSAGE_RE = re.compile(r".* \(based on bleeding_edge revision r(\d+)\)$") 39 40 class Preparation(Step): 41 MESSAGE = "Preparation." 42 43 def RunStep(self): 44 self.InitialEnvironmentChecks(self.default_cwd) 45 self.CommonPrepare() 46 47 if(self["current_branch"] == self.Config("TRUNKBRANCH") 48 or self["current_branch"] == self.Config("BRANCHNAME")): 49 print "Warning: Script started on branch %s" % self["current_branch"] 50 51 self.PrepareBranch() 52 self.DeleteBranch(self.Config("TRUNKBRANCH")) 53 54 55 class FreshBranch(Step): 56 MESSAGE = "Create a fresh branch." 57 58 def RunStep(self): 59 self.GitCreateBranch(self.Config("BRANCHNAME"), "svn/bleeding_edge") 60 61 62 class PreparePushRevision(Step): 63 MESSAGE = "Check which revision to push." 64 65 def RunStep(self): 66 if self._options.revision: 67 self["push_hash"] = self.GitSVNFindGitHash(self._options.revision) 68 else: 69 self["push_hash"] = self.GitLog(n=1, format="%H", git_hash="HEAD") 70 if not self["push_hash"]: # pragma: no cover 71 self.Die("Could not determine the git hash for the push.") 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.last_push or self.FindLastTrunkPush() 79 while True: 80 # Print assumed commit, circumventing git's pager. 81 print self.GitLog(n=1, git_hash=last_push) 82 if self.Confirm("Is the commit printed above the last push to trunk?"): 83 break 84 last_push = self.FindLastTrunkPush(parent_hash=last_push) 85 86 if self._options.last_bleeding_edge: 87 # Read the bleeding edge revision of the last push from a command-line 88 # option. 89 last_push_bleeding_edge = self._options.last_bleeding_edge 90 else: 91 # Retrieve the bleeding edge revision of the last push from the text in 92 # the push commit message. 93 last_push_title = self.GitLog(n=1, format="%s", git_hash=last_push) 94 last_push_be_svn = PUSH_MESSAGE_RE.match(last_push_title).group(1) 95 if not last_push_be_svn: # pragma: no cover 96 self.Die("Could not retrieve bleeding edge revision for trunk push %s" 97 % last_push) 98 last_push_bleeding_edge = self.GitSVNFindGitHash(last_push_be_svn) 99 if not last_push_bleeding_edge: # pragma: no cover 100 self.Die("Could not retrieve bleeding edge git hash for trunk push %s" 101 % last_push) 102 103 # This points to the svn revision of the last push on trunk. 104 self["last_push_trunk"] = last_push 105 # This points to the last bleeding_edge revision that went into the last 106 # push. 107 # TODO(machenbach): Do we need a check to make sure we're not pushing a 108 # revision older than the last push? If we do this, the output of the 109 # current change log preparation won't make much sense. 110 self["last_push_bleeding_edge"] = last_push_bleeding_edge 111 112 113 # TODO(machenbach): Code similarities with bump_up_version.py. Merge after 114 # turning this script into a pure git script. 115 class GetCurrentBleedingEdgeVersion(Step): 116 MESSAGE = "Get latest bleeding edge version." 117 118 def RunStep(self): 119 self.GitCheckoutFile(VERSION_FILE, "svn/bleeding_edge") 120 121 # Store latest version. 122 self.ReadAndPersistVersion("latest_") 123 self["latest_version"] = self.ArrayToVersion("latest_") 124 print "Bleeding edge version: %s" % self["latest_version"] 125 126 127 class IncrementVersion(Step): 128 MESSAGE = "Increment version number." 129 130 def RunStep(self): 131 # Retrieve current version from last trunk push. 132 self.GitCheckoutFile(VERSION_FILE, self["last_push_trunk"]) 133 self.ReadAndPersistVersion() 134 self["trunk_version"] = self.ArrayToVersion("") 135 136 if self["latest_build"] == "9999": # pragma: no cover 137 # If version control on bleeding edge was switched off, just use the last 138 # trunk version. 139 self["latest_version"] = self["trunk_version"] 140 141 if SortingKey(self["trunk_version"]) < SortingKey(self["latest_version"]): 142 # If the version on bleeding_edge is newer than on trunk, use it. 143 self.GitCheckoutFile(VERSION_FILE, "svn/bleeding_edge") 144 self.ReadAndPersistVersion() 145 146 if self.Confirm(("Automatically increment BUILD_NUMBER? (Saying 'n' will " 147 "fire up your EDITOR on %s so you can make arbitrary " 148 "changes. When you're done, save the file and exit your " 149 "EDITOR.)" % VERSION_FILE)): 150 151 text = FileToText(os.path.join(self.default_cwd, VERSION_FILE)) 152 text = MSub(r"(?<=#define BUILD_NUMBER)(?P<space>\s+)\d*$", 153 r"\g<space>%s" % str(int(self["build"]) + 1), 154 text) 155 TextToFile(text, os.path.join(self.default_cwd, VERSION_FILE)) 156 else: 157 self.Editor(os.path.join(self.default_cwd, VERSION_FILE)) 158 159 # Variables prefixed with 'new_' contain the new version numbers for the 160 # ongoing trunk push. 161 self.ReadAndPersistVersion("new_") 162 163 # Make sure patch level is 0 in a new push. 164 self["new_patch"] = "0" 165 166 self["version"] = "%s.%s.%s" % (self["new_major"], 167 self["new_minor"], 168 self["new_build"]) 169 170 171 class PrepareChangeLog(Step): 172 MESSAGE = "Prepare raw ChangeLog entry." 173 174 def Reload(self, body): 175 """Attempts to reload the commit message from rietveld in order to allow 176 late changes to the LOG flag. Note: This is brittle to future changes of 177 the web page name or structure. 178 """ 179 match = re.search(r"^Review URL: https://codereview\.chromium\.org/(\d+)$", 180 body, flags=re.M) 181 if match: 182 cl_url = ("https://codereview.chromium.org/%s/description" 183 % match.group(1)) 184 try: 185 # Fetch from Rietveld but only retry once with one second delay since 186 # there might be many revisions. 187 body = self.ReadURL(cl_url, wait_plan=[1]) 188 except urllib2.URLError: # pragma: no cover 189 pass 190 return body 191 192 def RunStep(self): 193 self["date"] = self.GetDate() 194 output = "%s: Version %s\n\n" % (self["date"], self["version"]) 195 TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE")) 196 commits = self.GitLog(format="%H", 197 git_hash="%s..%s" % (self["last_push_bleeding_edge"], 198 self["push_hash"])) 199 200 # Cache raw commit messages. 201 commit_messages = [ 202 [ 203 self.GitLog(n=1, format="%s", git_hash=commit), 204 self.Reload(self.GitLog(n=1, format="%B", git_hash=commit)), 205 self.GitLog(n=1, format="%an", git_hash=commit), 206 ] for commit in commits.splitlines() 207 ] 208 209 # Auto-format commit messages. 210 body = MakeChangeLogBody(commit_messages, auto_format=True) 211 AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE")) 212 213 msg = (" Performance and stability improvements on all platforms." 214 "\n#\n# The change log above is auto-generated. Please review if " 215 "all relevant\n# commit messages from the list below are included." 216 "\n# All lines starting with # will be stripped.\n#\n") 217 AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE")) 218 219 # Include unformatted commit messages as a reference in a comment. 220 comment_body = MakeComment(MakeChangeLogBody(commit_messages)) 221 AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE")) 222 223 224 class EditChangeLog(Step): 225 MESSAGE = "Edit ChangeLog entry." 226 227 def RunStep(self): 228 print ("Please press <Return> to have your EDITOR open the ChangeLog " 229 "entry, then edit its contents to your liking. When you're done, " 230 "save the file and exit your EDITOR. ") 231 self.ReadLine(default="") 232 self.Editor(self.Config("CHANGELOG_ENTRY_FILE")) 233 234 # Strip comments and reformat with correct indentation. 235 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip() 236 changelog_entry = StripComments(changelog_entry) 237 changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines())) 238 changelog_entry = changelog_entry.lstrip() 239 240 if changelog_entry == "": # pragma: no cover 241 self.Die("Empty ChangeLog entry.") 242 243 # Safe new change log for adding it later to the trunk patch. 244 TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE")) 245 246 247 class StragglerCommits(Step): 248 MESSAGE = ("Fetch straggler commits that sneaked in since this script was " 249 "started.") 250 251 def RunStep(self): 252 self.GitSVNFetch() 253 self.GitCheckout("svn/bleeding_edge") 254 255 256 class SquashCommits(Step): 257 MESSAGE = "Squash commits into one." 258 259 def RunStep(self): 260 # Instead of relying on "git rebase -i", we'll just create a diff, because 261 # that's easier to automate. 262 TextToFile(self.GitDiff("svn/trunk", self["push_hash"]), 263 self.Config("PATCH_FILE")) 264 265 # Convert the ChangeLog entry to commit message format. 266 text = FileToText(self.Config("CHANGELOG_ENTRY_FILE")) 267 268 # Remove date and trailing white space. 269 text = re.sub(r"^%s: " % self["date"], "", text.rstrip()) 270 271 # Retrieve svn revision for showing the used bleeding edge revision in the 272 # commit message. 273 self["svn_revision"] = self.GitSVNFindSVNRev(self["push_hash"]) 274 suffix = PUSH_MESSAGE_SUFFIX % int(self["svn_revision"]) 275 text = MSub(r"^(Version \d+\.\d+\.\d+)$", "\\1%s" % suffix, text) 276 277 # Remove indentation and merge paragraphs into single long lines, keeping 278 # empty lines between them. 279 def SplitMapJoin(split_text, fun, join_text): 280 return lambda text: join_text.join(map(fun, text.split(split_text))) 281 strip = lambda line: line.strip() 282 text = SplitMapJoin("\n\n", SplitMapJoin("\n", strip, " "), "\n\n")(text) 283 284 if not text: # pragma: no cover 285 self.Die("Commit message editing failed.") 286 TextToFile(text, self.Config("COMMITMSG_FILE")) 287 288 289 class NewBranch(Step): 290 MESSAGE = "Create a new branch from trunk." 291 292 def RunStep(self): 293 self.GitCreateBranch(self.Config("TRUNKBRANCH"), "svn/trunk") 294 295 296 class ApplyChanges(Step): 297 MESSAGE = "Apply squashed changes." 298 299 def RunStep(self): 300 self.ApplyPatch(self.Config("PATCH_FILE")) 301 os.remove(self.Config("PATCH_FILE")) 302 303 304 class AddChangeLog(Step): 305 MESSAGE = "Add ChangeLog changes to trunk branch." 306 307 def RunStep(self): 308 # The change log has been modified by the patch. Reset it to the version 309 # on trunk and apply the exact changes determined by this PrepareChangeLog 310 # step above. 311 self.GitCheckoutFile(self.Config("CHANGELOG_FILE"), "svn/trunk") 312 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")) 313 old_change_log = FileToText(self.Config("CHANGELOG_FILE")) 314 new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log) 315 TextToFile(new_change_log, self.Config("CHANGELOG_FILE")) 316 os.remove(self.Config("CHANGELOG_ENTRY_FILE")) 317 318 319 class SetVersion(Step): 320 MESSAGE = "Set correct version for trunk." 321 322 def RunStep(self): 323 # The version file has been modified by the patch. Reset it to the version 324 # on trunk and apply the correct version. 325 self.GitCheckoutFile(VERSION_FILE, "svn/trunk") 326 self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_") 327 328 329 class CommitTrunk(Step): 330 MESSAGE = "Commit to local trunk branch." 331 332 def RunStep(self): 333 self.GitCommit(file_name = self.Config("COMMITMSG_FILE")) 334 os.remove(self.Config("COMMITMSG_FILE")) 335 336 337 class SanityCheck(Step): 338 MESSAGE = "Sanity check." 339 340 def RunStep(self): 341 # TODO(machenbach): Run presubmit script here as it is now missing in the 342 # prepare push process. 343 if not self.Confirm("Please check if your local checkout is sane: Inspect " 344 "%s, compile, run tests. Do you want to commit this new trunk " 345 "revision to the repository?" % VERSION_FILE): 346 self.Die("Execution canceled.") # pragma: no cover 347 348 349 class CommitSVN(Step): 350 MESSAGE = "Commit to SVN." 351 352 def RunStep(self): 353 result = self.GitSVNDCommit() 354 if not result: # pragma: no cover 355 self.Die("'git svn dcommit' failed.") 356 result = filter(lambda x: re.search(r"^Committed r[0-9]+", x), 357 result.splitlines()) 358 if len(result) > 0: 359 self["trunk_revision"] = re.sub(r"^Committed r([0-9]+)", r"\1",result[0]) 360 361 # Sometimes grepping for the revision fails. No idea why. If you figure 362 # out why it is flaky, please do fix it properly. 363 if not self["trunk_revision"]: 364 print("Sorry, grepping for the SVN revision failed. Please look for it " 365 "in the last command's output above and provide it manually (just " 366 "the number, without the leading \"r\").") 367 self.DieNoManualMode("Can't prompt in forced mode.") 368 while not self["trunk_revision"]: 369 print "> ", 370 self["trunk_revision"] = self.ReadLine() 371 372 373 class TagRevision(Step): 374 MESSAGE = "Tag the new revision." 375 376 def RunStep(self): 377 self.GitSVNTag(self["version"]) 378 379 380 class CleanUp(Step): 381 MESSAGE = "Done!" 382 383 def RunStep(self): 384 print("Congratulations, you have successfully created the trunk " 385 "revision %s. Please don't forget to roll this new version into " 386 "Chromium, and to update the v8rel spreadsheet:" 387 % self["version"]) 388 print "%s\ttrunk\t%s" % (self["version"], self["trunk_revision"]) 389 390 self.CommonCleanup() 391 if self.Config("TRUNKBRANCH") != self["current_branch"]: 392 self.GitDeleteBranch(self.Config("TRUNKBRANCH")) 393 394 395 class PushToTrunk(ScriptsBase): 396 def _PrepareOptions(self, parser): 397 group = parser.add_mutually_exclusive_group() 398 group.add_argument("-f", "--force", 399 help="Don't prompt the user.", 400 default=False, action="store_true") 401 group.add_argument("-m", "--manual", 402 help="Prompt the user at every important step.", 403 default=False, action="store_true") 404 parser.add_argument("-b", "--last-bleeding-edge", 405 help=("The git commit ID of the last bleeding edge " 406 "revision that was pushed to trunk. This is " 407 "used for the auto-generated ChangeLog entry.")) 408 parser.add_argument("-l", "--last-push", 409 help="The git commit ID of the last push to trunk.") 410 parser.add_argument("-R", "--revision", 411 help="The svn revision to push (defaults to HEAD).") 412 413 def _ProcessOptions(self, options): # pragma: no cover 414 if not options.manual and not options.reviewer: 415 print "A reviewer (-r) is required in (semi-)automatic mode." 416 return False 417 if not options.manual and not options.author: 418 print "Specify your chromium.org email with -a in (semi-)automatic mode." 419 return False 420 if options.revision and not int(options.revision) > 0: 421 print("The --revision flag must be a positiv integer pointing to a " 422 "valid svn revision.") 423 return False 424 425 options.tbr_commit = not options.manual 426 return True 427 428 def _Config(self): 429 return { 430 "BRANCHNAME": "prepare-push", 431 "TRUNKBRANCH": "trunk-push", 432 "PERSISTFILE_BASENAME": "/tmp/v8-push-to-trunk-tempfile", 433 "CHANGELOG_FILE": "ChangeLog", 434 "CHANGELOG_ENTRY_FILE": "/tmp/v8-push-to-trunk-tempfile-changelog-entry", 435 "PATCH_FILE": "/tmp/v8-push-to-trunk-tempfile-patch-file", 436 "COMMITMSG_FILE": "/tmp/v8-push-to-trunk-tempfile-commitmsg", 437 } 438 439 def _Steps(self): 440 return [ 441 Preparation, 442 FreshBranch, 443 PreparePushRevision, 444 DetectLastPush, 445 GetCurrentBleedingEdgeVersion, 446 IncrementVersion, 447 PrepareChangeLog, 448 EditChangeLog, 449 StragglerCommits, 450 SquashCommits, 451 NewBranch, 452 ApplyChanges, 453 AddChangeLog, 454 SetVersion, 455 CommitTrunk, 456 SanityCheck, 457 CommitSVN, 458 TagRevision, 459 CleanUp, 460 ] 461 462 463 if __name__ == "__main__": # pragma: no cover 464 sys.exit(PushToTrunk().Run()) 465