I started writing a create-a-release-branch script to automate the part of the procedure documented in http://subversion.apache.org/docs/community-guide/releasing.html#release-branches

Attached, in early draft form:
tools/dist/create-minor-release-branch.py
(contains bits from release.py)

Suggestions invited.

- Julian
#!/usr/bin/env python
# python: coding=utf-8
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#


# About this script:
#   This script is intended to simplify creating Subversion releases for
#   any of the supported release lines of Subversion.
#   It works well with our Apache infrastructure, and should make rolling,
#   posting, and announcing releases dirt simple.
#
#   This script may be run on a number of platforms, but it is intended to
#   be run on people.apache.org.  As such, it may have dependencies (such
#   as Python version) which may not be common, but are guaranteed to be
#   available on people.apache.org.

# It'd be kind of nice to use the Subversion python bindings in this script,
# but people.apache.org doesn't currently have them installed

# Stuff we need
import os
import re
import sys
import glob
import fnmatch
import shutil
import urllib2
import hashlib
import tarfile
import logging
import datetime
import tempfile
import operator
import itertools
import subprocess
import argparse       # standard in Python 2.7


# Some constants
repos = 'https://svn.apache.org/repos/asf/subversion'
secure_repos = 'https://svn.apache.org/repos/asf/subversion'


#----------------------------------------------------------------------
# Utility functions

class Version(object):
    regex = re.compile(r'(\d+).(\d+).(\d+)(?:-(?:(rc|alpha|beta)(\d+)))?')

    def __init__(self, ver_str):
        # Special case the 'trunk-nightly' version
        if ver_str == 'trunk-nightly':
            self.major = None
            self.minor = None
            self.patch = None
            self.pre = 'nightly'
            self.pre_num = None
            self.base = 'nightly'
            self.branch = 'trunk'
            return

        match = self.regex.search(ver_str)

        if not match:
            raise RuntimeError("Bad version string '%s'" % ver_str)

        self.major = int(match.group(1))
        self.minor = int(match.group(2))
        self.patch = int(match.group(3))

        if match.group(4):
            self.pre = match.group(4)
            self.pre_num = int(match.group(5))
        else:
            self.pre = None
            self.pre_num = None

        self.base = '%d.%d.%d' % (self.major, self.minor, self.patch)
        self.branch = '%d.%d' % (self.major, self.minor)

    def is_prerelease(self):
        return self.pre != None

    def is_recommended(self):
        return self.branch == recommended_release

    def get_download_anchor(self):
        if self.is_prerelease():
            return 'pre-releases'
        else:
            if self.is_recommended():
                return 'recommended-release'
            else:
                return 'supported-releases'

    def get_ver_tags(self, revnum):
        # These get substituted into svn_version.h
        ver_tag = ''
        ver_numtag = ''
        if self.pre == 'alpha':
            ver_tag = '" (Alpha %d)"' % self.pre_num
            ver_numtag = '"-alpha%d"' % self.pre_num
        elif self.pre == 'beta':
            ver_tag = '" (Beta %d)"' % args.version.pre_num
            ver_numtag = '"-beta%d"' % self.pre_num
        elif self.pre == 'rc':
            ver_tag = '" (Release Candidate %d)"' % self.pre_num
            ver_numtag = '"-rc%d"' % self.pre_num
        elif self.pre == 'nightly':
            ver_tag = '" (Nightly Build r%d)"' % revnum
            ver_numtag = '"-nightly-r%d"' % revnum
        else:
            ver_tag = '" (r%d)"' % revnum 
            ver_numtag = '""'
        return (ver_tag, ver_numtag)

    def __serialize(self):
        return (self.major, self.minor, self.patch, self.pre, self.pre_num)

    def __eq__(self, that):
        return self.__serialize() == that.__serialize()

    def __ne__(self, that):
        return self.__serialize() != that.__serialize()

    def __hash__(self):
        return hash(self.__serialize())

    def __lt__(self, that):
        if self.major < that.major: return True
        if self.major > that.major: return False

        if self.minor < that.minor: return True
        if self.minor > that.minor: return False

        if self.patch < that.patch: return True
        if self.patch > that.patch: return False

        if not self.pre and not that.pre: return False
        if not self.pre and that.pre: return False
        if self.pre and not that.pre: return True

        # We are both pre-releases
        if self.pre != that.pre:
            return self.pre < that.pre
        else:
            return self.pre_num < that.pre_num

    def __str__(self):
        "Return an SVN_VER_NUMBER-formatted string, or 'nightly'."
        if self.pre:
            if self.pre == 'nightly':
                return 'nightly'
            else:
                extra = '-%s%d' % (self.pre, self.pre_num)
        else:
            extra = ''

        return self.base + extra

    def __repr__(self):

        return "Version(%s)" % repr(str(self))

def get_prefix(base_dir):
    return os.path.join(base_dir, 'prefix')

def get_tempdir(base_dir):
    return os.path.join(base_dir, 'tempdir')

def get_workdir(base_dir):
    return os.path.join(get_tempdir(base_dir), 'working')

