#!/usr/bin/env python
#
#                     RELEASE -- MAKE A RELEASE
#             Gareth Rees, Ravenbrook Limited, 2014-03-18
#
# $Id: //info.ravenbrook.com/project/mps/master/tool/release#11 $
# Copyright (c) 2014-2018 Ravenbrook Limited. See end of file for license.
#
#
# 1. INTRODUCTION
#
# This script automates the process of making a release, based on
# [RELEASE-BUILD].
#
# This script is idempotent: that is, you can run it repeatedly and it
# will skip steps that have already been performed.


from __future__ import unicode_literals
import argparse
from contextlib import contextmanager
import datetime
import os
import re
import subprocess
import sys
import p4

class Error(Exception): pass

@contextmanager
def pushdir(dir):
    """Context manager that changes directory to dir for the body of the
    with statement.

    """
    cwd = os.getcwd()
    os.chdir(dir)
    yield
    os.chdir(cwd)

DEPOT = '//info.ravenbrook.com'
PROJECT_RE = r'[a-z][a-z0-9.-]*'
PROJECT_FILESPEC_RE = r'{}/project/({})/'.format(re.escape(DEPOT), PROJECT_RE)
VERSION_RE = r'\d+\.\d+'
CUSTOMER_RE = r'[a-z][a-z0-9.-]*'
BRANCH_RE = (r'master|(?:custom/({})/)?(?:main|version/({}))'
             .format(CUSTOMER_RE, VERSION_RE))
BRANCH_FILESPEC_RE = r'{}({})(?:/|$)'.format(PROJECT_FILESPEC_RE, BRANCH_RE)

RELEASE_ENTRY = '''

<tr valign="top">
  <td><a href="{release}/">{release}</a></td>
  <td>{today}<br />
  <a href="https://info.ravenbrook.com/infosys/cgi/perfbrowse.cgi?@files+{origin}/...@{changelevel}">{branch}/...@{changelevel}</a></td>
  <td>
    {description}
  </td>
  <td>
    <a href="/project/{project}/issue/?action=known&amp;sort=Priority%3AJob&amp;release={release}">Known</a> <br />
    <a href="/project/{project}/issue/?action=fixed&amp;release={release}">Fixed</a>
  </td>
</tr>
'''

VERSION_ENTRY = '''
      <a href="/project/{project}/release/{release}/">{release}</a>'''

