gerrit: rewrite helpers to use classes
This adds more LOC, but it makes the parsing more robust, and allows
for better encapsulation of subcommand-specific options. The current
code forces all options for all subcommands to be part of the single
global parser leading to a lot of options that only make sense for a
few subcommands. It also makes it hard to build more complicated
subcommands with multiple optional settings.
BUG=None
TEST=ran various `gerrit` commands
Change-Id: Ice2bb390f6c542325886e8b2da73c4b426ea9803
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2048210
Tested-by: Mike Frysinger <vapier@chromium.org>
Reviewed-by: Alex Klein <saklein@chromium.org>
Commit-Queue: Mike Frysinger <vapier@chromium.org>
diff --git a/scripts/gerrit.py b/scripts/gerrit.py
index cfe8321..a025ddd 100644
--- a/scripts/gerrit.py
+++ b/scripts/gerrit.py
@@ -13,6 +13,7 @@
from __future__ import print_function
import collections
+import functools
import inspect
import json
import re
@@ -34,9 +35,19 @@
assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
-# Locate actions that are exposed to the user. All functions that start
-# with "UserAct" are fair game.
-ACTION_PREFIX = 'UserAct'
+class UserAction(object):
+ """Base class for all custom user actions."""
+
+ # The name of the command the user types in.
+ COMMAND = None
+
+ @staticmethod
+ def init_subparser(parser):
+ """Add arguments to this action's subparser."""
+
+ @staticmethod
+ def __call__(opts):
+ """Implement the action."""
# How many connections we'll use in parallel. We don't want this to be too high
@@ -256,28 +267,77 @@
return sorted(ret, key=key)
-def UserActTodo(opts):
+class _ActionSearchQuery(UserAction):
+ """Base class for actions that perform searches."""
+
+ @staticmethod
+ def init_subparser(parser):
+ """Add arguments to this action's subparser."""
+ parser.add_argument('--sort', default='number',
+ help='Key to sort on (number, project); use "unsorted" '
+ 'to disable')
+ parser.add_argument('-b', '--branch',
+ help='Limit output to the specific branch')
+ parser.add_argument('-p', '--project',
+ help='Limit output to the specific project')
+ parser.add_argument('-t', '--topic',
+ help='Limit output to the specific topic')
+
+
+class ActionTodo(_ActionSearchQuery):
"""List CLs needing your review"""
- cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
- 'label:Code-Review=0,user=self '
- 'NOT label:Verified<0'))
- PrintCls(opts, cls)
+
+ COMMAND = 'todo'
+
+ @staticmethod
+ def __call__(opts):
+ """Implement the action."""
+ cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
+ 'label:Code-Review=0,user=self '
+ 'NOT label:Verified<0'))
+ PrintCls(opts, cls)
-def UserActSearch(opts, query):
+class ActionSearch(_ActionSearchQuery):
"""List CLs matching the search query"""
- cls = FilteredQuery(opts, query)
- PrintCls(opts, cls)
-UserActSearch.usage = '<query>'
+
+ COMMAND = 'search'
+
+ @staticmethod
+ def init_subparser(parser):
+ """Add arguments to this action's subparser."""
+ _ActionSearchQuery.init_subparser(parser)
+ parser.add_argument('query',
+ help='The search query')
+
+ @staticmethod
+ def __call__(opts):
+ """Implement the action."""
+ cls = FilteredQuery(opts, opts.query)
+ PrintCls(opts, cls)
-def UserActMine(opts):
+class ActionMine(_ActionSearchQuery):
"""List your CLs with review statuses"""
- if opts.draft:
- rule = 'is:draft'
- else:
- rule = 'status:new'
- UserActSearch(opts, 'owner:self %s' % (rule,))
+
+ COMMAND = 'mine'
+
+ @staticmethod
+ def init_subparser(parser):
+ """Add arguments to this action's subparser."""
+ _ActionSearchQuery.init_subparser(parser)
+ parser.add_argument('--draft', default=False, action='store_true',
+ help='Show draft changes')
+
+ @staticmethod
+ def __call__(opts):
+ """Implement the action."""
+ if opts.draft:
+ rule = 'is:draft'
+ else:
+ rule = 'status:new'
+ cls = FilteredQuery(opts, 'owner:self %s' % (rule,))
+ PrintCls(opts, cls)
def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
@@ -304,15 +364,35 @@
return to_visit
-def UserActDeps(opts, query):
+class ActionDeps(_ActionSearchQuery):
"""List CLs matching a query, and all transitive dependencies of those CLs"""
- cls = _Query(opts, query, raw=False)
- @memoize.Memoize
- def _QueryChange(cl, helper=None):
- return _Query(opts, cl, raw=False, helper=helper)
+ COMMAND = 'deps'
- def _ProcessDeps(cl, deps, required):
+ @staticmethod
+ def init_subparser(parser):
+ """Add arguments to this action's subparser."""
+ _ActionSearchQuery.init_subparser(parser)
+ parser.add_argument('query',
+ help='The search query')
+
+ def __call__(self, opts):
+ """Implement the action."""
+ cls = _Query(opts, opts.query, raw=False)
+
+ @memoize.Memoize
+ def _QueryChange(cl, helper=None):
+ return _Query(opts, cl, raw=False, helper=helper)
+
+ transitives = _BreadthFirstSearch(
+ cls, functools.partial(self._Children, opts, _QueryChange),
+ visited_key=lambda cl: cl.gerrit_number)
+
+ transitives_raw = [cl.patch_dict for cl in transitives]
+ PrintCls(opts, transitives_raw)
+
+ @staticmethod
+ def _ProcessDeps(opts, querier, cl, deps, required):
"""Yields matching dependencies for a patch"""
# We need to query the change to guarantee that we have a .gerrit_number
for dep in deps:
@@ -322,7 +402,7 @@
helper = opts.gerrit[dep.remote]
# TODO(phobbs) this should maybe catch network errors.
- changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
+ changes = querier(dep.ToGerritQueryText(), helper=helper)
# Handle empty results. If we found a commit that was pushed directly
# (e.g. a bot commit), then gerrit won't know about it.
@@ -344,241 +424,369 @@
if change.status == 'NEW':
yield change
- def _Children(cl):
+ @classmethod
+ def _Children(cls, opts, querier, cl):
"""Yields the Gerrit and CQ-Depends dependencies of a patch"""
- for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
+ for change in cls._ProcessDeps(
+ opts, querier, cl, cl.PaladinDependencies(None), True):
yield change
- for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
+ for change in cls._ProcessDeps(
+ opts, querier, cl, cl.GerritDependencies(), False):
yield change
- transitives = _BreadthFirstSearch(
- cls, _Children,
- visited_key=lambda cl: cl.gerrit_number)
- transitives_raw = [cl.patch_dict for cl in transitives]
- PrintCls(opts, transitives_raw)
-UserActDeps.usage = '<query>'
-
-
-def UserActInspect(opts, *args):
+class ActionInspect(_ActionSearchQuery):
"""Show the details of one or more CLs"""
- cls = []
- for arg in args:
- helper, cl = GetGerrit(opts, arg)
- change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
- if change:
- cls.extend(change)
- else:
- logging.warning('no results found for CL %s', arg)
- PrintCls(opts, cls)
-UserActInspect.usage = '<CLs...>'
+
+ COMMAND = 'inspect'
+
+ @staticmethod
+ def init_subparser(parser):
+ """Add arguments to this action's subparser."""
+ _ActionSearchQuery.init_subparser(parser)
+ parser.add_argument('cls', nargs='+', metavar='CL',
+ help='The CL(s) to update')
+
+ @staticmethod
+ def __call__(opts):
+ """Implement the action."""
+ cls = []
+ for arg in opts.cls:
+ helper, cl = GetGerrit(opts, arg)
+ change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
+ if change:
+ cls.extend(change)
+ else:
+ logging.warning('no results found for CL %s', arg)
+ PrintCls(opts, cls)
-def UserActLabel_as(opts, *args):
+class _ActionLabeler(UserAction):
+ """Base helper for setting labels."""
+
+ LABEL = None
+ VALUES = None
+
+ @classmethod
+ def init_subparser(cls, parser):
+ """Add arguments to this action's subparser."""
+ parser.add_argument('--ne', '--no-emails', dest='notify',
+ default='ALL', action='store_const', const='NONE',
+ help='Do not send e-mail notifications')
+ parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
+ help='Optional message to include')
+ parser.add_argument('cls', nargs='+', metavar='CL',
+ help='The CL(s) to update')
+ parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
+ help='The label value; one of [%(choices)s]')
+
+ @classmethod
+ def __call__(cls, opts):
+ """Implement the action."""
+ # Convert user friendly command line option into a gerrit parameter.
+ def task(arg):
+ helper, cl = GetGerrit(opts, arg)
+ helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
+ dryrun=opts.dryrun, notify=opts.notify)
+ _run_parallel_tasks(task, *opts.cls)
+
+
+class ActionLabelAutoSubmit(_ActionLabeler):
"""Change the Auto-Submit label"""
- num = args[-1]
- def task(arg):
- helper, cl = GetGerrit(opts, arg)
- helper.SetReview(cl, labels={'Auto-Submit': num},
- dryrun=opts.dryrun, notify=opts.notify)
- _run_parallel_tasks(task, *args[:-1])
-UserActLabel_as.arg_min = 2
-UserActLabel_as.usage = '<CLs...> <0|1>'
+
+ COMMAND = 'label-as'
+ LABEL = 'Auto-Submit'
+ VALUES = ('0', '1')
-def UserActLabel_cr(opts, *args):
+class ActionLabelCodeReview(_ActionLabeler):
"""Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
- num = args[-1]
- def task(arg):
- helper, cl = GetGerrit(opts, arg)
- helper.SetReview(cl, labels={'Code-Review': num},
- dryrun=opts.dryrun, notify=opts.notify)
- _run_parallel_tasks(task, *args[:-1])
-UserActLabel_cr.arg_min = 2
-UserActLabel_cr.usage = '<CLs...> <-2|-1|0|1|2>'
+
+ COMMAND = 'label-cr'
+ LABEL = 'Code-Review'
+ VALUES = ('-2', '-1', '0', '1', '2')
-def UserActLabel_v(opts, *args):
+class ActionLabelVerified(_ActionLabeler):
"""Change the Verified label"""
- num = args[-1]
- def task(arg):
- helper, cl = GetGerrit(opts, arg)
- helper.SetReview(cl, labels={'Verified': num},
- dryrun=opts.dryrun, notify=opts.notify)
- _run_parallel_tasks(task, *args[:-1])
-UserActLabel_v.arg_min = 2
-UserActLabel_v.usage = '<CLs...> <-1|0|1>'
+
+ COMMAND = 'label-v'
+ LABEL = 'Verified'
+ VALUES = ('-1', '0', '1')
-def UserActLabel_cq(opts, *args):
+class ActionLabelCommitQueue(_ActionLabeler):
"""Change the Commit-Queue label (1=dry-run 2=commit)"""
- num = args[-1]
- def task(arg):
- helper, cl = GetGerrit(opts, arg)
- helper.SetReview(cl, labels={'Commit-Queue': num},
- dryrun=opts.dryrun, notify=opts.notify)
- _run_parallel_tasks(task, *args[:-1])
-UserActLabel_cq.arg_min = 2
-UserActLabel_cq.usage = '<CLs...> <0|1|2>'
+
+ COMMAND = 'label-cq'
+ LABEL = 'Commit-Queue'
+ VALUES = ('0', '1', '2')
-def UserActSubmit(opts, *args):
+class _ActionSimpleParallelCLs(UserAction):
+ """Base helper for actions that only accept CLs."""
+
+ @staticmethod
+ def init_subparser(parser):
+ """Add arguments to this action's subparser."""
+ parser.add_argument('cls', nargs='+', metavar='CL',
+ help='The CL(s) to update')
+
+ def __call__(self, opts):
+ """Implement the action."""
+ def task(arg):
+ helper, cl = GetGerrit(opts, arg)
+ self._process_one(helper, cl, opts)
+ _run_parallel_tasks(task, *opts.cls)
+
+
+class ActionSubmit(_ActionSimpleParallelCLs):
"""Submit CLs"""
- def task(arg):
- helper, cl = GetGerrit(opts, arg)
+
+ COMMAND = 'submit'
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
helper.SubmitChange(cl, dryrun=opts.dryrun)
- _run_parallel_tasks(task, *args)
-UserActSubmit.usage = '<CLs...>'
-def UserActAbandon(opts, *args):
+class ActionAbandon(_ActionSimpleParallelCLs):
"""Abandon CLs"""
- def task(arg):
- helper, cl = GetGerrit(opts, arg)
+
+ COMMAND = 'abandon'
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
helper.AbandonChange(cl, dryrun=opts.dryrun)
- _run_parallel_tasks(task, *args)
-UserActAbandon.usage = '<CLs...>'
-def UserActRestore(opts, *args):
+class ActionRestore(_ActionSimpleParallelCLs):
"""Restore CLs that were abandoned"""
- def task(arg):
- helper, cl = GetGerrit(opts, arg)
+
+ COMMAND = 'restore'
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
helper.RestoreChange(cl, dryrun=opts.dryrun)
- _run_parallel_tasks(task, *args)
-UserActRestore.usage = '<CLs...>'
-def UserActReviewers(opts, cl, *args):
+class ActionReviewers(UserAction):
"""Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
- emails = args
- # Allow for optional leading '~'.
- email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
- add_list, remove_list, invalid_list = [], [], []
- for x in emails:
- if not email_validator.match(x):
- invalid_list.append(x)
- elif x[0] == '~':
- remove_list.append(x[1:])
- else:
- add_list.append(x)
+ COMMAND = 'reviewers'
- if invalid_list:
- cros_build_lib.Die(
- 'Invalid email address(es): %s' % ', '.join(invalid_list))
+ @staticmethod
+ def init_subparser(parser):
+ """Add arguments to this action's subparser."""
+ parser.add_argument('--ne', '--no-emails', dest='notify',
+ default='ALL', action='store_const', const='NONE',
+ help='Do not send e-mail notifications')
+ parser.add_argument('cl', metavar='CL',
+ help='The CL to update')
+ parser.add_argument('reviewers', nargs='+',
+ help='The reviewers to add/remove')
- if add_list or remove_list:
- helper, cl = GetGerrit(opts, cl)
- helper.SetReviewers(cl, add=add_list, remove=remove_list,
- dryrun=opts.dryrun, notify=opts.notify)
-UserActReviewers.usage = '<CL> <emails...>'
+ @staticmethod
+ def __call__(opts):
+ """Implement the action."""
+ # Allow for optional leading '~'.
+ email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
+ add_list, remove_list, invalid_list = [], [], []
+
+ for email in opts.reviewers:
+ if not email_validator.match(email):
+ invalid_list.append(email)
+ elif email[0] == '~':
+ remove_list.append(email[1:])
+ else:
+ add_list.append(email)
+
+ if invalid_list:
+ cros_build_lib.Die(
+ 'Invalid email address(es): %s' % ', '.join(invalid_list))
+
+ if add_list or remove_list:
+ helper, cl = GetGerrit(opts, opts.cl)
+ helper.SetReviewers(cl, add=add_list, remove=remove_list,
+ dryrun=opts.dryrun, notify=opts.notify)
-def UserActAssign(opts, cl, assignee):
- """Set the assignee for a CL"""
- helper, cl = GetGerrit(opts, cl)
- helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
-UserActAssign.usage = '<CL> <assignee>'
+class ActionAssign(_ActionSimpleParallelCLs):
+ """Set the assignee for CLs"""
+
+ COMMAND = 'assign'
+
+ @staticmethod
+ def init_subparser(parser):
+ """Add arguments to this action's subparser."""
+ parser.add_argument('assignee',
+ help='The new assignee')
+ _ActionSimpleParallelCLs.init_subparser(parser)
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
+ helper.SetAssignee(cl, opts.assignee, dryrun=opts.dryrun)
-def UserActMessage(opts, cl, message):
+class ActionMessage(UserAction):
"""Add a message to a CL"""
- helper, cl = GetGerrit(opts, cl)
- helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
-UserActMessage.usage = '<CL> <message>'
+
+ COMMAND = 'message'
+
+ @staticmethod
+ def init_subparser(parser):
+ """Add arguments to this action's subparser."""
+ parser.add_argument('message',
+ help='The message to post')
+ _ActionSimpleParallelCLs.init_subparser(parser)
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
+ helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
-def UserActTopic(opts, topic, *args):
+class ActionTopic(UserAction):
"""Set a topic for one or more CLs"""
- def task(arg):
- helper, arg = GetGerrit(opts, arg)
- helper.SetTopic(arg, topic, dryrun=opts.dryrun)
- _run_parallel_tasks(task, *args)
-UserActTopic.usage = '<topic> <CLs...>'
+
+ COMMAND = 'topic'
+
+ @staticmethod
+ def init_subparser(parser):
+ """Add arguments to this action's subparser."""
+ parser.add_argument('topic',
+ help='The topic to set')
+ _ActionSimpleParallelCLs.init_subparser(parser)
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
+ helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
-def UserActPrivate(opts, cl, private_str):
- """Set the private bit on a CL to private"""
- try:
- private = cros_build_lib.BooleanShellValue(private_str, False)
- except ValueError:
- raise RuntimeError('Unknown "boolean" value: %s' % private_str)
+class ActionPrivate(_ActionSimpleParallelCLs):
+ """Mark CLs private"""
- helper, cl = GetGerrit(opts, cl)
- helper.SetPrivate(cl, private, dryrun=opts.dryrun)
-UserActPrivate.usage = '<CL> <private str>'
+ COMMAND = 'private'
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
+ helper.SetPrivate(cl, True, dryrun=opts.dryrun)
-def UserActSethashtags(opts, cl, *args):
+class ActionPublic(_ActionSimpleParallelCLs):
+ """Mark CLs public"""
+
+ COMMAND = 'public'
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
+ helper.SetPrivate(cl, False, dryrun=opts.dryrun)
+
+
+class ActionSethashtags(UserAction):
"""Add/remove hashtags on a CL (prepend with '~' to remove)"""
- hashtags = args
- add = []
- remove = []
- for hashtag in hashtags:
- if hashtag.startswith('~'):
- remove.append(hashtag[1:])
- else:
- add.append(hashtag)
- helper, cl = GetGerrit(opts, cl)
- helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
-UserActSethashtags.usage = '<CL> <hashtags...>'
+
+ COMMAND = 'hashtags'
+
+ @staticmethod
+ def init_subparser(parser):
+ """Add arguments to this action's subparser."""
+ parser.add_argument('cl', metavar='CL',
+ help='The CL to update')
+ parser.add_argument('hashtags', nargs='+',
+ help='The hashtags to add/remove')
+
+ @staticmethod
+ def __call__(opts):
+ """Implement the action."""
+ add = []
+ remove = []
+ for hashtag in opts.hashtags:
+ if hashtag.startswith('~'):
+ remove.append(hashtag[1:])
+ else:
+ add.append(hashtag)
+ helper, cl = GetGerrit(opts, opts.cl)
+ helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
-def UserActDeletedraft(opts, *args):
+class ActionDeletedraft(_ActionSimpleParallelCLs):
"""Delete draft CLs"""
- def task(arg):
- helper, cl = GetGerrit(opts, arg)
+
+ COMMAND = 'deletedraft'
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
helper.DeleteDraft(cl, dryrun=opts.dryrun)
- _run_parallel_tasks(task, *args)
-UserActDeletedraft.usage = '<CLs...>'
-def UserActReviewed(opts, *args):
+class ActionReviewed(_ActionSimpleParallelCLs):
"""Mark CLs as reviewed"""
- def task(arg):
- helper, cl = GetGerrit(opts, arg)
+
+ COMMAND = 'reviewed'
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
helper.ReviewedChange(cl, dryrun=opts.dryrun)
- _run_parallel_tasks(task, *args)
-UserActReviewed.usage = '<CLs...>'
-def UserActUnreviewed(opts, *args):
+class ActionUnreviewed(_ActionSimpleParallelCLs):
"""Mark CLs as unreviewed"""
- def task(arg):
- helper, cl = GetGerrit(opts, arg)
+
+ COMMAND = 'unreviewed'
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
helper.UnreviewedChange(cl, dryrun=opts.dryrun)
- _run_parallel_tasks(task, *args)
-UserActUnreviewed.usage = '<CLs...>'
-def UserActIgnore(opts, *args):
+class ActionIgnore(_ActionSimpleParallelCLs):
"""Ignore CLs (suppress notifications/dashboard/etc...)"""
- def task(arg):
- helper, cl = GetGerrit(opts, arg)
+
+ COMMAND = 'ignore'
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
helper.IgnoreChange(cl, dryrun=opts.dryrun)
- _run_parallel_tasks(task, *args)
-UserActIgnore.usage = '<CLs...>'
-def UserActUnignore(opts, *args):
+class ActionUnignore(_ActionSimpleParallelCLs):
"""Unignore CLs (enable notifications/dashboard/etc...)"""
- def task(arg):
- helper, cl = GetGerrit(opts, arg)
+
+ COMMAND = 'unignore'
+
+ @staticmethod
+ def _process_one(helper, cl, opts):
+ """Use |helper| to process the single |cl|."""
helper.UnignoreChange(cl, dryrun=opts.dryrun)
- _run_parallel_tasks(task, *args)
-UserActUnignore.usage = '<CLs...>'
-def UserActAccount(opts):
+class ActionAccount(UserAction):
"""Get the current user account information"""
- helper, _ = GetGerrit(opts)
- acct = helper.GetAccount()
- if opts.json:
- json.dump(acct, sys.stdout)
- else:
- print('account_id:%i %s <%s>' %
- (acct['_account_id'], acct['name'], acct['email']))
+
+ COMMAND = 'account'
+
+ @staticmethod
+ def __call__(opts):
+ """Implement the action."""
+ helper, _ = GetGerrit(opts)
+ acct = helper.GetAccount()
+ if opts.json:
+ json.dump(acct, sys.stdout)
+ else:
+ print('account_id:%i %s <%s>' %
+ (acct['_account_id'], acct['name'], acct['email']))
@memoize.Memoize
@@ -589,26 +797,23 @@
An ordered dictionary mapping the user subcommand (e.g. "foo") to the
function that implements that command (e.g. UserActFoo).
"""
- ret = collections.OrderedDict()
- for funcname in sorted(globals()):
- if not funcname.startswith(ACTION_PREFIX):
+ VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
+
+ actions = {}
+ for cls in globals().values():
+ if (not inspect.isclass(cls) or
+ not issubclass(cls, UserAction) or
+ not getattr(cls, 'COMMAND', None)):
continue
- # Turn "UserActFoo" into just "Foo" for further checking below.
- funcname_chopped = funcname[len(ACTION_PREFIX):]
-
# Sanity check names for devs adding new commands. Should be quick.
- expected_funcname = funcname_chopped.lower().capitalize()
- if funcname_chopped != expected_funcname:
- raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
- (funcname_chopped, expected_funcname))
+ cmd = cls.COMMAND
+ assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
+ assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
- # Turn "Foo_bar" into "foo-bar".
- cmdname = funcname_chopped.lower().replace('_', '-')
- func = globals()[funcname]
- ret[cmdname] = func
+ actions[cmd] = cls
- return ret
+ return collections.OrderedDict(sorted(actions.items()))
def _GetActionUsages():
@@ -618,7 +823,7 @@
cmds = list(actions.keys())
functions = list(actions.values())
usages = [getattr(x, 'usage', '') for x in functions]
- docs = [x.__doc__ for x in functions]
+ docs = [x.__doc__.splitlines()[0] for x in functions]
cmd_indent = len(max(cmds, key=len))
usage_indent = len(max(usages, key=len))
@@ -630,8 +835,7 @@
def GetParser():
"""Returns the parser to use for this module."""
- usage = """%(prog)s [options] <action> [action args]
-
+ description = """\
There is no support for doing line-by-line code review via the command line.
This helps you manage various bits and CL status.
@@ -659,12 +863,12 @@
Actions:
"""
- usage += _GetActionUsages()
+ description += _GetActionUsages()
actions = _GetActions()
site_params = config_lib.GetSiteParams()
- parser = commandline.ArgumentParser(usage=usage)
+ parser = commandline.ArgumentParser(description=description)
parser.add_argument('-i', '--internal', dest='gob', action='store_const',
default=site_params.EXTERNAL_GOB_INSTANCE,
const=site_params.INTERNAL_GOB_INSTANCE,
@@ -673,9 +877,6 @@
default=site_params.EXTERNAL_GOB_INSTANCE,
help=('Gerrit (on borg) instance to query (default: %s)' %
(site_params.EXTERNAL_GOB_INSTANCE)))
- parser.add_argument('--sort', default='number',
- help='Key to sort on (number, project); use "unsorted" '
- 'to disable')
parser.add_argument('--raw', default=False, action='store_true',
help='Return raw results (suitable for scripting)')
parser.add_argument('--json', default=False, action='store_true',
@@ -683,23 +884,22 @@
parser.add_argument('-n', '--dry-run', default=False, action='store_true',
dest='dryrun',
help='Show what would be done, but do not make changes')
- parser.add_argument('--ne', '--no-emails', default=True, action='store_false',
- dest='send_email',
- help='Do not send email for some operations '
- '(e.g. ready/review/trybotready/verify)')
parser.add_argument('-v', '--verbose', default=False, action='store_true',
help='Be more verbose in output')
- parser.add_argument('-b', '--branch',
- help='Limit output to the specific branch')
- parser.add_argument('--draft', default=False, action='store_true',
- help="Show draft changes (applicable to 'mine' only)")
- parser.add_argument('-p', '--project',
- help='Limit output to the specific project')
- parser.add_argument('-t', '--topic',
- help='Limit output to the specific topic')
- parser.add_argument('action', choices=list(actions.keys()),
- help='The gerrit action to perform')
- parser.add_argument('args', nargs='*', help='Action arguments')
+
+ # Subparsers are required by default under Python 2. Python 3 changed to
+ # not required, but didn't include a required option until 3.7. Setting
+ # the required member works in all versions (and setting dest name).
+ subparsers = parser.add_subparsers(dest='action')
+ subparsers.required = True
+ for cmd, cls in actions.items():
+ # Format the full docstring by removing the file level indentation.
+ description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
+ subparser = subparsers.add_parser(cmd, description=description)
+ subparser.add_argument('-n', '--dry-run', dest='dryrun',
+ default=False, action='store_true',
+ help='Show what would be done only')
+ cls.init_subparser(subparser)
return parser
@@ -711,8 +911,6 @@
# A cache of gerrit helpers we'll load on demand.
opts.gerrit = {}
- # Convert user friendly command line option into a gerrit parameter.
- opts.notify = 'ALL' if opts.send_email else 'NONE'
opts.Freeze()
# pylint: disable=global-statement
@@ -721,18 +919,9 @@
# Now look up the requested user action and run it.
actions = _GetActions()
- functor = actions[opts.action]
- argspec = inspect.getargspec(functor)
- if argspec.varargs:
- arg_min = getattr(functor, 'arg_min', len(argspec.args))
- if len(opts.args) < arg_min:
- parser.error('incorrect number of args: %s expects at least %s' %
- (opts.action, arg_min))
- elif len(argspec.args) - 1 != len(opts.args):
- parser.error('incorrect number of args: %s expects %s' %
- (opts.action, len(argspec.args) - 1))
+ obj = actions[opts.action]()
try:
- functor(opts, *opts.args)
+ obj(opts)
except (cros_build_lib.RunCommandError, gerrit.GerritException,
gob_util.GOBError) as e:
cros_build_lib.Die(e)