#!/usr/bin/env python # # BRANCH -- CREATE VERSION OR TASK BRANCH # Gareth Rees, Ravenbrook Limited, 2014-03-18 # # $Id$ # 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 = ''' {date}/{task} Changes {desc_html} Diffs ''' VERSION_BRANCH_ENTRY = ''' {version} {parent}/...@{changelevel} {desc_html} base
changelists ''' # 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'\1'), 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', '(?=\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', '(?<=\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. # # # [VERSION-CREATE] Richard Kistruck; "Memory Pool System Version # Create Procedure"; Ravenbrook Limited; 2008-10-29. # # # # 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 . # # 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$