1# SPDX-License-Identifier: GPL-2.0+
2#
3# Copyright 2023 Google LLC
4#
5
6"""Handles parsing of buildman arguments
7
8This creates the argument parser and uses it to parse the arguments passed in
9"""
10
11import argparse
12import os
13import pathlib
14import sys
15
16from u_boot_pylib import gitutil
17from patman import project
18from patman import settings
19
20PATMAN_DIR = pathlib.Path(__file__).parent
21HAS_TESTS = os.path.exists(PATMAN_DIR / "func_test.py")
22
23# Aliases for subcommands
24ALIASES = {
25    'series': ['s', 'ser'],
26    'status': ['st'],
27    'patchwork': ['pw'],
28    'upstream': ['us'],
29
30    # Series aliases
31    'archive': ['ar'],
32    'autolink': ['au'],
33    'gather': ['g'],
34    'open': ['o'],
35    'progress': ['p', 'pr', 'prog'],
36    'rm-version': ['rmv'],
37    'unarchive': ['unar'],
38    }
39
40
41class ErrorCatchingArgumentParser(argparse.ArgumentParser):
42    def __init__(self, **kwargs):
43        self.exit_state = None
44        self.catch_error = False
45        super().__init__(**kwargs)
46
47    def error(self, message):
48        if self.catch_error:
49            self.message = message
50        else:
51            super().error(message)
52
53    def exit(self, status=0, message=None):
54        if self.catch_error:
55            self.exit_state = True
56        else:
57            super().exit(status, message)
58
59
60def add_send_args(par):
61    """Add arguments for the 'send' command
62
63    Arguments:
64        par (ArgumentParser): Parser to add to
65    """
66    par.add_argument(
67        '-c', '--count', dest='count', type=int, default=-1,
68        help='Automatically create patches from top n commits')
69    par.add_argument(
70        '-e', '--end', type=int, default=0,
71        help='Commits to skip at end of patch list')
72    par.add_argument(
73        '-i', '--ignore-errors', action='store_true',
74        dest='ignore_errors', default=False,
75        help='Send patches email even if patch errors are found')
76    par.add_argument(
77        '-l', '--limit-cc', dest='limit', type=int, default=None,
78        help='Limit the cc list to LIMIT entries [default: %(default)s]')
79    par.add_argument(
80        '-m', '--no-maintainers', action='store_false',
81        dest='add_maintainers', default=True,
82        help="Don't cc the file maintainers automatically")
83    default_arg = None
84    top_level = gitutil.get_top_level()
85    if top_level:
86        default_arg = os.path.join(top_level, 'scripts',
87                                   'get_maintainer.pl') + ' --norolestats'
88    par.add_argument(
89        '--get-maintainer-script', dest='get_maintainer_script', type=str,
90        action='store',
91        default=default_arg,
92        help='File name of the get_maintainer.pl (or compatible) script.')
93    par.add_argument(
94        '-r', '--in-reply-to', type=str, action='store',
95        help="Message ID that this series is in reply to")
96    par.add_argument(
97        '-s', '--start', dest='start', type=int, default=0,
98        help='Commit to start creating patches from (0 = HEAD)')
99    par.add_argument(
100        '-t', '--ignore-bad-tags', action='store_true', default=False,
101        help='Ignore bad tags / aliases (default=warn)')
102    par.add_argument(
103        '--no-binary', action='store_true', dest='ignore_binary',
104        default=False,
105        help="Do not output contents of changes in binary files")
106    par.add_argument(
107        '--no-check', action='store_false', dest='check_patch', default=True,
108        help="Don't check for patch compliance")
109    par.add_argument(
110        '--tree', dest='check_patch_use_tree', default=False,
111        action='store_true',
112        help=("Set `tree` to True. If `tree` is False then we'll pass "
113              "'--no-tree' to checkpatch (default: tree=%(default)s)"))
114    par.add_argument(
115        '--no-tree', dest='check_patch_use_tree', action='store_false',
116        help="Set `tree` to False")
117    par.add_argument(
118        '--no-tags', action='store_false', dest='process_tags', default=True,
119        help="Don't process subject tags as aliases")
120    par.add_argument(
121        '--no-signoff', action='store_false', dest='add_signoff',
122        default=True, help="Don't add Signed-off-by to patches")
123    par.add_argument(
124        '--smtp-server', type=str,
125        help="Specify the SMTP server to 'git send-email'")
126    par.add_argument(
127        '--keep-change-id', action='store_true',
128        help='Preserve Change-Id tags in patches to send.')
129
130
131def _add_show_comments(parser):
132    parser.add_argument('-c', '--show-comments', action='store_true',
133                        help='Show comments from each patch')
134
135
136def _add_show_cover_comments(parser):
137    parser.add_argument('-C', '--show-cover-comments', action='store_true',
138                        help='Show comments from the cover letter')
139
140
141def add_patchwork_subparser(subparsers):
142    """Add the 'patchwork' subparser
143
144    Args:
145        subparsers (argparse action): Subparser parent
146
147    Return:
148        ArgumentParser: patchwork subparser
149    """
150    patchwork = subparsers.add_parser(
151        'patchwork', aliases=ALIASES['patchwork'],
152        help='Manage patchwork connection')
153    patchwork.defaults_cmds = [
154        ['set-project', 'U-Boot'],
155    ]
156    patchwork_subparsers = patchwork.add_subparsers(dest='subcmd')
157    patchwork_subparsers.add_parser('get-project')
158    uset = patchwork_subparsers.add_parser('set-project')
159    uset.add_argument(
160        'project_name', help="Patchwork project name, e.g. 'U-Boot'")
161    return patchwork
162
163
164def add_series_subparser(subparsers):
165    """Add the 'series' subparser
166
167    Args:
168        subparsers (argparse action): Subparser parent
169
170    Return:
171        ArgumentParser: series subparser
172    """
173    def _add_allow_unmarked(parser):
174        parser.add_argument('-M', '--allow-unmarked', action='store_true',
175                            default=False,
176                            help="Don't require commits to be marked")
177
178    def _add_mark(parser):
179        parser.add_argument(
180            '-m', '--mark', action='store_true',
181            help='Mark unmarked commits with a Change-Id field')
182
183    def _add_update(parser):
184        parser.add_argument('-u', '--update', action='store_true',
185                            help='Update the branch commit')
186
187    def _add_wait(parser, default_s):
188        """Add a -w option to a parser
189
190        Args:
191            parser (ArgumentParser): Parser to adjust
192            default_s (int): Default value to use, in seconds
193        """
194        parser.add_argument(
195            '-w', '--autolink-wait', type=int, default=default_s,
196            help='Seconds to wait for patchwork to get a sent series')
197
198    def _upstream_add(parser):
199        parser.add_argument('-U', '--upstream', help='Commit to end before')
200
201    def _add_gather(parser):
202        parser.add_argument(
203            '-G', '--no-gather-tags', dest='gather_tags', default=True,
204            action='store_false',
205            help="Don't gather review/test tags / update local series")
206
207    series = subparsers.add_parser('series', aliases=ALIASES['series'],
208                                   help='Manage series of patches')
209    series.defaults_cmds = [
210        ['set-link', 'fred'],
211    ]
212    series.add_argument(
213        '-n', '--dry-run', action='store_true', dest='dry_run', default=False,
214        help="Do a dry run (create but don't email patches)")
215    series.add_argument('-s', '--series', help='Name of series')
216    series.add_argument('-V', '--version', type=int,
217                        help='Version number to link')
218    series_subparsers = series.add_subparsers(dest='subcmd')
219
220    # This causes problem at present, perhaps due to the 'defaults' handling in
221    # settings
222    # series_subparsers.required = True
223
224    add = series_subparsers.add_parser('add')
225    add.add_argument('-D', '--desc',
226                     help='Series description / cover-letter title')
227    add.add_argument(
228        '-f', '--force-version', action='store_true',
229        help='Change the Series-version on a series to match its branch')
230    _add_mark(add)
231    _add_allow_unmarked(add)
232    _upstream_add(add)
233
234    series_subparsers.add_parser('archive', aliases=ALIASES['archive'])
235
236    auto = series_subparsers.add_parser('autolink',
237                                        aliases=ALIASES['autolink'])
238    _add_update(auto)
239    _add_wait(auto, 0)
240
241    aall = series_subparsers.add_parser('autolink-all')
242    aall.add_argument('-a', '--link-all-versions', action='store_true',
243                      help='Link all series versions, not just the latest')
244    aall.add_argument('-r', '--replace-existing', action='store_true',
245                      help='Replace existing links')
246    _add_update(aall)
247
248    series_subparsers.add_parser('dec')
249
250    gat = series_subparsers.add_parser('gather', aliases=ALIASES['gather'])
251    _add_gather(gat)
252    _add_show_comments(gat)
253    _add_show_cover_comments(gat)
254
255    sall = series_subparsers.add_parser('gather-all')
256    sall.add_argument(
257        '-a', '--gather-all-versions', action='store_true',
258        help='Gather tags from all series versions, not just the latest')
259    _add_gather(sall)
260    _add_show_comments(sall)
261    _add_show_cover_comments(sall)
262
263    series_subparsers.add_parser('get-link')
264    series_subparsers.add_parser('inc')
265    series_subparsers.add_parser('ls')
266
267    mar = series_subparsers.add_parser('mark')
268    mar.add_argument('-m', '--allow-marked', action='store_true',
269                     default=False,
270                     help="Don't require commits to be unmarked")
271
272    series_subparsers.add_parser('open', aliases=ALIASES['open'])
273    pat = series_subparsers.add_parser(
274        'patches', epilog='Show a list of patches and optional details')
275    pat.add_argument('-t', '--commit', action='store_true',
276                     help='Show the commit and diffstat')
277    pat.add_argument('-p', '--patch', action='store_true',
278                     help='Show the patch body')
279
280    prog = series_subparsers.add_parser('progress',
281                                        aliases=ALIASES['progress'])
282    prog.add_argument('-a', '--show-all-versions', action='store_true',
283                      help='Show all series versions, not just the latest')
284    prog.add_argument('-l', '--list-patches', action='store_true',
285                      help='List patch subject and status')
286
287    ren = series_subparsers.add_parser('rename')
288    ren.add_argument('-N', '--new-name', help='New name for the series')
289
290    series_subparsers.add_parser('rm')
291    series_subparsers.add_parser('rm-version', aliases=ALIASES['rm-version'])
292
293    scan = series_subparsers.add_parser('scan')
294    _add_mark(scan)
295    _add_allow_unmarked(scan)
296    _upstream_add(scan)
297
298    ssend = series_subparsers.add_parser('send')
299    add_send_args(ssend)
300    ssend.add_argument(
301        '--no-autolink', action='store_false', default=True, dest='autolink',
302        help='Monitor patchwork after sending so the series can be autolinked')
303    _add_wait(ssend, 120)
304
305    setl = series_subparsers.add_parser('set-link')
306    _add_update(setl)
307
308    setl.add_argument(
309        'link', help='Link to use, i.e. patchwork series number (e.g. 452329)')
310    stat = series_subparsers.add_parser('status', aliases=ALIASES['status'])
311    _add_show_comments(stat)
312    _add_show_cover_comments(stat)
313
314    series_subparsers.add_parser('summary')
315
316    series_subparsers.add_parser('unarchive', aliases=ALIASES['unarchive'])
317
318    unm = series_subparsers.add_parser('unmark')
319    _add_allow_unmarked(unm)
320
321    ver = series_subparsers.add_parser(
322        'version-change', help='Change a version to a different version')
323    ver.add_argument('--new-version', type=int,
324                     help='New version number to change this one too')
325
326    return series
327
328
329def add_send_subparser(subparsers):
330    """Add the 'send' subparser
331
332    Args:
333        subparsers (argparse action): Subparser parent
334
335    Return:
336        ArgumentParser: send subparser
337    """
338    send = subparsers.add_parser(
339        'send', help='Format, check and email patches (default command)')
340    send.add_argument(
341        '-b', '--branch', type=str,
342        help="Branch to process (by default, the current branch)")
343    send.add_argument(
344        '-n', '--dry-run', action='store_true', dest='dry_run',
345        default=False, help="Do a dry run (create but don't email patches)")
346    send.add_argument(
347        '--cc-cmd', dest='cc_cmd', type=str, action='store',
348        default=None, help='Output cc list for patch file (used by git)')
349    add_send_args(send)
350    send.add_argument('patchfiles', nargs='*')
351    return send
352
353
354def add_status_subparser(subparsers):
355    """Add the 'status' subparser
356
357    Args:
358        subparsers (argparse action): Subparser parent
359
360    Return:
361        ArgumentParser: status subparser
362    """
363    status = subparsers.add_parser('status', aliases=ALIASES['status'],
364                                   help='Check status of patches in patchwork')
365    _add_show_comments(status)
366    status.add_argument(
367        '-d', '--dest-branch', type=str,
368        help='Name of branch to create with collected responses')
369    status.add_argument('-f', '--force', action='store_true',
370                        help='Force overwriting an existing branch')
371    status.add_argument('-T', '--single-thread', action='store_true',
372                        help='Disable multithreading when reading patchwork')
373    return status
374
375
376def add_upstream_subparser(subparsers):
377    """Add the 'status' subparser
378
379    Args:
380        subparsers (argparse action): Subparser parent
381
382    Return:
383        ArgumentParser: status subparser
384    """
385    upstream = subparsers.add_parser('upstream', aliases=ALIASES['upstream'],
386                                     help='Manage upstream destinations')
387    upstream.defaults_cmds = [
388        ['add', 'us', 'http://fred'],
389        ['delete', 'us'],
390    ]
391    upstream_subparsers = upstream.add_subparsers(dest='subcmd')
392    uadd = upstream_subparsers.add_parser('add')
393    uadd.add_argument('remote_name',
394                      help="Git remote name used for this upstream, e.g. 'us'")
395    uadd.add_argument(
396        'url', help='URL to use for this upstream, e.g. '
397                    "'https://gitlab.denx.de/u-boot/u-boot.git'")
398    udel = upstream_subparsers.add_parser('delete')
399    udel.add_argument(
400        'remote_name',
401        help="Git remote name used for this upstream, e.g. 'us'")
402    upstream_subparsers.add_parser('list')
403    udef = upstream_subparsers.add_parser('default')
404    udef.add_argument('-u', '--unset', action='store_true',
405                      help='Unset the default upstream')
406    udef.add_argument('remote_name', nargs='?',
407                      help="Git remote name used for this upstream, e.g. 'us'")
408    return upstream
409
410
411def setup_parser():
412    """Set up command-line parser
413
414    Returns:
415        argparse.Parser object
416    """
417    epilog = '''Create patches from commits in a branch, check them and email
418        them as specified by tags you place in the commits. Use -n to do a dry
419        run first.'''
420
421    parser = ErrorCatchingArgumentParser(epilog=epilog)
422    parser.add_argument(
423        '-D', '--debug', action='store_true',
424        help='Enabling debugging (provides a full traceback on error)')
425    parser.add_argument(
426        '-N', '--no-capture', action='store_true',
427        help='Disable capturing of console output in tests')
428    parser.add_argument('-p', '--project', default=project.detect_project(),
429                        help="Project name; affects default option values and "
430                        "aliases [default: %(default)s]")
431    parser.add_argument('-P', '--patchwork-url',
432                        default='https://patchwork.ozlabs.org',
433                        help='URL of patchwork server [default: %(default)s]')
434    parser.add_argument(
435        '-T', '--thread', action='store_true', dest='thread',
436        default=False, help='Create patches as a single thread')
437    parser.add_argument(
438        '-v', '--verbose', action='store_true', dest='verbose', default=False,
439        help='Verbose output of errors and warnings')
440    parser.add_argument(
441        '-X', '--test-preserve-dirs', action='store_true',
442        help='Preserve and display test-created directories')
443    parser.add_argument(
444        '-H', '--full-help', action='store_true', dest='full_help',
445        default=False, help='Display the README file')
446
447    subparsers = parser.add_subparsers(dest='cmd')
448    add_send_subparser(subparsers)
449    patchwork = add_patchwork_subparser(subparsers)
450    series = add_series_subparser(subparsers)
451    add_status_subparser(subparsers)
452    upstream = add_upstream_subparser(subparsers)
453
454    # Only add the 'test' action if the test data files are available.
455    if HAS_TESTS:
456        test_parser = subparsers.add_parser('test', help='Run tests')
457        test_parser.add_argument('testname', type=str, default=None, nargs='?',
458                                 help="Specify the test to run")
459
460    parsers = {
461        'main': parser,
462        'series': series,
463        'patchwork': patchwork,
464        'upstream': upstream,
465        }
466    return parsers
467
468
469def parse_args(argv=None, config_fname=None, parsers=None):
470    """Parse command line arguments from sys.argv[]
471
472    Args:
473        argv (str or None): Arguments to process, or None to use sys.argv[1:]
474        config_fname (str): Config file to read, or None for default, or False
475            for an empty config
476
477    Returns:
478        tuple containing:
479            options: command line options
480            args: command lin arguments
481    """
482    if not parsers:
483        parsers = setup_parser()
484    parser = parsers['main']
485
486    # Parse options twice: first to get the project and second to handle
487    # defaults properly (which depends on project)
488    # Use parse_known_args() in case 'cmd' is omitted
489    if not argv:
490        argv = sys.argv[1:]
491
492    args, rest = parser.parse_known_args(argv)
493    if hasattr(args, 'project'):
494        settings.Setup(parser, args.project, argv, config_fname)
495        args, rest = parser.parse_known_args(argv)
496
497    # If we have a command, it is safe to parse all arguments
498    if args.cmd:
499        args = parser.parse_args(argv)
500    elif not args.full_help:
501        # No command, so insert it after the known arguments and before the ones
502        # that presumably relate to the 'send' subcommand
503        nargs = len(rest)
504        argv = argv[:-nargs] + ['send'] + rest
505        args = parser.parse_args(argv)
506
507    # Resolve aliases
508    for full, aliases in ALIASES.items():
509        if args.cmd in aliases:
510            args.cmd = full
511        if 'subcmd' in args and args.subcmd in aliases:
512            args.subcmd = full
513    if args.cmd in ['series', 'upstream', 'patchwork'] and not args.subcmd:
514        parser.parse_args([args.cmd, '--help'])
515
516    return args
517