Add new script to create release branches.
authormachenbach <machenbach@chromium.org>
Fri, 30 Jan 2015 16:46:41 +0000 (08:46 -0800)
committerCommit bot <commit-bot@chromium.org>
Fri, 30 Jan 2015 16:46:49 +0000 (16:46 +0000)
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/881683004

Cr-Commit-Position: refs/heads/master@{#26364}

tools/release/common_includes.py
tools/release/create_release.py [new file with mode: 0755]
tools/release/test_scripts.py

index 0857ea0..ee8c0f7 100644 (file)
@@ -682,6 +682,9 @@ class Step(GitRecipesMixin):
         line = re.sub("\d+$", self[prefix + "build"], line)
       elif line.startswith("#define PATCH_LEVEL"):
         line = re.sub("\d+$", self[prefix + "patch"], line)
+      elif (self[prefix + "candidate"] and
+            line.startswith("#define IS_CANDIDATE_VERSION")):
+        line = re.sub("\d+$", self[prefix + "candidate"], line)
       output += "%s\n" % line
     TextToFile(output, version_file)
 
diff --git a/tools/release/create_release.py b/tools/release/create_release.py
new file mode 100755 (executable)
index 0000000..0ae7f96
--- /dev/null
@@ -0,0 +1,311 @@
+#!/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):
+    self.Git("fetch origin +refs/heads/*:refs/heads/*")
+    self.Git("fetch origin +refs/branch-heads/*:refs/branch-heads/*")
+    self.Git("fetch origin +refs/pending/*:refs/pending/*")
+    self.Git("fetch origin +refs/pending-tags/*:refs/pending-tags/*")
+    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/branch-heads/%s" % self["version"],
+      "%s:refs/pending-tags/branch-heads/%s" %
+      (self["pending_hash"], self["version"]),
+      "%s:refs/branch-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"],
+                  "branch-heads/%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())
index 21364d3..1ea69a8 100644 (file)
@@ -37,6 +37,8 @@ from auto_push import LastReleaseBailout
 import auto_roll
 import common_includes
 from common_includes import *
+import create_release
+from create_release import CreateRelease
 import merge_to_branch
 from merge_to_branch import *
 import push_to_candidates
