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 29import argparse 30import os 31import sys 32import tempfile 33import urllib2 34 35from common_includes import * 36 37PUSH_MSG_GIT_SUFFIX = " (based on %s)" 38 39 40class 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("CANDIDATESBRANCH") 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("CANDIDATESBRANCH")) 53 54 55class FreshBranch(Step): 56 MESSAGE = "Create a fresh branch." 57 58 def RunStep(self): 59 self.GitCreateBranch(self.Config("BRANCHNAME"), 60 self.vc.RemoteMasterBranch()) 61 62 63class PreparePushRevision(Step): 64 MESSAGE = "Check which revision to push." 65 66 def RunStep(self): 67 if self._options.revision: 68 self["push_hash"] = self._options.revision 69 else: 70 self["push_hash"] = self.GitLog(n=1, format="%H", git_hash="HEAD") 71 if not self["push_hash"]: # pragma: no cover 72 self.Die("Could not determine the git hash for the push.") 73 74 75class IncrementVersion(Step): 76 MESSAGE = "Increment version number." 77 78 def RunStep(self): 79 latest_version = self.GetLatestVersion() 80 81 # The version file on master can be used to bump up major/minor at 82 # branch time. 83 self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteMasterBranch()) 84 self.ReadAndPersistVersion("master_") 85 master_version = self.ArrayToVersion("master_") 86 87 # Use the highest version from master or from tags to determine the new 88 # version. 89 authoritative_version = sorted( 90 [master_version, latest_version], key=SortingKey)[1] 91 self.StoreVersion(authoritative_version, "authoritative_") 92 93 # Variables prefixed with 'new_' contain the new version numbers for the 94 # ongoing candidates push. 95 self["new_major"] = self["authoritative_major"] 96 self["new_minor"] = self["authoritative_minor"] 97 self["new_build"] = str(int(self["authoritative_build"]) + 1) 98 99 # Make sure patch level is 0 in a new push. 100 self["new_patch"] = "0" 101 102 self["version"] = "%s.%s.%s" % (self["new_major"], 103 self["new_minor"], 104 self["new_build"]) 105 106 print ("Incremented version to %s" % self["version"]) 107 108 109class DetectLastRelease(Step): 110 MESSAGE = "Detect commit ID of last release base." 111 112 def RunStep(self): 113 if self._options.last_master: 114 self["last_push_master"] = self._options.last_master 115 else: 116 self["last_push_master"] = self.GetLatestReleaseBase() 117 118 119class PrepareChangeLog(Step): 120 MESSAGE = "Prepare raw ChangeLog entry." 121 122 def Reload(self, body): 123 """Attempts to reload the commit message from rietveld in order to allow 124 late changes to the LOG flag. Note: This is brittle to future changes of 125 the web page name or structure. 126 """ 127 match = re.search(r"^Review URL: https://codereview\.chromium\.org/(\d+)$", 128 body, flags=re.M) 129 if match: 130 cl_url = ("https://codereview.chromium.org/%s/description" 131 % match.group(1)) 132 try: 133 # Fetch from Rietveld but only retry once with one second delay since 134 # there might be many revisions. 135 body = self.ReadURL(cl_url, wait_plan=[1]) 136 except urllib2.URLError: # pragma: no cover 137 pass 138 return body 139 140 def RunStep(self): 141 self["date"] = self.GetDate() 142 output = "%s: Version %s\n\n" % (self["date"], self["version"]) 143 TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE")) 144 commits = self.GitLog(format="%H", 145 git_hash="%s..%s" % (self["last_push_master"], 146 self["push_hash"])) 147 148 # Cache raw commit messages. 149 commit_messages = [ 150 [ 151 self.GitLog(n=1, format="%s", git_hash=commit), 152 self.Reload(self.GitLog(n=1, format="%B", git_hash=commit)), 153 self.GitLog(n=1, format="%an", git_hash=commit), 154 ] for commit in commits.splitlines() 155 ] 156 157 # Auto-format commit messages. 158 body = MakeChangeLogBody(commit_messages, auto_format=True) 159 AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE")) 160 161 msg = (" Performance and stability improvements on all platforms." 162 "\n#\n# The change log above is auto-generated. Please review if " 163 "all relevant\n# commit messages from the list below are included." 164 "\n# All lines starting with # will be stripped.\n#\n") 165 AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE")) 166 167 # Include unformatted commit messages as a reference in a comment. 168 comment_body = MakeComment(MakeChangeLogBody(commit_messages)) 169 AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE")) 170 171 172class EditChangeLog(Step): 173 MESSAGE = "Edit ChangeLog entry." 174 175 def RunStep(self): 176 print ("Please press <Return> to have your EDITOR open the ChangeLog " 177 "entry, then edit its contents to your liking. When you're done, " 178 "save the file and exit your EDITOR. ") 179 self.ReadLine(default="") 180 self.Editor(self.Config("CHANGELOG_ENTRY_FILE")) 181 182 # Strip comments and reformat with correct indentation. 183 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip() 184 changelog_entry = StripComments(changelog_entry) 185 changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines())) 186 changelog_entry = changelog_entry.lstrip() 187 188 if changelog_entry == "": # pragma: no cover 189 self.Die("Empty ChangeLog entry.") 190 191 # Safe new change log for adding it later to the candidates patch. 192 TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE")) 193 194 195class StragglerCommits(Step): 196 MESSAGE = ("Fetch straggler commits that sneaked in since this script was " 197 "started.") 198 199 def RunStep(self): 200 self.vc.Fetch() 201 self.GitCheckout(self.vc.RemoteMasterBranch()) 202 203 204class SquashCommits(Step): 205 MESSAGE = "Squash commits into one." 206 207 def RunStep(self): 208 # Instead of relying on "git rebase -i", we'll just create a diff, because 209 # that's easier to automate. 210 TextToFile(self.GitDiff(self.vc.RemoteCandidateBranch(), 211 self["push_hash"]), 212 self.Config("PATCH_FILE")) 213 214 # Convert the ChangeLog entry to commit message format. 215 text = FileToText(self.Config("CHANGELOG_ENTRY_FILE")) 216 217 # Remove date and trailing white space. 218 text = re.sub(r"^%s: " % self["date"], "", text.rstrip()) 219 220 # Show the used master hash in the commit message. 221 suffix = PUSH_MSG_GIT_SUFFIX % self["push_hash"] 222 text = MSub(r"^(Version \d+\.\d+\.\d+)$", "\\1%s" % suffix, text) 223 224 # Remove indentation and merge paragraphs into single long lines, keeping 225 # empty lines between them. 226 def SplitMapJoin(split_text, fun, join_text): 227 return lambda text: join_text.join(map(fun, text.split(split_text))) 228 strip = lambda line: line.strip() 229 text = SplitMapJoin("\n\n", SplitMapJoin("\n", strip, " "), "\n\n")(text) 230 231 if not text: # pragma: no cover 232 self.Die("Commit message editing failed.") 233 self["commit_title"] = text.splitlines()[0] 234 TextToFile(text, self.Config("COMMITMSG_FILE")) 235 236 237class NewBranch(Step): 238 MESSAGE = "Create a new branch from candidates." 239 240 def RunStep(self): 241 self.GitCreateBranch(self.Config("CANDIDATESBRANCH"), 242 self.vc.RemoteCandidateBranch()) 243 244 245class ApplyChanges(Step): 246 MESSAGE = "Apply squashed changes." 247 248 def RunStep(self): 249 self.ApplyPatch(self.Config("PATCH_FILE")) 250 os.remove(self.Config("PATCH_FILE")) 251 # The change log has been modified by the patch. Reset it to the version 252 # on candidates and apply the exact changes determined by this 253 # PrepareChangeLog step above. 254 self.GitCheckoutFile(CHANGELOG_FILE, self.vc.RemoteCandidateBranch()) 255 # The version file has been modified by the patch. Reset it to the version 256 # on candidates. 257 self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteCandidateBranch()) 258 259 260class CommitSquash(Step): 261 MESSAGE = "Commit to local candidates branch." 262 263 def RunStep(self): 264 # Make a first commit with a slightly different title to not confuse 265 # the tagging. 266 msg = FileToText(self.Config("COMMITMSG_FILE")).splitlines() 267 msg[0] = msg[0].replace("(based on", "(squashed - based on") 268 self.GitCommit(message = "\n".join(msg)) 269 270 271class PrepareVersionBranch(Step): 272 MESSAGE = "Prepare new branch to commit version and changelog file." 273 274 def RunStep(self): 275 self.GitCheckout("master") 276 self.Git("fetch") 277 self.GitDeleteBranch(self.Config("CANDIDATESBRANCH")) 278 self.GitCreateBranch(self.Config("CANDIDATESBRANCH"), 279 self.vc.RemoteCandidateBranch()) 280 281 282class AddChangeLog(Step): 283 MESSAGE = "Add ChangeLog changes to candidates branch." 284 285 def RunStep(self): 286 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")) 287 old_change_log = FileToText(os.path.join(self.default_cwd, CHANGELOG_FILE)) 288 new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log) 289 TextToFile(new_change_log, os.path.join(self.default_cwd, CHANGELOG_FILE)) 290 os.remove(self.Config("CHANGELOG_ENTRY_FILE")) 291 292 293class SetVersion(Step): 294 MESSAGE = "Set correct version for candidates." 295 296 def RunStep(self): 297 self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_") 298 299 300class CommitCandidate(Step): 301 MESSAGE = "Commit version and changelog to local candidates branch." 302 303 def RunStep(self): 304 self.GitCommit(file_name = self.Config("COMMITMSG_FILE")) 305 os.remove(self.Config("COMMITMSG_FILE")) 306 307 308class SanityCheck(Step): 309 MESSAGE = "Sanity check." 310 311 def RunStep(self): 312 # TODO(machenbach): Run presubmit script here as it is now missing in the 313 # prepare push process. 314 if not self.Confirm("Please check if your local checkout is sane: Inspect " 315 "%s, compile, run tests. Do you want to commit this new candidates " 316 "revision to the repository?" % VERSION_FILE): 317 self.Die("Execution canceled.") # pragma: no cover 318 319 320class Land(Step): 321 MESSAGE = "Land the patch." 322 323 def RunStep(self): 324 self.vc.CLLand() 325 326 327class TagRevision(Step): 328 MESSAGE = "Tag the new revision." 329 330 def RunStep(self): 331 self.vc.Tag( 332 self["version"], self.vc.RemoteCandidateBranch(), self["commit_title"]) 333 334 335class CleanUp(Step): 336 MESSAGE = "Done!" 337 338 def RunStep(self): 339 print("Congratulations, you have successfully created the candidates " 340 "revision %s." 341 % self["version"]) 342 343 self.CommonCleanup() 344 if self.Config("CANDIDATESBRANCH") != self["current_branch"]: 345 self.GitDeleteBranch(self.Config("CANDIDATESBRANCH")) 346 347 348class PushToCandidates(ScriptsBase): 349 def _PrepareOptions(self, parser): 350 group = parser.add_mutually_exclusive_group() 351 group.add_argument("-f", "--force", 352 help="Don't prompt the user.", 353 default=False, action="store_true") 354 group.add_argument("-m", "--manual", 355 help="Prompt the user at every important step.", 356 default=False, action="store_true") 357 parser.add_argument("-b", "--last-master", 358 help=("The git commit ID of the last master " 359 "revision that was pushed to candidates. This is" 360 " used for the auto-generated ChangeLog entry.")) 361 parser.add_argument("-l", "--last-push", 362 help="The git commit ID of the last candidates push.") 363 parser.add_argument("-R", "--revision", 364 help="The git commit ID to push (defaults to HEAD).") 365 366 def _ProcessOptions(self, options): # pragma: no cover 367 if not options.manual and not options.reviewer: 368 print "A reviewer (-r) is required in (semi-)automatic mode." 369 return False 370 if not options.manual and not options.author: 371 print "Specify your chromium.org email with -a in (semi-)automatic mode." 372 return False 373 374 options.tbr_commit = not options.manual 375 return True 376 377 def _Config(self): 378 return { 379 "BRANCHNAME": "prepare-push", 380 "CANDIDATESBRANCH": "candidates-push", 381 "PERSISTFILE_BASENAME": "/tmp/v8-push-to-candidates-tempfile", 382 "CHANGELOG_ENTRY_FILE": 383 "/tmp/v8-push-to-candidates-tempfile-changelog-entry", 384 "PATCH_FILE": "/tmp/v8-push-to-candidates-tempfile-patch-file", 385 "COMMITMSG_FILE": "/tmp/v8-push-to-candidates-tempfile-commitmsg", 386 } 387 388 def _Steps(self): 389 return [ 390 Preparation, 391 FreshBranch, 392 PreparePushRevision, 393 IncrementVersion, 394 DetectLastRelease, 395 PrepareChangeLog, 396 EditChangeLog, 397 StragglerCommits, 398 SquashCommits, 399 NewBranch, 400 ApplyChanges, 401 CommitSquash, 402 SanityCheck, 403 Land, 404 PrepareVersionBranch, 405 AddChangeLog, 406 SetVersion, 407 CommitCandidate, 408 Land, 409 TagRevision, 410 CleanUp, 411 ] 412 413 414if __name__ == "__main__": # pragma: no cover 415 sys.exit(PushToCandidates().Run()) 416