1#!/usr/bin/env python3 2# Copyright 2020 The ChromiumOS Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5"""Wrapper around bb invocation of the cl_factory recipe.""" 6 7import argparse 8import json 9import re 10import subprocess 11import sys 12 13from shlex import quote 14 15 16def Main(args): # pylint: disable=invalid-name 17 """Wrapper around `bb add` invocation to invoke cl_factory. 18 19 Friendlier wrapper around the `bb add` command to aid invoking the cl_factory 20 recipe. A typical invocation would look like the following: 21 22 cl_factory \ 23 --cl https://chrome-internal-review.googlesource.com/c/chromeos/program/galaxy/+/3095418 \ 24 --regex src/program src/project \ 25 --reviewers reviewer1@chromium.org reviewer2@google.com \ 26 --ccs cc1@google.com \ 27 --hashtag regen-audio-configs \ 28 --replace_strings *.star foo bar \ 29 --message "Hello world 30 31BUG=chromium:1092954 32TEST=CQ" 33 34 Note that by default the command is only printed out but not ran. To run the 35 command and cause the invocation of the cl_factory recipe additionally pass: 36 37 --nodryrun 38 39 Also note that you likely to want to provide a "hashtag". This will allow 40 gerrit cli commands to accurately target the generated CLs for batch review, 41 verify, and commit. These commands are output in the "summarize results" step 42 of the ClFactory recipe. 43 44 For more information on arguments, such as the variables available for 45 interpolation in the CL message, see: 46 47 http://cs/chromeos_public/infra/recipes/recipes/cl_factory.proto 48 49 Args: 50 args: Namespace object holding parsed args. 51 """ 52 cmd = ['bb', 'add'] 53 54 for cl in args.cl or []: 55 cmd.extend(['-cl', cl]) 56 57 cmd.extend(csv_param('repo_regexes', args.regex)) 58 cmd.extend(csv_param('reviewers', args.reviewers)) 59 cmd.extend(csv_param('ccs', args.ccs)) 60 cmd.extend(csv_param('hashtags', args.hashtag)) 61 62 if args.set_source_depends: 63 cmd.extend(['-p', 'set_source_depends=true']) 64 65 replace_strings = list(map(lambda rs: replace_strings_dict(rs), 66 args.replace_strings or [])) 67 if replace_strings: 68 cmd.extend( 69 ['-p', '\'replace_strings={}\''.format(json.dumps(replace_strings))]) 70 71 if args.manifest_branch: 72 cmd.extend(['-p', '\'manifest_branch={}\''.format(args.manifest_branch)]) 73 74 cmd.extend(['-p', quote('message_template={}'.format(args.message))]) 75 cmd.append('chromeos/infra/ClFactory') 76 77 cmd = ' '.join(cmd) 78 79 print('Command:') 80 print(cmd) 81 print('\n') 82 83 if args.dryrun: 84 print('Dry run only. Command not executed. Pass:') 85 print(' --nodryrun') 86 print('to invoke the recipe.') 87 else: 88 print('Executing command.') 89 subprocess.run(cmd, shell=True, check=True) 90 91 92def csv_param(name, values): 93 if not values: 94 return [] 95 values = ', '.join(map(lambda v: '"{}"'.format(v), values)) 96 return ['-p', '\'{}=[{}]\''.format(name, values)] 97 98 99def replace_strings_dict(rs): 100 return { 101 'file_glob': rs[0], 102 'before': rs[1], 103 'after': rs[2], 104 } 105 106 107def validate_regex(value): 108 try: 109 re.compile(value) 110 return value 111 except: 112 raise argparse.ArgumentTypeError('Invalid regex "{}"'.format(value)) 113 114 115def main(): 116 """Main program which parses args and runs Main.""" 117 parser = argparse.ArgumentParser(description=__doc__) 118 parser.add_argument( 119 '--cl', 120 nargs='+', 121 type=str, 122 required=False, 123 help='CL URL(s) input to patch into the projects.') 124 parser.add_argument( 125 '--regex', 126 nargs='+', 127 type=validate_regex, 128 required=True, 129 help='Operate on projects matching the regex or ' 130 'wildcard. Usage as in `repo forall`.') 131 parser.add_argument( 132 '--reviewers', 133 nargs='+', 134 type=str, 135 required=True, 136 help='Reviewers to add to the generated CLs.') 137 parser.add_argument( 138 '--ccs', 139 nargs='+', 140 type=str, 141 required=False, 142 help='CCs to add to the generated CLs.') 143 parser.add_argument( 144 '--hashtag', 145 nargs='+', 146 type=str, 147 default=[], 148 help='Hashtags to add to the generated CLs.') 149 parser.add_argument( 150 '--replace_strings', 151 action='append', 152 nargs=3, 153 metavar=('glob', 'before', 'after'), 154 help='String replacements to perform on the generated CLs.') 155 parser.add_argument( 156 '--message', 157 type=str, 158 required=True, 159 help='Message template for the generated CLs.') 160 parser.add_argument( 161 '--set_source_depends', 162 type=bool, 163 required=False, 164 default=False, 165 help='Whether to set Cq-Depends on the input CLs to the list ' 166 'of generated CLs.') 167 parser.add_argument( 168 '--nodryrun', 169 dest='dryrun', 170 default=True, 171 action='store_false') 172 parser.add_argument( 173 '--manifest_branch', 174 type=str, 175 required=False, 176 help='Manifest branch to checkout and upload changes for. ' 177 'If not specified, ToT is used.') 178 args = parser.parse_args() 179 Main(args) 180 181 182if __name__ == '__main__': 183 sys.exit(main()) 184