blob: 273705347a3973ae3479da5112bf064a044e205a [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
Douglas Andersoncf9e9632022-05-24 14:55:16 -0700228 if opts.markdown:
229 print('* %s - %s' % (uri_lib.ShortenUri(cl['url']), cl['subject']))
230 else:
231 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
232 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400233
234 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400235 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400236 functor = red if int(approver['value']) < 0 else green
237 n = functor('%2s' % approver['value'])
238 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
239 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500240 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400241
242
Mike Frysingera1b4b272017-04-05 16:11:00 -0400243def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400244 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400245 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600246 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400247 pfx = ''
248 # Special case internal Chrome GoB as that is what most devs use.
249 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600250 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
251 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400252 for cl in cls:
253 print('%s%s' % (pfx, cl['number']))
254
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400255 elif opts.json:
256 json.dump(cls, sys.stdout)
257
Mike Frysingera1b4b272017-04-05 16:11:00 -0400258 else:
259 if lims is None:
260 lims = limits(cls)
261
262 for cl in cls:
263 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
264
265
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400266def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700267 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800268 if opts.branch is not None:
269 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800270 if opts.project is not None:
271 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800272 if opts.topic is not None:
273 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800274
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400275 if helper is None:
276 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700277 return helper.Query(query, raw=raw, bypass_cache=False)
278
279
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400280def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700281 """Query gerrit and filter/clean up the results"""
282 ret = []
283
Mike Frysinger2cd56022017-01-12 20:56:27 -0500284 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400285 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400286 # Gerrit likes to return a stats record too.
287 if not 'project' in cl:
288 continue
289
290 # Strip off common leading names since the result is still
291 # unique over the whole tree.
292 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400293 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
294 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400295 if cl['project'].startswith('%s/' % pfx):
296 cl['project'] = cl['project'][len(pfx) + 1:]
297
Mike Frysinger479f1192017-09-14 22:36:30 -0400298 cl['url'] = uri_lib.ShortenUri(cl['url'])
299
Mike Frysinger13f23a42013-05-13 17:32:01 -0400300 ret.append(cl)
301
Mike Frysingerb62313a2017-06-30 16:38:58 -0400302 if opts.sort == 'unsorted':
303 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700304 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400305 key = lambda x: int(x[opts.sort])
306 else:
307 key = lambda x: x[opts.sort]
308 return sorted(ret, key=key)
309
310
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500311class _ActionSearchQuery(UserAction):
312 """Base class for actions that perform searches."""
313
314 @staticmethod
315 def init_subparser(parser):
316 """Add arguments to this action's subparser."""
317 parser.add_argument('--sort', default='number',
318 help='Key to sort on (number, project); use "unsorted" '
319 'to disable')
320 parser.add_argument('-b', '--branch',
321 help='Limit output to the specific branch')
322 parser.add_argument('-p', '--project',
323 help='Limit output to the specific project')
324 parser.add_argument('-t', '--topic',
325 help='Limit output to the specific topic')
326
327
328class ActionTodo(_ActionSearchQuery):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400329 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500330
331 COMMAND = 'todo'
332
333 @staticmethod
334 def __call__(opts):
335 """Implement the action."""
Mike Frysinger242d2922021-02-09 14:31:50 -0500336 cls = FilteredQuery(opts, 'attention:self')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500337 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400338
339
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500340class ActionSearch(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800341 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500342
343 COMMAND = 'search'
344
345 @staticmethod
346 def init_subparser(parser):
347 """Add arguments to this action's subparser."""
348 _ActionSearchQuery.init_subparser(parser)
349 parser.add_argument('query',
350 help='The search query')
351
352 @staticmethod
353 def __call__(opts):
354 """Implement the action."""
355 cls = FilteredQuery(opts, opts.query)
356 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400357
358
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500359class ActionMine(_ActionSearchQuery):
Mike Frysingera1db2c42014-06-15 00:42:48 -0700360 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500361
362 COMMAND = 'mine'
363
364 @staticmethod
365 def init_subparser(parser):
366 """Add arguments to this action's subparser."""
367 _ActionSearchQuery.init_subparser(parser)
368 parser.add_argument('--draft', default=False, action='store_true',
369 help='Show draft changes')
370
371 @staticmethod
372 def __call__(opts):
373 """Implement the action."""
374 if opts.draft:
375 rule = 'is:draft'
376 else:
377 rule = 'status:new'
378 cls = FilteredQuery(opts, 'owner:self %s' % (rule,))
379 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700380
381
Paul Hobbs89765232015-06-24 14:07:49 -0700382def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
383 """Runs breadth first search starting from the nodes in |to_visit|
384
385 Args:
386 to_visit: the starting nodes
387 children: a function which takes a node and returns the nodes adjacent to it
388 visited_key: a function for deduplicating node visits. Defaults to the
389 identity function (lambda x: x)
390
391 Returns:
392 A list of nodes which are reachable from any node in |to_visit| by calling
393 |children| any number of times.
394 """
395 to_visit = list(to_visit)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400396 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700397 for node in to_visit:
398 for child in children(node):
399 key = visited_key(child)
400 if key not in seen:
401 seen.add(key)
402 to_visit.append(child)
403 return to_visit
404
405
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500406class ActionDeps(_ActionSearchQuery):
Paul Hobbs89765232015-06-24 14:07:49 -0700407 """List CLs matching a query, and all transitive dependencies of those CLs"""
Paul Hobbs89765232015-06-24 14:07:49 -0700408
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500409 COMMAND = 'deps'
Paul Hobbs89765232015-06-24 14:07:49 -0700410
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500411 @staticmethod
412 def init_subparser(parser):
413 """Add arguments to this action's subparser."""
414 _ActionSearchQuery.init_subparser(parser)
415 parser.add_argument('query',
416 help='The search query')
417
418 def __call__(self, opts):
419 """Implement the action."""
420 cls = _Query(opts, opts.query, raw=False)
421
422 @memoize.Memoize
423 def _QueryChange(cl, helper=None):
424 return _Query(opts, cl, raw=False, helper=helper)
425
426 transitives = _BreadthFirstSearch(
Mike Nicholsa1414162021-04-22 20:07:22 +0000427 cls, functools.partial(self._Children, opts, _QueryChange),
Mike Frysingerdc407f52020-05-08 00:34:56 -0400428 visited_key=lambda cl: cl.PatchLink())
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500429
Mike Frysingerdc407f52020-05-08 00:34:56 -0400430 # This is a hack to avoid losing GoB host for each CL. The PrintCls
431 # function assumes the GoB host specified by the user is the only one
432 # that is ever used, but the deps command walks across hosts.
433 if opts.raw:
434 print('\n'.join(x.PatchLink() for x in transitives))
435 else:
436 transitives_raw = [cl.patch_dict for cl in transitives]
437 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500438
439 @staticmethod
440 def _ProcessDeps(opts, querier, cl, deps, required):
Mike Frysinger5726da92017-09-20 22:14:25 -0400441 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700442 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400443 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400444 if not dep.remote in opts.gerrit:
445 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
446 remote=dep.remote, print_cmd=opts.debug)
447 helper = opts.gerrit[dep.remote]
448
Paul Hobbs89765232015-06-24 14:07:49 -0700449 # TODO(phobbs) this should maybe catch network errors.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500450 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400451
452 # Handle empty results. If we found a commit that was pushed directly
453 # (e.g. a bot commit), then gerrit won't know about it.
454 if not changes:
455 if required:
456 logging.error('CL %s depends on %s which cannot be found',
457 cl, dep.ToGerritQueryText())
458 continue
459
460 # Our query might have matched more than one result. This can come up
461 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
462 # across multiple repos/branches. We blindly check all of them in the
463 # hopes that all open ones are what the user wants, but then again the
Alex Kleinea9cc822022-05-25 12:39:48 -0600464 # CQ-DEPEND syntax itself is unable to differentiate. *shrug*
Mike Frysinger5726da92017-09-20 22:14:25 -0400465 if len(changes) > 1:
466 logging.warning('CL %s has an ambiguous CQ dependency %s',
467 cl, dep.ToGerritQueryText())
468 for change in changes:
469 if change.status == 'NEW':
470 yield change
471
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500472 @classmethod
Mike Nicholsa1414162021-04-22 20:07:22 +0000473 def _Children(cls, opts, querier, cl):
Mike Frysinger7cbd88c2021-02-12 03:52:25 -0500474 """Yields the Gerrit dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500475 for change in cls._ProcessDeps(
476 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400477 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700478
Paul Hobbs89765232015-06-24 14:07:49 -0700479
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500480class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800481 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500482
483 COMMAND = 'inspect'
484
485 @staticmethod
486 def init_subparser(parser):
487 """Add arguments to this action's subparser."""
488 _ActionSearchQuery.init_subparser(parser)
489 parser.add_argument('cls', nargs='+', metavar='CL',
490 help='The CL(s) to update')
491
492 @staticmethod
493 def __call__(opts):
494 """Implement the action."""
495 cls = []
496 for arg in opts.cls:
497 helper, cl = GetGerrit(opts, arg)
498 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
499 if change:
500 cls.extend(change)
501 else:
502 logging.warning('no results found for CL %s', arg)
503 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400504
505
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500506class _ActionLabeler(UserAction):
507 """Base helper for setting labels."""
508
509 LABEL = None
510 VALUES = None
511
512 @classmethod
513 def init_subparser(cls, parser):
514 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500515 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
516 help='Optional message to include')
517 parser.add_argument('cls', nargs='+', metavar='CL',
518 help='The CL(s) to update')
519 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
520 help='The label value; one of [%(choices)s]')
521
522 @classmethod
523 def __call__(cls, opts):
524 """Implement the action."""
Alex Kleinea9cc822022-05-25 12:39:48 -0600525 # Convert user-friendly command line option into a gerrit parameter.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500526 def task(arg):
527 helper, cl = GetGerrit(opts, arg)
528 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
529 dryrun=opts.dryrun, notify=opts.notify)
530 _run_parallel_tasks(task, *opts.cls)
531
532
533class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500534 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500535
536 COMMAND = 'label-as'
537 LABEL = 'Auto-Submit'
538 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600539
540
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500541class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500542 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500543
544 COMMAND = 'label-cr'
545 LABEL = 'Code-Review'
546 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400547
548
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500549class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500550 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500551
552 COMMAND = 'label-v'
553 LABEL = 'Verified'
554 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400555
556
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500557class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500558 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500559
560 COMMAND = 'label-cq'
561 LABEL = 'Commit-Queue'
562 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500563
C Shapiro3f1f8242021-08-02 15:28:29 -0500564class ActionLabelOwnersOverride(_ActionLabeler):
565 """Change the Owners-Override label (1=Override)"""
566
567 COMMAND = 'label-oo'
568 LABEL = 'Owners-Override'
569 VALUES = ('0', '1')
570
Mike Frysinger15b23e42014-12-05 17:00:05 -0500571
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500572class _ActionSimpleParallelCLs(UserAction):
573 """Base helper for actions that only accept CLs."""
574
575 @staticmethod
576 def init_subparser(parser):
577 """Add arguments to this action's subparser."""
578 parser.add_argument('cls', nargs='+', metavar='CL',
579 help='The CL(s) to update')
580
581 def __call__(self, opts):
582 """Implement the action."""
583 def task(arg):
584 helper, cl = GetGerrit(opts, arg)
585 self._process_one(helper, cl, opts)
586 _run_parallel_tasks(task, *opts.cls)
587
588
589class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800590 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500591
592 COMMAND = 'submit'
593
594 @staticmethod
595 def _process_one(helper, cl, opts):
596 """Use |helper| to process the single |cl|."""
Mike Frysinger8674a112021-02-09 14:44:17 -0500597 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400598
599
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500600class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800601 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500602
603 COMMAND = 'abandon'
604
605 @staticmethod
Mike Frysinger3af378b2021-03-12 01:34:04 -0500606 def init_subparser(parser):
607 """Add arguments to this action's subparser."""
608 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
609 help='Include a message')
610 _ActionSimpleParallelCLs.init_subparser(parser)
611
612 @staticmethod
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500613 def _process_one(helper, cl, opts):
614 """Use |helper| to process the single |cl|."""
Mike Frysinger3af378b2021-03-12 01:34:04 -0500615 helper.AbandonChange(cl, msg=opts.msg, dryrun=opts.dryrun,
616 notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400617
618
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500619class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800620 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500621
622 COMMAND = 'restore'
623
624 @staticmethod
625 def _process_one(helper, cl, opts):
626 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700627 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400628
629
Tomasz Figa54d70992021-01-20 13:48:59 +0900630class ActionWorkInProgress(_ActionSimpleParallelCLs):
631 """Mark CLs as work in progress"""
632
633 COMMAND = 'wip'
634
635 @staticmethod
636 def _process_one(helper, cl, opts):
637 """Use |helper| to process the single |cl|."""
638 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
639
640
641class ActionReadyForReview(_ActionSimpleParallelCLs):
642 """Mark CLs as ready for review"""
643
644 COMMAND = 'ready'
645
646 @staticmethod
647 def _process_one(helper, cl, opts):
648 """Use |helper| to process the single |cl|."""
649 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
650
651
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500652class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800653 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700654
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500655 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700656
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500657 @staticmethod
658 def init_subparser(parser):
659 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500660 parser.add_argument('cl', metavar='CL',
661 help='The CL to update')
662 parser.add_argument('reviewers', nargs='+',
663 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700664
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500665 @staticmethod
666 def __call__(opts):
667 """Implement the action."""
668 # Allow for optional leading '~'.
669 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
670 add_list, remove_list, invalid_list = [], [], []
671
672 for email in opts.reviewers:
673 if not email_validator.match(email):
674 invalid_list.append(email)
675 elif email[0] == '~':
676 remove_list.append(email[1:])
677 else:
678 add_list.append(email)
679
680 if invalid_list:
681 cros_build_lib.Die(
682 'Invalid email address(es): %s' % ', '.join(invalid_list))
683
684 if add_list or remove_list:
685 helper, cl = GetGerrit(opts, opts.cl)
686 helper.SetReviewers(cl, add=add_list, remove=remove_list,
687 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700688
689
Brian Norrisd25af082021-10-29 11:25:31 -0700690class ActionAttentionSet(UserAction):
691 """Add/remove emails from the attention set (prepend with '~' to remove)"""
692
693 COMMAND = 'attention'
694
695 @staticmethod
696 def init_subparser(parser):
697 """Add arguments to this action's subparser."""
698 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
699 help='Optional message to include',
700 default='gerrit CLI')
701 parser.add_argument('cl', metavar='CL',
702 help='The CL to update')
703 parser.add_argument('users', nargs='+',
704 help='The users to add/remove from attention set')
705
706 @staticmethod
707 def __call__(opts):
708 """Implement the action."""
709 # Allow for optional leading '~'.
710 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
711 add_list, remove_list, invalid_list = [], [], []
712
713 for email in opts.users:
714 if not email_validator.match(email):
715 invalid_list.append(email)
716 elif email[0] == '~':
717 remove_list.append(email[1:])
718 else:
719 add_list.append(email)
720
721 if invalid_list:
722 cros_build_lib.Die(
723 'Invalid email address(es): %s' % ', '.join(invalid_list))
724
725 if add_list or remove_list:
726 helper, cl = GetGerrit(opts, opts.cl)
727 helper.SetAttentionSet(cl, add=add_list, remove=remove_list,
728 dryrun=opts.dryrun, notify=opts.notify,
729 message=opts.msg)
730
731
Mike Frysinger62178ae2020-03-20 01:37:43 -0400732class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800733 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500734
735 COMMAND = 'message'
736
737 @staticmethod
738 def init_subparser(parser):
739 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400740 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500741 parser.add_argument('message',
742 help='The message to post')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500743
744 @staticmethod
745 def _process_one(helper, cl, opts):
746 """Use |helper| to process the single |cl|."""
747 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530748
749
Mike Frysinger62178ae2020-03-20 01:37:43 -0400750class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800751 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500752
753 COMMAND = 'topic'
754
755 @staticmethod
756 def init_subparser(parser):
757 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400758 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500759 parser.add_argument('topic',
760 help='The topic to set')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500761
762 @staticmethod
763 def _process_one(helper, cl, opts):
764 """Use |helper| to process the single |cl|."""
765 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800766
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800767
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500768class ActionPrivate(_ActionSimpleParallelCLs):
769 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700770
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500771 COMMAND = 'private'
772
773 @staticmethod
774 def _process_one(helper, cl, opts):
775 """Use |helper| to process the single |cl|."""
776 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700777
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800778
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500779class ActionPublic(_ActionSimpleParallelCLs):
780 """Mark CLs public"""
781
782 COMMAND = 'public'
783
784 @staticmethod
785 def _process_one(helper, cl, opts):
786 """Use |helper| to process the single |cl|."""
787 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
788
789
790class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800791 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500792
793 COMMAND = 'hashtags'
794
795 @staticmethod
796 def init_subparser(parser):
797 """Add arguments to this action's subparser."""
798 parser.add_argument('cl', metavar='CL',
799 help='The CL to update')
800 parser.add_argument('hashtags', nargs='+',
801 help='The hashtags to add/remove')
802
803 @staticmethod
804 def __call__(opts):
805 """Implement the action."""
806 add = []
807 remove = []
808 for hashtag in opts.hashtags:
809 if hashtag.startswith('~'):
810 remove.append(hashtag[1:])
811 else:
812 add.append(hashtag)
813 helper, cl = GetGerrit(opts, opts.cl)
814 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800815
816
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500817class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800818 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500819
820 COMMAND = 'deletedraft'
821
822 @staticmethod
823 def _process_one(helper, cl, opts):
824 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700825 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800826
827
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500828class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500829 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500830
831 COMMAND = 'reviewed'
832
833 @staticmethod
834 def _process_one(helper, cl, opts):
835 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500836 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500837
838
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500839class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500840 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500841
842 COMMAND = 'unreviewed'
843
844 @staticmethod
845 def _process_one(helper, cl, opts):
846 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500847 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500848
849
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500850class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500851 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500852
853 COMMAND = 'ignore'
854
855 @staticmethod
856 def _process_one(helper, cl, opts):
857 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500858 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500859
860
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500861class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500862 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500863
864 COMMAND = 'unignore'
865
866 @staticmethod
867 def _process_one(helper, cl, opts):
868 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500869 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500870
871
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400872class ActionCherryPick(UserAction):
Alex Kleinea9cc822022-05-25 12:39:48 -0600873 """Cherry-pick CLs to branches."""
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400874
875 COMMAND = 'cherry-pick'
876
877 @staticmethod
878 def init_subparser(parser):
879 """Add arguments to this action's subparser."""
880 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
881 parser.add_argument('--rev', '--revision', default='current',
882 help='A specific revision or patchset')
883 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
884 help='Include a message')
885 parser.add_argument('--branches', '--branch', '--br', action='split_extend',
886 default=[], required=True,
887 help='The destination branches')
888 parser.add_argument('cls', nargs='+', metavar='CL',
889 help='The CLs to cherry-pick')
890
891 @staticmethod
892 def __call__(opts):
893 """Implement the action."""
894 # Process branches in parallel, but CLs in serial in case of CL stacks.
895 def task(branch):
896 for arg in opts.cls:
897 helper, cl = GetGerrit(opts, arg)
898 ret = helper.CherryPick(cl, branch, rev=opts.rev, msg=opts.msg,
Mike Frysinger8674a112021-02-09 14:44:17 -0500899 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400900 logging.debug('Response: %s', ret)
901 if opts.raw:
902 print(ret['_number'])
903 else:
904 uri = f'https://{helper.host}/c/{ret["_number"]}'
905 print(uri_lib.ShortenUri(uri))
906
907 _run_parallel_tasks(task, *opts.branches)
908
909
Mike Frysinger8037f752020-02-29 20:47:09 -0500910class ActionReview(_ActionSimpleParallelCLs):
911 """Review CLs with multiple settings
912
913 The label option supports extended/multiple syntax for easy use. The --label
914 option may be specified multiple times (as settings are merges), and multiple
915 labels are allowed in a single argument. Each label has the form:
916 <long or short name><=+-><value>
917
918 Common arguments:
919 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
920 'V+1 CQ+2'
921 'AS=1 V=1'
922 """
923
924 COMMAND = 'review'
925
926 class _SetLabel(argparse.Action):
927 """Argparse action for setting labels."""
928
929 LABEL_MAP = {
930 'AS': 'Auto-Submit',
931 'CQ': 'Commit-Queue',
932 'CR': 'Code-Review',
933 'V': 'Verified',
934 }
935
936 def __call__(self, parser, namespace, values, option_string=None):
937 labels = getattr(namespace, self.dest)
938 for request in values.split():
939 if '=' 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 elif '-' in request:
946 # Handle Verified-1 form.
947 short, value = request.split('-', 1)
948 value = '-%s' % (value,)
949 else:
950 parser.error('Invalid label setting "%s". Must be Commit-Queue=1 or '
951 'CQ+1 or CR-1.' % (request,))
952
953 # Convert possible short label names like "V" to "Verified".
954 label = self.LABEL_MAP.get(short)
955 if not label:
956 label = short
957
958 # We allow existing label requests to be overridden.
959 labels[label] = value
960
961 @classmethod
962 def init_subparser(cls, parser):
963 """Add arguments to this action's subparser."""
964 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
965 help='Include a message')
966 parser.add_argument('-l', '--label', dest='labels',
967 action=cls._SetLabel, default={},
968 help='Set a label with a value')
969 parser.add_argument('--ready', default=None, action='store_true',
970 help='Set CL status to ready-for-review')
971 parser.add_argument('--wip', default=None, action='store_true',
972 help='Set CL status to WIP')
973 parser.add_argument('--reviewers', '--re', action='append', default=[],
974 help='Add reviewers')
975 parser.add_argument('--cc', action='append', default=[],
976 help='Add people to CC')
977 _ActionSimpleParallelCLs.init_subparser(parser)
978
979 @staticmethod
980 def _process_one(helper, cl, opts):
981 """Use |helper| to process the single |cl|."""
982 helper.SetReview(cl, msg=opts.msg, labels=opts.labels, dryrun=opts.dryrun,
983 notify=opts.notify, reviewers=opts.reviewers, cc=opts.cc,
984 ready=opts.ready, wip=opts.wip)
985
986
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500987class ActionAccount(_ActionSimpleParallelCLs):
988 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500989
990 COMMAND = 'account'
991
992 @staticmethod
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500993 def init_subparser(parser):
994 """Add arguments to this action's subparser."""
995 parser.add_argument('accounts', nargs='*', default=['self'],
996 help='The accounts to query')
997
998 @classmethod
999 def __call__(cls, opts):
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001000 """Implement the action."""
1001 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001002
1003 def print_one(header, data):
1004 print(f'### {header}')
1005 print(pformat.json(data, compact=opts.json).rstrip())
1006
1007 def task(arg):
1008 detail = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/detail')
1009 if not detail:
1010 print(f'{arg}: account not found')
1011 else:
1012 print_one('detail', detail)
1013 for field in ('groups', 'capabilities', 'preferences', 'sshkeys',
1014 'gpgkeys'):
1015 data = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/{field}')
1016 print_one(field, data)
1017
1018 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -08001019
1020
Mike Frysinger2295d792021-03-08 15:55:23 -05001021class ActionConfig(UserAction):
1022 """Manage the gerrit tool's own config file
1023
1024 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
1025 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
1026
1027 # Set up subcommand aliases.
1028 [alias]
1029 common-search = search 'is:open project:something/i/care/about'
1030 """
1031
1032 COMMAND = 'config'
1033
1034 @staticmethod
1035 def __call__(opts):
1036 """Implement the action."""
1037 # For now, this is a place holder for raising visibility for the config file
1038 # and its associated help text documentation.
1039 opts.parser.parse_args(['config', '--help'])
1040
1041
Mike Frysingere5450602021-03-08 15:34:17 -05001042class ActionHelp(UserAction):
1043 """An alias to --help for CLI symmetry"""
1044
1045 COMMAND = 'help'
1046
1047 @staticmethod
1048 def init_subparser(parser):
1049 """Add arguments to this action's subparser."""
1050 parser.add_argument('command', nargs='?',
1051 help='The command to display.')
1052
1053 @staticmethod
1054 def __call__(opts):
1055 """Implement the action."""
1056 # Show global help.
1057 if not opts.command:
1058 opts.parser.print_help()
1059 return
1060
1061 opts.parser.parse_args([opts.command, '--help'])
1062
1063
Mike Frysinger484e2f82020-03-20 01:41:10 -04001064class ActionHelpAll(UserAction):
1065 """Show all actions help output at once."""
1066
1067 COMMAND = 'help-all'
1068
1069 @staticmethod
1070 def __call__(opts):
1071 """Implement the action."""
1072 first = True
1073 for action in _GetActions():
1074 if first:
1075 first = False
1076 else:
1077 print('\n\n')
1078
1079 try:
1080 opts.parser.parse_args([action, '--help'])
1081 except SystemExit:
1082 pass
1083
1084
Mike Frysinger65fc8632020-02-06 18:11:12 -05001085@memoize.Memoize
1086def _GetActions():
1087 """Get all the possible actions we support.
1088
1089 Returns:
1090 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1091 function that implements that command (e.g. UserActFoo).
1092 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001093 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
1094
1095 actions = {}
1096 for cls in globals().values():
1097 if (not inspect.isclass(cls) or
1098 not issubclass(cls, UserAction) or
1099 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -05001100 continue
1101
Mike Frysinger65fc8632020-02-06 18:11:12 -05001102 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001103 cmd = cls.COMMAND
1104 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1105 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001106
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001107 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001108
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001109 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001110
1111
Harry Cutts26076b32019-02-26 15:01:29 -08001112def _GetActionUsages():
1113 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -05001114 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001115
Mike Frysinger65fc8632020-02-06 18:11:12 -05001116 cmds = list(actions.keys())
1117 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -08001118 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001119 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001120
Harry Cutts26076b32019-02-26 15:01:29 -08001121 cmd_indent = len(max(cmds, key=len))
1122 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001123 return '\n'.join(
1124 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
1125 for cmd, usage, doc in zip(cmds, usages, docs)
1126 )
Harry Cutts26076b32019-02-26 15:01:29 -08001127
1128
Mike Frysinger2295d792021-03-08 15:55:23 -05001129def _AddCommonOptions(parser, subparser):
1130 """Add options that should work before & after the subcommand.
1131
1132 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1133 """
1134 parser.add_common_argument_to_group(
1135 subparser, '--ne', '--no-emails', dest='notify',
1136 default='ALL', action='store_const', const='NONE',
1137 help='Do not send e-mail notifications')
1138 parser.add_common_argument_to_group(
1139 subparser, '-n', '--dry-run', dest='dryrun',
1140 default=False, action='store_true',
1141 help='Show what would be done, but do not make changes')
1142
1143
1144def GetBaseParser() -> commandline.ArgumentParser:
1145 """Returns the common parser (i.e. no subparsers added)."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001146 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001147There is no support for doing line-by-line code review via the command line.
1148This helps you manage various bits and CL status.
1149
Mike Frysingera1db2c42014-06-15 00:42:48 -07001150For general Gerrit documentation, see:
1151 https://gerrit-review.googlesource.com/Documentation/
1152The Searching Changes page covers the search query syntax:
1153 https://gerrit-review.googlesource.com/Documentation/user-search.html
1154
Mike Frysinger13f23a42013-05-13 17:32:01 -04001155Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001156 $ gerrit todo # List all the CLs that await your review.
1157 $ gerrit mine # List all of your open CLs.
1158 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1159 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1160 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001161 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
116228123.
1163 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1164CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001165Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001166 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1167with Commit-Queue=1.
1168 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1169CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001170 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001171
Harry Cutts26076b32019-02-26 15:01:29 -08001172Actions:
1173"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001174 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001175
Alex Klein2ab29cc2018-07-19 12:01:00 -06001176 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -04001177 parser = commandline.ArgumentParser(
Mike Frysinger50917ad2022-04-12 20:37:14 -04001178 description=description, default_log_level='notice',
1179 epilog='For subcommand help, use `gerrit help <command>`.')
Mike Frysinger8674a112021-02-09 14:44:17 -05001180
1181 group = parser.add_argument_group('Server options')
1182 group.add_argument('-i', '--internal', dest='gob', action='store_const',
1183 default=site_params.EXTERNAL_GOB_INSTANCE,
1184 const=site_params.INTERNAL_GOB_INSTANCE,
1185 help='Query internal Chrome Gerrit instance')
1186 group.add_argument('-g', '--gob',
1187 default=site_params.EXTERNAL_GOB_INSTANCE,
Brian Norrisd25af082021-10-29 11:25:31 -07001188 help=('Gerrit (on borg) instance to query '
1189 '(default: %(default)s)'))
Mike Frysinger8674a112021-02-09 14:44:17 -05001190
Mike Frysinger8674a112021-02-09 14:44:17 -05001191 group = parser.add_argument_group('CL options')
Mike Frysinger2295d792021-03-08 15:55:23 -05001192 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001193
Mike Frysingerf70bdc72014-06-15 00:44:06 -07001194 parser.add_argument('--raw', default=False, action='store_true',
1195 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -04001196 parser.add_argument('--json', default=False, action='store_true',
1197 help='Return results in JSON (suitable for scripting)')
Douglas Andersoncf9e9632022-05-24 14:55:16 -07001198 parser.add_argument('--markdown', default=False, action='store_true',
1199 help='Return results in markdown (for pasting in a bug)')
Mike Frysinger2295d792021-03-08 15:55:23 -05001200 return parser
1201
1202
1203def GetParser(parser: commandline.ArgumentParser = None) -> (
1204 commandline.ArgumentParser):
1205 """Returns the full parser to use for this module."""
1206 if parser is None:
1207 parser = GetBaseParser()
1208
1209 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001210
1211 # Subparsers are required by default under Python 2. Python 3 changed to
1212 # not required, but didn't include a required option until 3.7. Setting
1213 # the required member works in all versions (and setting dest name).
1214 subparsers = parser.add_subparsers(dest='action')
1215 subparsers.required = True
1216 for cmd, cls in actions.items():
1217 # Format the full docstring by removing the file level indentation.
1218 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
1219 subparser = subparsers.add_parser(cmd, description=description)
Mike Frysinger2295d792021-03-08 15:55:23 -05001220 _AddCommonOptions(parser, subparser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001221 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001222
1223 return parser
1224
1225
1226def main(argv):
Mike Frysinger2295d792021-03-08 15:55:23 -05001227 base_parser = GetBaseParser()
1228 opts, subargs = base_parser.parse_known_args(argv)
1229
1230 config = Config()
1231 if subargs:
1232 # If the action is an alias to an expanded value, we need to mutate the argv
1233 # and reparse things.
1234 action = config.expand_alias(subargs[0])
1235 if action != subargs[0]:
1236 pos = argv.index(subargs[0])
1237 argv = argv[:pos] + action + argv[pos + 1:]
1238
1239 parser = GetParser(parser=base_parser)
Mike Frysingerddf86eb2014-02-07 22:51:41 -05001240 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001241
Mike Frysinger484e2f82020-03-20 01:41:10 -04001242 # In case the action wants to throw a parser error.
1243 opts.parser = parser
1244
Mike Frysinger88f27292014-06-17 09:40:45 -07001245 # A cache of gerrit helpers we'll load on demand.
1246 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001247
Mike Frysinger88f27292014-06-17 09:40:45 -07001248 opts.Freeze()
1249
Mike Frysinger27e21b72018-07-12 14:20:21 -04001250 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001251 global COLOR
1252 COLOR = terminal.Color(enabled=opts.color)
1253
Mike Frysinger13f23a42013-05-13 17:32:01 -04001254 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001255 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001256 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -05001257 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001258 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001259 except (cros_build_lib.RunCommandError, gerrit.GerritException,
1260 gob_util.GOBError) as e:
1261 cros_build_lib.Die(e)