1#!/usr/bin/env python 2# Copyright 2015 the V8 project authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import argparse 7import os 8import sys 9import tempfile 10import urllib2 11 12from common_includes import * 13 14class Preparation(Step): 15 MESSAGE = "Preparation." 16 17 def RunStep(self): 18 self.Git("fetch origin +refs/heads/*:refs/heads/*") 19 self.GitCheckout("origin/master") 20 self.DeleteBranch("work-branch") 21 22 23class PrepareBranchRevision(Step): 24 MESSAGE = "Check from which revision to branch off." 25 26 def RunStep(self): 27 self["push_hash"] = (self._options.revision or 28 self.GitLog(n=1, format="%H", branch="origin/master")) 29 assert self["push_hash"] 30 print "Release revision %s" % self["push_hash"] 31 32 33class IncrementVersion(Step): 34 MESSAGE = "Increment version number." 35 36 def RunStep(self): 37 latest_version = self.GetLatestVersion() 38 39 # The version file on master can be used to bump up major/minor at 40 # branch time. 41 self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteMasterBranch()) 42 self.ReadAndPersistVersion("master_") 43 master_version = self.ArrayToVersion("master_") 44 45 # Use the highest version from master or from tags to determine the new 46 # version. 47 authoritative_version = sorted( 48 [master_version, latest_version], key=SortingKey)[1] 49 self.StoreVersion(authoritative_version, "authoritative_") 50 51 # Variables prefixed with 'new_' contain the new version numbers for the 52 # ongoing candidates push. 53 self["new_major"] = self["authoritative_major"] 54 self["new_minor"] = self["authoritative_minor"] 55 self["new_build"] = str(int(self["authoritative_build"]) + 1) 56 57 # Make sure patch level is 0 in a new push. 58 self["new_patch"] = "0" 59 60 # The new version is not a candidate. 61 self["new_candidate"] = "0" 62 63 self["version"] = "%s.%s.%s" % (self["new_major"], 64 self["new_minor"], 65 self["new_build"]) 66 67 print ("Incremented version to %s" % self["version"]) 68 69 70class DetectLastRelease(Step): 71 MESSAGE = "Detect commit ID of last release base." 72 73 def RunStep(self): 74 self["last_push_master"] = self.GetLatestReleaseBase() 75 76 77class PrepareChangeLog(Step): 78 MESSAGE = "Prepare raw ChangeLog entry." 79 80 def RunStep(self): 81 self["date"] = self.GetDate() 82 output = "%s: Version %s\n\n" % (self["date"], self["version"]) 83 TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE")) 84 commits = self.GitLog(format="%H", 85 git_hash="%s..%s" % (self["last_push_master"], 86 self["push_hash"])) 87 88 # Cache raw commit messages. 89 commit_messages = [ 90 [ 91 self.GitLog(n=1, format="%s", git_hash=commit), 92 self.GitLog(n=1, format="%B", git_hash=commit), 93 self.GitLog(n=1, format="%an", git_hash=commit), 94 ] for commit in commits.splitlines() 95 ] 96 97 # Auto-format commit messages. 98 body = MakeChangeLogBody(commit_messages, auto_format=True) 99 AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE")) 100 101 msg = (" Performance and stability improvements on all platforms." 102 "\n#\n# The change log above is auto-generated. Please review if " 103 "all relevant\n# commit messages from the list below are included." 104 "\n# All lines starting with # will be stripped.\n#\n") 105 AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE")) 106 107 # Include unformatted commit messages as a reference in a comment. 108 comment_body = MakeComment(MakeChangeLogBody(commit_messages)) 109 AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE")) 110 111 112class EditChangeLog(Step): 113 MESSAGE = "Edit ChangeLog entry." 114 115 def RunStep(self): 116 print ("Please press <Return> to have your EDITOR open the ChangeLog " 117 "entry, then edit its contents to your liking. When you're done, " 118 "save the file and exit your EDITOR. ") 119 self.ReadLine(default="") 120 self.Editor(self.Config("CHANGELOG_ENTRY_FILE")) 121 122 # Strip comments and reformat with correct indentation. 123 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip() 124 changelog_entry = StripComments(changelog_entry) 125 changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines())) 126 changelog_entry = changelog_entry.lstrip() 127 128 if changelog_entry == "": # pragma: no cover 129 self.Die("Empty ChangeLog entry.") 130 131 # Safe new change log for adding it later to the candidates patch. 132 TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE")) 133 134 135class PushBranchRef(Step): 136 MESSAGE = "Create branch ref." 137 138 def RunStep(self): 139 cmd = "push origin %s:refs/heads/%s" % (self["push_hash"], self["version"]) 140 if self._options.dry_run: 141 print "Dry run. Command:\ngit %s" % cmd 142 else: 143 self.Git(cmd) 144 145 146class MakeBranch(Step): 147 MESSAGE = "Create the branch." 148 149 def RunStep(self): 150 self.Git("reset --hard origin/master") 151 self.Git("new-branch work-branch --upstream origin/%s" % self["version"]) 152 self.GitCheckoutFile(CHANGELOG_FILE, self["latest_version"]) 153 self.GitCheckoutFile(VERSION_FILE, self["latest_version"]) 154 self.GitCheckoutFile(WATCHLISTS_FILE, self["latest_version"]) 155 156 157class AddChangeLog(Step): 158 MESSAGE = "Add ChangeLog changes to release branch." 159 160 def RunStep(self): 161 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")) 162 old_change_log = FileToText(os.path.join(self.default_cwd, CHANGELOG_FILE)) 163 new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log) 164 TextToFile(new_change_log, os.path.join(self.default_cwd, CHANGELOG_FILE)) 165 166 167class SetVersion(Step): 168 MESSAGE = "Set correct version for candidates." 169 170 def RunStep(self): 171 self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_") 172 173 174class EnableMergeWatchlist(Step): 175 MESSAGE = "Enable watchlist entry for merge notifications." 176 177 def RunStep(self): 178 old_watchlist_content = FileToText(os.path.join(self.default_cwd, 179 WATCHLISTS_FILE)) 180 new_watchlist_content = re.sub("(# 'v8-merges@googlegroups\.com',)", 181 "'v8-merges@googlegroups.com',", 182 old_watchlist_content) 183 TextToFile(new_watchlist_content, os.path.join(self.default_cwd, 184 WATCHLISTS_FILE)) 185 186 187class CommitBranch(Step): 188 MESSAGE = "Commit version and changelog to new branch." 189 190 def RunStep(self): 191 # Convert the ChangeLog entry to commit message format. 192 text = FileToText(self.Config("CHANGELOG_ENTRY_FILE")) 193 194 # Remove date and trailing white space. 195 text = re.sub(r"^%s: " % self["date"], "", text.rstrip()) 196 197 # Remove indentation and merge paragraphs into single long lines, keeping 198 # empty lines between them. 199 def SplitMapJoin(split_text, fun, join_text): 200 return lambda text: join_text.join(map(fun, text.split(split_text))) 201 text = SplitMapJoin( 202 "\n\n", SplitMapJoin("\n", str.strip, " "), "\n\n")(text) 203 204 if not text: # pragma: no cover 205 self.Die("Commit message editing failed.") 206 text += "\n\nTBR=%s" % self._options.reviewer 207 self["commit_title"] = text.splitlines()[0] 208 TextToFile(text, self.Config("COMMITMSG_FILE")) 209 210 self.GitCommit(file_name = self.Config("COMMITMSG_FILE")) 211 os.remove(self.Config("COMMITMSG_FILE")) 212 os.remove(self.Config("CHANGELOG_ENTRY_FILE")) 213 214 215class LandBranch(Step): 216 MESSAGE = "Upload and land changes." 217 218 def RunStep(self): 219 if self._options.dry_run: 220 print "Dry run - upload CL." 221 else: 222 self.GitUpload(author=self._options.author, 223 force=True, 224 bypass_hooks=True, 225 private=True) 226 cmd = "cl land --bypass-hooks -f" 227 if self._options.dry_run: 228 print "Dry run. Command:\ngit %s" % cmd 229 else: 230 self.Git(cmd) 231 232 233class TagRevision(Step): 234 MESSAGE = "Tag the new revision." 235 236 def RunStep(self): 237 if self._options.dry_run: 238 print ("Dry run. Tagging \"%s\" with %s" % 239 (self["commit_title"], self["version"])) 240 else: 241 self.vc.Tag(self["version"], 242 "origin/%s" % self["version"], 243 self["commit_title"]) 244 245 246class CleanUp(Step): 247 MESSAGE = "Done!" 248 249 def RunStep(self): 250 print("Congratulations, you have successfully created version %s." 251 % self["version"]) 252 253 self.GitCheckout("origin/master") 254 self.DeleteBranch("work-branch") 255 self.Git("gc") 256 257 258class CreateRelease(ScriptsBase): 259 def _PrepareOptions(self, parser): 260 group = parser.add_mutually_exclusive_group() 261 group.add_argument("-f", "--force", 262 help="Don't prompt the user.", 263 default=True, action="store_true") 264 group.add_argument("-m", "--manual", 265 help="Prompt the user at every important step.", 266 default=False, action="store_true") 267 parser.add_argument("-R", "--revision", 268 help="The git commit ID to push (defaults to HEAD).") 269 270 def _ProcessOptions(self, options): # pragma: no cover 271 if not options.author or not options.reviewer: 272 print "Reviewer (-r) and author (-a) are required." 273 return False 274 return True 275 276 def _Config(self): 277 return { 278 "PERSISTFILE_BASENAME": "/tmp/create-releases-tempfile", 279 "CHANGELOG_ENTRY_FILE": 280 "/tmp/v8-create-releases-tempfile-changelog-entry", 281 "COMMITMSG_FILE": "/tmp/v8-create-releases-tempfile-commitmsg", 282 } 283 284 def _Steps(self): 285 return [ 286 Preparation, 287 PrepareBranchRevision, 288 IncrementVersion, 289 DetectLastRelease, 290 PrepareChangeLog, 291 EditChangeLog, 292 PushBranchRef, 293 MakeBranch, 294 AddChangeLog, 295 SetVersion, 296 EnableMergeWatchlist, 297 CommitBranch, 298 LandBranch, 299 TagRevision, 300 CleanUp, 301 ] 302 303 304if __name__ == "__main__": # pragma: no cover 305 sys.exit(CreateRelease().Run()) 306