#!/usr/bin/env python3 # Copyright 2020 The Pigweed Authors # # Licensed 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 # # https://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. """Create transitive CLs for requirements on internal Gerrits. This is only intended to be used by Googlers. If the current CL needs to be tested alongside internal-project:1234 on an internal project, but "internal-project" is something that can't be referenced publicly, this automates creation of a CL on the pigweed-internal Gerrit that references internal-project:1234 so the current commit effectively has a requirement on internal-project:1234. For more see http://go/pigweed-ci-cq-intro. """ import argparse import logging from pathlib import Path import re import subprocess import sys import tempfile import uuid HELPER_GERRIT = 'pigweed-internal' HELPER_PROJECT = 'requires-helper' HELPER_REPO = 'sso://{}/{}'.format(HELPER_GERRIT, HELPER_PROJECT) # Subset of the output from pushing to Gerrit. DEFAULT_OUTPUT = f''' remote: remote: https://{HELPER_GERRIT}-review.git.corp.google.com/c/{HELPER_PROJECT}/+/123456789 DO NOT SUBMIT [NEW] remote: '''.strip() _LOG = logging.getLogger(__name__) def parse_args() -> argparse.Namespace: """Creates an argument parser and parses arguments.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( 'requirements', nargs='+', help='Requirements to be added (":").', ) parser.add_argument( '--no-push', dest='push', action='store_false', help=argparse.SUPPRESS, # This option is only for debugging. ) return parser.parse_args() def _run_command(*args, **kwargs): kwargs.setdefault('capture_output', True) _LOG.debug('%s', args) _LOG.debug('%s', kwargs) res = subprocess.run(*args, **kwargs) _LOG.debug('%s', res.stdout) _LOG.debug('%s', res.stderr) res.check_returncode() return res def check_status() -> bool: res = subprocess.run(['git', 'status'], capture_output=True) if res.returncode: _LOG.error('repository not clean, commit to suppress this warning') return False return True def clone(requires_dir: Path) -> None: _LOG.info('cloning helper repository into %s', requires_dir) _run_command(['git', 'clone', HELPER_REPO, '.'], cwd=requires_dir) def create_commit(requires_dir: Path, requirements) -> None: change_id = str(uuid.uuid4()).replace('-', '00') _LOG.debug('change_id %s', change_id) path = requires_dir / change_id _LOG.debug('path %s', path) with open(path, 'w'): pass _run_command(['git', 'add', path], cwd=requires_dir) commit_message = [ f'DO NOT SUBMIT {change_id[0:10]}', '', f'Change-Id: I{change_id}', ] for req in requirements: commit_message.append(f'Requires: {req}') _LOG.debug('message %s', commit_message) _run_command( ['git', 'commit', '-m', '\n'.join(commit_message)], cwd=requires_dir, ) # Not strictly necessary, only used for logging. _run_command(['git', 'show'], cwd=requires_dir) def push_commit(requires_dir: Path, push=True) -> str: output = DEFAULT_OUTPUT if push: res = _run_command( ['git', 'push', HELPER_REPO, '+HEAD:refs/for/master'], cwd=requires_dir, ) output = res.stderr.decode() _LOG.debug('output: %s', output) regex = re.compile( f'^\\s*remote:\\s*' f'https://{HELPER_GERRIT}-review.(?:git.corp.google|googlesource).com/' f'c/{HELPER_PROJECT}/\\+/(?P\\d+)\\s+', re.MULTILINE, ) _LOG.debug('regex %r', regex) match = regex.search(output) if not match: raise ValueError(f"invalid output from 'git push': {output}") change_num = match.group('num') _LOG.info('created %s change %s', HELPER_PROJECT, change_num) return f'{HELPER_GERRIT}:{change_num}' def amend_existing_change(change: str) -> None: res = _run_command(['git', 'log', '-1', '--pretty=%B']) original = res.stdout.rstrip().decode() addition = f'Requires: {change}' _LOG.info('adding "%s" to current commit message', addition) message = '\n'.join((original, addition)) _run_command(['git', 'commit', '--amend', '--message', message]) def run(requirements, push=True) -> int: """Entry point for requires.""" if not check_status(): return -1 # Create directory for checking out helper repository. with tempfile.TemporaryDirectory() as requires_dir_str: requires_dir = Path(requires_dir_str) # Clone into helper repository. clone(requires_dir) # Make commit with requirements from command line. create_commit(requires_dir, requirements) # Push that commit and save its number. change = push_commit(requires_dir, push=push) # Add dependency on newly pushed commit on current commit. amend_existing_change(change) return 0 def main() -> int: return run(**vars(parse_args())) if __name__ == '__main__': try: # If pw_cli is available, use it to initialize logs. from pw_cli import log log.install(logging.INFO) except ImportError: # If pw_cli isn't available, display log messages like a simple print. logging.basicConfig(format='%(message)s', level=logging.INFO) sys.exit(main())