#!/usr/bin/env python
#
#              BRANCH -- CREATE VERSION OR TASK BRANCH
#             Gareth Rees, Ravenbrook Limited, 2014-03-18
#
# $Id: //info.ravenbrook.com/project/mps/master/tool/branch#22 $
# Copyright (c) 2014-2020 Ravenbrook Limited. See end of file for license.
#
#
# 1. INTRODUCTION
#
# This script automates the process of branching the master sources
# (or customer mainline sources) of a project. It can create a version
# branch as described in [VERSION-CREATE], or a development (task)
# branch as described in [BRANCH-MERGE].


from __future__ import unicode_literals
import argparse
from collections import deque
import datetime
import os
import re
import subprocess
import sys
import p4
import uuid

if sys.version_info < (3,):
    from codecs import open

class Error(Exception): pass

DEPOT = '//info.ravenbrook.com'
PROJECT_RE = r'[a-z][a-z0-9.-]*'
PROJECT_FILESPEC_RE = r'{}/project/({})/?'.format(re.escape(DEPOT), PROJECT_RE)
CUSTOMER_RE = r'[a-z][a-z0-9.-]*'
PARENT_RE = r'master|custom/({})/main'.format(CUSTOMER_RE)
PARENT_FILESPEC_RE = r'{}({})(?:/|$)'.format(PROJECT_FILESPEC_RE, PARENT_RE)
TASK_RE = r'[a-zA-Z][a-zA-Z0-9._-]*'
TASK_BRANCH_RE = r'branch/(\d\d\d\d-\d\d-\d\d)/({})'.format(TASK_RE)
VERSION_RE = r'\d+\.\d+'
CHILD_RE = r'(?:custom/({})/)?(?:{}|version/({}))$'.format(CUSTOMER_RE, TASK_BRANCH_RE, VERSION_RE)

TASK_BRANCH_ENTRY = '''
  <tr valign="top">
    <td><code><a href="{date}/{task}/">{date}/{task}</a></code></td>
    <td><a href="https://info.ravenbrook.com/infosys/cgi/perfbrowse.cgi?@changes+{depot}/project/{project}/{child}/...">Changes</a></td>
    <td>{desc_html}</td>
    <td><a href="https://info.ravenbrook.com/infosys/cgi/perfbrowse.cgi?@diff2+{depot}/project/{project}/{child}/...@{base}+{depot}/project/{project}/{child}/...">Diffs</a></td>
  </tr>

'''

VERSION_BRANCH_ENTRY = '''
  <tr valign="top">
    <td> <a href="{version}/">{version}</a> </td>
    <td>
    </td>
    <td> <a href="https://info.ravenbrook.com/infosys/cgi/perfbrowse.cgi?@files+{depot}/project/{project}/{parent}/...@{changelevel}">{parent}/...@{changelevel}</a> </td>
    <td>
      {desc_html}
    </td>
    <td>
      <a href="https://info.ravenbrook.com/infosys/cgi/perfbrowse.cgi?@describe+{base}">base</a><br />
      <a href="https://info.ravenbrook.com/infosys/cgi/perfbrowse.cgi?@changes+{depot}/project/{project}/{child}/...">changelists</a>
    </td>
  </tr>

'''

# Git Fusion repos in which to register new branches.
GF_REPOS = ['mps', 'mps-public']

# Regular expression matching the view of the master codeline in Git Fusion
# configuration files.
GF_VIEW_RE = r'git-branch-name\s*=\s*master\s*view\s*=\s*(.*?)\n\n'

# Template for the new entry in the Git Fusion configuration file
GF_ENTRY = '''

[{uuid}]
git-branch-name = {child}
view = {gf_view}
'''