def main(argv):
    parser = argparse.ArgumentParser()
    parser.add_argument('-P', '--project',
                        help='Name of the project.')
    parser.add_argument('-b', '--branch',
                        help='Name of the branch to make the release from.')
    parser.add_argument('-C', '--changelevel', type=int,
                        help='Changelevel at which to make the release.')
    parser.add_argument('-d', '--description',
                        help='Description of the release.')
    parser.add_argument('-y', '--yes', action='store_true',
                        help='Yes, really make the release.')
    args = parser.parse_args(argv[1:])
    args.depot = DEPOT
    args.today = datetime.date.today().strftime('%Y-%m-%d')
    fmt = lambda s: s.format(**vars(args))

    if not args.project:
        # Deduce project from current directory.
        filespec = next(p4.run('dirs', '.'))['dir']
        m = re.match(PROJECT_FILESPEC_RE, filespec)
        if not m:
            raise Error("Can't deduce project from current directory.")
        args.project = m.group(1)
        print(fmt("project={project}"))

    if not any(p4.run('dirs', fmt('{depot}/project/{project}'))):
        raise Error(fmt("No such project: {project}"))

    if not args.branch:
        # Deduce branch from current directory.
        filespec = next(p4.run('dirs', '.'))['dir']
        m = re.match(BRANCH_FILESPEC_RE, filespec)
        if not m:
            raise Error("Can't deduce branch from {}".format(filespec))
        if args.project != m.group(1):
            raise Error("Specified project={} but current directory belongs "
                        "to project={}.".format(args.project, m.group(1)))
        args.branch = m.group(2)
        print(fmt("branch={branch}"))

    m = re.match(BRANCH_RE, args.branch)
    if not m:
        raise Error(fmt("Invalid branch {branch}"))
    args.customer = m.group(1)
    args.version = m.group(2)
    if args.customer:
        print(fmt("customer={customer}"))
    if args.version:
        print(fmt("version={version}"))

    args.origin = fmt('{depot}/project/{project}/{branch}')
    if not any(p4.run('dirs', args.origin)):
        raise Error(fmt("No such branch: {branch}"))

    if not args.changelevel:
        cmd = p4.run('changes', '-m', '1', fmt('{origin}/...'))
        args.changelevel = int(next(cmd)['change'])
        print(fmt("changelevel={changelevel}"))

    # Deduce release from code/version.c.
    f = fmt('{origin}/code/version.c@{changelevel}')
    m = re.search(r'^#define MPS_RELEASE "release/((\d+\.\d+)\.\d+)"$',
                  p4.contents(f), re.M)
    if not m:
        raise Error("Failed to extract release from {}.".format(f))
    args.release = m.group(1)
    print(fmt("release={release}"))
    if args.version and args.version != m.group(2):
        raise Error(fmt("Version {version} does not match release {release}"))
    if args.customer:
        args.reldir = fmt('{depot}/project/{project}/custom/{customer}/release/{release}')
    else:
        args.reldir = fmt('{depot}/project/{project}/release/{release}')

    if not args.description:
        args.description = fmt("Release {release}.")
        print(fmt("description={description}"))

    args.kit = fmt('mps-kit-{release}')
    client_spec = dict(
        View0=fmt('{origin}/... //__CLIENT__/{kit}/...'),
        View1=fmt('{reldir}/... //__CLIENT__/release/{release}/...'))
    srcs = fmt('{origin}/...@{changelevel}')
    for line_end, args.ext, cmd in (('local', 'tar.gz', ['tar', 'czf']),
                                    ('win',   'zip',    ['zip', '-r'])):
        client_spec['LineEnd'] = line_end
        archive = fmt('release/{release}/{kit}.{ext}')
        with p4.temp_client(client_spec) as (conn, client_root):
            try:
                conn.do('files', fmt('{reldir}/{kit}.{ext}'))
            except p4.Error as e:
                print("Adding {}".format(archive))
                conn.do('sync', '-f', srcs)
                with pushdir(client_root):
                    os.makedirs(fmt('release/{release}'))
                    subprocess.check_call(cmd + [archive, args.kit],
                                          stdout=subprocess.DEVNULL)
                if not args.yes:
                    print(fmt("--yes omitted: skipping submit of {kit}.{ext}"))
                else:
                    conn.do('add', os.path.join(client_root, archive))
                    desc = fmt("Adding the MPS Kit {ext} archive for "
                               "release {release}.")
                    conn.do('submit', '-d', desc)
            else:
                print("{} already exists: skipping.".format(archive))

    def register(filespec, search, replace):
        args.filespec = fmt(filespec)
        if p4.contents(args.filespec).find(args.release) != -1:
            print(fmt("{filespec} already updated: skipping."))
            return
        client_spec = dict(View0=fmt('{filespec} //__CLIENT__/target'))
        with p4.temp_client(client_spec) as (conn, client_root):
            filename = os.path.join(client_root, 'target')
            conn.do('sync', filename)
            conn.do('edit', filename)
            with open(filename, encoding='utf8') as f:
                text = re.sub(search, fmt(replace), f.read(), 1)
            with open(filename, 'w', encoding='utf8') as f:
                f.write(text)
            for result in conn.run('diff'):
                if 'data' in result:
                    print(result['data'])
            if args.yes:
                conn.do('submit', '-d', fmt("Registering release {release}."),
                        filename)
            else:
                print(fmt("--yes omitted: skipping submit of {filespec}"))

    if not args.customer:
        register('{depot}/project/{project}/release/index.html',
                 '(?<=<tbody>\n)', RELEASE_ENTRY)
        register('{depot}/project/{project}/version/index.html',
                 (r'(?<=<td> <a href="{0}/">{0}</a> </td>\n    <td>)'
                  .format(re.escape(args.version))),
                 VERSION_ENTRY)
        register('{depot}/project/{project}/index.rst',
                 r'release/\d+\.\d+\.\d+', 'release/{release}')


if __name__ == '__main__':
    main(sys.argv)


# A. REFERENCES
#
# [RELEASE-BUILD] Richard Brooksby; "Memory Pool System Release Build
# Procedure"; Ravenbrook Limited; 2002-06-17.
# <https://info.ravenbrook.com/project/mps/master/procedure/release-build>
#
#
# B. DOCUMENT HISTORY
#
# 2014-03-18 GDR Created based on [RELEASE-BUILD].
# 2020-07-29 PNJ Updated licence.
#
# C. COPYRIGHT AND LICENSE
#
# Copyright (C) 2014-2020 Ravenbrook Limited <https://www.ravenbrook.com/>.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the
#    distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#
# $Id: //info.ravenbrook.com/project/mps/master/tool/release#11 $