• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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