v8/tools/release/create_release.py
machenbach 44bc8646c3 Create v8 roll branches in refs/heads.
This is needed as normal chromium checkouts don't fetch
branch-heads.

These branches will also be replicated by GitHub and are
fetched when cloning from the repository.

The expected object size will be the same or smaller than
before as we currently push the same information onto the
candidates branch (which already is in refs/heads). The only
difference is the increasing number of refs in refs/heads.

BUG=chromium:451975
TBR=tandrii@chromium.org
NOTRY=true
LOG=n

TEST=./script_test.py
TEST=tools/release/create_release.py -r machenbach@chromium.org -a machenbach@chromium.org --dry-run

Review URL: https://codereview.chromium.org/914973002

Cr-Commit-Position: refs/heads/master@{#26629}
2015-02-12 18:34:32 +00:00

314 lines
10 KiB
Python
Executable File

#!/usr/bin/env python
# Copyright 2015 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import os
import sys
import tempfile
import urllib2
from common_includes import *
class Preparation(Step):
MESSAGE = "Preparation."
def RunStep(self):
fetchspecs = [
"+refs/heads/*:refs/heads/*",
"+refs/pending/*:refs/pending/*",
"+refs/pending-tags/*:refs/pending-tags/*",
]
self.Git("fetch origin %s" % " ".join(fetchspecs))
self.GitCheckout("origin/master")
self.DeleteBranch("work-branch")
class PrepareBranchRevision(Step):
MESSAGE = "Check from which revision to branch off."
def RunStep(self):
if self._options.revision:
self["push_hash"], tree_object = self.GitLog(
n=1, format="\"%H %T\"", git_hash=self._options.revision).split(" ")
else:
self["push_hash"], tree_object = self.GitLog(
n=1, format="\"%H %T\"", branch="origin/master").split(" ")
print "Release revision %s" % self["push_hash"]
assert self["push_hash"]
pending_tuples = self.GitLog(
n=200, format="\"%H %T\"", branch="refs/pending/heads/master")
for hsh, tree in map(lambda s: s.split(" "), pending_tuples.splitlines()):
if tree == tree_object:
self["pending_hash"] = hsh
break
print "Pending release revision %s" % self["pending_hash"]
assert self["pending_hash"]
class IncrementVersion(Step):
MESSAGE = "Increment version number."
def RunStep(self):
latest_version = self.GetLatestVersion()
# The version file on master can be used to bump up major/minor at
# branch time.
self.GitCheckoutFile(VERSION_FILE, self.vc.RemoteMasterBranch())
self.ReadAndPersistVersion("master_")
master_version = self.ArrayToVersion("master_")
# Use the highest version from master or from tags to determine the new
# version.
authoritative_version = sorted(
[master_version, latest_version], key=SortingKey)[1]
self.StoreVersion(authoritative_version, "authoritative_")
# Variables prefixed with 'new_' contain the new version numbers for the
# ongoing candidates push.
self["new_major"] = self["authoritative_major"]
self["new_minor"] = self["authoritative_minor"]
self["new_build"] = str(int(self["authoritative_build"]) + 1)
# Make sure patch level is 0 in a new push.
self["new_patch"] = "0"
# The new version is not a candidate.
self["new_candidate"] = "0"
self["version"] = "%s.%s.%s" % (self["new_major"],
self["new_minor"],
self["new_build"])
print ("Incremented version to %s" % self["version"])
class DetectLastRelease(Step):
MESSAGE = "Detect commit ID of last release base."
def RunStep(self):
self["last_push_master"] = self.GetLatestReleaseBase()
class PrepareChangeLog(Step):
MESSAGE = "Prepare raw ChangeLog entry."
def Reload(self, body):
"""Attempts to reload the commit message from rietveld in order to allow
late changes to the LOG flag. Note: This is brittle to future changes of
the web page name or structure.
"""
match = re.search(r"^Review URL: https://codereview\.chromium\.org/(\d+)$",
body, flags=re.M)
if match:
cl_url = ("https://codereview.chromium.org/%s/description"
% match.group(1))
try:
# Fetch from Rietveld but only retry once with one second delay since
# there might be many revisions.
body = self.ReadURL(cl_url, wait_plan=[1])
except urllib2.URLError: # pragma: no cover
pass
return body
def RunStep(self):
self["date"] = self.GetDate()
output = "%s: Version %s\n\n" % (self["date"], self["version"])
TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE"))
commits = self.GitLog(format="%H",
git_hash="%s..%s" % (self["last_push_master"],
self["push_hash"]))
# Cache raw commit messages.
commit_messages = [
[
self.GitLog(n=1, format="%s", git_hash=commit),
self.Reload(self.GitLog(n=1, format="%B", git_hash=commit)),
self.GitLog(n=1, format="%an", git_hash=commit),
] for commit in commits.splitlines()
]
# Auto-format commit messages.
body = MakeChangeLogBody(commit_messages, auto_format=True)
AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE"))
msg = (" Performance and stability improvements on all platforms."
"\n#\n# The change log above is auto-generated. Please review if "
"all relevant\n# commit messages from the list below are included."
"\n# All lines starting with # will be stripped.\n#\n")
AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE"))
# Include unformatted commit messages as a reference in a comment.
comment_body = MakeComment(MakeChangeLogBody(commit_messages))
AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE"))
class EditChangeLog(Step):
MESSAGE = "Edit ChangeLog entry."
def RunStep(self):
print ("Please press <Return> to have your EDITOR open the ChangeLog "
"entry, then edit its contents to your liking. When you're done, "
"save the file and exit your EDITOR. ")
self.ReadLine(default="")
self.Editor(self.Config("CHANGELOG_ENTRY_FILE"))
# Strip comments and reformat with correct indentation.
changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip()
changelog_entry = StripComments(changelog_entry)
changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines()))
changelog_entry = changelog_entry.lstrip()
if changelog_entry == "": # pragma: no cover
self.Die("Empty ChangeLog entry.")
# Safe new change log for adding it later to the candidates patch.
TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE"))
class MakeBranch(Step):
MESSAGE = "Create the branch."
def RunStep(self):
self.Git("reset --hard origin/master")
self.Git("checkout -b work-branch %s" % self["pending_hash"])
self.GitCheckoutFile(CHANGELOG_FILE, self["latest_version"])
self.GitCheckoutFile(VERSION_FILE, self["latest_version"])
class AddChangeLog(Step):
MESSAGE = "Add ChangeLog changes to release branch."
def RunStep(self):
changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
old_change_log = FileToText(os.path.join(self.default_cwd, CHANGELOG_FILE))
new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log)
TextToFile(new_change_log, os.path.join(self.default_cwd, CHANGELOG_FILE))
class SetVersion(Step):
MESSAGE = "Set correct version for candidates."
def RunStep(self):
self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_")
class CommitBranch(Step):
MESSAGE = "Commit version and changelog to new branch."
def RunStep(self):
# Convert the ChangeLog entry to commit message format.
text = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
# Remove date and trailing white space.
text = re.sub(r"^%s: " % self["date"], "", text.rstrip())
# Remove indentation and merge paragraphs into single long lines, keeping
# empty lines between them.
def SplitMapJoin(split_text, fun, join_text):
return lambda text: join_text.join(map(fun, text.split(split_text)))
text = SplitMapJoin(
"\n\n", SplitMapJoin("\n", str.strip, " "), "\n\n")(text)
if not text: # pragma: no cover
self.Die("Commit message editing failed.")
self["commit_title"] = text.splitlines()[0]
TextToFile(text, self.Config("COMMITMSG_FILE"))
self.GitCommit(file_name = self.Config("COMMITMSG_FILE"))
os.remove(self.Config("COMMITMSG_FILE"))
os.remove(self.Config("CHANGELOG_ENTRY_FILE"))
class PushBranch(Step):
MESSAGE = "Push changes."
def RunStep(self):
pushspecs = [
"refs/heads/work-branch:refs/pending/heads/%s" % self["version"],
"%s:refs/pending-tags/heads/%s" %
(self["pending_hash"], self["version"]),
"%s:refs/heads/%s" % (self["push_hash"], self["version"]),
]
cmd = "push origin %s" % " ".join(pushspecs)
if self._options.dry_run:
print "Dry run. Command:\ngit %s" % cmd
else:
self.Git(cmd)
class TagRevision(Step):
MESSAGE = "Tag the new revision."
def RunStep(self):
if self._options.dry_run:
print ("Dry run. Tagging \"%s\" with %s" %
(self["commit_title"], self["version"]))
else:
self.vc.Tag(self["version"],
"origin/%s" % self["version"],
self["commit_title"])
class CleanUp(Step):
MESSAGE = "Done!"
def RunStep(self):
print("Congratulations, you have successfully created version %s."
% self["version"])
self.GitCheckout("origin/master")
self.DeleteBranch("work-branch")
self.Git("gc")
class CreateRelease(ScriptsBase):
def _PrepareOptions(self, parser):
group = parser.add_mutually_exclusive_group()
group.add_argument("-f", "--force",
help="Don't prompt the user.",
default=True, action="store_true")
group.add_argument("-m", "--manual",
help="Prompt the user at every important step.",
default=False, action="store_true")
parser.add_argument("-R", "--revision",
help="The git commit ID to push (defaults to HEAD).")
def _ProcessOptions(self, options): # pragma: no cover
if not options.author or not options.reviewer:
print "Reviewer (-r) and author (-a) are required."
return False
return True
def _Config(self):
return {
"PERSISTFILE_BASENAME": "/tmp/create-releases-tempfile",
"CHANGELOG_ENTRY_FILE":
"/tmp/v8-create-releases-tempfile-changelog-entry",
"COMMITMSG_FILE": "/tmp/v8-create-releases-tempfile-commitmsg",
}
def _Steps(self):
return [
Preparation,
PrepareBranchRevision,
IncrementVersion,
DetectLastRelease,
PrepareChangeLog,
EditChangeLog,
MakeBranch,
AddChangeLog,
SetVersion,
CommitBranch,
PushBranch,
TagRevision,
CleanUp,
]
if __name__ == "__main__": # pragma: no cover
sys.exit(CreateRelease().Run())