blob: d59eb76fac78c672dd4b1518d210ce1e9c6f2f32 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Mike Frysinger13f23a42013-05-13 17:32:01 -04002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysinger08737512014-02-07 22:58:26 -05006"""A command line interface to Gerrit-on-borg instances.
Mike Frysinger13f23a42013-05-13 17:32:01 -04007
8Internal Note:
9To expose a function directly to the command line interface, name your function
10with the prefix "UserAct".
11"""
12
Mike Frysinger31ff6f92014-02-08 04:33:03 -050013from __future__ import print_function
14
Mike Frysinger8037f752020-02-29 20:47:09 -050015import argparse
Mike Frysinger65fc8632020-02-06 18:11:12 -050016import collections
Mike Frysinger2295d792021-03-08 15:55:23 -050017import configparser
Mike Frysingerc7796cf2020-02-06 23:55:15 -050018import functools
Mike Frysinger13f23a42013-05-13 17:32:01 -040019import inspect
Mike Frysinger87c74ce2017-04-04 16:12:31 -040020import json
Mike Frysinger2295d792021-03-08 15:55:23 -050021from pathlib import Path
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070022import re
Mike Frysinger2295d792021-03-08 15:55:23 -050023import shlex
Mike Frysinger87c74ce2017-04-04 16:12:31 -040024import sys
Mike Frysinger13f23a42013-05-13 17:32:01 -040025
Mike Frysinger2295d792021-03-08 15:55:23 -050026from chromite.lib import chromite_config
Aviv Keshetb7519e12016-10-04 00:50:00 -070027from chromite.lib import config_lib
28from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040029from chromite.lib import commandline
30from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070031from chromite.lib import cros_logging as logging
Mike Frysinger13f23a42013-05-13 17:32:01 -040032from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050033from chromite.lib import gob_util
Mike Frysinger254f33f2019-12-11 13:54:29 -050034from chromite.lib import parallel
Mike Frysinger7f2018d2021-02-04 00:10:58 -050035from chromite.lib import pformat
Mike Frysinger13f23a42013-05-13 17:32:01 -040036from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040037from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060038from chromite.utils import memoize
Mike Frysinger13f23a42013-05-13 17:32:01 -040039
40
Mike Frysinger1c76d4c2020-02-08 23:35:29 -050041assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
42
43
Mike Frysinger2295d792021-03-08 15:55:23 -050044class Config:
45 """Manage the user's gerrit config settings.
46
47 This is entirely unique to this gerrit command. Inspiration for naming and
48 layout is taken from ~/.gitconfig settings.
49 """
50
51 def __init__(self, path: Path = chromite_config.GERRIT_CONFIG):
52 self.cfg = configparser.ConfigParser(interpolation=None)
53 if path.exists():
54 self.cfg.read(chromite_config.GERRIT_CONFIG)
55
56 def expand_alias(self, action):
57 """Expand any aliases."""
58 alias = self.cfg.get('alias', action, fallback=None)
59 if alias is not None:
60 return shlex.split(alias)
61 return action
62
63
Mike Frysingerc7796cf2020-02-06 23:55:15 -050064class UserAction(object):
65 """Base class for all custom user actions."""
66
67 # The name of the command the user types in.
68 COMMAND = None
69
70 @staticmethod
71 def init_subparser(parser):
72 """Add arguments to this action's subparser."""
73
74 @staticmethod
75 def __call__(opts):
76 """Implement the action."""
Mike Frysinger62178ae2020-03-20 01:37:43 -040077 raise RuntimeError('Internal error: action missing __call__ implementation')
Mike Frysinger108eda22018-06-06 18:45:12 -040078
79
Mike Frysinger254f33f2019-12-11 13:54:29 -050080# How many connections we'll use in parallel. We don't want this to be too high
81# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
82# seems to be good enough for users.
83CONNECTION_LIMIT = 10
84
85
Mike Frysinger031ad0b2013-05-14 18:15:34 -040086COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040087
88# Map the internal names to the ones we normally show on the web ui.
89GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080090 'COMR': ['CQ', 'Commit Queue ',],
91 'CRVW': ['CR', 'Code Review ',],
92 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080093 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060094 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040095}
96
97# Order is important -- matches the web ui. This also controls the short
98# entries that we summarize in non-verbose mode.
99GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
100
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400101# Shorter strings for CL status messages.
102GERRIT_SUMMARY_MAP = {
103 'ABANDONED': 'ABD',
104 'MERGED': 'MRG',
105 'NEW': 'NEW',
106 'WIP': 'WIP',
107}
108
Mike Frysinger13f23a42013-05-13 17:32:01 -0400109
110def red(s):
111 return COLOR.Color(terminal.Color.RED, s)
112
113
114def green(s):
115 return COLOR.Color(terminal.Color.GREEN, s)
116
117
118def blue(s):
119 return COLOR.Color(terminal.Color.BLUE, s)
120
121
Mike Frysinger254f33f2019-12-11 13:54:29 -0500122def _run_parallel_tasks(task, *args):
123 """Small wrapper around BackgroundTaskRunner to enforce job count."""
124 with parallel.BackgroundTaskRunner(task, processes=CONNECTION_LIMIT) as q:
125 for arg in args:
126 q.put([arg])
127
128
Mike Frysinger13f23a42013-05-13 17:32:01 -0400129def limits(cls):
130 """Given a dict of fields, calculate the longest string lengths
131
132 This allows you to easily format the output of many results so that the
133 various cols all line up correctly.
134 """
135 lims = {}
136 for cl in cls:
137 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -0400138 # Use %s rather than str() to avoid codec issues.
139 # We also do this so we can format integers.
140 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400141 return lims
142
143
Mike Frysinger88f27292014-06-17 09:40:45 -0700144# TODO: This func really needs to be merged into the core gerrit logic.
145def GetGerrit(opts, cl=None):
146 """Auto pick the right gerrit instance based on the |cl|
147
148 Args:
149 opts: The general options object.
150 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
151
152 Returns:
153 A tuple of a gerrit object and a sanitized CL #.
154 """
155 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700156 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600157 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600158 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600159 if cl.startswith('*'):
160 cl = cl[1:]
161 else:
162 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700163 elif ':' in cl:
164 gob, cl = cl.split(':', 1)
165
166 if not gob in opts.gerrit:
167 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
168
169 return (opts.gerrit[gob], cl)
170
171
Mike Frysinger13f23a42013-05-13 17:32:01 -0400172def GetApprovalSummary(_opts, cls):
173 """Return a dict of the most important approvals"""
174 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700175 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
176 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
177 if not cats:
178 logging.warning('unknown gerrit approval type: %s', approver['type'])
179 continue
180 cat = cats[0].strip()
181 val = int(approver['value'])
182 if not cat in approvs:
183 # Ignore the extended categories in the summary view.
184 continue
185 elif approvs[cat] == '':
186 approvs[cat] = val
187 elif val < 0:
188 approvs[cat] = min(approvs[cat], val)
189 else:
190 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400191 return approvs
192
193
Mike Frysingera1b4b272017-04-05 16:11:00 -0400194def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400195 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400196 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400197 lims = {'url': 0, 'project': 0}
198
199 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400200
201 if opts.verbose:
202 status += '%s ' % (cl['status'],)
203 else:
204 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
205
Mike Frysinger13f23a42013-05-13 17:32:01 -0400206 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400207 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400208 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400209 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400210 functor = lambda x: x
211 elif approvs[cat] < 0:
212 functor = red
213 else:
214 functor = green
215 status += functor('%s:%2s ' % (cat, approvs[cat]))
216
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400217 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
218 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400219
220 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400221 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400222 functor = red if int(approver['value']) < 0 else green
223 n = functor('%2s' % approver['value'])
224 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
225 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500226 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400227
228
Mike Frysingera1b4b272017-04-05 16:11:00 -0400229def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400230 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400231 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600232 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400233 pfx = ''
234 # Special case internal Chrome GoB as that is what most devs use.
235 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600236 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
237 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400238 for cl in cls:
239 print('%s%s' % (pfx, cl['number']))
240
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400241 elif opts.json:
242 json.dump(cls, sys.stdout)
243
Mike Frysingera1b4b272017-04-05 16:11:00 -0400244 else:
245 if lims is None:
246 lims = limits(cls)
247
248 for cl in cls:
249 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
250
251
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400252def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700253 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800254 if opts.branch is not None:
255 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800256 if opts.project is not None:
257 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800258 if opts.topic is not None:
259 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800260
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400261 if helper is None:
262 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700263 return helper.Query(query, raw=raw, bypass_cache=False)
264
265
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400266def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700267 """Query gerrit and filter/clean up the results"""
268 ret = []
269
Mike Frysinger2cd56022017-01-12 20:56:27 -0500270 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400271 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400272 # Gerrit likes to return a stats record too.
273 if not 'project' in cl:
274 continue
275
276 # Strip off common leading names since the result is still
277 # unique over the whole tree.
278 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400279 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
280 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400281 if cl['project'].startswith('%s/' % pfx):
282 cl['project'] = cl['project'][len(pfx) + 1:]
283
Mike Frysinger479f1192017-09-14 22:36:30 -0400284 cl['url'] = uri_lib.ShortenUri(cl['url'])
285
Mike Frysinger13f23a42013-05-13 17:32:01 -0400286 ret.append(cl)
287
Mike Frysingerb62313a2017-06-30 16:38:58 -0400288 if opts.sort == 'unsorted':
289 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700290 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400291 key = lambda x: int(x[opts.sort])
292 else:
293 key = lambda x: x[opts.sort]
294 return sorted(ret, key=key)
295
296
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500297class _ActionSearchQuery(UserAction):
298 """Base class for actions that perform searches."""
299
300 @staticmethod
301 def init_subparser(parser):
302 """Add arguments to this action's subparser."""
303 parser.add_argument('--sort', default='number',
304 help='Key to sort on (number, project); use "unsorted" '
305 'to disable')
306 parser.add_argument('-b', '--branch',
307 help='Limit output to the specific branch')
308 parser.add_argument('-p', '--project',
309 help='Limit output to the specific project')
310 parser.add_argument('-t', '--topic',
311 help='Limit output to the specific topic')
312
313
314class ActionTodo(_ActionSearchQuery):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400315 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500316
317 COMMAND = 'todo'
318
319 @staticmethod
320 def __call__(opts):
321 """Implement the action."""
Mike Frysinger242d2922021-02-09 14:31:50 -0500322 cls = FilteredQuery(opts, 'attention:self')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500323 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400324
325
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500326class ActionSearch(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800327 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500328
329 COMMAND = 'search'
330
331 @staticmethod
332 def init_subparser(parser):
333 """Add arguments to this action's subparser."""
334 _ActionSearchQuery.init_subparser(parser)
335 parser.add_argument('query',
336 help='The search query')
337
338 @staticmethod
339 def __call__(opts):
340 """Implement the action."""
341 cls = FilteredQuery(opts, opts.query)
342 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400343
344
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500345class ActionMine(_ActionSearchQuery):
Mike Frysingera1db2c42014-06-15 00:42:48 -0700346 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500347
348 COMMAND = 'mine'
349
350 @staticmethod
351 def init_subparser(parser):
352 """Add arguments to this action's subparser."""
353 _ActionSearchQuery.init_subparser(parser)
354 parser.add_argument('--draft', default=False, action='store_true',
355 help='Show draft changes')
356
357 @staticmethod
358 def __call__(opts):
359 """Implement the action."""
360 if opts.draft:
361 rule = 'is:draft'
362 else:
363 rule = 'status:new'
364 cls = FilteredQuery(opts, 'owner:self %s' % (rule,))
365 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700366
367
Paul Hobbs89765232015-06-24 14:07:49 -0700368def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
369 """Runs breadth first search starting from the nodes in |to_visit|
370
371 Args:
372 to_visit: the starting nodes
373 children: a function which takes a node and returns the nodes adjacent to it
374 visited_key: a function for deduplicating node visits. Defaults to the
375 identity function (lambda x: x)
376
377 Returns:
378 A list of nodes which are reachable from any node in |to_visit| by calling
379 |children| any number of times.
380 """
381 to_visit = list(to_visit)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400382 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700383 for node in to_visit:
384 for child in children(node):
385 key = visited_key(child)
386 if key not in seen:
387 seen.add(key)
388 to_visit.append(child)
389 return to_visit
390
391
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500392class ActionDeps(_ActionSearchQuery):
Paul Hobbs89765232015-06-24 14:07:49 -0700393 """List CLs matching a query, and all transitive dependencies of those CLs"""
Paul Hobbs89765232015-06-24 14:07:49 -0700394
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500395 COMMAND = 'deps'
Paul Hobbs89765232015-06-24 14:07:49 -0700396
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500397 @staticmethod
398 def init_subparser(parser):
399 """Add arguments to this action's subparser."""
400 _ActionSearchQuery.init_subparser(parser)
401 parser.add_argument('query',
402 help='The search query')
403
404 def __call__(self, opts):
405 """Implement the action."""
406 cls = _Query(opts, opts.query, raw=False)
407
408 @memoize.Memoize
409 def _QueryChange(cl, helper=None):
410 return _Query(opts, cl, raw=False, helper=helper)
411
412 transitives = _BreadthFirstSearch(
Mike Nichols802df682021-04-13 18:24:17 -0600413 cls, functools.partial(self._Nodes, opts, _QueryChange),
Mike Frysingerdc407f52020-05-08 00:34:56 -0400414 visited_key=lambda cl: cl.PatchLink())
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500415
Mike Frysingerdc407f52020-05-08 00:34:56 -0400416 # This is a hack to avoid losing GoB host for each CL. The PrintCls
417 # function assumes the GoB host specified by the user is the only one
418 # that is ever used, but the deps command walks across hosts.
419 if opts.raw:
420 print('\n'.join(x.PatchLink() for x in transitives))
421 else:
422 transitives_raw = [cl.patch_dict for cl in transitives]
423 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500424
425 @staticmethod
426 def _ProcessDeps(opts, querier, cl, deps, required):
Mike Frysinger5726da92017-09-20 22:14:25 -0400427 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700428 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400429 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400430 if not dep.remote in opts.gerrit:
431 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
432 remote=dep.remote, print_cmd=opts.debug)
433 helper = opts.gerrit[dep.remote]
434
Paul Hobbs89765232015-06-24 14:07:49 -0700435 # TODO(phobbs) this should maybe catch network errors.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500436 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400437
438 # Handle empty results. If we found a commit that was pushed directly
439 # (e.g. a bot commit), then gerrit won't know about it.
440 if not changes:
441 if required:
442 logging.error('CL %s depends on %s which cannot be found',
443 cl, dep.ToGerritQueryText())
444 continue
445
446 # Our query might have matched more than one result. This can come up
447 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
448 # across multiple repos/branches. We blindly check all of them in the
449 # hopes that all open ones are what the user wants, but then again the
450 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
451 if len(changes) > 1:
452 logging.warning('CL %s has an ambiguous CQ dependency %s',
453 cl, dep.ToGerritQueryText())
454 for change in changes:
455 if change.status == 'NEW':
456 yield change
457
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500458 @classmethod
Mike Nichols802df682021-04-13 18:24:17 -0600459 def _Nodes(cls, opts, querier, cl):
Mike Frysinger7cbd88c2021-02-12 03:52:25 -0500460 """Yields the Gerrit dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500461 for change in cls._ProcessDeps(
462 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400463 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700464
Paul Hobbs89765232015-06-24 14:07:49 -0700465
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500466class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800467 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500468
469 COMMAND = 'inspect'
470
471 @staticmethod
472 def init_subparser(parser):
473 """Add arguments to this action's subparser."""
474 _ActionSearchQuery.init_subparser(parser)
475 parser.add_argument('cls', nargs='+', metavar='CL',
476 help='The CL(s) to update')
477
478 @staticmethod
479 def __call__(opts):
480 """Implement the action."""
481 cls = []
482 for arg in opts.cls:
483 helper, cl = GetGerrit(opts, arg)
484 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
485 if change:
486 cls.extend(change)
487 else:
488 logging.warning('no results found for CL %s', arg)
489 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400490
491
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500492class _ActionLabeler(UserAction):
493 """Base helper for setting labels."""
494
495 LABEL = None
496 VALUES = None
497
498 @classmethod
499 def init_subparser(cls, parser):
500 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500501 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
502 help='Optional message to include')
503 parser.add_argument('cls', nargs='+', metavar='CL',
504 help='The CL(s) to update')
505 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
506 help='The label value; one of [%(choices)s]')
507
508 @classmethod
509 def __call__(cls, opts):
510 """Implement the action."""
511 # Convert user friendly command line option into a gerrit parameter.
512 def task(arg):
513 helper, cl = GetGerrit(opts, arg)
514 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
515 dryrun=opts.dryrun, notify=opts.notify)
516 _run_parallel_tasks(task, *opts.cls)
517
518
519class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500520 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500521
522 COMMAND = 'label-as'
523 LABEL = 'Auto-Submit'
524 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600525
526
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500527class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500528 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500529
530 COMMAND = 'label-cr'
531 LABEL = 'Code-Review'
532 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400533
534
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500535class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500536 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500537
538 COMMAND = 'label-v'
539 LABEL = 'Verified'
540 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400541
542
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500543class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500544 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500545
546 COMMAND = 'label-cq'
547 LABEL = 'Commit-Queue'
548 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500549
550
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500551class _ActionSimpleParallelCLs(UserAction):
552 """Base helper for actions that only accept CLs."""
553
554 @staticmethod
555 def init_subparser(parser):
556 """Add arguments to this action's subparser."""
557 parser.add_argument('cls', nargs='+', metavar='CL',
558 help='The CL(s) to update')
559
560 def __call__(self, opts):
561 """Implement the action."""
562 def task(arg):
563 helper, cl = GetGerrit(opts, arg)
564 self._process_one(helper, cl, opts)
565 _run_parallel_tasks(task, *opts.cls)
566
567
568class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800569 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500570
571 COMMAND = 'submit'
572
573 @staticmethod
574 def _process_one(helper, cl, opts):
575 """Use |helper| to process the single |cl|."""
Mike Frysinger8674a112021-02-09 14:44:17 -0500576 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400577
578
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500579class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800580 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500581
582 COMMAND = 'abandon'
583
584 @staticmethod
Mike Frysinger3af378b2021-03-12 01:34:04 -0500585 def init_subparser(parser):
586 """Add arguments to this action's subparser."""
587 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
588 help='Include a message')
589 _ActionSimpleParallelCLs.init_subparser(parser)
590
591 @staticmethod
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500592 def _process_one(helper, cl, opts):
593 """Use |helper| to process the single |cl|."""
Mike Frysinger3af378b2021-03-12 01:34:04 -0500594 helper.AbandonChange(cl, msg=opts.msg, dryrun=opts.dryrun,
595 notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400596
597
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500598class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800599 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500600
601 COMMAND = 'restore'
602
603 @staticmethod
604 def _process_one(helper, cl, opts):
605 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700606 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400607
608
Tomasz Figa54d70992021-01-20 13:48:59 +0900609class ActionWorkInProgress(_ActionSimpleParallelCLs):
610 """Mark CLs as work in progress"""
611
612 COMMAND = 'wip'
613
614 @staticmethod
615 def _process_one(helper, cl, opts):
616 """Use |helper| to process the single |cl|."""
617 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
618
619
620class ActionReadyForReview(_ActionSimpleParallelCLs):
621 """Mark CLs as ready for review"""
622
623 COMMAND = 'ready'
624
625 @staticmethod
626 def _process_one(helper, cl, opts):
627 """Use |helper| to process the single |cl|."""
628 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
629
630
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500631class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800632 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700633
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500634 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700635
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500636 @staticmethod
637 def init_subparser(parser):
638 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500639 parser.add_argument('cl', metavar='CL',
640 help='The CL to update')
641 parser.add_argument('reviewers', nargs='+',
642 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700643
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500644 @staticmethod
645 def __call__(opts):
646 """Implement the action."""
647 # Allow for optional leading '~'.
648 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
649 add_list, remove_list, invalid_list = [], [], []
650
651 for email in opts.reviewers:
652 if not email_validator.match(email):
653 invalid_list.append(email)
654 elif email[0] == '~':
655 remove_list.append(email[1:])
656 else:
657 add_list.append(email)
658
659 if invalid_list:
660 cros_build_lib.Die(
661 'Invalid email address(es): %s' % ', '.join(invalid_list))
662
663 if add_list or remove_list:
664 helper, cl = GetGerrit(opts, opts.cl)
665 helper.SetReviewers(cl, add=add_list, remove=remove_list,
666 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700667
668
Mike Frysinger62178ae2020-03-20 01:37:43 -0400669class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800670 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500671
672 COMMAND = 'message'
673
674 @staticmethod
675 def init_subparser(parser):
676 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400677 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500678 parser.add_argument('message',
679 help='The message to post')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500680
681 @staticmethod
682 def _process_one(helper, cl, opts):
683 """Use |helper| to process the single |cl|."""
684 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530685
686
Mike Frysinger62178ae2020-03-20 01:37:43 -0400687class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800688 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500689
690 COMMAND = 'topic'
691
692 @staticmethod
693 def init_subparser(parser):
694 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400695 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500696 parser.add_argument('topic',
697 help='The topic to set')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500698
699 @staticmethod
700 def _process_one(helper, cl, opts):
701 """Use |helper| to process the single |cl|."""
702 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800703
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800704
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500705class ActionPrivate(_ActionSimpleParallelCLs):
706 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700707
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500708 COMMAND = 'private'
709
710 @staticmethod
711 def _process_one(helper, cl, opts):
712 """Use |helper| to process the single |cl|."""
713 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700714
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800715
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500716class ActionPublic(_ActionSimpleParallelCLs):
717 """Mark CLs public"""
718
719 COMMAND = 'public'
720
721 @staticmethod
722 def _process_one(helper, cl, opts):
723 """Use |helper| to process the single |cl|."""
724 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
725
726
727class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800728 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500729
730 COMMAND = 'hashtags'
731
732 @staticmethod
733 def init_subparser(parser):
734 """Add arguments to this action's subparser."""
735 parser.add_argument('cl', metavar='CL',
736 help='The CL to update')
737 parser.add_argument('hashtags', nargs='+',
738 help='The hashtags to add/remove')
739
740 @staticmethod
741 def __call__(opts):
742 """Implement the action."""
743 add = []
744 remove = []
745 for hashtag in opts.hashtags:
746 if hashtag.startswith('~'):
747 remove.append(hashtag[1:])
748 else:
749 add.append(hashtag)
750 helper, cl = GetGerrit(opts, opts.cl)
751 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800752
753
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500754class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800755 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500756
757 COMMAND = 'deletedraft'
758
759 @staticmethod
760 def _process_one(helper, cl, opts):
761 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700762 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800763
764
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500765class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500766 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500767
768 COMMAND = 'reviewed'
769
770 @staticmethod
771 def _process_one(helper, cl, opts):
772 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500773 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500774
775
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500776class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500777 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500778
779 COMMAND = 'unreviewed'
780
781 @staticmethod
782 def _process_one(helper, cl, opts):
783 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500784 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500785
786
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500787class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500788 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500789
790 COMMAND = 'ignore'
791
792 @staticmethod
793 def _process_one(helper, cl, opts):
794 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500795 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500796
797
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500798class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500799 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500800
801 COMMAND = 'unignore'
802
803 @staticmethod
804 def _process_one(helper, cl, opts):
805 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500806 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500807
808
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400809class ActionCherryPick(UserAction):
810 """Cherry pick CLs to branches."""
811
812 COMMAND = 'cherry-pick'
813
814 @staticmethod
815 def init_subparser(parser):
816 """Add arguments to this action's subparser."""
817 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
818 parser.add_argument('--rev', '--revision', default='current',
819 help='A specific revision or patchset')
820 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
821 help='Include a message')
822 parser.add_argument('--branches', '--branch', '--br', action='split_extend',
823 default=[], required=True,
824 help='The destination branches')
825 parser.add_argument('cls', nargs='+', metavar='CL',
826 help='The CLs to cherry-pick')
827
828 @staticmethod
829 def __call__(opts):
830 """Implement the action."""
831 # Process branches in parallel, but CLs in serial in case of CL stacks.
832 def task(branch):
833 for arg in opts.cls:
834 helper, cl = GetGerrit(opts, arg)
835 ret = helper.CherryPick(cl, branch, rev=opts.rev, msg=opts.msg,
Mike Frysinger8674a112021-02-09 14:44:17 -0500836 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400837 logging.debug('Response: %s', ret)
838 if opts.raw:
839 print(ret['_number'])
840 else:
841 uri = f'https://{helper.host}/c/{ret["_number"]}'
842 print(uri_lib.ShortenUri(uri))
843
844 _run_parallel_tasks(task, *opts.branches)
845
846
Mike Frysinger8037f752020-02-29 20:47:09 -0500847class ActionReview(_ActionSimpleParallelCLs):
848 """Review CLs with multiple settings
849
850 The label option supports extended/multiple syntax for easy use. The --label
851 option may be specified multiple times (as settings are merges), and multiple
852 labels are allowed in a single argument. Each label has the form:
853 <long or short name><=+-><value>
854
855 Common arguments:
856 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
857 'V+1 CQ+2'
858 'AS=1 V=1'
859 """
860
861 COMMAND = 'review'
862
863 class _SetLabel(argparse.Action):
864 """Argparse action for setting labels."""
865
866 LABEL_MAP = {
867 'AS': 'Auto-Submit',
868 'CQ': 'Commit-Queue',
869 'CR': 'Code-Review',
870 'V': 'Verified',
871 }
872
873 def __call__(self, parser, namespace, values, option_string=None):
874 labels = getattr(namespace, self.dest)
875 for request in values.split():
876 if '=' in request:
877 # Handle Verified=1 form.
878 short, value = request.split('=', 1)
879 elif '+' in request:
880 # Handle Verified+1 form.
881 short, value = request.split('+', 1)
882 elif '-' in request:
883 # Handle Verified-1 form.
884 short, value = request.split('-', 1)
885 value = '-%s' % (value,)
886 else:
887 parser.error('Invalid label setting "%s". Must be Commit-Queue=1 or '
888 'CQ+1 or CR-1.' % (request,))
889
890 # Convert possible short label names like "V" to "Verified".
891 label = self.LABEL_MAP.get(short)
892 if not label:
893 label = short
894
895 # We allow existing label requests to be overridden.
896 labels[label] = value
897
898 @classmethod
899 def init_subparser(cls, parser):
900 """Add arguments to this action's subparser."""
901 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
902 help='Include a message')
903 parser.add_argument('-l', '--label', dest='labels',
904 action=cls._SetLabel, default={},
905 help='Set a label with a value')
906 parser.add_argument('--ready', default=None, action='store_true',
907 help='Set CL status to ready-for-review')
908 parser.add_argument('--wip', default=None, action='store_true',
909 help='Set CL status to WIP')
910 parser.add_argument('--reviewers', '--re', action='append', default=[],
911 help='Add reviewers')
912 parser.add_argument('--cc', action='append', default=[],
913 help='Add people to CC')
914 _ActionSimpleParallelCLs.init_subparser(parser)
915
916 @staticmethod
917 def _process_one(helper, cl, opts):
918 """Use |helper| to process the single |cl|."""
919 helper.SetReview(cl, msg=opts.msg, labels=opts.labels, dryrun=opts.dryrun,
920 notify=opts.notify, reviewers=opts.reviewers, cc=opts.cc,
921 ready=opts.ready, wip=opts.wip)
922
923
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500924class ActionAccount(_ActionSimpleParallelCLs):
925 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500926
927 COMMAND = 'account'
928
929 @staticmethod
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500930 def init_subparser(parser):
931 """Add arguments to this action's subparser."""
932 parser.add_argument('accounts', nargs='*', default=['self'],
933 help='The accounts to query')
934
935 @classmethod
936 def __call__(cls, opts):
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500937 """Implement the action."""
938 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500939
940 def print_one(header, data):
941 print(f'### {header}')
942 print(pformat.json(data, compact=opts.json).rstrip())
943
944 def task(arg):
945 detail = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/detail')
946 if not detail:
947 print(f'{arg}: account not found')
948 else:
949 print_one('detail', detail)
950 for field in ('groups', 'capabilities', 'preferences', 'sshkeys',
951 'gpgkeys'):
952 data = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/{field}')
953 print_one(field, data)
954
955 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800956
957
Mike Frysinger2295d792021-03-08 15:55:23 -0500958class ActionConfig(UserAction):
959 """Manage the gerrit tool's own config file
960
961 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
962 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
963
964 # Set up subcommand aliases.
965 [alias]
966 common-search = search 'is:open project:something/i/care/about'
967 """
968
969 COMMAND = 'config'
970
971 @staticmethod
972 def __call__(opts):
973 """Implement the action."""
974 # For now, this is a place holder for raising visibility for the config file
975 # and its associated help text documentation.
976 opts.parser.parse_args(['config', '--help'])
977
978
Mike Frysingere5450602021-03-08 15:34:17 -0500979class ActionHelp(UserAction):
980 """An alias to --help for CLI symmetry"""
981
982 COMMAND = 'help'
983
984 @staticmethod
985 def init_subparser(parser):
986 """Add arguments to this action's subparser."""
987 parser.add_argument('command', nargs='?',
988 help='The command to display.')
989
990 @staticmethod
991 def __call__(opts):
992 """Implement the action."""
993 # Show global help.
994 if not opts.command:
995 opts.parser.print_help()
996 return
997
998 opts.parser.parse_args([opts.command, '--help'])
999
1000
Mike Frysinger484e2f82020-03-20 01:41:10 -04001001class ActionHelpAll(UserAction):
1002 """Show all actions help output at once."""
1003
1004 COMMAND = 'help-all'
1005
1006 @staticmethod
1007 def __call__(opts):
1008 """Implement the action."""
1009 first = True
1010 for action in _GetActions():
1011 if first:
1012 first = False
1013 else:
1014 print('\n\n')
1015
1016 try:
1017 opts.parser.parse_args([action, '--help'])
1018 except SystemExit:
1019 pass
1020
1021
Mike Frysinger65fc8632020-02-06 18:11:12 -05001022@memoize.Memoize
1023def _GetActions():
1024 """Get all the possible actions we support.
1025
1026 Returns:
1027 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1028 function that implements that command (e.g. UserActFoo).
1029 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001030 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
1031
1032 actions = {}
1033 for cls in globals().values():
1034 if (not inspect.isclass(cls) or
1035 not issubclass(cls, UserAction) or
1036 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -05001037 continue
1038
Mike Frysinger65fc8632020-02-06 18:11:12 -05001039 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001040 cmd = cls.COMMAND
1041 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1042 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001043
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001044 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001045
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001046 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001047
1048
Harry Cutts26076b32019-02-26 15:01:29 -08001049def _GetActionUsages():
1050 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -05001051 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001052
Mike Frysinger65fc8632020-02-06 18:11:12 -05001053 cmds = list(actions.keys())
1054 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -08001055 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001056 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001057
Harry Cutts26076b32019-02-26 15:01:29 -08001058 cmd_indent = len(max(cmds, key=len))
1059 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001060 return '\n'.join(
1061 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
1062 for cmd, usage, doc in zip(cmds, usages, docs)
1063 )
Harry Cutts26076b32019-02-26 15:01:29 -08001064
1065
Mike Frysinger2295d792021-03-08 15:55:23 -05001066def _AddCommonOptions(parser, subparser):
1067 """Add options that should work before & after the subcommand.
1068
1069 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1070 """
1071 parser.add_common_argument_to_group(
1072 subparser, '--ne', '--no-emails', dest='notify',
1073 default='ALL', action='store_const', const='NONE',
1074 help='Do not send e-mail notifications')
1075 parser.add_common_argument_to_group(
1076 subparser, '-n', '--dry-run', dest='dryrun',
1077 default=False, action='store_true',
1078 help='Show what would be done, but do not make changes')
1079
1080
1081def GetBaseParser() -> commandline.ArgumentParser:
1082 """Returns the common parser (i.e. no subparsers added)."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001083 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001084There is no support for doing line-by-line code review via the command line.
1085This helps you manage various bits and CL status.
1086
Mike Frysingera1db2c42014-06-15 00:42:48 -07001087For general Gerrit documentation, see:
1088 https://gerrit-review.googlesource.com/Documentation/
1089The Searching Changes page covers the search query syntax:
1090 https://gerrit-review.googlesource.com/Documentation/user-search.html
1091
Mike Frysinger13f23a42013-05-13 17:32:01 -04001092Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001093 $ gerrit todo # List all the CLs that await your review.
1094 $ gerrit mine # List all of your open CLs.
1095 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1096 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1097 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001098 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
109928123.
1100 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1101CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001102Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001103 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1104with Commit-Queue=1.
1105 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1106CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001107 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001108
Harry Cutts26076b32019-02-26 15:01:29 -08001109Actions:
1110"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001111 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001112
Alex Klein2ab29cc2018-07-19 12:01:00 -06001113 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -04001114 parser = commandline.ArgumentParser(
1115 description=description, default_log_level='notice')
Mike Frysinger8674a112021-02-09 14:44:17 -05001116
1117 group = parser.add_argument_group('Server options')
1118 group.add_argument('-i', '--internal', dest='gob', action='store_const',
1119 default=site_params.EXTERNAL_GOB_INSTANCE,
1120 const=site_params.INTERNAL_GOB_INSTANCE,
1121 help='Query internal Chrome Gerrit instance')
1122 group.add_argument('-g', '--gob',
1123 default=site_params.EXTERNAL_GOB_INSTANCE,
1124 help='Gerrit (on borg) instance to query (default: %s)' %
1125 (site_params.EXTERNAL_GOB_INSTANCE))
1126
Mike Frysinger8674a112021-02-09 14:44:17 -05001127 group = parser.add_argument_group('CL options')
Mike Frysinger2295d792021-03-08 15:55:23 -05001128 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001129
Mike Frysingerf70bdc72014-06-15 00:44:06 -07001130 parser.add_argument('--raw', default=False, action='store_true',
1131 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -04001132 parser.add_argument('--json', default=False, action='store_true',
1133 help='Return results in JSON (suitable for scripting)')
Mike Frysinger2295d792021-03-08 15:55:23 -05001134 return parser
1135
1136
1137def GetParser(parser: commandline.ArgumentParser = None) -> (
1138 commandline.ArgumentParser):
1139 """Returns the full parser to use for this module."""
1140 if parser is None:
1141 parser = GetBaseParser()
1142
1143 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001144
1145 # Subparsers are required by default under Python 2. Python 3 changed to
1146 # not required, but didn't include a required option until 3.7. Setting
1147 # the required member works in all versions (and setting dest name).
1148 subparsers = parser.add_subparsers(dest='action')
1149 subparsers.required = True
1150 for cmd, cls in actions.items():
1151 # Format the full docstring by removing the file level indentation.
1152 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
1153 subparser = subparsers.add_parser(cmd, description=description)
Mike Frysinger2295d792021-03-08 15:55:23 -05001154 _AddCommonOptions(parser, subparser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001155 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001156
1157 return parser
1158
1159
1160def main(argv):
Mike Frysinger2295d792021-03-08 15:55:23 -05001161 base_parser = GetBaseParser()
1162 opts, subargs = base_parser.parse_known_args(argv)
1163
1164 config = Config()
1165 if subargs:
1166 # If the action is an alias to an expanded value, we need to mutate the argv
1167 # and reparse things.
1168 action = config.expand_alias(subargs[0])
1169 if action != subargs[0]:
1170 pos = argv.index(subargs[0])
1171 argv = argv[:pos] + action + argv[pos + 1:]
1172
1173 parser = GetParser(parser=base_parser)
Mike Frysingerddf86eb2014-02-07 22:51:41 -05001174 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001175
Mike Frysinger484e2f82020-03-20 01:41:10 -04001176 # In case the action wants to throw a parser error.
1177 opts.parser = parser
1178
Mike Frysinger88f27292014-06-17 09:40:45 -07001179 # A cache of gerrit helpers we'll load on demand.
1180 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001181
Mike Frysinger88f27292014-06-17 09:40:45 -07001182 opts.Freeze()
1183
Mike Frysinger27e21b72018-07-12 14:20:21 -04001184 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001185 global COLOR
1186 COLOR = terminal.Color(enabled=opts.color)
1187
Mike Frysinger13f23a42013-05-13 17:32:01 -04001188 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001189 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001190 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -05001191 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001192 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001193 except (cros_build_lib.RunCommandError, gerrit.GerritException,
1194 gob_util.GOBError) as e:
1195 cros_build_lib.Die(e)