def main(argv):
    parser = argparse.ArgumentParser()
    parser.add_argument('-P', '--project',
                        help='Name of the project.')
    parser.add_argument('-p', '--parent',
                        help='Name of the parent branch.')
    parser.add_argument('-C', '--changelevel', type=int,
                        help='Changelevel at which to make the branch.')
    parser.add_argument('-d', '--description',
                        help='Description of the branch (for the branch spec).')
    parser.add_argument('-g', '--github', action='store_true',
                        help='Push this branch to GitHub.')
    parser.add_argument('-y', '--yes', action='store_true',
                        help='Yes, really make the branch.')
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('-c', '--child',
                       help='Name of the child branch.')
    group.add_argument('-v', '--version', action='store_true',
                       help='Make the next version branch.')
    group.add_argument('-t', '--task',
                       help='Name of the task branch.')
    args = parser.parse_args(argv[1:])
    args.depot = DEPOT
    args.date = 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.parent:
        # Deduce parent branch from current directory.
        filespec = next(p4.run('dirs', '.'))['dir']
        m = re.match(PARENT_FILESPEC_RE, filespec)
        if not m:
            raise Error("Can't deduce parent 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.parent = m.group(2)
        print(fmt("parent={parent}"))

    m = re.match(PARENT_RE, args.parent)
    if not m:
        raise Error("Invalid parent branch: must be master or custom/*/main.")
    args.customer = m.group(1)
    if not any(p4.run('dirs', fmt('{depot}/project/{project}/{parent}'))):
        raise Error(fmt("No such branch: {parent}"))

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

    if args.task:
        if not re.match(TASK_RE, args.task):
            raise Error(fmt("Invalid task: {task}"))
        if args.parent == 'master':
            args.child = fmt('branch/{date}/{task}')
        else:
            args.child = fmt('custom/{customer}/branch/{date}/{task}')
        print(fmt("child={child}"))
    elif args.version:
        # Deduce version number from code/version.c.
        f = fmt('{depot}/project/{project}/{parent}/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 version from {}.".format(f))
        args.version = m.group(1)
        if args.parent == 'master':
            args.child = fmt('version/{version}')
        else:
            args.child = fmt('custom/{customer}/version/{version}')
        print(fmt("child={child}"))

    m = re.match(CHILD_RE, args.child)
    if not m:
        raise Error(fmt("Invalid child: {child}"))
    if args.customer != m.group(1):
        raise Error(fmt("Customer mismatch between {parent} and {child}."))
    _, args.date, args.task, args.version = m.groups()

    if not args.description:
        args.description = fmt("Branching {parent} to {child}.")
        print(fmt("description={description}"))
    args.desc_html = re.sub(r'\b(job\d{6})\b',
                            fmt(r'<a href="/project/{project}/issue/\1/">\1</a>'),
                            args.description)

    # Create the branch specification
    args.branch = fmt('{project}/{child}')
    branch_spec = dict(Branch=args.branch,
                       Description=args.description,
                       View0=fmt('{depot}/project/{project}/{parent}/... '
                                 '{depot}/project/{project}/{child}/...'))
    print("view={}".format(branch_spec['View0']))
    have_branch = False
    if any(p4.run('branches', '-E', args.branch)):
        print(fmt("Branch spec {branch} already exists: skipping."))
        have_branch = True
    elif args.yes:
        print(fmt("Creating branch spec {branch}."))
        p4.run('branch', '-i').send(branch_spec).done()
        have_branch = True
    else:
        print("--yes omitted: skipping branch creation.")

    # Populate the branch
    if any(p4.run('dirs', fmt('{depot}/project/{project}/{child}'))):
        print("Child branch already populated: skipping.")
    else:
        srcs = fmt('{depot}/project/{project}/{parent}/...@{changelevel}')
        populate_args = ['populate', '-n',
                         '-b', args.branch,
                         '-d', fmt("Branching {parent} to {child}."),
                         '-s', srcs]
        if args.yes:
            print(fmt("Populating branch {branch}..."))
            populate_args.remove('-n')
            p4.do(*populate_args)
        elif have_branch:
            print("--yes omitted: populate -n ...")
            p4.do(*populate_args)
        else:
            print("--yes omitted: skipping populate.")

    # Determine the first change on the branch
    cmd = p4.run('changes', fmt('{depot}/project/{project}/{child}/...'))
    try:
        args.base = int(deque(cmd, maxlen=1).pop()['change'])
        print(fmt("base={base}"))
    except IndexError:
        args.yes = False
        args.base = args.changelevel
        print(fmt("Branch {child} not populated: using base={base}"))

    def register(filespec, search, replace):
        args.filespec = fmt(filespec)
        if p4.contents(args.filespec).find(args.child) != -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 {child}."), filename)
            else:
                print(fmt("--yes omitted: skipping submit of {filespec}"))

    def p4read(filespec):
        return ''.join([d['data'] for d in p4.run('print', filespec)
                                  if d['code'] == 'text'])

    if not args.version and not args.customer:
        # Public task branch
        register('{depot}/project/{project}/branch/index.html',
                 '(?=</table>\n)', TASK_BRANCH_ENTRY)
        args.git_name = fmt('{project}-{task}')
        args.git_branch = fmt('dev/{date}/{task}')
    elif args.version and not args.customer:
        # Public version branch
        register('{depot}/project/{project}/version/index.html',
                 '(?<=<tbody>\n)', VERSION_BRANCH_ENTRY)
        args.github = True
        args.git_name = fmt('{project}-version-{version}')
        args.git_branch = fmt('version/{version}')
    else:
        args.git_name = None

    if args.github and not args.git_name:
        print(fmt("Don't know how to push {child} to GitHub: skipping."))
    elif args.github:

        # Invent a UUID to use as the section title for the branch in
        # the Git Fusion configuration files.
        args.uuid = uuid.uuid5(uuid.NAMESPACE_URL, str(args.child))
        print(fmt("uuid={uuid}"))

        for repo in GF_REPOS:
            config = '//.git-fusion/repos/{}/p4gf_config'.format(repo)
            text = p4read(config)
            if re.search(str(args.uuid), text):
                print('Already registered in Git Fusion repo "{}": skipping.'.format(repo))
            else:
                view = re.search(GF_VIEW_RE, text, re.MULTILINE | re.DOTALL).group(1)
                args.gf_view = view.replace('/master/', fmt('/{child}/'))
                print('Registering in Git Fusion repo "{}".'.format(repo))
                register(config, r"\n*\Z", GF_ENTRY)


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


# A. REFERENCES
#
# [BRANCH-MERGE] Gareth Rees; "Memory Pool System branching and
# merging procedures"; Ravenbrook Limited; 2014-01-09.
# <https://info.ravenbrook.com/project/mps/master/procedure/branch-merge>
#
# [VERSION-CREATE] Richard Kistruck; "Memory Pool System Version
# Create Procedure"; Ravenbrook Limited; 2008-10-29.
# <https://info.ravenbrook.com/project/mps/master/procedure/version-create>
#
#
# B. DOCUMENT HISTORY
#
# 2014-03-18 GDR Created based on [BRANCH-MERGE] and [VERSION-CREATE].
#
# 2016-02-13 RB  Adapting to Git Fusion 2.
#
# 2016-09-13 GDR Support for customer task branches.
#
# 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/branch#22 $