#----------------------------------------------------------------------
def edit_file(relpath, pattern, replacement):
    print("Editing '%s'" % (relpath,))
    print("  pattern='%s'" % (pattern,))
    print("  replace='%s'" % (replacement,))

#----------------------------------------------------------------------
def make_release_branch(ver):
    cmd = ['echo', 'svn', 'copy',
           repos + '/trunk',
           repos + '/branches/' + ver.branch + '.x',
           '-m', 'Create the ' + ver.branch + '.x release branch.']
    stdout = subprocess.check_output(cmd)
    print(stdout)

#----------------------------------------------------------------------
def update_minor_ver_in_trunk(ver):
    prev_minor_ver = str(ver.minor - 1)
    edit_file('subversion/include/svn_version.h',
              '^#define SVN_VER_MINOR *(' + prev_minor_ver + ')$',
              ver.minor)
    edit_file('subversion/bindings/javahl/src/org/apache/subversion/javahl/NativeResources.java',
              'version.isAtLeast\(1, (' + prev_minor_ver + '), 0\)',
              ver.minor)
    edit_file('subversion/tests/cmdline/svntest/main.py',
              '^SVN_VER_MINOR = (' + prev_minor_ver + ')$',
              ver.minor)
    next_ver = Version('1.%d.0' % (ver.minor + 1,))
    edit_file('CHANGES',
              '',
              'Version ' + next_ver.base + '\n'
              '(?? ??? 20XX, from /branches/' + next_ver.branch + '.x)\n'
              + secure_repos + '/tags/' + next_ver.base)
    cmd = ['echo', 'svn', 'commit', '-m', '''\
Increment the trunk version number, and introduce a new CHANGES
section for the upcoming ''' + ver.branch + '''.0 release.

* subversion/bindings/javahl/src/org/apache/subversion/javahl/NativeResources.java,
  subversion/include/svn_version.h (SVN_VER_MINOR),
  subversion/tests/cmdline/svntest/main.py (SVN_VER_MINOR):
    Increment version number.

* CHANGES: New section for ''' + ver.branch + '.0.'
          ]
    stdout = subprocess.check_output(cmd)
    print(stdout)

#----------------------------------------------------------------------
def create_status_file_on_branch(ver):
    branch_dir = os.path.join('svn-branches', ver.branch + '.x')
    status_local_path = os.path.join(branch_dir, 'STATUS')
    text='''\
      * * * * * * * * * * * * * * * * * * * * * * * * * * * *
      *                                                     *
      *  THIS RELEASE STREAM IS OPEN FOR STABILIZATION.     *
      *                                                     *
      * * * * * * * * * * * * * * * * * * * * * * * * * * * *

This file tracks the status of releases in the 1.9.x line.

See http://subversion.apache.org/docs/community-guide/releasing.html#release-stabilization
for details on how release lines and voting work, what kinds of bugs can
delay a release, etc.

Status of 1.9.0:

Candidate changes:
==================


Veto-blocked changes:
=====================


Approved changes:
=================
'''
    cmd = ['echo', 'svn', 'add', status_local_path]
    stdout = subprocess.check_output(cmd)
    print(stdout)
    cmd = ['echo', 'svn', 'commit', status_local_path,
           '-m', '* branches/' + ver.branch + '.x/STATUS: New file.']
    stdout = subprocess.check_output(cmd)
    print(stdout)

#----------------------------------------------------------------------
def steps(args):
    ver = Version('1.10.0')
    make_release_branch(ver)
    update_minor_ver_in_trunk(ver)
    create_status_file_on_branch(ver)


#----------------------------------------------------------------------
# Main entry point for argument parsing and handling

def main():
    'Parse arguments, and drive the appropriate subcommand.'

    # Setup our main parser
    parser = argparse.ArgumentParser(
                            description='Create an Apache Subversion release branch.')
    parser.add_argument('--verbose', action='store_true', default=False,
                   help='Increase output verbosity')
    parser.add_argument('--base-dir', default=os.getcwd(),
                   help='''The directory in which to create needed files and
                           folders.  The default is the current working
                           directory.''')
    subparsers = parser.add_subparsers(title='subcommands')

    # Setup the parser for the build-env subcommand
    subparser = subparsers.add_parser('steps',
                    help='''Run the release-branch-creation steps.''')
    subparser.set_defaults(func=steps)

    # Parse the arguments
    args = parser.parse_args()

    # Set up logging
    logger = logging.getLogger()
    if args.verbose:
        logger.setLevel(logging.DEBUG)
    else:
        logger.setLevel(logging.INFO)

    # Fix up our path so we can use our installed versions
    os.environ['PATH'] = os.path.join(get_prefix(args.base_dir), 'bin') + ':' \
                                                            + os.environ['PATH']

    # Make timestamps in tarballs independent of local timezone
    os.environ['TZ'] = 'UTC'

    # finally, run the subcommand, and give it the parsed arguments
    args.func(args)


if __name__ == '__main__':
    main()

Reply via email to