#!/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()
