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 RunStep(self): 123 self["date"] = self.GetDate() 124 output = "%s: Version %s\n\n" % (self["date"], self["version"]) 125 TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE")) 126 commits = self.GitLog(format="%H", 127 git_hash="%s..%s" % (self["last_push_master"], 128 self["push_hash"])) 129 130 # Cache raw commit messages. 131 commit_messages = [ 132 [ 133 self.GitLog(n=1, format="%s", git_hash=commit), 134 self.GitLog(n=1, format="%B", git_hash=commit), 135 self.GitLog(n=1, format="%an", git_hash=commit), 136 ] for commit in commits.splitlines() 137 ] 138 139 # Auto-format commit messages. 140 body = MakeChangeLogBody(commit_messages, auto_format=True) 141 AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE")) 142 143 msg = (" Performance and stability improvements on all platforms." 144 "\n#\n# The change log above is auto-generated. Please review if " 145 "all relevant\n# commit messages from the list below are included." 146 "\n# All lines starting with # will be stripped.\n#\n") 147 AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE")) 148 149 # Include unformatted commit messages as a reference in a comment. 150 comment_body = MakeComment(MakeChangeLogBody(commit_messages)) 151 AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE")) 152 153 154class EditChangeLog(Step): 155 MESSAGE = "Edit ChangeLog entry." 156 157 def RunStep(self): 158 print ("Please press <Return> to have your EDITOR open the ChangeLog " 159 "entry, then edit its contents to your liking. When you're done, " 160 "save the file and exit your EDITOR. ") 161 self.ReadLine(default="") 162 self.Editor(self.Config("CHANGELOG_ENTRY_FILE")) 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 == "": # pragma: no cover 171 self.Die("Empty ChangeLog entry.") 172 173 # Safe new change log for adding it later to the candidates patch. 174 TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE")) 175 176 177class StragglerCommits(Step): 178 MESSAGE = ("Fetch straggler commits that sneaked in since this script was " 179 "started.") 180 181 def RunStep(self): 182 self.vc.Fetch() 183 self.GitCheckout(self.vc.RemoteMasterBranch()) 184 185 186class SquashCommits(Step): 187 MESSAGE = "Squash commits into one." 188 189 def RunStep(self): 190 # Instead of relying on "git rebase -i", we'll just create a diff, because 191 # that's easier to automate. 192 TextToFile(self.GitDiff(self.vc.RemoteCandidateBranch(), 193 self["push_hash"]), 194 self.Config("PATCH_FILE")) 195 196 # Convert the ChangeLog entry to commit message format. 197 text = FileToText(self.Config("CHANGELOG_ENTRY_FILE")) 198 199 # Remove date and trailing white space. 200 text = re.sub(r"^%s: " % self["date"], "", text.rstrip()) 201 202 # Show the used master hash in the commit message. 203 suffix = PUSH_MSG_GIT_SUFFIX % self["push_hash"] 204 text = MSub(r"^(Version \d+\.\d+\.\d+)$", "\\1%s" % suffix, text) 205 206 # Remove indentation and merge paragraphs into single long lines, keeping 207 # empty lines between them. 208 def SplitMapJoin(split_text, fun, join_text): 209 return lambda text: join_text.join(map(fun, text.split(split_text))) 210 strip = lambda line: line.strip() 211 text = SplitMapJoin("\n\n", SplitMapJoin("\n", strip, " "), "\n\n")(text) 212 213 if not text: # pragma: no cover 214 self.Die("Commit message editing failed.") 215 self["commit_title"] = text.splitlines()[0] 216 TextToFile(text, self.Config("COMMITMSG_FILE")) 217 218 219class NewBranch(Step): 220 MESSAGE = "Create a new branch from candidates." 221 222 def RunStep(self): 223 self.GitCreateBranch(self.Config("CANDIDATESBRANCH"), 224 self.vc.RemoteCandidateBranch()) 225 226 227class ApplyChanges(Step): 228 MESSAGE = "Apply squashed changes." 229 230 def RunStep(self): 231 self.ApplyPatch(self.Config("PATCH_FILE")) 232 os.remove(self.Config("PATCH_FILE")) 233 # The change log has been modified by the patch. Reset it to the version 234 # on candidates and apply the exact changes determined by this 235 # PrepareChangeLog step above. 236 self.GitCheckoutFile(CHANGELOG_FILE, self.vc.RemoteCandidateBranch()) 237 # The version file has been modified by the patch. Reset it to the version 238 # on candidates. 239 self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteCandidateBranch()) 240 241 242class CommitSquash(Step): 243 MESSAGE = "Commit to local candidates branch." 244 245 def RunStep(self): 246 # Make a first commit with a slightly different title to not confuse 247 # the tagging. 248 msg = FileToText(self.Config("COMMITMSG_FILE")).splitlines() 249 msg[0] = msg[0].replace("(based on", "(squashed - based on") 250 self.GitCommit(message = "\n".join(msg)) 251 252 253class PrepareVersionBranch(Step): 254 MESSAGE = "Prepare new branch to commit version and changelog file." 255 256 def RunStep(self): 257 self.GitCheckout("master") 258 self.Git("fetch") 259 self.GitDeleteBranch(self.Config("CANDIDATESBRANCH")) 260 self.GitCreateBranch(self.Config("CANDIDATESBRANCH"), 261 self.vc.RemoteCandidateBranch()) 262 263 264class AddChangeLog(Step): 265 MESSAGE = "Add ChangeLog changes to candidates branch." 266 267 def RunStep(self): 268 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")) 269 old_change_log = FileToText(os.path.join(self.default_cwd, CHANGELOG_FILE)) 270 new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log) 271 TextToFile(new_change_log, os.path.join(self.default_cwd, CHANGELOG_FILE)) 272 os.remove(self.Config("CHANGELOG_ENTRY_FILE")) 273 274 275class SetVersion(Step): 276 MESSAGE = "Set correct version for candidates." 277 278 def RunStep(self): 279 self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_") 280 281 282class CommitCandidate(Step): 283 MESSAGE = "Commit version and changelog to local candidates branch." 284 285 def RunStep(self): 286 self.GitCommit(file_name = self.Config("COMMITMSG_FILE")) 287 os.remove(self.Config("COMMITMSG_FILE")) 288 289 290class SanityCheck(Step): 291 MESSAGE = "Sanity check." 292 293 def RunStep(self): 294 # TODO(machenbach): Run presubmit script here as it is now missing in the 295 # prepare push process. 296 if not self.Confirm("Please check if your local checkout is sane: Inspect " 297 "%s, compile, run tests. Do you want to commit this new candidates " 298 "revision to the repository?" % VERSION_FILE): 299 self.Die("Execution canceled.") # pragma: no cover 300 301 302class Land(Step): 303 MESSAGE = "Land the patch." 304 305 def RunStep(self): 306 self.vc.CLLand() 307 308 309class TagRevision(Step): 310 MESSAGE = "Tag the new revision." 311 312 def RunStep(self): 313 self.vc.Tag( 314 self["version"], self.vc.RemoteCandidateBranch(), self["commit_title"]) 315 316 317class CleanUp(Step): 318 MESSAGE = "Done!" 319 320 def RunStep(self): 321 print("Congratulations, you have successfully created the candidates " 322 "revision %s." 323 % self["version"]) 324 325 self.CommonCleanup() 326 if self.Config("CANDIDATESBRANCH") != self["current_branch"]: 327 self.GitDeleteBranch(self.Config("CANDIDATESBRANCH")) 328 329 330class PushToCandidates(ScriptsBase): 331 def _PrepareOptions(self, parser): 332 group = parser.add_mutually_exclusive_group() 333 group.add_argument("-f", "--force", 334 help="Don't prompt the user.", 335 default=False, action="store_true") 336 group.add_argument("-m", "--manual", 337 help="Prompt the user at every important step.", 338 default=False, action="store_true") 339 parser.add_argument("-b", "--last-master", 340 help=("The git commit ID of the last master " 341 "revision that was pushed to candidates. This is" 342 " used for the auto-generated ChangeLog entry.")) 343 parser.add_argument("-l", "--last-push", 344 help="The git commit ID of the last candidates push.") 345 parser.add_argument("-R", "--revision", 346 help="The git commit ID to push (defaults to HEAD).") 347 348 def _ProcessOptions(self, options): # pragma: no cover 349 if not options.manual and not options.reviewer: 350 print "A reviewer (-r) is required in (semi-)automatic mode." 351 return False 352 if not options.manual and not options.author: 353 print "Specify your chromium.org email with -a in (semi-)automatic mode." 354 return False 355 356 options.tbr_commit = not options.manual 357 return True 358 359 def _Config(self): 360 return { 361 "BRANCHNAME": "prepare-push", 362 "CANDIDATESBRANCH": "candidates-push", 363 "PERSISTFILE_BASENAME": "/tmp/v8-push-to-candidates-tempfile", 364 "CHANGELOG_ENTRY_FILE": 365 "/tmp/v8-push-to-candidates-tempfile-changelog-entry", 366 "PATCH_FILE": "/tmp/v8-push-to-candidates-tempfile-patch-file", 367 "COMMITMSG_FILE": "/tmp/v8-push-to-candidates-tempfile-commitmsg", 368 } 369 370 def _Steps(self): 371 return [ 372 Preparation, 373 FreshBranch, 374 PreparePushRevision, 375 IncrementVersion, 376 DetectLastRelease, 377 PrepareChangeLog, 378 EditChangeLog, 379 StragglerCommits, 380 SquashCommits, 381 NewBranch, 382 ApplyChanges, 383 CommitSquash, 384 SanityCheck, 385 Land, 386 PrepareVersionBranch, 387 AddChangeLog, 388 SetVersion, 389 CommitCandidate, 390 Land, 391 TagRevision, 392 CleanUp, 393 ] 394 395 396if __name__ == "__main__": # pragma: no cover 397 sys.exit(PushToCandidates().Run()) 398