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