@@ -860,6 +862,115 @@ Performance and stability improvements on all platforms."""
   def testPushToCandidatesForced(self):
     self._PushToCandidates(force=True)
 
+  def testCreateRelease(self):
+    TextToFile("", os.path.join(TEST_CONFIG["DEFAULT_CWD"], ".git"))
+
+    # The version file on master has build level 5.
+    self.WriteFakeVersionFile(build=5)
+
+    master_change_log = "2014-03-17: Sentinel\n"
+    TextToFile(master_change_log,
+               os.path.join(TEST_CONFIG["DEFAULT_CWD"], CHANGELOG_FILE))
+
+    commit_msg = """Version 3.22.5
+
+Log text 1 (issue 321).
+
+Performance and stability improvements on all platforms."""
+
+    def ResetChangeLog():
+      last_change_log = """1999-04-05: Version 3.22.4
+
+        Performance and stability improvements on all platforms.\n"""
+      TextToFile(last_change_log,
+                 os.path.join(TEST_CONFIG["DEFAULT_CWD"], CHANGELOG_FILE))
+
+
+    def CheckVersionCommit():
+      commit = FileToText(TEST_CONFIG["COMMITMSG_FILE"])
+      self.assertEquals(commit_msg, commit)
+      version = FileToText(
+          os.path.join(TEST_CONFIG["DEFAULT_CWD"], VERSION_FILE))
+      self.assertTrue(re.search(r"#define MINOR_VERSION\s+22", version))
+      self.assertTrue(re.search(r"#define BUILD_NUMBER\s+5", version))
+      self.assertFalse(re.search(r"#define BUILD_NUMBER\s+6", version))
+      self.assertTrue(re.search(r"#define PATCH_LEVEL\s+0", version))
+      self.assertTrue(re.search(r"#define IS_CANDIDATE_VERSION\s+0", version))
+
+      # Check that the change log on the candidates branch got correctly
+      # modified.
+      change_log = FileToText(
+          os.path.join(TEST_CONFIG["DEFAULT_CWD"], CHANGELOG_FILE))
+      self.assertEquals(
+"""1999-07-31: Version 3.22.5
+
+        Log text 1 (issue 321).
+
+        Performance and stability improvements on all platforms.
+
+
+1999-04-05: Version 3.22.4
+
+        Performance and stability improvements on all platforms.\n""",
+          change_log)
+
+    expectations = [
+      Cmd("git fetch origin +refs/heads/*:refs/heads/*", ""),
+      Cmd("git fetch origin +refs/branch-heads/*:refs/branch-heads/*", ""),
+      Cmd("git fetch origin +refs/pending/*:refs/pending/*", ""),
+      Cmd("git fetch origin +refs/pending-tags/*:refs/pending-tags/*", ""),
+      Cmd("git checkout -f origin/master", ""),
+      Cmd("git branch", ""),
+      Cmd("git log -1 --format=\"%H %T\" push_hash", "push_hash tree_hash"),
+      Cmd("git log -200 --format=\"%H %T\" refs/pending/heads/master",
+          "not_right wrong\npending_hash tree_hash\nsome other\n"),
+      Cmd("git fetch origin +refs/tags/*:refs/tags/*", ""),
+      Cmd("git tag", self.TAGS),
+      Cmd("git checkout -f origin/master -- src/version.cc",
+          "", cb=self.WriteFakeVersionFile),
+      Cmd("git log -1 --format=%H 3.22.4", "release_hash\n"),
+      Cmd("git log -1 --format=%s release_hash",
+          "Version 3.22.4 (based on abc3)\n"),
+      Cmd("git log --format=%H abc3..push_hash", "rev1\n"),
+      Cmd("git log -1 --format=%s rev1", "Log text 1.\n"),
+      Cmd("git log -1 --format=%B rev1", "Text\nLOG=YES\nBUG=v8:321\nText\n"),
+      Cmd("git log -1 --format=%an rev1", "author1@chromium.org\n"),
+      Cmd("git reset --hard origin/master", ""),
+      Cmd("git checkout -b work-branch pending_hash", ""),
+      Cmd("git checkout -f 3.22.4 -- ChangeLog", "", cb=ResetChangeLog),
+      Cmd("git checkout -f 3.22.4 -- src/version.cc", "",
+          cb=self.WriteFakeVersionFile),
+      Cmd("git commit -aF \"%s\"" % TEST_CONFIG["COMMITMSG_FILE"], "",
+          cb=CheckVersionCommit),
+      Cmd("git push origin "
+          "refs/heads/work-branch:refs/pending/branch-heads/3.22.5 "
+          "pending_hash:refs/pending-tags/branch-heads/3.22.5 "
+          "push_hash:refs/branch-heads/3.22.5", ""),
+      Cmd("git fetch", ""),
+      Cmd("git log -1 --format=%H --grep="
+          "\"Version 3.22.5\" branch-heads/3.22.5", "hsh_to_tag"),
+      Cmd("git tag 3.22.5 hsh_to_tag", ""),
+      Cmd("git push origin 3.22.5", ""),
+      Cmd("git checkout -f origin/master", ""),
+      Cmd("git branch", "* master\n  work-branch\n"),
+      Cmd("git branch -D work-branch", ""),
+      Cmd("git gc", ""),
+    ]
+    self.Expect(expectations)
+
+    args = ["-a", "author@chromium.org",
+            "-r", "reviewer@chromium.org",
+            "--revision", "push_hash"]
+    CreateRelease(TEST_CONFIG, self).Run(args)
+
+    cl = FileToText(os.path.join(TEST_CONFIG["DEFAULT_CWD"], CHANGELOG_FILE))
+    self.assertTrue(re.search(r"^\d\d\d\d\-\d+\-\d+: Version 3\.22\.5", cl))
+    self.assertTrue(re.search(r"        Log text 1 \(issue 321\).", cl))
+    self.assertTrue(re.search(r"1999\-04\-05: Version 3\.22\.4", cl))
+
+    # Note: The version file is on build number 5 again in the end of this test
+    # since the git command that merges to master is mocked out.
+
   C_V8_22624_LOG = """V8 CL.
 
 git-svn-id: https://v8.googlecode.com/svn/branches/bleeding_edge@22624 123