gerrit: add alias support via config file
Allow users to define custom command aliases like git supports.
This way they can define common workflows or short names.
BUG=None
TEST=created an alias and it worked
Change-Id: Ia81d7e1cbe5ad8bdec011d98226bdf9662f91dac
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2743836
Commit-Queue: Mike Frysinger <vapier@chromium.org>
Tested-by: Mike Frysinger <vapier@chromium.org>
Reviewed-by: Alex Klein <saklein@chromium.org>
diff --git a/scripts/gerrit.py b/scripts/gerrit.py
index e826bef..ce42dd1 100644
--- a/scripts/gerrit.py
+++ b/scripts/gerrit.py
@@ -14,12 +14,16 @@
import argparse
import collections
+import configparser
import functools
import inspect
import json
+from pathlib import Path
import re
+import shlex
import sys
+from chromite.lib import chromite_config
from chromite.lib import config_lib
from chromite.lib import constants
from chromite.lib import commandline
@@ -37,6 +41,26 @@
assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
+class Config:
+ """Manage the user's gerrit config settings.
+
+ This is entirely unique to this gerrit command. Inspiration for naming and
+ layout is taken from ~/.gitconfig settings.
+ """
+
+ def __init__(self, path: Path = chromite_config.GERRIT_CONFIG):
+ self.cfg = configparser.ConfigParser(interpolation=None)
+ if path.exists():
+ self.cfg.read(chromite_config.GERRIT_CONFIG)
+
+ def expand_alias(self, action):
+ """Expand any aliases."""
+ alias = self.cfg.get('alias', action, fallback=None)
+ if alias is not None:
+ return shlex.split(alias)
+ return action
+
+
class UserAction(object):
"""Base class for all custom user actions."""
@@ -923,6 +947,27 @@
_run_parallel_tasks(task, *opts.accounts)
+class ActionConfig(UserAction):
+ """Manage the gerrit tool's own config file
+
+ Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
+ It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
+
+ # Set up subcommand aliases.
+ [alias]
+ common-search = search 'is:open project:something/i/care/about'
+ """
+
+ COMMAND = 'config'
+
+ @staticmethod
+ def __call__(opts):
+ """Implement the action."""
+ # For now, this is a place holder for raising visibility for the config file
+ # and its associated help text documentation.
+ opts.parser.parse_args(['config', '--help'])
+
+
class ActionHelp(UserAction):
"""An alias to --help for CLI symmetry"""
@@ -1010,8 +1055,23 @@
)
-def GetParser():
- """Returns the parser to use for this module."""
+def _AddCommonOptions(parser, subparser):
+ """Add options that should work before & after the subcommand.
+
+ Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
+ """
+ parser.add_common_argument_to_group(
+ subparser, '--ne', '--no-emails', dest='notify',
+ default='ALL', action='store_const', const='NONE',
+ help='Do not send e-mail notifications')
+ parser.add_common_argument_to_group(
+ subparser, '-n', '--dry-run', dest='dryrun',
+ default=False, action='store_true',
+ help='Show what would be done, but do not make changes')
+
+
+def GetBaseParser() -> commandline.ArgumentParser:
+ """Returns the common parser (i.e. no subparsers added)."""
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.
@@ -1042,8 +1102,6 @@
"""
description += _GetActionUsages()
- actions = _GetActions()
-
site_params = config_lib.GetSiteParams()
parser = commandline.ArgumentParser(
description=description, default_log_level='notice')
@@ -1058,27 +1116,23 @@
help='Gerrit (on borg) instance to query (default: %s)' %
(site_params.EXTERNAL_GOB_INSTANCE))
- def _AddCommonOptions(p):
- """Add options that should work before & after the subcommand.
-
- Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
- """
- parser.add_common_argument_to_group(
- p, '--ne', '--no-emails', dest='notify',
- default='ALL', action='store_const', const='NONE',
- help='Do not send e-mail notifications')
- parser.add_common_argument_to_group(
- p, '-n', '--dry-run', dest='dryrun',
- default=False, action='store_true',
- help='Show what would be done, but do not make changes')
-
group = parser.add_argument_group('CL options')
- _AddCommonOptions(group)
+ _AddCommonOptions(parser, group)
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',
help='Return results in JSON (suitable for scripting)')
+ return parser
+
+
+def GetParser(parser: commandline.ArgumentParser = None) -> (
+ commandline.ArgumentParser):
+ """Returns the full parser to use for this module."""
+ if parser is None:
+ parser = GetBaseParser()
+
+ actions = _GetActions()
# 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
@@ -1089,14 +1143,26 @@
# 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)
- _AddCommonOptions(subparser)
+ _AddCommonOptions(parser, subparser)
cls.init_subparser(subparser)
return parser
def main(argv):
- parser = GetParser()
+ base_parser = GetBaseParser()
+ opts, subargs = base_parser.parse_known_args(argv)
+
+ config = Config()
+ if subargs:
+ # If the action is an alias to an expanded value, we need to mutate the argv
+ # and reparse things.
+ action = config.expand_alias(subargs[0])
+ if action != subargs[0]:
+ pos = argv.index(subargs[0])
+ argv = argv[:pos] + action + argv[pos + 1:]
+
+ parser = GetParser(parser=base_parser)
opts = parser.parse_args(argv)
# In case the action wants to throw a parser error.