Update release script to create new branch

Change-Id: I74215ccdb102c29b823a361d1bba471a2f9eb315
diff --git a/tools/r8_release.py b/tools/r8_release.py
index 0048d52..0f129ec 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -80,20 +80,7 @@
         version_diff_output = subprocess.check_output([
           'git', 'diff', '%s..HEAD' % commithash])
 
-        invalid = version_change_diff(version_diff_output, "master", version)
-        if invalid:
-          print "Unexpected diff:"
-          print "=" * 80
-          print version_diff_output
-          print "=" * 80
-          accept_string = 'THE DIFF IS OK!'
-          input = raw_input(
-            "Accept the additonal diff as part of the release? "
-            "Type '%s' to accept: " % accept_string)
-          if input != accept_string:
-            print "You did not type '%s'" % accept_string
-            print 'Aborting dev release for %s' % version
-            sys.exit(1)
+        validate_version_change_diff(version_diff_output, "master", version)
 
         # Double check that we want to push the release.
         if not args.dry_run:
@@ -104,10 +91,7 @@
 
         maybe_check_call(args, [
           'git', 'push', 'origin', 'HEAD:%s' % R8_DEV_BRANCH])
-        maybe_check_call(args, [
-          'git', 'tag', '-a', version, '-m', '"%s"' % version])
-        maybe_check_call(args, [
-          'git', 'push', 'origin', 'refs/tags/%s' % version])
+        maybe_tag(args, version)
 
         return "%s dev version %s from hash %s" % (
           'DryRun: omitted publish of' if args.dry_run else 'Published',
@@ -117,6 +101,12 @@
   return make_release
 
 
+def maybe_tag(args, version):
+  maybe_check_call(args, [
+    'git', 'tag', '-a', version, '-m', '"%s"' % version])
+  maybe_check_call(args, [
+    'git', 'push', 'origin', 'refs/tags/%s' % version])
+
 def version_change_diff(diff, old_version, new_version):
   invalid_line = None
   for line in diff.splitlines():
@@ -128,6 +118,22 @@
       invalid_line = line
   return invalid_line
 
+def validate_version_change_diff(version_diff_output, old_version, new_version):
+  invalid = version_change_diff(version_diff_output, old_version, new_version)
+  if invalid:
+    print "Unexpected diff:"
+    print "=" * 80
+    print version_diff_output
+    print "=" * 80
+    accept_string = 'THE DIFF IS OK!'
+    input = raw_input(
+      "Accept the additonal diff as part of the release? "
+      "Type '%s' to accept: " % accept_string)
+    if input != accept_string:
+      print "You did not type '%s'" % accept_string
+      print 'Aborting dev release for %s' % version
+      sys.exit(1)
+
 
 def maybe_check_call(args, cmd):
   if args.dry_run:
@@ -344,6 +350,62 @@
   return release_google3
 
 
+def prepare_branch(args):
+  branch_version = args.new_dev_branch[0]
+  commithash = args.new_dev_branch[1]
+
+  current_semver = utils.check_basic_semver_version(
+    R8_DEV_BRANCH, ", current release branch version should be x.y", 2)
+  semver = utils.check_basic_semver_version(
+    branch_version, ", release branch version should be x.y", 2)
+  if not semver.larger_than(current_semver):
+    print ('New branch version "'
+      + branch_version
+      + '" must be strictly larger than the current "'
+      + R8_DEV_BRANCH
+      + '"')
+    sys.exit(1)
+
+  def make_branch(options):
+    subprocess.check_call(['git', 'branch', branch_version, commithash])
+
+    subprocess.check_call(['git', 'checkout', branch_version])
+
+    # Rewrite the version, commit and validate.
+    old_version = 'master'
+    full_version = branch_version + '.0-dev'
+    version_prefix = 'LABEL = "'
+    sed(version_prefix + old_version,
+      version_prefix + full_version,
+      R8_VERSION_FILE)
+
+    subprocess.check_call([
+      'git', 'commit', '-a', '-m', 'Version %s' % full_version])
+
+    version_diff_output = subprocess.check_output([
+      'git', 'diff', '%s..HEAD' % commithash])
+
+    validate_version_change_diff(version_diff_output, old_version, full_version)
+
+    # Double check that we want to create a new release branch.
+    if not options.dry_run:
+      input = raw_input('Create new branch for %s [y/N]:' % branch_version)
+      if input != 'y':
+        print 'Aborting new branch for %s' % branch_version
+        sys.exit(1)
+
+    maybe_check_call(options, [
+      'git', 'push', 'origin', 'HEAD:%s' % branch_version])
+    maybe_tag(options, full_version)
+
+    # TODO(sgjesse): Automate this part as well!
+    print ('REMEMBER TO UPDATE R8_DEV_BRANCH in tools/r8_release.py to "'
+      + full_version
+      + '"!!!')
+
+  return make_branch
+
+
 def parse_options():
   result = argparse.ArgumentParser(description='Release r8')
   group = result.add_mutually_exclusive_group()
@@ -351,6 +413,10 @@
                       help='The hash to use for the new dev version of R8')
   group.add_argument('--version',
                       help='The new version of R8 (e.g., 1.4.51)')
+  group.add_argument('--new-dev-branch',
+                      nargs=2,
+                      metavar=('VERSION', 'HASH'),
+                      help='Create a new branch starting a version line')
   result.add_argument('--no-sync', '--no_sync',
                       default=False,
                       action='store_true',
@@ -389,6 +455,12 @@
   args = parse_options()
   targets_to_run = []
 
+  if args.new_dev_branch:
+    if args.google3 or args.studio or args.aosp:
+      print 'Cannot create a branch and roll at the same time.'
+      sys.exit(1)
+    targets_to_run.append(prepare_branch(args))
+
   if args.dev_release:
     if args.google3 or args.studio or args.aosp:
       print 'Cannot create a dev release and roll at the same time.'
diff --git a/tools/utils.py b/tools/utils.py
index 67ead07..733d190 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -579,12 +579,46 @@
     check_basic_semver_version(version, 'in ' + DESUGAR_CONFIGURATION)
     return version
 
+class SemanticVersion:
+  def __init__(self, major, minor, patch):
+    self.major = major
+    self.minor = minor
+    self.patch = patch
+    # Build metadata currently not suppported
+
+  def larger_than(self, other):
+    if self.major > other.major:
+      return True
+    if self.major == other.major and self.minor > other.minor:
+      return True
+    if self.patch:
+      return (self.major == other.major
+        and self.minor == other.minor
+        and self.patch > other.patch)
+    else:
+      return False
+
+
 # Check that the passed string is formatted as a basic semver version (x.y.z)
 # See https://semver.org/.
-def check_basic_semver_version(version, error_context = ''):
-    reg = re.compile('^([0-9]+)\\.([0-9]+)\\.([0-9]+)$')
-    if not reg.match(version):
+def check_basic_semver_version(version, error_context = '', components = 3):
+    regexp = '^'
+    for x in range(components):
+      regexp += '([0-9]+)'
+      if x < components - 1:
+        regexp += '\\.'
+    regexp += '$'
+    reg = re.compile(regexp)
+    match = reg.match(version)
+    if not match:
       raise Exception("Invalid version '"
             + version
             + "'"
             + (' ' + error_context) if len(error_context) > 0 else '')
+    if components == 2:
+      return SemanticVersion(int(match.group(1)), int(match.group(2)), None)
+    elif components == 3:
+      return SemanticVersion(
+        int(match.group(1)), int(match.group(2)), int(match.group(3)))
+    else:
+      raise Exception('Argument "components" must be 2 or 3')