blob: a68fa232f8c5ad001f2b7db35b53967ea27bfa1d [file] [log] [blame]
#!/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 automate steps in creating a new Subversion
# minor release.
import os
import re
import sys
import logging
import subprocess
import argparse # standard in Python 2.7
from release import Version
# Some constants
repos = 'https://svn.apache.org/repos/asf/subversion'
secure_repos = 'https://svn.apache.org/repos/asf/subversion'
buildbot_repos = 'https://svn.apache.org/repos/infra/infrastructure/buildbot/aegis/buildmaster'
# Parameters
dry_run = False
# Local working copies
base_dir = None # set by main()
def get_trunk_wc_path(path=None):
trunk_wc_path = os.path.join(base_dir, 'svn-trunk')
if path is None: return trunk_wc_path
return os.path.join(trunk_wc_path, path)
def get_branch_wc_path(ver, path=None):
branch_wc_path = os.path.join(base_dir, ver.branch + '.x')
if path is None: return branch_wc_path
return os.path.join(branch_wc_path, path)
def get_buildbot_wc_path(path=None):
buildbot_wc_path = os.path.join(base_dir, 'svn-buildmaster')
if path is None: return buildbot_wc_path
return os.path.join(buildbot_wc_path, path)
def get_trunk_url():
return secure_repos + '/trunk'
def get_branch_url(ver):
return secure_repos + '/branches/' + ver.branch + '.x'
def get_tag_url(ver):
return secure_repos + '/tags/' + ver.base
def get_buildbot_url():
return buildbot_repos
#----------------------------------------------------------------------
# Utility functions
def run(cmd, dry_run=False):
print('+ ' + ' '.join(cmd))
if not dry_run:
stdout = subprocess.check_output(cmd)
print(stdout)
else:
print(' ## dry-run; not executed')
def run_svn(cmd, dry_run=False):
run(['svn'] + cmd, dry_run)
def svn_commit(cmd):
run_svn(['commit'] + cmd, dry_run=dry_run)
def svn_copy_branch(src, dst, message):
args = ['copy', src, dst, '-m', message]
run_svn(args, dry_run=dry_run)
def svn_checkout(url, wc, *args):
args = ['checkout', url, wc] + list(args)
run_svn(args)
#----------------------------------------------------------------------
def edit_file(path, pattern, replacement):
print("Editing '%s'" % (path,))
print(" pattern='%s'" % (pattern,))
print(" replace='%s'" % (replacement,))
old_text = open(path, 'r').read()
new_text = re.sub(pattern, replacement, old_text)
assert new_text != old_text
open(path, 'w').write(new_text)
def edit_changes_file(path, newtext):
"""Insert NEWTEXT in the 'CHANGES' file found at PATH,
just before the first line that starts with 'Version '.
"""
print("Prepending to '%s'" % (path,))
print(" text='%s'" % (newtext,))
lines = open(path, 'r').readlines()
for i, line in enumerate(lines):
if line.startswith('Version '):
with open(path, 'w') as newfile:
newfile.writelines(lines[:i])
newfile.write(newtext)
newfile.writelines(lines[i:])
break
#----------------------------------------------------------------------
def make_release_branch(ver, revnum):
svn_copy_branch(get_trunk_url() + '@' + (str(revnum) if revnum else ''),
get_branch_url(ver),
'Create the ' + ver.branch + '.x release branch.')
#----------------------------------------------------------------------
def update_minor_ver_in_trunk(ver, revnum):
"""Change the minor version in trunk to the next (future) minor version.
"""
trunk_wc = get_trunk_wc_path()
trunk_url = get_trunk_url()
svn_checkout(trunk_url + '@' + (str(revnum) if revnum else ''),
trunk_wc)
prev_ver = Version('1.%d.0' % (ver.minor - 1,))
next_ver = Version('1.%d.0' % (ver.minor + 1,))
relpaths = []
relpath = 'subversion/include/svn_version.h'
relpaths.append(relpath)
edit_file(get_trunk_wc_path(relpath),
r'(#define SVN_VER_MINOR *)%s' % (ver.minor,),
r'\g<1>%s' % (next_ver.minor,))
relpath = 'subversion/tests/cmdline/svntest/main.py'
relpaths.append(relpath)
edit_file(get_trunk_wc_path(relpath),
r'(SVN_VER_MINOR = )%s' % (ver.minor,),
r'\g<1>%s' % (next_ver.minor,))
relpath = 'subversion/bindings/javahl/src/org/apache/subversion/javahl/NativeResources.java'
relpaths.append(relpath)
try:
# since r1817921 (just after branching 1.10)
edit_file(get_trunk_wc_path(relpath),
r'SVN_VER_MINOR = %s;' % (ver.minor,),
r'SVN_VER_MINOR = %s;' % (next_ver.minor,))
except:
# before r1817921: two separate places
edit_file(get_trunk_wc_path(relpath),
r'version.isAtLeast\(1, %s, 0\)' % (ver.minor,),
r'version.isAtLeast\(1, %s, 0\)' % (next_ver.minor,))
edit_file(get_trunk_wc_path(relpath),
r'1.%s.0, but' % (ver.minor,),
r'1.%s.0, but' % (next_ver.minor,))
relpath = 'CHANGES'
relpaths.append(relpath)
# insert at beginning of CHANGES file
edit_changes_file(get_trunk_wc_path(relpath),
'Version ' + next_ver.base + '\n'
+ '(?? ??? 20XX, from /branches/' + next_ver.branch + '.x)\n'
+ get_tag_url(next_ver) + '\n'
+ '\n')
log_msg = '''\
Increment the trunk version number to %s, and introduce a new CHANGES
section, following the creation of the %s.x release branch.
* subversion/include/svn_version.h,
subversion/bindings/javahl/src/org/apache/subversion/javahl/NativeResources.java,
subversion/tests/cmdline/svntest/main.py
(SVN_VER_MINOR): Increment to %s.
* CHANGES: New section for %s.0.
''' % (next_ver.branch, ver.branch, next_ver.minor, next_ver.branch)
commit_paths = [get_trunk_wc_path(p) for p in relpaths]
svn_commit(commit_paths + ['-m', log_msg])
#----------------------------------------------------------------------
def create_status_file_on_branch(ver):
branch_wc = get_branch_wc_path(ver)
branch_url = get_branch_url(ver)
svn_checkout(branch_url, branch_wc, '--depth=immediates')
status_local_path = os.path.join(branch_wc, 'STATUS')
text='''\
* * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* THIS RELEASE STREAM IS OPEN FOR STABILIZATION. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * *
This file tracks the status of releases in the %s.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 %s:
Candidate changes:
==================
Veto-blocked changes:
=====================
Approved changes:
=================
''' % (ver.branch, ver.base)
open(status_local_path, 'wx').write(text)
run_svn(['add', status_local_path])
svn_commit([status_local_path,
'-m', '* branches/' + ver.branch + '.x/STATUS: New file.'])
#----------------------------------------------------------------------
def update_backport_bot(ver):
print("""MANUAL STEP: Fork & edit & pull-request on GitHub:
https://github.com/apache/infrastructure-puppet/blob/deployment/modules/svnqavm_pvm_asf/manifests/init.pp
"Add new %s.x branch to list of backport branches"
""" % (ver.branch,))
print("""Someone needs to run the 'svn checkout' manually.
The exact checkout command is documented in machines/svn-qavm2/notes.txt
in the private repository (need to use a trunk client and the svn-master.a.o
hostname).
""")
#----------------------------------------------------------------------
def update_buildbot_config(ver):
"""Add the new branch to the list of branches monitored by the buildbot
master.
"""
buildbot_wc = get_buildbot_wc_path()
buildbot_url = get_buildbot_url()
svn_checkout(buildbot_url, buildbot_wc)
prev_ver = Version('1.%d.0' % (ver.minor - 1,))
next_ver = Version('1.%d.0' % (ver.minor + 1,))
relpath = 'master1/projects/subversion.conf'
edit_file(get_buildbot_wc_path(relpath),
r'(MINOR_LINES=\[.*%s)(\])' % (prev_ver.minor,),
r'\1, %s\2' % (ver.minor,))
log_msg = '''\
Subversion: start monitoring the %s branch.
''' % (ver.branch)
commit_paths = [get_buildbot_wc_path(relpath)]
svn_commit(commit_paths + ['-m', log_msg])
#----------------------------------------------------------------------
def create_release_branch(args):
make_release_branch(args.version, args.revnum)
update_minor_ver_in_trunk(args.version, args.revnum)
create_status_file_on_branch(args.version)
update_backport_bot(args.version)
update_buildbot_config(args.version)
#----------------------------------------------------------------------
# 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.')
subparsers = parser.add_subparsers(title='subcommands')
# Setup the parser for the create-release-branch subcommand
subparser = subparsers.add_parser('create-release-branch',
help='''Create a minor release branch: branch from trunk,
update version numbers on trunk, create status
file on branch, update backport bot,
update buildbot config.''')
subparser.set_defaults(func=create_release_branch)
subparser.add_argument('version', type=Version,
help='''A version number to indicate the branch, such as
'1.7.0' (the '.0' is required).''')
subparser.add_argument('revnum', type=lambda arg: int(arg.lstrip('r')),
nargs='?', default=None,
help='''The trunk revision number to base the branch on.
Default is HEAD.''')
subparser.add_argument('--dry-run', action='store_true', default=False,
help='Avoid committing any changes to repositories.')
subparser.add_argument('--verbose', action='store_true', default=False,
help='Increase output verbosity')
subparser.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.''')
# Parse the arguments
args = parser.parse_args()
global base_dir, dry_run
base_dir = args.base_dir
dry_run = args.dry_run
# Set up logging
logger = logging.getLogger()
if args.verbose:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
# 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()