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 fetchspecs = [ 19 "+refs/heads/*:refs/heads/*", 20 "+refs/pending/*:refs/pending/*", 21 "+refs/pending-tags/*:refs/pending-tags/*", 22 ] 23 self.Git("fetch origin %s" % " ".join(fetchspecs)) 24 self.GitCheckout("origin/master") 25 self.DeleteBranch("work-branch") 26 27 28class PrepareBranchRevision(Step): 29 MESSAGE = "Check from which revision to branch off." 30 31 def RunStep(self): 32 self["push_hash"] = (self._options.revision or 33 self.GitLog(n=1, format="%H", branch="origin/master")) 34 assert self["push_hash"] 35 print "Release revision %s" % self["push_hash"] 36 37 38class IncrementVersion(Step): 39 MESSAGE = "Increment version number." 40 41 def RunStep(self): 42 latest_version = self.GetLatestVersion() 43 44 # The version file on master can be used to bump up major/minor at 45 # branch time. 46 self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteMasterBranch()) 47 self.ReadAndPersistVersion("master_") 48 master_version = self.ArrayToVersion("master_") 49 50 # Use the highest version from master or from tags to determine the new 51 # version. 52 authoritative_version = sorted( 53 [master_version, latest_version], key=SortingKey)[1] 54 self.StoreVersion(authoritative_version, "authoritative_") 55 56 # Variables prefixed with 'new_' contain the new version numbers for the 57 # ongoing candidates push. 58 self["new_major"] = self["authoritative_major"] 59 self["new_minor"] = self["authoritative_minor"] 60 self["new_build"] = str(int(self["authoritative_build"]) + 1) 61 62 # Make sure patch level is 0 in a new push. 63 self["new_patch"] = "0" 64 65 # The new version is not a candidate. 66 self["new_candidate"] = "0" 67 68 self["version"] = "%s.%s.%s" % (self["new_major"], 69 self["new_minor"], 70 self["new_build"]) 71 72 print ("Incremented version to %s" % self["version"]) 73 74 75class DetectLastRelease(Step): 76 MESSAGE = "Detect commit ID of last release base." 77 78 def RunStep(self): 79 self["last_push_master"] = self.GetLatestReleaseBase() 80 81 82class PrepareChangeLog(Step): 83 MESSAGE = "Prepare raw ChangeLog entry." 84 85 def Reload(self, body): 86 """Attempts to reload the commit message from rietveld in order to allow 87 late changes to the LOG flag. Note: This is brittle to future changes of 88 the web page name or structure. 89 """ 90 match = re.search(r"^Review URL: https://codereview\.chromium\.org/(\d+)$", 91 body, flags=re.M) 92 if match: 93 cl_url = ("https://codereview.chromium.org/%s/description" 94 % match.group(1)) 95 try: 96 # Fetch from Rietveld but only retry once with one second delay since 97 # there might be many revisions. 98 body = self.ReadURL(cl_url, wait_plan=[1]) 99 except urllib2.URLError: # pragma: no cover 100 pass 101 return body 102 103 def RunStep(self): 104 self["date"] = self.GetDate() 105 output = "%s: Version %s\n\n" % (self["date"], self["version"]) 106 TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE")) 107 commits = self.GitLog(format="%H", 108 git_hash="%s..%s" % (self["last_push_master"], 109 self["push_hash"])) 110 111 # Cache raw commit messages. 112 commit_messages = [ 113 [ 114 self.GitLog(n=1, format="%s", git_hash=commit), 115 self.Reload(self.GitLog(n=1, format="%B", git_hash=commit)), 116 self.GitLog(n=1, format="%an", git_hash=commit), 117 ] for commit in commits.splitlines() 118 ] 119 120 # Auto-format commit messages. 121 body = MakeChangeLogBody(commit_messages, auto_format=True) 122 AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE")) 123 124 msg = (" Performance and stability improvements on all platforms." 125 "\n#\n# The change log above is auto-generated. Please review if " 126 "all relevant\n# commit messages from the list below are included." 127 "\n# All lines starting with # will be stripped.\n#\n") 128 AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE")) 129 130 # Include unformatted commit messages as a reference in a comment. 131 comment_body = MakeComment(MakeChangeLogBody(commit_messages)) 132 AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE")) 133 134 135class EditChangeLog(Step): 136 MESSAGE = "Edit ChangeLog entry." 137 138 def RunStep(self): 139 print ("Please press <Return> to have your EDITOR open the ChangeLog " 140 "entry, then edit its contents to your liking. When you're done, " 141 "save the file and exit your EDITOR. ") 142 self.ReadLine(default="") 143 self.Editor(self.Config("CHANGELOG_ENTRY_FILE")) 144 145 # Strip comments and reformat with correct indentation. 146 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip() 147 changelog_entry = StripComments(changelog_entry) 148 changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines())) 149 changelog_entry = changelog_entry.lstrip() 150 151 if changelog_entry == "": # pragma: no cover 152 self.Die("Empty ChangeLog entry.") 153 154 # Safe new change log for adding it later to the candidates patch. 155 TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE")) 156 157 158class MakeBranch(Step): 159 MESSAGE = "Create the branch." 160 161 def RunStep(self): 162 self.Git("reset --hard origin/master") 163 self.Git("checkout -b work-branch %s" % self["push_hash"]) 164 self.GitCheckoutFile(CHANGELOG_FILE, self["latest_version"]) 165 self.GitCheckoutFile(VERSION_FILE, self["latest_version"]) 166 self.GitCheckoutFile(WATCHLISTS_FILE, self["latest_version"]) 167 168 169class AddChangeLog(Step): 170 MESSAGE = "Add ChangeLog changes to release branch." 171 172 def RunStep(self): 173 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")) 174 old_change_log = FileToText(os.path.join(self.default_cwd, CHANGELOG_FILE)) 175 new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log) 176 TextToFile(new_change_log, os.path.join(self.default_cwd, CHANGELOG_FILE)) 177 178 179class SetVersion(Step): 180 MESSAGE = "Set correct version for candidates." 181 182 def RunStep(self): 183 self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_") 184 185 186class EnableMergeWatchlist(Step): 187 MESSAGE = "Enable watchlist entry for merge notifications." 188 189 def RunStep(self): 190 old_watchlist_content = FileToText(os.path.join(self.default_cwd, 191 WATCHLISTS_FILE)) 192 new_watchlist_content = re.sub("(# 'v8-merges@googlegroups\.com',)", 193 "'v8-merges@googlegroups.com',", 194 old_watchlist_content) 195 TextToFile(new_watchlist_content, os.path.join(self.default_cwd, 196 WATCHLISTS_FILE)) 197 198 199class CommitBranch(Step): 200 MESSAGE = "Commit version and changelog to new branch." 201 202 def RunStep(self): 203 # Convert the ChangeLog entry to commit message format. 204 text = FileToText(self.Config("CHANGELOG_ENTRY_FILE")) 205 206 # Remove date and trailing white space. 207 text = re.sub(r"^%s: " % self["date"], "", text.rstrip()) 208 209 # Remove indentation and merge paragraphs into single long lines, keeping 210 # empty lines between them. 211 def SplitMapJoin(split_text, fun, join_text): 212 return lambda text: join_text.join(map(fun, text.split(split_text))) 213 text = SplitMapJoin( 214 "\n\n", SplitMapJoin("\n", str.strip, " "), "\n\n")(text) 215 216 if not text: # pragma: no cover 217 self.Die("Commit message editing failed.") 218 self["commit_title"] = text.splitlines()[0] 219 TextToFile(text, self.Config("COMMITMSG_FILE")) 220 221 self.GitCommit(file_name = self.Config("COMMITMSG_FILE")) 222 os.remove(self.Config("COMMITMSG_FILE")) 223 os.remove(self.Config("CHANGELOG_ENTRY_FILE")) 224 225 226class FixBrokenTag(Step): 227 MESSAGE = "Check for a missing tag and fix that instead." 228 229 def RunStep(self): 230 commit = None 231 try: 232 commit = self.GitLog( 233 n=1, format="%H", 234 grep=self["commit_title"], 235 branch="origin/%s" % self["version"], 236 ) 237 except GitFailedException: 238 # In the normal case, the remote doesn't exist yet and git will fail. 239 pass 240 if commit: 241 print "Found %s. Trying to repair tag and bail out." % self["version"] 242 self.Git("tag %s %s" % (self["version"], commit)) 243 self.Git("push origin refs/tags/%s" % self["version"]) 244 return True 245 246 247class PushBranch(Step): 248 MESSAGE = "Push changes." 249 250 def RunStep(self): 251 pushspecs = [ 252 "refs/heads/work-branch:refs/pending/heads/%s" % self["version"], 253 "%s:refs/pending-tags/heads/%s" % (self["push_hash"], self["version"]), 254 "%s:refs/heads/%s" % (self["push_hash"], self["version"]), 255 ] 256 cmd = "push origin %s" % " ".join(pushspecs) 257 if self._options.dry_run: 258 print "Dry run. Command:\ngit %s" % cmd 259 else: 260 self.Git(cmd) 261 262 263class TagRevision(Step): 264 MESSAGE = "Tag the new revision." 265 266 def RunStep(self): 267 if self._options.dry_run: 268 print ("Dry run. Tagging \"%s\" with %s" % 269 (self["commit_title"], self["version"])) 270 else: 271 self.vc.Tag(self["version"], 272 "origin/%s" % self["version"], 273 self["commit_title"]) 274 275 276class CleanUp(Step): 277 MESSAGE = "Done!" 278 279 def RunStep(self): 280 print("Congratulations, you have successfully created version %s." 281 % self["version"]) 282 283 self.GitCheckout("origin/master") 284 self.DeleteBranch("work-branch") 285 self.Git("gc") 286 287 288class CreateRelease(ScriptsBase): 289 def _PrepareOptions(self, parser): 290 group = parser.add_mutually_exclusive_group() 291 group.add_argument("-f", "--force", 292 help="Don't prompt the user.", 293 default=True, action="store_true") 294 group.add_argument("-m", "--manual", 295 help="Prompt the user at every important step.", 296 default=False, action="store_true") 297 parser.add_argument("-R", "--revision", 298 help="The git commit ID to push (defaults to HEAD).") 299 300 def _ProcessOptions(self, options): # pragma: no cover 301 if not options.author or not options.reviewer: 302 print "Reviewer (-r) and author (-a) are required." 303 return False 304 return True 305 306 def _Config(self): 307 return { 308 "PERSISTFILE_BASENAME": "/tmp/create-releases-tempfile", 309 "CHANGELOG_ENTRY_FILE": 310 "/tmp/v8-create-releases-tempfile-changelog-entry", 311 "COMMITMSG_FILE": "/tmp/v8-create-releases-tempfile-commitmsg", 312 } 313 314 def _Steps(self): 315 return [ 316 Preparation, 317 PrepareBranchRevision, 318 IncrementVersion, 319 DetectLastRelease, 320 PrepareChangeLog, 321 EditChangeLog, 322 MakeBranch, 323 AddChangeLog, 324 SetVersion, 325 EnableMergeWatchlist, 326 CommitBranch, 327 FixBrokenTag, 328 PushBranch, 329 TagRevision, 330 CleanUp, 331 ] 332 333 334if __name__ == "__main__": # pragma: no cover 335 sys.exit(CreateRelease().Run()) 336