blob: b00d7190947890eeb1393f75934e988678559e01 [file] [log] [blame]
Mike Frysinger13f23a42013-05-13 17:32:01 -04001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Mike Frysinger08737512014-02-07 22:58:26 -05005"""A command line interface to Gerrit-on-borg instances.
Mike Frysinger13f23a42013-05-13 17:32:01 -04006
7Internal Note:
8To expose a function directly to the command line interface, name your function
9with the prefix "UserAct".
10"""
11
Mike Frysinger8037f752020-02-29 20:47:09 -050012import argparse
Mike Frysinger65fc8632020-02-06 18:11:12 -050013import collections
Mike Frysinger2295d792021-03-08 15:55:23 -050014import configparser
Mike Frysingerc7796cf2020-02-06 23:55:15 -050015import functools
Mike Frysinger13f23a42013-05-13 17:32:01 -040016import inspect
Mike Frysinger87c74ce2017-04-04 16:12:31 -040017import json
Chris McDonald59650c32021-07-20 15:29:28 -060018import logging
Mike Frysinger2295d792021-03-08 15:55:23 -050019from pathlib import Path
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070020import re
Mike Frysinger2295d792021-03-08 15:55:23 -050021import shlex
Mike Frysinger87c74ce2017-04-04 16:12:31 -040022import sys
Mike Frysinger13f23a42013-05-13 17:32:01 -040023
Mike Frysinger2295d792021-03-08 15:55:23 -050024from chromite.lib import chromite_config
Chris McDonald59650c32021-07-20 15:29:28 -060025from chromite.lib import commandline
Aviv Keshetb7519e12016-10-04 00:50:00 -070026from chromite.lib import config_lib
27from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040028from chromite.lib import cros_build_lib
29from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050030from chromite.lib import gob_util
Mike Frysinger254f33f2019-12-11 13:54:29 -050031from chromite.lib import parallel
Mike Frysingera9751c92021-04-30 10:12:37 -040032from chromite.lib import retry_util
Mike Frysinger13f23a42013-05-13 17:32:01 -040033from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040034from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060035from chromite.utils import memoize
Alex Klein73eba212021-09-09 11:43:33 -060036from chromite.utils import pformat
Mike Frysinger13f23a42013-05-13 17:32:01 -040037
38
Mike Frysinger2295d792021-03-08 15:55:23 -050039class Config:
40 """Manage the user's gerrit config settings.
41
42 This is entirely unique to this gerrit command. Inspiration for naming and
43 layout is taken from ~/.gitconfig settings.
44 """
45
46 def __init__(self, path: Path = chromite_config.GERRIT_CONFIG):
47 self.cfg = configparser.ConfigParser(interpolation=None)
48 if path.exists():
49 self.cfg.read(chromite_config.GERRIT_CONFIG)
50
51 def expand_alias(self, action):
52 """Expand any aliases."""
53 alias = self.cfg.get('alias', action, fallback=None)
54 if alias is not None:
55 return shlex.split(alias)
56 return action
57
58
Mike Frysingerc7796cf2020-02-06 23:55:15 -050059class UserAction(object):
60 """Base class for all custom user actions."""
61
62 # The name of the command the user types in.
63 COMMAND = None
64
65 @staticmethod
66 def init_subparser(parser):
67 """Add arguments to this action's subparser."""
68
69 @staticmethod
70 def __call__(opts):
71 """Implement the action."""
Mike Frysinger62178ae2020-03-20 01:37:43 -040072 raise RuntimeError('Internal error: action missing __call__ implementation')
Mike Frysinger108eda22018-06-06 18:45:12 -040073
74
Mike Frysinger254f33f2019-12-11 13:54:29 -050075# How many connections we'll use in parallel. We don't want this to be too high
76# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
77# seems to be good enough for users.
78CONNECTION_LIMIT = 10
79
80
Mike Frysinger031ad0b2013-05-14 18:15:34 -040081COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040082
83# Map the internal names to the ones we normally show on the web ui.
84GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080085 'COMR': ['CQ', 'Commit Queue ',],
86 'CRVW': ['CR', 'Code Review ',],
87 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080088 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060089 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040090}
91
92# Order is important -- matches the web ui. This also controls the short
93# entries that we summarize in non-verbose mode.
94GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
95
Mike Frysinger4aea5dc2019-07-17 13:39:56 -040096# Shorter strings for CL status messages.
97GERRIT_SUMMARY_MAP = {
98 'ABANDONED': 'ABD',
99 'MERGED': 'MRG',
100 'NEW': 'NEW',
101 'WIP': 'WIP',
102}
103
Mike Frysinger13f23a42013-05-13 17:32:01 -0400104
105def red(s):
106 return COLOR.Color(terminal.Color.RED, s)
107
108
109def green(s):
110 return COLOR.Color(terminal.Color.GREEN, s)
111
112
113def blue(s):
114 return COLOR.Color(terminal.Color.BLUE, s)
115
116
Mike Frysinger254f33f2019-12-11 13:54:29 -0500117def _run_parallel_tasks(task, *args):
118 """Small wrapper around BackgroundTaskRunner to enforce job count."""
Mike Frysingera9751c92021-04-30 10:12:37 -0400119 # When we run in parallel, we can hit the max requests limit.
120 def check_exc(e):
121 if not isinstance(e, gob_util.GOBError):
122 raise e
123 return e.http_status == 429
124
125 @retry_util.WithRetry(5, handler=check_exc, sleep=1, backoff_factor=2)
126 def retry(*args):
127 try:
128 task(*args)
129 except gob_util.GOBError as e:
130 if e.http_status != 429:
131 logging.warning('%s: skipping due: %s', args, e)
132 else:
133 raise
134
135 with parallel.BackgroundTaskRunner(retry, processes=CONNECTION_LIMIT) as q:
Mike Frysinger254f33f2019-12-11 13:54:29 -0500136 for arg in args:
137 q.put([arg])
138
139
Mike Frysinger13f23a42013-05-13 17:32:01 -0400140def limits(cls):
141 """Given a dict of fields, calculate the longest string lengths
142
143 This allows you to easily format the output of many results so that the
144 various cols all line up correctly.
145 """
146 lims = {}
147 for cl in cls:
148 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -0400149 # Use %s rather than str() to avoid codec issues.
150 # We also do this so we can format integers.
151 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400152 return lims
153
154
Mike Frysinger88f27292014-06-17 09:40:45 -0700155# TODO: This func really needs to be merged into the core gerrit logic.
156def GetGerrit(opts, cl=None):
157 """Auto pick the right gerrit instance based on the |cl|
158
159 Args:
160 opts: The general options object.
161 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
162
163 Returns:
164 A tuple of a gerrit object and a sanitized CL #.
165 """
166 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700167 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600168 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600169 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600170 if cl.startswith('*'):
171 cl = cl[1:]
172 else:
173 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700174 elif ':' in cl:
175 gob, cl = cl.split(':', 1)
176
177 if not gob in opts.gerrit:
178 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
179
180 return (opts.gerrit[gob], cl)
181
182
Mike Frysinger13f23a42013-05-13 17:32:01 -0400183def GetApprovalSummary(_opts, cls):
184 """Return a dict of the most important approvals"""
185 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700186 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
187 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
188 if not cats:
189 logging.warning('unknown gerrit approval type: %s', approver['type'])
190 continue
191 cat = cats[0].strip()
192 val = int(approver['value'])
193 if not cat in approvs:
194 # Ignore the extended categories in the summary view.
195 continue
196 elif approvs[cat] == '':
197 approvs[cat] = val
198 elif val < 0:
199 approvs[cat] = min(approvs[cat], val)
200 else:
201 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400202 return approvs
203
204
Mike Frysingera1b4b272017-04-05 16:11:00 -0400205def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400206 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400207 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400208 lims = {'url': 0, 'project': 0}
209
210 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400211
212 if opts.verbose:
213 status += '%s ' % (cl['status'],)
214 else:
215 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
216
Mike Frysinger13f23a42013-05-13 17:32:01 -0400217 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400218 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400219 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400220 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400221 functor = lambda x: x
222 elif approvs[cat] < 0:
223 functor = red
224 else:
225 functor = green
226 status += functor('%s:%2s ' % (cat, approvs[cat]))
227
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400228 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
229 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400230
231 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400232 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400233 functor = red if int(approver['value']) < 0 else green
234 n = functor('%2s' % approver['value'])
235 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
236 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500237 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400238
239
Mike Frysingera1b4b272017-04-05 16:11:00 -0400240def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400241 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400242 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600243 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400244 pfx = ''
245 # Special case internal Chrome GoB as that is what most devs use.
246 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600247 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
248 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400249 for cl in cls:
250 print('%s%s' % (pfx, cl['number']))
251
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400252 elif opts.json:
253 json.dump(cls, sys.stdout)
254
Mike Frysingera1b4b272017-04-05 16:11:00 -0400255 else:
256 if lims is None:
257 lims = limits(cls)
258
259 for cl in cls:
260 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
261
262
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400263def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700264 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800265 if opts.branch is not None:
266 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800267 if opts.project is not None:
268 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800269 if opts.topic is not None:
270 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800271
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400272 if helper is None:
273 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700274 return helper.Query(query, raw=raw, bypass_cache=False)
275
276
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400277def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700278 """Query gerrit and filter/clean up the results"""
279 ret = []
280
Mike Frysinger2cd56022017-01-12 20:56:27 -0500281 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400282 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400283 # Gerrit likes to return a stats record too.
284 if not 'project' in cl:
285 continue
286
287 # Strip off common leading names since the result is still
288 # unique over the whole tree.
289 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400290 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
291 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400292 if cl['project'].startswith('%s/' % pfx):
293 cl['project'] = cl['project'][len(pfx) + 1:]
294
Mike Frysinger479f1192017-09-14 22:36:30 -0400295 cl['url'] = uri_lib.ShortenUri(cl['url'])
296
Mike Frysinger13f23a42013-05-13 17:32:01 -0400297 ret.append(cl)
298
Mike Frysingerb62313a2017-06-30 16:38:58 -0400299 if opts.sort == 'unsorted':
300 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700301 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400302 key = lambda x: int(x[opts.sort])
303 else:
304 key = lambda x: x[opts.sort]
305 return sorted(ret, key=key)
306
307
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500308class _ActionSearchQuery(UserAction):
309 """Base class for actions that perform searches."""
310
311 @staticmethod
312 def init_subparser(parser):
313 """Add arguments to this action's subparser."""
314 parser.add_argument('--sort', default='number',
315 help='Key to sort on (number, project); use "unsorted" '
316 'to disable')
317 parser.add_argument('-b', '--branch',
318 help='Limit output to the specific branch')
319 parser.add_argument('-p', '--project',
320 help='Limit output to the specific project')
321 parser.add_argument('-t', '--topic',
322 help='Limit output to the specific topic')
323
324
325class ActionTodo(_ActionSearchQuery):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400326 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500327
328 COMMAND = 'todo'
329
330 @staticmethod
331 def __call__(opts):
332 """Implement the action."""
Mike Frysinger242d2922021-02-09 14:31:50 -0500333 cls = FilteredQuery(opts, 'attention:self')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500334 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400335
336
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500337class ActionSearch(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800338 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500339
340 COMMAND = 'search'
341
342 @staticmethod
343 def init_subparser(parser):
344 """Add arguments to this action's subparser."""
345 _ActionSearchQuery.init_subparser(parser)
346 parser.add_argument('query',
347 help='The search query')
348
349 @staticmethod
350 def __call__(opts):
351 """Implement the action."""
352 cls = FilteredQuery(opts, opts.query)
353 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400354
355
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500356class ActionMine(_ActionSearchQuery):
Mike Frysingera1db2c42014-06-15 00:42:48 -0700357 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500358
359 COMMAND = 'mine'
360
361 @staticmethod
362 def init_subparser(parser):
363 """Add arguments to this action's subparser."""
364 _ActionSearchQuery.init_subparser(parser)
365 parser.add_argument('--draft', default=False, action='store_true',
366 help='Show draft changes')
367
368 @staticmethod
369 def __call__(opts):
370 """Implement the action."""
371 if opts.draft:
372 rule = 'is:draft'
373 else:
374 rule = 'status:new'
375 cls = FilteredQuery(opts, 'owner:self %s' % (rule,))
376 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700377
378
Paul Hobbs89765232015-06-24 14:07:49 -0700379def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
380 """Runs breadth first search starting from the nodes in |to_visit|
381
382 Args:
383 to_visit: the starting nodes
384 children: a function which takes a node and returns the nodes adjacent to it
385 visited_key: a function for deduplicating node visits. Defaults to the
386 identity function (lambda x: x)
387
388 Returns:
389 A list of nodes which are reachable from any node in |to_visit| by calling
390 |children| any number of times.
391 """
392 to_visit = list(to_visit)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400393 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700394 for node in to_visit:
395 for child in children(node):
396 key = visited_key(child)
397 if key not in seen:
398 seen.add(key)
399 to_visit.append(child)
400 return to_visit
401
402
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500403class ActionDeps(_ActionSearchQuery):
Paul Hobbs89765232015-06-24 14:07:49 -0700404 """List CLs matching a query, and all transitive dependencies of those CLs"""
Paul Hobbs89765232015-06-24 14:07:49 -0700405
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500406 COMMAND = 'deps'
Paul Hobbs89765232015-06-24 14:07:49 -0700407
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500408 @staticmethod
409 def init_subparser(parser):
410 """Add arguments to this action's subparser."""
411 _ActionSearchQuery.init_subparser(parser)
412 parser.add_argument('query',
413 help='The search query')
414
415 def __call__(self, opts):
416 """Implement the action."""
417 cls = _Query(opts, opts.query, raw=False)
418
419 @memoize.Memoize
420 def _QueryChange(cl, helper=None):
421 return _Query(opts, cl, raw=False, helper=helper)
422
423 transitives = _BreadthFirstSearch(
Mike Nicholsa1414162021-04-22 20:07:22 +0000424 cls, functools.partial(self._Children, opts, _QueryChange),
Mike Frysingerdc407f52020-05-08 00:34:56 -0400425 visited_key=lambda cl: cl.PatchLink())
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500426
Mike Frysingerdc407f52020-05-08 00:34:56 -0400427 # This is a hack to avoid losing GoB host for each CL. The PrintCls
428 # function assumes the GoB host specified by the user is the only one
429 # that is ever used, but the deps command walks across hosts.
430 if opts.raw:
431 print('\n'.join(x.PatchLink() for x in transitives))
432 else:
433 transitives_raw = [cl.patch_dict for cl in transitives]
434 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500435
436 @staticmethod
437 def _ProcessDeps(opts, querier, cl, deps, required):
Mike Frysinger5726da92017-09-20 22:14:25 -0400438 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700439 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400440 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400441 if not dep.remote in opts.gerrit:
442 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
443 remote=dep.remote, print_cmd=opts.debug)
444 helper = opts.gerrit[dep.remote]
445
Paul Hobbs89765232015-06-24 14:07:49 -0700446 # TODO(phobbs) this should maybe catch network errors.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500447 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400448
449 # Handle empty results. If we found a commit that was pushed directly
450 # (e.g. a bot commit), then gerrit won't know about it.
451 if not changes:
452 if required:
453 logging.error('CL %s depends on %s which cannot be found',
454 cl, dep.ToGerritQueryText())
455 continue
456
457 # Our query might have matched more than one result. This can come up
458 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
459 # across multiple repos/branches. We blindly check all of them in the
460 # hopes that all open ones are what the user wants, but then again the
461 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
462 if len(changes) > 1:
463 logging.warning('CL %s has an ambiguous CQ dependency %s',
464 cl, dep.ToGerritQueryText())
465 for change in changes:
466 if change.status == 'NEW':
467 yield change
468
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500469 @classmethod
Mike Nicholsa1414162021-04-22 20:07:22 +0000470 def _Children(cls, opts, querier, cl):
Mike Frysinger7cbd88c2021-02-12 03:52:25 -0500471 """Yields the Gerrit dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500472 for change in cls._ProcessDeps(
473 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400474 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700475
Paul Hobbs89765232015-06-24 14:07:49 -0700476
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500477class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800478 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500479
480 COMMAND = 'inspect'
481
482 @staticmethod
483 def init_subparser(parser):
484 """Add arguments to this action's subparser."""
485 _ActionSearchQuery.init_subparser(parser)
486 parser.add_argument('cls', nargs='+', metavar='CL',
487 help='The CL(s) to update')
488
489 @staticmethod
490 def __call__(opts):
491 """Implement the action."""
492 cls = []
493 for arg in opts.cls:
494 helper, cl = GetGerrit(opts, arg)
495 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
496 if change:
497 cls.extend(change)
498 else:
499 logging.warning('no results found for CL %s', arg)
500 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400501
502
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500503class _ActionLabeler(UserAction):
504 """Base helper for setting labels."""
505
506 LABEL = None
507 VALUES = None
508
509 @classmethod
510 def init_subparser(cls, parser):
511 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500512 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
513 help='Optional message to include')
514 parser.add_argument('cls', nargs='+', metavar='CL',
515 help='The CL(s) to update')
516 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
517 help='The label value; one of [%(choices)s]')
518
519 @classmethod
520 def __call__(cls, opts):
521 """Implement the action."""
522 # Convert user friendly command line option into a gerrit parameter.
523 def task(arg):
524 helper, cl = GetGerrit(opts, arg)
525 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
526 dryrun=opts.dryrun, notify=opts.notify)
527 _run_parallel_tasks(task, *opts.cls)
528
529
530class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500531 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500532
533 COMMAND = 'label-as'
534 LABEL = 'Auto-Submit'
535 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600536
537
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500538class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500539 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500540
541 COMMAND = 'label-cr'
542 LABEL = 'Code-Review'
543 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400544
545
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500546class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500547 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500548
549 COMMAND = 'label-v'
550 LABEL = 'Verified'
551 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400552
553
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500554class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500555 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500556
557 COMMAND = 'label-cq'
558 LABEL = 'Commit-Queue'
559 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500560
C Shapiro3f1f8242021-08-02 15:28:29 -0500561class ActionLabelOwnersOverride(_ActionLabeler):
562 """Change the Owners-Override label (1=Override)"""
563
564 COMMAND = 'label-oo'
565 LABEL = 'Owners-Override'
566 VALUES = ('0', '1')
567
Mike Frysinger15b23e42014-12-05 17:00:05 -0500568
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500569class _ActionSimpleParallelCLs(UserAction):
570 """Base helper for actions that only accept CLs."""
571
572 @staticmethod
573 def init_subparser(parser):
574 """Add arguments to this action's subparser."""
575 parser.add_argument('cls', nargs='+', metavar='CL',
576 help='The CL(s) to update')
577
578 def __call__(self, opts):
579 """Implement the action."""
580 def task(arg):
581 helper, cl = GetGerrit(opts, arg)
582 self._process_one(helper, cl, opts)
583 _run_parallel_tasks(task, *opts.cls)
584
585
586class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800587 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500588
589 COMMAND = 'submit'
590
591 @staticmethod
592 def _process_one(helper, cl, opts):
593 """Use |helper| to process the single |cl|."""
Mike Frysinger8674a112021-02-09 14:44:17 -0500594 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400595
596
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500597class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800598 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500599
600 COMMAND = 'abandon'
601
602 @staticmethod
Mike Frysinger3af378b2021-03-12 01:34:04 -0500603 def init_subparser(parser):
604 """Add arguments to this action's subparser."""
605 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
606 help='Include a message')
607 _ActionSimpleParallelCLs.init_subparser(parser)
608
609 @staticmethod
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500610 def _process_one(helper, cl, opts):
611 """Use |helper| to process the single |cl|."""
Mike Frysinger3af378b2021-03-12 01:34:04 -0500612 helper.AbandonChange(cl, msg=opts.msg, dryrun=opts.dryrun,
613 notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400614
615
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500616class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800617 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500618
619 COMMAND = 'restore'
620
621 @staticmethod
622 def _process_one(helper, cl, opts):
623 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700624 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400625
626
Tomasz Figa54d70992021-01-20 13:48:59 +0900627class ActionWorkInProgress(_ActionSimpleParallelCLs):
628 """Mark CLs as work in progress"""
629
630 COMMAND = 'wip'
631
632 @staticmethod
633 def _process_one(helper, cl, opts):
634 """Use |helper| to process the single |cl|."""
635 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
636
637
638class ActionReadyForReview(_ActionSimpleParallelCLs):
639 """Mark CLs as ready for review"""
640
641 COMMAND = 'ready'
642
643 @staticmethod
644 def _process_one(helper, cl, opts):
645 """Use |helper| to process the single |cl|."""
646 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
647
648
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500649class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800650 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700651
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500652 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700653
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500654 @staticmethod
655 def init_subparser(parser):
656 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500657 parser.add_argument('cl', metavar='CL',
658 help='The CL to update')
659 parser.add_argument('reviewers', nargs='+',
660 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700661
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500662 @staticmethod
663 def __call__(opts):
664 """Implement the action."""
665 # Allow for optional leading '~'.
666 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
667 add_list, remove_list, invalid_list = [], [], []
668
669 for email in opts.reviewers:
670 if not email_validator.match(email):
671 invalid_list.append(email)
672 elif email[0] == '~':
673 remove_list.append(email[1:])
674 else:
675 add_list.append(email)
676
677 if invalid_list:
678 cros_build_lib.Die(
679 'Invalid email address(es): %s' % ', '.join(invalid_list))
680
681 if add_list or remove_list:
682 helper, cl = GetGerrit(opts, opts.cl)
683 helper.SetReviewers(cl, add=add_list, remove=remove_list,
684 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700685
686
Brian Norrisd25af082021-10-29 11:25:31 -0700687class ActionAttentionSet(UserAction):
688 """Add/remove emails from the attention set (prepend with '~' to remove)"""
689
690 COMMAND = 'attention'
691
692 @staticmethod
693 def init_subparser(parser):
694 """Add arguments to this action's subparser."""
695 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
696 help='Optional message to include',
697 default='gerrit CLI')
698 parser.add_argument('cl', metavar='CL',
699 help='The CL to update')
700 parser.add_argument('users', nargs='+',
701 help='The users to add/remove from attention set')
702
703 @staticmethod
704 def __call__(opts):
705 """Implement the action."""
706 # Allow for optional leading '~'.
707 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
708 add_list, remove_list, invalid_list = [], [], []
709
710 for email in opts.users:
711 if not email_validator.match(email):
712 invalid_list.append(email)
713 elif email[0] == '~':
714 remove_list.append(email[1:])
715 else:
716 add_list.append(email)
717
718 if invalid_list:
719 cros_build_lib.Die(
720 'Invalid email address(es): %s' % ', '.join(invalid_list))
721
722 if add_list or remove_list:
723 helper, cl = GetGerrit(opts, opts.cl)
724 helper.SetAttentionSet(cl, add=add_list, remove=remove_list,
725 dryrun=opts.dryrun, notify=opts.notify,
726 message=opts.msg)
727
728
Mike Frysinger62178ae2020-03-20 01:37:43 -0400729class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800730 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500731
732 COMMAND = 'message'
733
734 @staticmethod
735 def init_subparser(parser):
736 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400737 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500738 parser.add_argument('message',
739 help='The message to post')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500740
741 @staticmethod
742 def _process_one(helper, cl, opts):
743 """Use |helper| to process the single |cl|."""
744 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530745
746
Mike Frysinger62178ae2020-03-20 01:37:43 -0400747class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800748 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500749
750 COMMAND = 'topic'
751
752 @staticmethod
753 def init_subparser(parser):
754 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400755 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500756 parser.add_argument('topic',
757 help='The topic to set')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500758
759 @staticmethod
760 def _process_one(helper, cl, opts):
761 """Use |helper| to process the single |cl|."""
762 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800763
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800764
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500765class ActionPrivate(_ActionSimpleParallelCLs):
766 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700767
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500768 COMMAND = 'private'
769
770 @staticmethod
771 def _process_one(helper, cl, opts):
772 """Use |helper| to process the single |cl|."""
773 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700774
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800775
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500776class ActionPublic(_ActionSimpleParallelCLs):
777 """Mark CLs public"""
778
779 COMMAND = 'public'
780
781 @staticmethod
782 def _process_one(helper, cl, opts):
783 """Use |helper| to process the single |cl|."""
784 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
785
786
787class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800788 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500789
790 COMMAND = 'hashtags'
791
792 @staticmethod
793 def init_subparser(parser):
794 """Add arguments to this action's subparser."""
795 parser.add_argument('cl', metavar='CL',
796 help='The CL to update')
797 parser.add_argument('hashtags', nargs='+',
798 help='The hashtags to add/remove')
799
800 @staticmethod
801 def __call__(opts):
802 """Implement the action."""
803 add = []
804 remove = []
805 for hashtag in opts.hashtags:
806 if hashtag.startswith('~'):
807 remove.append(hashtag[1:])
808 else:
809 add.append(hashtag)
810 helper, cl = GetGerrit(opts, opts.cl)
811 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800812
813
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500814class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800815 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500816
817 COMMAND = 'deletedraft'
818
819 @staticmethod
820 def _process_one(helper, cl, opts):
821 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700822 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800823
824
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500825class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500826 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500827
828 COMMAND = 'reviewed'
829
830 @staticmethod
831 def _process_one(helper, cl, opts):
832 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500833 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500834
835
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500836class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500837 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500838
839 COMMAND = 'unreviewed'
840
841 @staticmethod
842 def _process_one(helper, cl, opts):
843 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500844 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500845
846
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500847class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500848 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500849
850 COMMAND = 'ignore'
851
852 @staticmethod
853 def _process_one(helper, cl, opts):
854 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500855 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500856
857
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500858class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500859 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500860
861 COMMAND = 'unignore'
862
863 @staticmethod
864 def _process_one(helper, cl, opts):
865 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500866 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500867
868
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400869class ActionCherryPick(UserAction):
870 """Cherry pick CLs to branches."""
871
872 COMMAND = 'cherry-pick'
873
874 @staticmethod
875 def init_subparser(parser):
876 """Add arguments to this action's subparser."""
877 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
878 parser.add_argument('--rev', '--revision', default='current',
879 help='A specific revision or patchset')
880 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
881 help='Include a message')
882 parser.add_argument('--branches', '--branch', '--br', action='split_extend',
883 default=[], required=True,
884 help='The destination branches')
885 parser.add_argument('cls', nargs='+', metavar='CL',
886 help='The CLs to cherry-pick')
887
888 @staticmethod
889 def __call__(opts):
890 """Implement the action."""
891 # Process branches in parallel, but CLs in serial in case of CL stacks.
892 def task(branch):
893 for arg in opts.cls:
894 helper, cl = GetGerrit(opts, arg)
895 ret = helper.CherryPick(cl, branch, rev=opts.rev, msg=opts.msg,
Mike Frysinger8674a112021-02-09 14:44:17 -0500896 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400897 logging.debug('Response: %s', ret)
898 if opts.raw:
899 print(ret['_number'])
900 else:
901 uri = f'https://{helper.host}/c/{ret["_number"]}'
902 print(uri_lib.ShortenUri(uri))
903
904 _run_parallel_tasks(task, *opts.branches)
905
906
Mike Frysinger8037f752020-02-29 20:47:09 -0500907class ActionReview(_ActionSimpleParallelCLs):
908 """Review CLs with multiple settings
909
910 The label option supports extended/multiple syntax for easy use. The --label
911 option may be specified multiple times (as settings are merges), and multiple
912 labels are allowed in a single argument. Each label has the form:
913 <long or short name><=+-><value>
914
915 Common arguments:
916 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
917 'V+1 CQ+2'
918 'AS=1 V=1'
919 """
920
921 COMMAND = 'review'
922
923 class _SetLabel(argparse.Action):
924 """Argparse action for setting labels."""
925
926 LABEL_MAP = {
927 'AS': 'Auto-Submit',
928 'CQ': 'Commit-Queue',
929 'CR': 'Code-Review',
930 'V': 'Verified',
931 }
932
933 def __call__(self, parser, namespace, values, option_string=None):
934 labels = getattr(namespace, self.dest)
935 for request in values.split():
936 if '=' in request:
937 # Handle Verified=1 form.
938 short, value = request.split('=', 1)
939 elif '+' in request:
940 # Handle Verified+1 form.
941 short, value = request.split('+', 1)
942 elif '-' in request:
943 # Handle Verified-1 form.
944 short, value = request.split('-', 1)
945 value = '-%s' % (value,)
946 else:
947 parser.error('Invalid label setting "%s". Must be Commit-Queue=1 or '
948 'CQ+1 or CR-1.' % (request,))
949
950 # Convert possible short label names like "V" to "Verified".
951 label = self.LABEL_MAP.get(short)
952 if not label:
953 label = short
954
955 # We allow existing label requests to be overridden.
956 labels[label] = value
957
958 @classmethod
959 def init_subparser(cls, parser):
960 """Add arguments to this action's subparser."""
961 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
962 help='Include a message')
963 parser.add_argument('-l', '--label', dest='labels',
964 action=cls._SetLabel, default={},
965 help='Set a label with a value')
966 parser.add_argument('--ready', default=None, action='store_true',
967 help='Set CL status to ready-for-review')
968 parser.add_argument('--wip', default=None, action='store_true',
969 help='Set CL status to WIP')
970 parser.add_argument('--reviewers', '--re', action='append', default=[],
971 help='Add reviewers')
972 parser.add_argument('--cc', action='append', default=[],
973 help='Add people to CC')
974 _ActionSimpleParallelCLs.init_subparser(parser)
975
976 @staticmethod
977 def _process_one(helper, cl, opts):
978 """Use |helper| to process the single |cl|."""
979 helper.SetReview(cl, msg=opts.msg, labels=opts.labels, dryrun=opts.dryrun,
980 notify=opts.notify, reviewers=opts.reviewers, cc=opts.cc,
981 ready=opts.ready, wip=opts.wip)
982
983
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500984class ActionAccount(_ActionSimpleParallelCLs):
985 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500986
987 COMMAND = 'account'
988
989 @staticmethod
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500990 def init_subparser(parser):
991 """Add arguments to this action's subparser."""
992 parser.add_argument('accounts', nargs='*', default=['self'],
993 help='The accounts to query')
994
995 @classmethod
996 def __call__(cls, opts):
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500997 """Implement the action."""
998 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500999
1000 def print_one(header, data):
1001 print(f'### {header}')
1002 print(pformat.json(data, compact=opts.json).rstrip())
1003
1004 def task(arg):
1005 detail = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/detail')
1006 if not detail:
1007 print(f'{arg}: account not found')
1008 else:
1009 print_one('detail', detail)
1010 for field in ('groups', 'capabilities', 'preferences', 'sshkeys',
1011 'gpgkeys'):
1012 data = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/{field}')
1013 print_one(field, data)
1014
1015 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -08001016
1017
Mike Frysinger2295d792021-03-08 15:55:23 -05001018class ActionConfig(UserAction):
1019 """Manage the gerrit tool's own config file
1020
1021 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
1022 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
1023
1024 # Set up subcommand aliases.
1025 [alias]
1026 common-search = search 'is:open project:something/i/care/about'
1027 """
1028
1029 COMMAND = 'config'
1030
1031 @staticmethod
1032 def __call__(opts):
1033 """Implement the action."""
1034 # For now, this is a place holder for raising visibility for the config file
1035 # and its associated help text documentation.
1036 opts.parser.parse_args(['config', '--help'])
1037
1038
Mike Frysingere5450602021-03-08 15:34:17 -05001039class ActionHelp(UserAction):
1040 """An alias to --help for CLI symmetry"""
1041
1042 COMMAND = 'help'
1043
1044 @staticmethod
1045 def init_subparser(parser):
1046 """Add arguments to this action's subparser."""
1047 parser.add_argument('command', nargs='?',
1048 help='The command to display.')
1049
1050 @staticmethod
1051 def __call__(opts):
1052 """Implement the action."""
1053 # Show global help.
1054 if not opts.command:
1055 opts.parser.print_help()
1056 return
1057
1058 opts.parser.parse_args([opts.command, '--help'])
1059
1060
Mike Frysinger484e2f82020-03-20 01:41:10 -04001061class ActionHelpAll(UserAction):
1062 """Show all actions help output at once."""
1063
1064 COMMAND = 'help-all'
1065
1066 @staticmethod
1067 def __call__(opts):
1068 """Implement the action."""
1069 first = True
1070 for action in _GetActions():
1071 if first:
1072 first = False
1073 else:
1074 print('\n\n')
1075
1076 try:
1077 opts.parser.parse_args([action, '--help'])
1078 except SystemExit:
1079 pass
1080
1081
Mike Frysinger65fc8632020-02-06 18:11:12 -05001082@memoize.Memoize
1083def _GetActions():
1084 """Get all the possible actions we support.
1085
1086 Returns:
1087 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1088 function that implements that command (e.g. UserActFoo).
1089 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001090 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
1091
1092 actions = {}
1093 for cls in globals().values():
1094 if (not inspect.isclass(cls) or
1095 not issubclass(cls, UserAction) or
1096 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -05001097 continue
1098
Mike Frysinger65fc8632020-02-06 18:11:12 -05001099 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001100 cmd = cls.COMMAND
1101 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1102 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001103
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001104 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001105
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001106 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001107
1108
Harry Cutts26076b32019-02-26 15:01:29 -08001109def _GetActionUsages():
1110 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -05001111 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001112
Mike Frysinger65fc8632020-02-06 18:11:12 -05001113 cmds = list(actions.keys())
1114 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -08001115 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001116 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001117
Harry Cutts26076b32019-02-26 15:01:29 -08001118 cmd_indent = len(max(cmds, key=len))
1119 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001120 return '\n'.join(
1121 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
1122 for cmd, usage, doc in zip(cmds, usages, docs)
1123 )
Harry Cutts26076b32019-02-26 15:01:29 -08001124
1125
Mike Frysinger2295d792021-03-08 15:55:23 -05001126def _AddCommonOptions(parser, subparser):
1127 """Add options that should work before & after the subcommand.
1128
1129 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1130 """
1131 parser.add_common_argument_to_group(
1132 subparser, '--ne', '--no-emails', dest='notify',
1133 default='ALL', action='store_const', const='NONE',
1134 help='Do not send e-mail notifications')
1135 parser.add_common_argument_to_group(
1136 subparser, '-n', '--dry-run', dest='dryrun',
1137 default=False, action='store_true',
1138 help='Show what would be done, but do not make changes')
1139
1140
1141def GetBaseParser() -> commandline.ArgumentParser:
1142 """Returns the common parser (i.e. no subparsers added)."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001143 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001144There is no support for doing line-by-line code review via the command line.
1145This helps you manage various bits and CL status.
1146
Mike Frysingera1db2c42014-06-15 00:42:48 -07001147For general Gerrit documentation, see:
1148 https://gerrit-review.googlesource.com/Documentation/
1149The Searching Changes page covers the search query syntax:
1150 https://gerrit-review.googlesource.com/Documentation/user-search.html
1151
Mike Frysinger13f23a42013-05-13 17:32:01 -04001152Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001153 $ gerrit todo # List all the CLs that await your review.
1154 $ gerrit mine # List all of your open CLs.
1155 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1156 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1157 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001158 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
115928123.
1160 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1161CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001162Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001163 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1164with Commit-Queue=1.
1165 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1166CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001167 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001168
Harry Cutts26076b32019-02-26 15:01:29 -08001169Actions:
1170"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001171 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001172
Alex Klein2ab29cc2018-07-19 12:01:00 -06001173 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -04001174 parser = commandline.ArgumentParser(
Mike Frysinger50917ad2022-04-12 20:37:14 -04001175 description=description, default_log_level='notice',
1176 epilog='For subcommand help, use `gerrit help <command>`.')
Mike Frysinger8674a112021-02-09 14:44:17 -05001177
1178 group = parser.add_argument_group('Server options')
1179 group.add_argument('-i', '--internal', dest='gob', action='store_const',
1180 default=site_params.EXTERNAL_GOB_INSTANCE,
1181 const=site_params.INTERNAL_GOB_INSTANCE,
1182 help='Query internal Chrome Gerrit instance')
1183 group.add_argument('-g', '--gob',
1184 default=site_params.EXTERNAL_GOB_INSTANCE,
Brian Norrisd25af082021-10-29 11:25:31 -07001185 help=('Gerrit (on borg) instance to query '
1186 '(default: %(default)s)'))
Mike Frysinger8674a112021-02-09 14:44:17 -05001187
Mike Frysinger8674a112021-02-09 14:44:17 -05001188 group = parser.add_argument_group('CL options')
Mike Frysinger2295d792021-03-08 15:55:23 -05001189 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001190
Mike Frysingerf70bdc72014-06-15 00:44:06 -07001191 parser.add_argument('--raw', default=False, action='store_true',
1192 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -04001193 parser.add_argument('--json', default=False, action='store_true',
1194 help='Return results in JSON (suitable for scripting)')
Mike Frysinger2295d792021-03-08 15:55:23 -05001195 return parser
1196
1197
1198def GetParser(parser: commandline.ArgumentParser = None) -> (
1199 commandline.ArgumentParser):
1200 """Returns the full parser to use for this module."""
1201 if parser is None:
1202 parser = GetBaseParser()
1203
1204 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001205
1206 # Subparsers are required by default under Python 2. Python 3 changed to
1207 # not required, but didn't include a required option until 3.7. Setting
1208 # the required member works in all versions (and setting dest name).
1209 subparsers = parser.add_subparsers(dest='action')
1210 subparsers.required = True
1211 for cmd, cls in actions.items():
1212 # Format the full docstring by removing the file level indentation.
1213 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
1214 subparser = subparsers.add_parser(cmd, description=description)
Mike Frysinger2295d792021-03-08 15:55:23 -05001215 _AddCommonOptions(parser, subparser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001216 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001217
1218 return parser
1219
1220
1221def main(argv):
Mike Frysinger2295d792021-03-08 15:55:23 -05001222 base_parser = GetBaseParser()
1223 opts, subargs = base_parser.parse_known_args(argv)
1224
1225 config = Config()
1226 if subargs:
1227 # If the action is an alias to an expanded value, we need to mutate the argv
1228 # and reparse things.
1229 action = config.expand_alias(subargs[0])
1230 if action != subargs[0]:
1231 pos = argv.index(subargs[0])
1232 argv = argv[:pos] + action + argv[pos + 1:]
1233
1234 parser = GetParser(parser=base_parser)
Mike Frysingerddf86eb2014-02-07 22:51:41 -05001235 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001236
Mike Frysinger484e2f82020-03-20 01:41:10 -04001237 # In case the action wants to throw a parser error.
1238 opts.parser = parser
1239
Mike Frysinger88f27292014-06-17 09:40:45 -07001240 # A cache of gerrit helpers we'll load on demand.
1241 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001242
Mike Frysinger88f27292014-06-17 09:40:45 -07001243 opts.Freeze()
1244
Mike Frysinger27e21b72018-07-12 14:20:21 -04001245 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001246 global COLOR
1247 COLOR = terminal.Color(enabled=opts.color)
1248
Mike Frysinger13f23a42013-05-13 17:32:01 -04001249 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001250 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001251 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -05001252 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001253 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001254 except (cros_build_lib.RunCommandError, gerrit.GerritException,
1255 gob_util.GOBError) as e:
1256 cros_build_lib.Die(e)