1# Copyright 2020 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Argument parsing code for presubmit checks.""" 15 16import argparse 17import logging 18from pathlib import Path 19import re 20import shutil 21from typing import Callable, Collection, Optional, Sequence 22 23from pw_presubmit import git_repo, presubmit 24 25_LOG = logging.getLogger(__name__) 26 27 28def add_path_arguments(parser) -> None: 29 """Adds common presubmit check options to an argument parser.""" 30 31 parser.add_argument( 32 'paths', 33 metavar='pathspec', 34 nargs='*', 35 help=('Paths or patterns to which to restrict the checks. These are ' 36 'interpreted as Git pathspecs. If --base is provided, only ' 37 'paths changed since that commit are checked.')) 38 parser.add_argument( 39 '-b', 40 '--base', 41 metavar='commit', 42 help=('Git revision against which to diff for changed files. ' 43 'If none is provided, the entire repository is used.')) 44 parser.add_argument( 45 '-e', 46 '--exclude', 47 metavar='regular_expression', 48 default=[], 49 action='append', 50 type=re.compile, 51 help=('Exclude paths matching any of these regular expressions, ' 52 "which are interpreted relative to each Git repository's root.")) 53 54 55def _add_programs_arguments(exclusive: argparse.ArgumentParser, 56 programs: presubmit.Programs, default: str): 57 def presubmit_program(arg: str) -> presubmit.Program: 58 if arg not in programs: 59 raise argparse.ArgumentTypeError( 60 f'{arg} is not the name of a presubmit program') 61 62 return programs[arg] 63 64 exclusive.add_argument('-p', 65 '--program', 66 choices=programs.values(), 67 type=presubmit_program, 68 default=default, 69 help='Which presubmit program to run') 70 71 all_steps = programs.all_steps() 72 73 # The step argument appends steps to a program. No "step" argument is 74 # created on the resulting argparse.Namespace. 75 class AddToCustomProgram(argparse.Action): 76 def __call__(self, parser, namespace, values, unused_option=None): 77 if not isinstance(namespace.program, list): 78 namespace.program = [] 79 80 if values not in all_steps: 81 raise parser.error( 82 f'argument --step: {values} is not the name of a ' 83 'presubmit check\n\nValid values for --step:\n' 84 f'{{{",".join(sorted(all_steps))}}}') 85 86 namespace.program.append(all_steps[values]) 87 88 exclusive.add_argument( 89 '--step', 90 action=AddToCustomProgram, 91 default=argparse.SUPPRESS, # Don't create a "step" argument. 92 help='Provide explicit steps instead of running a predefined program.', 93 ) 94 95 96def add_arguments(parser: argparse.ArgumentParser, 97 programs: Optional[presubmit.Programs] = None, 98 default: str = '') -> None: 99 """Adds common presubmit check options to an argument parser.""" 100 101 add_path_arguments(parser) 102 parser.add_argument('-k', 103 '--keep-going', 104 action='store_true', 105 help='Continue instead of aborting when errors occur.') 106 parser.add_argument( 107 '--output-directory', 108 type=Path, 109 help='Output directory (default: <repo root>/.presubmit)', 110 ) 111 parser.add_argument( 112 '--package-root', 113 type=Path, 114 help='Package root directory (default: <output directory>/packages)', 115 ) 116 117 exclusive = parser.add_mutually_exclusive_group() 118 exclusive.add_argument( 119 '--clear', 120 '--clean', 121 action='store_true', 122 help='Delete the presubmit output directory and exit.', 123 ) 124 125 if programs: 126 if not default: 127 raise ValueError('A default must be provided with programs') 128 129 _add_programs_arguments(parser, programs, default) 130 131 # LUCI builders extract the list of steps from the program and run them 132 # individually for a better UX in MILO. 133 parser.add_argument( 134 '--only-list-steps', 135 action='store_true', 136 help=argparse.SUPPRESS, 137 ) 138 139 140def run( 141 program: Sequence[Callable], 142 output_directory: Optional[Path], 143 package_root: Path, 144 clear: bool, 145 root: Path = None, 146 repositories: Collection[Path] = (), 147 only_list_steps=False, 148 **other_args, 149) -> int: 150 """Processes arguments from add_arguments and runs the presubmit. 151 152 Args: 153 program: from the --program option 154 output_directory: from --output-directory option 155 package_root: from --package-root option 156 clear: from the --clear option 157 root: base path from which to run presubmit checks; defaults to the root 158 of the current directory's repository 159 repositories: roots of Git repositories on which to run presubmit checks; 160 defaults to the root of the current directory's repository 161 only_list_steps: list the steps that would be executed, one per line, 162 instead of executing them 163 **other_args: remaining arguments defined by by add_arguments 164 165 Returns: 166 exit code for sys.exit; 0 if succesful, 1 if an error occurred 167 """ 168 if root is None: 169 root = git_repo.root() 170 171 if not repositories: 172 repositories = [root] 173 174 if output_directory is None: 175 output_directory = root / '.presubmit' 176 177 if not package_root: 178 package_root = output_directory / 'packages' 179 180 _LOG.debug('Using environment at %s', output_directory) 181 182 if clear: 183 _LOG.info('Clearing presubmit output directory') 184 185 if output_directory.exists(): 186 shutil.rmtree(output_directory) 187 _LOG.info('Deleted %s', output_directory) 188 189 return 0 190 191 if only_list_steps: 192 for step in program: 193 print(step.__name__) 194 return 0 195 196 if presubmit.run(program, 197 root, 198 repositories, 199 output_directory=output_directory, 200 package_root=package_root, 201 **other_args): 202 return 0 203 204 return 1 205