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