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 Reload(self, body): 81 """Attempts to reload the commit message from rietveld in order to allow 82 late changes to the LOG flag. Note: This is brittle to future changes of 83 the web page name or structure. 84 """ 85 match = re.search(r"^Review URL: https://codereview\.chromium\.org/(\d+)$", 86 body, flags=re.M) 87 if match: 88 cl_url = ("https://codereview.chromium.org/%s/description" 89 % match.group(1)) 90 try: 91 # Fetch from Rietveld but only retry once with one second delay since 92 # there might be many revisions. 93 body = self.ReadURL(cl_url, wait_plan=[1]) 94 except urllib2.URLError: # pragma: no cover 95 pass 96 return body 97 98 def RunStep(self): 99 self["date"] = self.GetDate() 100 output = "%s: Version %s\n\n" % (self["date"], self["version"]) 101 TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE")) 102 commits = self.GitLog(format="%H", 103 git_hash="%s..%s" % (self["last_push_master"], 104 self["push_hash"])) 105 106 # Cache raw commit messages. 107 commit_messages = [ 108 [ 109 self.GitLog(n=1, format="%s", git_hash=commit), 110 self.Reload(self.GitLog(n=1, format="%B", git_hash=commit)), 111 self.GitLog(n=1, format="%an", git_hash=commit), 112 ] for commit in commits.splitlines() 113 ] 114 115 # Auto-format commit messages. 116 body = MakeChangeLogBody(commit_messages, auto_format=True) 117 AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE")) 118 119 msg = (" Performance and stability improvements on all platforms." 120 "\n#\n# The change log above is auto-generated. Please review if " 121 "all relevant\n# commit messages from the list below are included." 122 "\n# All lines starting with # will be stripped.\n#\n") 123 AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE")) 124 125 # Include unformatted commit messages as a reference in a comment. 126 comment_body = MakeComment(MakeChangeLogBody(commit_messages)) 127 AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE")) 128 129 130class EditChangeLog(Step): 131 MESSAGE = "Edit ChangeLog entry." 132 133 def RunStep(self): 134 print ("Please press <Return> to have your EDITOR open the ChangeLog " 135 "entry, then edit its contents to your liking. When you're done, " 136 "save the file and exit your EDITOR. ") 137 self.ReadLine(default="") 138 self.Editor(self.Config("CHANGELOG_ENTRY_FILE")) 139 140 # Strip comments and reformat with correct indentation. 141 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip() 142 changelog_entry = StripComments(changelog_entry) 143 changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines())) 144 changelog_entry = changelog_entry.lstrip() 145 146 if changelog_entry == "": # pragma: no cover 147 self.Die("Empty ChangeLog entry.") 148 149 # Safe new change log for adding it later to the candidates patch. 150 TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE")) 151 152 153class PushBranchRef(Step): 154 MESSAGE = "Create branch ref." 155 156 def RunStep(self): 157 cmd = "push origin %s:refs/heads/%s" % (self["push_hash"], self["version"]) 158 if self._options.dry_run: 159 print "Dry run. Command:\ngit %s" % cmd 160 else: 161 self.Git(cmd) 162 163 164class MakeBranch(Step): 165 MESSAGE = "Create the branch." 166 167 def RunStep(self): 168 self.Git("reset --hard origin/master") 169 self.Git("new-branch work-branch --upstream origin/%s" % self["version"]) 170 self.GitCheckoutFile(CHANGELOG_FILE, self["latest_version"]) 171 self.GitCheckoutFile(VERSION_FILE, self["latest_version"]) 172 self.GitCheckoutFile(WATCHLISTS_FILE, self["latest_version"]) 173 174 175class AddChangeLog(Step): 176 MESSAGE = "Add ChangeLog changes to release branch." 177 178 def RunStep(self): 179 changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")) 180 old_change_log = FileToText(os.path.join(self.default_cwd, CHANGELOG_FILE)) 181 new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log) 182 TextToFile(new_change_log, os.path.join(self.default_cwd, CHANGELOG_FILE)) 183 184 185class SetVersion(Step): 186 MESSAGE = "Set correct version for candidates." 187 188 def RunStep(self): 189 self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_") 190 191 192class EnableMergeWatchlist(Step): 193 MESSAGE = "Enable watchlist entry for merge notifications." 194 195 def RunStep(self): 196 old_watchlist_content = FileToText(os.path.join(self.default_cwd, 197 WATCHLISTS_FILE)) 198 new_watchlist_content = re.sub("(# 'v8-merges@googlegroups\.com',)", 199 "'v8-merges@googlegroups.com',", 200 old_watchlist_content) 201 TextToFile(new_watchlist_content, os.path.join(self.default_cwd, 202 WATCHLISTS_FILE)) 203 204 205class CommitBranch(Step): 206 MESSAGE = "Commit version and changelog to new branch." 207 208 def RunStep(self): 209 # Convert the ChangeLog entry to commit message format. 210 text = FileToText(self.Config("CHANGELOG_ENTRY_FILE")) 211 212 # Remove date and trailing white space. 213 text = re.sub(r"^%s: " % self["date"], "", text.rstrip()) 214 215 # Remove indentation and merge paragraphs into single long lines, keeping 216 # empty lines between them. 217 def SplitMapJoin(split_text, fun, join_text): 218 return lambda text: join_text.join(map(fun, text.split(split_text))) 219 text = SplitMapJoin( 220 "\n\n", SplitMapJoin("\n", str.strip, " "), "\n\n")(text) 221 222 if not text: # pragma: no cover 223 self.Die("Commit message editing failed.") 224 self["commit_title"] = text.splitlines()[0] 225 TextToFile(text, self.Config("COMMITMSG_FILE")) 226 227 self.GitCommit(file_name = self.Config("COMMITMSG_FILE")) 228 os.remove(self.Config("COMMITMSG_FILE")) 229 os.remove(self.Config("CHANGELOG_ENTRY_FILE")) 230 231 232class PushBranch(Step): 233 MESSAGE = "Push changes." 234 235 def RunStep(self): 236 cmd = "cl land --bypass-hooks -f" 237 if self._options.dry_run: 238 print "Dry run. Command:\ngit %s" % cmd 239 else: 240 self.Git(cmd) 241 242 243class TagRevision(Step): 244 MESSAGE = "Tag the new revision." 245 246 def RunStep(self): 247 if self._options.dry_run: 248 print ("Dry run. Tagging \"%s\" with %s" % 249 (self["commit_title"], self["version"])) 250 else: 251 self.vc.Tag(self["version"], 252 "origin/%s" % self["version"], 253 self["commit_title"]) 254 255 256class CleanUp(Step): 257 MESSAGE = "Done!" 258 259 def RunStep(self): 260 print("Congratulations, you have successfully created version %s." 261 % self["version"]) 262 263 self.GitCheckout("origin/master") 264 self.DeleteBranch("work-branch") 265 self.Git("gc") 266 267 268class CreateRelease(ScriptsBase): 269 def _PrepareOptions(self, parser): 270 group = parser.add_mutually_exclusive_group() 271 group.add_argument("-f", "--force", 272 help="Don't prompt the user.", 273 default=True, action="store_true") 274 group.add_argument("-m", "--manual", 275 help="Prompt the user at every important step.", 276 default=False, action="store_true") 277 parser.add_argument("-R", "--revision", 278 help="The git commit ID to push (defaults to HEAD).") 279 280 def _ProcessOptions(self, options): # pragma: no cover 281 if not options.author or not options.reviewer: 282 print "Reviewer (-r) and author (-a) are required." 283 return False 284 return True 285 286 def _Config(self): 287 return { 288 "PERSISTFILE_BASENAME": "/tmp/create-releases-tempfile", 289 "CHANGELOG_ENTRY_FILE": 290 "/tmp/v8-create-releases-tempfile-changelog-entry", 291 "COMMITMSG_FILE": "/tmp/v8-create-releases-tempfile-commitmsg", 292 } 293 294 def _Steps(self): 295 return [ 296 Preparation, 297 PrepareBranchRevision, 298 IncrementVersion, 299 DetectLastRelease, 300 PrepareChangeLog, 301 EditChangeLog, 302 PushBranchRef, 303 MakeBranch, 304 AddChangeLog, 305 SetVersion, 306 EnableMergeWatchlist, 307 CommitBranch, 308 PushBranch, 309 TagRevision, 310 CleanUp, 311 ] 312 313 314if __name__ == "__main__": # pragma: no cover 315 sys.exit(CreateRelease().Run()) 316